Typescript TV Project

I thought I'd share a small personal project I completed using Typescript. The purpose was to create a view showing the listings of my favorite TV channels so that I can look up what's on TV from the TV while I'm sitting on the couch

You can view the completed project at this location

How it works

I press a button on my TV remote and the TV switches from shows to the TV listings as shown link above.

A Raspberry Pi with the Chromium browser full screen showing the listings page is plugged into an HDMI port on my TV. There is no user interaction other than switching TV inputs. The grid refreshes and scrolls to the bottom automatically after 10 seconds, then repeats again from the top.

The code

The Typescript code associated with this page can be viewed and stepped through, as source maps are attached. Use your browser developer tools, specifically the sources tab, to examine and step through the Typescript as it executes.

Here is an overview of how it operates:

  • The boot.ts script calls "function "main()" in the app.ts after window DOMContentLoaded is invoked.
  • The app.ts main asks for JSON data of the current listing through an XMLHttpRequest using the endpoint `./?method=listings&start=${start}&end=&${end}`.
  • A series of collections returned from the parsed JSON are iterated over to build the HTML that is the listings content.
  • The cell widths are calculated using show start/end times and some extra adornments are added, such as "cont'd", and start/end times in human readable form.
  • To scroll the grid a CSS transition is calculated based on the size of the browser window, channels listed, and the cell height of each row.
  • A dynamic stylesheet is requested based on that information. See http://tv.codebot.org/?method=css&height=1337 as an example.
  • When the hour changes new data listings JSON is requested and the process repeats.

My base Typescript library with configuration files for the tcs compiler and visual studio code is hosted on github.

The grid isn't a component or anything like that, it's just html divs with a stylesheet. When the window is resized or new show data is pulled in, this bit of Typescript is invoked.

function doResize() {
 
    function getShow(cell: HTMLElement): ShowData {
        let channel = parseInt(cell.getAttribute("channel-index"));
        let show = parseInt(cell.getAttribute("show-index"));
        return state.listings.Channels[channel].Shows[show];
    }
 
    // channelWidth is the blue area with the channel number 
    const channelWidth = 146;
    // if resized scroll, revert the css transition
    state.block.removeClass("bottom");
    // the width of a cell, or 30 minutes
    let width = (window.innerWidth - channelWidth) / 4.0;
    for (let cell of getAll("div.show")) {
        // retrieve the show
        let show = getShow(cell);
        let d = show.Duration;
        // truncate the duration if show is cont'd
        if (show.Start < now.time)
            d -= minutesBetween(now.time, show.Start);
        // calculate the cell width
        let w = (d / 30.0 * width) - 2.0;
        // and left starting position
        let x = channelWidth;
        // offset the cell left based on start time
        if (show.Start > now.time)
            x += minutesBetween(show.Start, now.time) / 30 * width;
        // and finally reposition the cell
        cell.style.left = x + "px"
        cell.style.width = w + "px";
    }
}
I'm using just plain HTML and CSS with some Typescript to figure out how cells (again div markup) are positioned. So no components are being used, just a Typescript function, partially listed below, that updates the grid headers (more divs) and creates channel rows (again more divs).

Arguably the most useful feature of Typescript is that you get type information while you work. When working with HTML elements and I get code insight as I type notifying me of element types properties, events, and methods. In my case I'm just setting the contents of an element using "innerHTML = html", but it's great to be able to examine the DOM and anything else while I type.

Another great thing Typescript feature is literal templates, that is available even if the user's browser doesn't support it. This allows for easy html generation in Typescript:

    state.listings = data;
    let html = "";
    let channelIndex = 0;
    for (let c of data.Channels) {
        html += `
<div class="row">
<div class="column channel">
    <div class="logo"><img src="https://imageserver/guide/${c.Logo}?width=48"></div>
    <div class="channel">${c.Channel}</div>
    <div class="sign">${c.Sign}</div>
</div>`;
        let showIndex = 0;
        for (let s of c.Shows) {
            // and so on building html
The "state.listings" object is a parsed JSON object as returned from the C# backend. If you go to the TV page linked in the original post, open your developer tools, hit the console tab and type "state.listings", you can see how it's structured.

The backend code is written in C#. It's based on my simple POCO IHttpHandler class library that is also hosted on github. HomePage returns a html template that can be filled out by properties on the page. You can also tag methods on the page for invocation either through query strings or form submits (POST or GET requests).

The page class specifies template "listings.htm" using an attribute. It has 3 methods. Method "css" returns the content of css based on an input height query string. Method "scan" is called by a cron job once a day to get new tv listing data. Method "listings" returns JSON data for my favorite channels given a start and end time corresponding to a long returned by the JavaScript Date.now() function.

Here is the listing of the backend code.

using System;
using Codebot.Web;

namespace Codebot.Television.Web
{
	[DefaultPage("listings.htm")]
	public class HomePage : PageHandler
	{
		[MethodPage("css")]
		public void MethodCss()
		{
			Response.ContentType = "text/css";
			var s = String.Format("#listings.bottom {{\ntop: -{0}px;\ntransition: top linear 10s;\n}}", Read("height"));
			Response.Write(s);
		}

		[MethodPage("scan")]
		public void MethodScan()
		{
			var listings = new Model.ListingData();
			try
			{
				listings.Scan();
				Response.Write("OK");
			}
			catch (Exception e)
			{
				Response.Write(e);
			}
		}

		[MethodPage("listings")]
		public void MethodListings()
		{
			try
			{
				var start = Read<long>("start", 0);
				var end = Read<long>("end", 0);
				var listings = new Model.ListingData();
				listings.Load(start, end);
				Response.Write(listings);
			}
			catch
			{
				Response.Write("Error");
			}
		}
	}
}
As you can see, the backend code is fairly straight forwards. If you're curious where I get the TV listing data, implemented in the Model namespace, sorry I can't release that information for fear of flooding my data provider.

If like these kinds of project write ups, let in know by responding here and I may create more in the future.