Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

TweeVo: Tweet What Your TiVo is Recording

Update 10/16/10: The application + source have been updated to work with Twitter's OAuth/xAuth authentication scheme.  If you have the old version installed, please upgrade at the download links below.


In this article, Brian Peek will take you through his latest Coding4Fun application, TweeVo. TweeVo is a simple background application that polls selected TiVo boxes in your home and tweets what they are recording to a specified Twitter account. The tweet includes a link to www.zap2it.com, which gives viewers of your Twitter stream more information on the show, and allows them to set up a recording to their own TiVo.

Introduction

I'm a huge fan of TiVo.  I've had one since June of 2000.  Recently, TiVo has added a variety of networking services to their platform, however there are still a few gaps that could be filled.  One service that TiVo has not connected with is Twitter.  I have quite a few friends who also have TiVo devices and we've often discussed shows we are watching, or upcoming events that we are going to record, but it would be far easier to have these items available for us, and others, to see automatically.  That's where TweeVo fits in.  TweeVo is a tray application that polls selected TiVos in your home, looks at the Now Playing List to determine what's new since the last update, and tweets those new shows to a specified Twitter account, along with channel and time information, and a link to Zap2it.com which lets viewers get more information about the program or directly set up a recording on their TiVo for the same show.

image

Follow PeekVo now and see what my TiVo's are recording!

The app itself is pretty simple: a tray application that runs in the background, periodically polling the TiVos, and uses some LINQ to XML to parse the resulting playlists.  When new shows are found, a text string containing the relevant details is created and posted to Twitter with a simple HTTP post.  So, let's dig in to the internals…

System Tray Application

TweeVo is a WPF application written as a “system tray” application, which means it runs in the background without user interaction, but has an icon in the system tray area near the clock.  Unfortunately, WPF doesn't provide any native support for building tray applications, so we must wedge some WinForms into the mix in order to get a tray icon on the screen.  Luckily, this isn't very difficult.

First, add a reference to the System.Windows.Forms assembly.  Next, create an event handler for the Startup event in your App.xaml file as shown.  Visual Studio should automatically create an event handler for you when typing in the event name.  The event handler code follows:

C#

private void App_Startup(object sender, StartupEventArgs e)
{
    // create a tray icon and setup an event handler for double-clicking it
    _notifyIcon = new NotifyIcon();
    _notifyIcon.Icon = TweeVo.Properties.Resources.Icon;
    _notifyIcon.Visible = true;
    _notifyIcon.DoubleClick += notifyIcon_DoubleClick;

    // setup 2 menu items
    MenuItem[] items = new[]
    {
        new MenuItem("&Settings", Settings_Click) { DefaultItem = true } ,
        new MenuItem("-"),
        new MenuItem("&Exit", Exit_Click)
    };
    _notifyIcon.ContextMenu = new ContextMenu(items);


    // create the window and show it if we're not configured
    _window = new TweeVoWindow();


    if(TweeVoSettings.Default.TiVos == null || TweeVoSettings.Default.TiVos.Count == 0)
        _window.Show();
    else
        TiVoPoller.Start();
}

VB

Private Sub App_Startup(ByVal sender As Object, ByVal e As StartupEventArgs)
    ' create a tray icon and setup an event handler for double-clicking it
    _notifyIcon = New NotifyIcon()
    _notifyIcon.Icon = My.Resources.Icon
    _notifyIcon.Visible = True
    AddHandler _notifyIcon.DoubleClick, AddressOf notifyIcon_DoubleClick
 
    ' setup 2 menu items
    Dim items() As MenuItem = { New MenuItem("&Settings", AddressOf Settings_Click) With {.DefaultItem = True}, New MenuItem("-"), New MenuItem("&Exit", AddressOf Exit_Click) }
    _notifyIcon.ContextMenu = New ContextMenu(items)
 
    ' create the window and show it if we're not configured
    _window = New TweeVoWindow()
 
    If TweeVoSettings.Default.TiVos Is Nothing OrElse TweeVoSettings.Default.TiVos.Count = 0 Then
        _window.Show()
    Else
        TiVoPoller.Start()
    End If
End Sub 

This code creates a new NotifyIcon object, sets a few properties, sets up a double-click event, and creates the pop-up menu that will appear when the icon is clicked.

You will notice this code uses an Icon resource as its icon.  To add an icon resource, double-click the Resources.resx file in Visual Studio.  This will open the resource editor.  Select Icons from the first drop-down menu and then drag and drop a .ico file to the open surface.  This will add the icon resource to the project with the name you specify.  You can then be reference this icon as shown in the code above.

The icon will not automatically disappear from the system tray area at application exit.  Therefore, we must add an event handler for the Exit event in the App.xaml file and implement that event as shown:

C#

void App_Exit(object sender, ExitEventArgs e)
{
    // remove the icon from the tray on exit
    _notifyIcon.Dispose();
}

VB

Private Sub App_Exit(ByVal sender As Object, ByVal e As ExitEventArgs)
    ' remove the icon from the tray on exit
    _notifyIcon.Dispose()
End Sub

This disposes the NotifyIcon object as the application terminates, causing it to be removed from the tray.

User Interface

Now that we have our basic system tray application setup, we need a user interface so the application can be configured by the user.  The current UI looks like this:

image

I created this UI in WPF.  It's a simple Grid-based layout with standard TextBox, ComboBox, etc. controls.  The ListBox control is a bit more complex; it uses what's called an ItemTemplate to define how the TiVo data is drawn into it.

A TiVo on your home network has an IP address and a name.  In order to easily display this information, and which TiVos have been selected by the user for polling, I created a custom ItemTemplate which displays each TiVo formatted as:

[ ] TiVoName (IP Address)

As TiVos are found on the network, a new entry is added.  This allows the user to select which TiVos are enabled, via a checkbox, based on their name and/or IP address.

The definition for the ItemTemplate is stored as a resource in the TweeVoWindow.xaml file as a DataTemplate.  The XAML for this can be seen here:

<Window.Resources>
    <DataTemplate x:Key="TivoItemTemplate">
        <StackPanel Orientation="Horizontal">
            <CheckBox IsChecked="{Binding Active}" VerticalAlignment="Center"/>
            <TextBlock Text="{Binding}" VerticalAlignment="Center" />
        </StackPanel>
    </DataTemplate>
</Window.Resources>

This XAML adds a new item to the Resources dictionary that defines what an entry in the ListBox will look like: a horizontal StackPanel comprised of a CheckBox and a TextBlock that are data bound to specific properties.

The XAML definition of the ListBox references this as shown below:

<ListBox x:Name="lbTiVo" ItemTemplate="{StaticResource TivoItemTemplate}" />

The UI for TweeVo uses a theme from the WPF Themes project on CodePlex.  Using this theme was simple: drop the appropriate Theme.xaml file into the project, add a MergedDictionary to the application resource dictionary in App.xaml as shown:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Theme.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

User Settings, Encryption, and Data Binding

.NET projects, by default, provide a way to define user settings by opening the Settings.Settings file located in the Properties folder.  Unfortunately, the design-time editor is a bit limited in what types of objects can be used for settings and how they are persisted to the configuration file.  For TweeVo, I maintain a Dictionary of TiVos keyed by their unique identifier.  Storing the data in this way allows me to quickly look up TiVos as they are discovered to see if they are already in the master list.  Using the design-time editor, a type of Dictionary can't be specified, nor that the object should be binary serialized instead of XML serialized, which is what the design-time editor will use.

To use a custom settings class, I created a new class inheriting from ApplicationSettingsBase named TweeVoSettings.  A portion of this class can be seen below:

C#

public class TweeVoSettings : ApplicationSettingsBase
{
    private static TweeVoSettings defaultInstance = ((TweeVoSettings)(Synchronized(new TweeVoSettings())));
    
    public static TweeVoSettings Default {
        get {
            return defaultInstance;
        }
    }
    
    [UserScopedSettingAttribute()]
    [System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [DefaultSettingValueAttribute("")]
    [SettingsSerializeAs(SettingsSerializeAs.Binary)]
    public Dictionary<string,TiVo> TiVos {
        get {
            return ((Dictionary<string,TiVo>)(this["TiVos"]));
        }
        set {
            this["TiVos"] = value;
        }
    }
 
    [UserScopedSettingAttribute()]
    [System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [DefaultSettingValueAttribute("")]
    public string TwitterUsername {
        get {
            return ((string)(this["TwitterUsername"]));
        }
        set {
            this["TwitterUsername"] = value;
        }
    }
}

VB

Public Class TweeVoSettings
    Inherits ApplicationSettingsBase
    Private Shared defaultInstance As TweeVoSettings = (CType(Synchronized(New TweeVoSettings()), TweeVoSettings))
 
    Public Shared ReadOnly Property [Default]() As TweeVoSettings
        Get
            Return defaultInstance
        End Get
    End Property
 
    <UserScopedSettingAttribute(), DebuggerNonUserCodeAttribute(), DefaultSettingValueAttribute(""), SettingsSerializeAs(SettingsSerializeAs.Binary)> _
    Public Property TiVos() As Dictionary(Of String,TiVo)
        Get
            Return (CType(Me("TiVos"), Dictionary(Of String,TiVo)))
        End Get
        Set(ByVal value As Dictionary(Of String,TiVo))
            Me("TiVos") = value
        End Set
    End Property
 
    <UserScopedSettingAttribute(), DebuggerNonUserCodeAttribute(), DefaultSettingValueAttribute("")> _
    Public Property TwitterUsername() As String
        Get
            Return (CStr(Me("TwitterUsername")))
        End Get
        Set(ByVal value As String)
            Me("TwitterUsername") = value
        End Set
    End Property
End Class

This class creates a static property which returns an instance of itself so it is very easily globally accessible.  This is the exact what the design-time editor would produce.

The TiVos property is the only one which requires special attention.  You will see that this property is of type Dictionary<string,TiVo> and is decorated with an additional attribute:

[SettingsSerializeAs(SettingsSerializeAs.Binary)]

This causes the property to be binary serialized as required, since a Dictionary object does not support XML serialization.

Encryption

Because we are storing some sensitive data -- a set of Twitter credentials and the user's Media Access Key from the TiVo -- these parameters should be encrypted before being persisted to the configuration file.  But, they need to be decrypted for display in the user interface. 

Let's talk about the encryption methods first.  In the Extensions class there are two extensions methods for encrypting and decrypting a string:

C#

public static string EncryptString(this string s)
{
    return string.IsNullOrEmpty(s) ? string.Empty : Convert.ToBase64String(ProtectedData.Protect(Encoding.Unicode.GetBytes(s), null, DataProtectionScope.CurrentUser));
}
 
public static string DecryptString(this string s)
{
    return string.IsNullOrEmpty(s) ? string.Empty : Encoding.Unicode.GetString(ProtectedData.Unprotect(Convert.FromBase64String(s), null, DataProtectionScope.CurrentUser));
}

VB

<System.Runtime.CompilerServices.Extension> _
Public Function EncryptString(ByVal s As String) As String
    Return If(String.IsNullOrEmpty(s), String.Empty, Convert.ToBase64String(ProtectedData.Protect(Encoding.Unicode.GetBytes(s), Nothing, DataProtectionScope.CurrentUser)))
End Function
 
<System.Runtime.CompilerServices.Extension> _
Public Function DecryptString(ByVal s As String) As String
    Return If(String.IsNullOrEmpty(s), String.Empty, Encoding.Unicode.GetString(ProtectedData.Unprotect(Convert.FromBase64String(s), Nothing, DataProtectionScope.CurrentUser)))
End Function

These methods use the ProtectedData class which lives in the System.Security.Cryptography namespace.  This class is a wrapper around the Data Protection API (DPAPI) which has been available since Windows 2000 and is very simple to use, having only two methods:  Protect and Unprotect.  As you might expect, the Protect method encrypts the data while the Unprotect method decrypts the data.  By converting the string into a byte array and passing to these methods, we can convert the resulting byte array back into a string and store it in the configuration file.

Data Binding

We could skip data binding and manually set the UI fields to the decrypted values on our own, but that wouldn't allow us the advantages of WPF data binding.  But, how do we get our encrypted data from the config file to the UI elements as decrypted strings?  The answer is to use an IValueConverter, an interface that can be implemented and used in WPF databinding to automatically convert from one data type to another.  A good example of this is the ability to specifiy a string name as a color in XAML:

<TextBlock Foreground="White"/>

WPF provides a value converter here that automatically converts the string “Red” into the proper color value for the WPF control, and vice versa.  In our case, our value converter will be used to encrypt and decrypt the string data using our extension methods.  The EncryptionConverter class is shown below:

C#

public class EncryptionConverter : IValueConverter
{
    // decrypt
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (value as string).DecryptString();
    }
 
    // encrypt
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (value as string).EncryptString();
    }
}

VB

Public Class EncryptionConverter
    Implements IValueConverter
    ' decrypt
    Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.Convert
        Return (TryCast(value, String)).DecryptString()
    End Function
 
    ' encrypt
    Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
        Return (TryCast(value, String)).EncryptString()
    End Function
End Class

Two methods must be implemented: Convert and ConvertBack.  The Convert method is used when data is being applied to the UI control, while ConvertBack is used when the data is being retrieved.  In either case, we use our simple extension methods to encrypt and decrypt the data as needed.

To use this type converter, we must add some code to the UI's XAML file.  In the resources section, we specify the converter and assign it a key:

<Window.Resources>
    <TweeVo:EncryptionConverter x:Key="ec"/>
</Window.Resources>

We can then use the ec identifier to specify the type converter.  The Text parameter for the txtMAK element (among others) can now use this type converter as shown:

<TextBox x:Name="txtMAK" Text="{Binding MediaAccessKey, Converter={StaticResource ec}}"
 Width="80" VerticalAlignment="Center" HorizontalAlignment="Left" MaxLength="10"
 Margin="5 0 0 0" />

When we go to use these values from the TweeVoSettings class, we still need to manually call the DecryptString extension method because the settings class itself stores the data internally in its encrypted format.

TiVo Beacons

Through the application, we need an object to represent a TiVo.  This class lists a few properties that each TiVo will possess and some methods that will perform actions on a real-life TiVo.  Some of these properties are determined at runtime by listening for “beacon” packets that all TiVos will send across the network approximately every 60 seconds to identify themselves to other TiVo devices.  We can listen for those packets, parse them, and then create TiVo objects that represent these machines in our application.  The TiVoBeaconListener class encapsulates this functionality.

Beacons are sent out via UDP on port 2190.  We can use the System.Net.Sockets namespace to talk at the socket level and listen for these packets as shown here:

C#

private static void DiscoverTiVos()
{
    MessageBoxResult dr = MessageBoxResult.None;
 
    do
    {
        try
        {
            // bind to port 2190
            _listener = new UdpClient(2190);
        }
        catch(SocketException ex)
        {
            // this error indicates something else is listening on the port, most likely TiVo Desktop
            if(ex.ErrorCode == 10048)
            {
                dr = MessageBox.Show("There is an application running already listening for TiVo beacons.  This means you're likely running TiVo Desktop on this computer.  If the TiVo list has not yet been loaded, please disable TiVo Desktop.  Once the list is filled in and settings are saved, TiVo Desktop can be restarted.  Try again?", "Error", MessageBoxButton.YesNo, MessageBoxImage.Exclamation);
                if(dr == MessageBoxResult.No)
                    return;
            }
            else
                throw;
        }
    }
    while(dr == MessageBoxResult.Yes);
 
    // receive data from any IP on this port
    IPEndPoint ep = new IPEndPoint(IPAddress.Any, 2190);
 
    while(_listener != null)
    {
        // get a beacon packet
        byte[] bytes = _listener.Receive(ref ep);
        string beacon = Encoding.ASCII.GetString(bytes);
 
        // parse it out
        TiVo t = ParseBeacon(beacon);
        // assign the IP of the incoming data (that's the TiVo's IP)
        t.IpAddress = ep.Address;
    }
}

VB

Private Shared Sub DiscoverTiVos()
    Dim dr As MessageBoxResult = MessageBoxResult.None
 
    Do
        Try
            ' bind to port 2190
            _listener = New UdpClient(2190)
        Catch ex As SocketException
            ' this error indicates something else is listening on the port, most likely TiVo Desktop
            If ex.ErrorCode = 10048 Then
                dr = MessageBox.Show("There is an application running already listening for TiVo beacons.  This means you're likely running TiVo Desktop on this computer.  If the TiVo list has not yet been loaded, please disable TiVo Desktop.  Once the list is filled in and settings are saved, TiVo Desktop can be restarted.  Try again?", "Error", MessageBoxButton.YesNo, MessageBoxImage.Exclamation)
                If dr = MessageBoxResult.No Then
                    Return
                End If
            Else
                Throw
            End If
        End Try
    Loop While dr = MessageBoxResult.Yes
 
    ' receive data from any IP on this port
    Dim ep As New IPEndPoint(IPAddress.Any, 2190)
 
    Do While _listener IsNot Nothing
        ' get a beacon packet
        Dim bytes() As Byte = _listener.Receive(ep)
        Dim beacon As String = Encoding.ASCII.GetString(bytes)
 
        ' parse it out
        Dim t As TiVo = ParseBeacon(beacon)
        ' assign the IP of the incoming data (that's the TiVo's IP)
        t.IpAddress = ep.Address
    Loop
End Sub

This class creates a new UdpClient object and binds it to port 2190.  The UdpClient then creates an endpoint that listens for data from any IP address on that port.  Next, the client calls the Receive method, which will sit and block until some data appears on the specified port.  When it does, we convert those bytes into a string that is parsed based on the TiVo beacon spec, and a new TiVo object is created.

A TiVo beacon packet is a carriage return-delimited list of strings.  Each string is a name/value pair in the format of:

[Property]=[Value]

and is parsed as shown:

C#

private static TiVo ParseBeacon(string beacon)
{
    TiVo t = new TiVo();
 
    // parse the beacon packet into properties
    string[] lines = beacon.Split('\n');
    foreach(string line in lines)
    {
        // parse the name/value pairs
        string[] values = line.Split('=');
        switch(values[0].ToUpperInvariant())
        {
            case "PLATFORM":
                t.Platform = values[1];
                break;
            case "MACHINE":
                t.Machine = values[1];
                break;
            case "IDENTITY":
                t.Identity = values[1];
                break;
        }
    }
 
    return t;
}
    }private static TiVo ParseBeacon(string beacon)
{
    TiVo t = new TiVo();
 
    // parse the beacon packet into properties
    string[] lines = beacon.Split('\n');
    foreach(string line in lines)
    {
        // parse the name/value pairs
        string[] values = line.Split('=');
        switch(values[0].ToUpperInvariant())
        {
            case "PLATFORM":
                t.Platform = values[1];
                break;
            case "MACHINE":
                t.Machine = values[1];
                break;
            case "IDENTITY":
                t.Identity = values[1];
                break;
        }
    }
 
    return t;
}

VB

Private Shared Function ParseBeacon(ByVal beacon As String) As TiVo
    Dim t As New TiVo()
 
    ' parse the beacon packet into properties
    Dim lines() As String = beacon.Split(ControlChars.Lf)
    For Each line As String In lines
        ' parse the name/value pairs
        Dim values() As String = line.Split("="c)
        Select Case values(0).ToUpperInvariant()
            Case "PLATFORM"
                t.Platform = values(1)
            Case "MACHINE"
                t.Machine = values(1)
            Case "IDENTITY"
                t.Identity = values(1)
        End Select
    Next line
 
    Return t
End Function

The properties we care about are PLATFORM, MACHINE and IDENTITY.  These are passed and placed into our TiVo object, which is then added to our master Dictionary of TiVos and keyed on the Identity field, a unique identifier per TiVo.  If the TiVo already exists in the Dictionary, it is not added to the list.

Now Playing List

Each TiVo has what's known as the Now Playing List.  This is the list of recorded shows that are currently stored on the TiVo.  A user normally accesses this from the master TiVo menu, but it can also be accessed as an XML payload via a web server running on every TiVo device.

To retrieve this document, you can browse to your TiVo's IP address and the following path:

https://<TiVo IP>/TiVoConnect?Command=QueryContainer&Container=%2FNowPlaying&Recurse=Yes

If you access this URL via a web browser, you will be prompted for credentials.  For every TiVo, the username is tivo and the password is your Media Access Key (MAK).  You can find your MAK on the TiVo device by navigating to:

TiVo Central –> Messages & Settings –> Account & System Information –> Media Access Key

Or, you can find this on TiVo's website by logging into your account and clicking “View Media Access Key” from the following URL:

https://www3.tivo.com/tivo-mma/index.do

Upon entering these credentials, you will see the XML for the current Now Playing List.  Here's a shortened sample:

<?xml version="1.0" encoding="utf-8"?>
<TiVoContainer xmlns="http://www.tivo.com/developer/calypso-protocol-1.6/">
    <Details>
        <ContentType>x-tivo-container/tivo-videos</ContentType>
        <SourceFormat>x-tivo-container/tivo-dvr</SourceFormat>
        <Title>Now Playing</Title>
        <LastChangeDate>0x4B73C716</LastChangeDate>
        <TotalItems>473</TotalItems>
        <UniqueId>/NowPlaying</UniqueId>
    </Details>
    <SortOrder>Type,CaptureDate</SortOrder>
    <GlobalSort>Yes</GlobalSort>
    <ItemStart>0</ItemStart>
    <ItemCount>128</ItemCount>
    <Item>
        <Details>
            <ContentType>video/x-tivo-raw-tts</ContentType>
            <SourceFormat>video/x-tivo-raw-tts</SourceFormat>
            <Title>Late Show With David Letterman</Title>
            <SourceSize>6532628480</SourceSize>
            <Duration>3720000</Duration>
            <CaptureDate>0x4B7388F4</CaptureDate>
            <EpisodeTitle>Jessica Biel; Christoph Waltz</EpisodeTitle>
            <Description>Actress Jessica Biel; actor Christoph Waltz; Allison Moorer performs. Copyright Tribune Media Services, Inc.</Description>
            <SourceChannel>1806</SourceChannel>
            <SourceStation>WRGBDT</SourceStation>
            <HighDefinition>Yes</HighDefinition>
            <ProgramId>EP0768384030</ProgramId>
            <SeriesId>SH076838</SeriesId>
            <ByteOffset>0</ByteOffset>
            <TvRating>4</TvRating>
        </Details>
        <Links>
            <Content>
                <Url>http://192.168.2.100:80/download/Late%20Show%20With%20David%20Letterman.TiVo?Container=%2FNowPlaying&amp;id=511516</Url>
                <ContentType>video/x-tivo-raw-tts</ContentType>
            </Content>
            <TiVoVideoDetails>
                <Url>https://192.168.2.100:443/TiVoVideoDetails?id=511516</Url>
                <ContentType>text/xml</ContentType>
                <AcceptsParams>No</AcceptsParams>
            </TiVoVideoDetails>
        </Links>
    </Item>
</TiVoContainer>

The URL above will return the entire Now Playing List, with Suggestions included.  Suggestions are recordings made by the TiVo automatically for shows it thinks you might like based on your other programming preferences.  TweeVo allows the option of marking Suggestions in the outgoing tweet, so we need to make sure we mark those recordings as such.  The easiest way I found to do this was to pull the items in the suggestions folder only, using this URL:

https://<TiVo IP>/TiVoConnect?Command=QueryContainer&Container=%2FNowPlaying%2f0

This equates to the “NowPlaying/0” container.  0 is the magic folder ID for the Suggestions folder.  For those that care, 1 is the ID for the “HD Recordings” folder on S3/HD TiVos.

With the list of Suggestion recordings in hand, we can match the items in the full Now Playing List to the IDs in the suggestions folder and mark them appropriately.

Getting and Parsing the XML data

To get the data, we use the HttpWebRequest object to pull the URL specified above, passing in the proper user credentials, and then read the response data into an XDocument object as shown below.

C#

private XDocument GetNowPlayingListDocument(string container, bool recurse)
{
    HttpWebRequest request;
    WebResponse response = null;
    XDocument doc;
 
    // pull the NPL
    string uri = string.Format("https://{0}/TiVoConnect?Command=QueryContainer&Container=%2F{1}&Recurse={2}", IpAddress, container, (recurse ? "Yes" : "No"));
    Logger.Log("Pulling " + uri + " from " + Machine + ", " + IpAddress + ", Last polled: " + LastPolled, LoggerSeverity.Info);
 
    try
    {
        request = (HttpWebRequest)WebRequest.Create(uri);
        request.Credentials = new NetworkCredential("tivo", TweeVoSettings.Default.MediaAccessKey.DecryptString());
        // accept any ssl certificate
        ServicePointManager.ServerCertificateValidationCallback += delegate { return true; };
 
        response = request.GetResponse();
        Logger.Log("List retrieved", LoggerSeverity.Info);
 
        XmlReader xmlReader = XmlReader.Create(response.GetResponseStream());
        doc = XDocument.Load(xmlReader);
    }
    finally
    {
        if(response != null)
            response.Close();
    }
 
    return doc;
}

VB

Private Function GetNowPlayingListDocument(ByVal container As String, ByVal recurse As Boolean) As XDocument
    Dim request As HttpWebRequest
    Dim response As WebResponse = Nothing
    Dim doc As XDocument
 
    ' pull the NPL
    Dim uri As String = String.Format("https://{0}/TiVoConnect?Command=QueryContainer&Container=%2F{1}&Recurse={2}", IpAddress, container, (If(recurse, "Yes", "No")))
    Logger.Log("Pulling " & uri & " from " & Machine & ", " & IpAddress.ToString() & ", Last polled: " & LastPolled, LoggerSeverity.Info)
 
    Try
        request = CType(WebRequest.Create(uri), HttpWebRequest)
        request.Credentials = New NetworkCredential("tivo", TweeVoSettings.Default.MediaAccessKey.DecryptString())
        ' accept any ssl certificate
        ServicePointManager.ServerCertificateValidationCallback = New RemoteCertificateValidationCallback(AddressOf AcceptAll)
 
        response = request.GetResponse()
        Logger.Log("List retrieved", LoggerSeverity.Info)
 
        Dim xmlReader As XmlReader = XmlReader.Create(response.GetResponseStream())
        doc = XDocument.Load(xmlReader)
    Finally
        If response IsNot Nothing Then
            response.Close()
        End If
    End Try
 
    Return doc
End Function

We can use this method to pull both the full Now Playing List and the Suggestions list.  The suggestion list is turned into a simple list of integer Program IDs as shown:

C#

XNamespace ns = docNowPlaying.Root.Name.Namespace;
 
if(ns == null)
    return null;
 
// get a list of ProgramId's for just the suggestions
var querySuggestions = from entry in docSuggestions.Descendants(ns + "Item")
            let details = entry.Element(ns + "Details")
            where details.Element(ns + "ProgramId") != null &&
                  !details.Element(ns + "ProgramId").Value.StartsWith("TS") &&
                  details.Element(ns + "CaptureDate") != null 
            select (string)details.Element(ns + "ProgramId");
List<string> suggestionIds = querySuggestions.ToList();

VB

Dim ns As XNamespace = docNowPlaying.Root.Name.Namespace
 
If ns Is Nothing Then
    Return Nothing
End If
 
' get a list of ProgramId's for just the suggestions
Dim querySuggestions = From entry In docSuggestions.Descendants(ns + "Item") _
                       Let details = entry.Element(ns + "Details") _
                       Where details.Element(ns + "ProgramId") IsNot Nothing AndAlso (Not details.Element(ns + "ProgramId").Value.StartsWith("TS")) AndAlso details.Element(ns + "CaptureDate") IsNot Nothing _
                       Select CStr(details.Element(ns + "ProgramId"))
Dim suggestionIds As List(Of String) = querySuggestions.ToList()

First, the namespace of the document is retrieved.  This is appended to every element name so it can be properly found and parsed. 

The full Now Playing List is a bit more complex.  With the XDocument object, we can parse the XML data into a List of NPLEntry objects as shown:

C#

var query = from entry in docNowPlaying.Descendants(ns + "Item")
            let details = entry.Element(ns + "Details")
            where details.Element(ns + "ProgramId") != null &&
                  !details.Element(ns + "ProgramId").Value.StartsWith("TS") &&    // no downloaded TiVo videos
                  details.Element(ns + "CaptureDate") != null 
            orderby details.Element(ns + "CaptureDate").Value ascending
            select new NPLEntry
               {
                       Title = (string)details.Element(ns + "Title"),
                       EpisodeTitle = (string)details.Element(ns + "EpisodeTitle"),
                       SourceChannel = (string)details.Element(ns + "SourceChannel"),
                       SourceStation = (string)details.Element(ns + "SourceStation"),
                       CaptureDate = (details.Element(ns + "CaptureDate").Value).EpochToDateTime().RoundToNearestMinute(),
                       ProgramID = (string)details.Element(ns + "ProgramId"),
                    // if the program ID lives in the suggestion list, than mark it as a suggestion
                    Suggestion = suggestionIds.Exists(s => s == (string)details.Element(ns + "ProgramId"))
               };
 
List<NPLEntry> entries = query.ToList();

VB

Dim ns As XNamespace = docNowPlaying.Root.Name.Namespace
 
If ns Is Nothing Then
    Return Nothing
End If
 
' get a list of ProgramId's for just the suggestions
Dim querySuggestions = From entry In docSuggestions.Descendants(ns + "Item") _
                       Let details = entry.Element(ns + "Details") _
                       Where details.Element(ns + "ProgramId") IsNot Nothing AndAlso (Not details.Element(ns + "ProgramId").Value.StartsWith("TS")) AndAlso details.Element(ns + "CaptureDate") IsNot Nothing _
                       Select CStr(details.Element(ns + "ProgramId"))
Dim suggestionIds As List(Of String) = querySuggestions.ToList()

We pull out data from the XML schema that matches the query specified, which ensure we're getting actual recorded shows and not downloaded web videos or other media.  The list of Suggestion IDs is used to mark the boolean Suggestion property “true” if the Program ID is found in that list.  All of these properties are placed into NPLEntry objects, turned into a list, and returned to the caller.

DateTime and Unix time

The CaptureDate element needs some special attention.  The time value returned in this field is in a Unix timestamp/epoch format and must be converted to a .NET DateTime object.  A Unix timestamp is an integer number that represents the number of seconds that have elapsed since January 1, 1970 in UTC (Coordinated Universal Time).  The EpochToDateTime method in the Extensions class, a .NET extension method, handles the conversion process:

C#

public static DateTime EpochToDateTime(this string date)
{
    return new DateTime(1970, 1, 1).AddSeconds(long.Parse(date.Remove(0, 2), NumberStyles.HexNumber)).ToLocalTime();
}

VB

<System.Runtime.CompilerServices.Extension> _
Public Function EpochToDateTime(ByVal [date] As String) As Date
    Return New Date(1970, 1, 1).AddSeconds(Long.Parse([date].Remove(0, 2), NumberStyles.HexNumber)).ToLocalTime()
End Function

EpochToDateTime creates a new DateTime object with the date of 1/1/1970.  We then use the AddSeconds method of the DateTime object to add on the Unix timestamp value.  Finally, the ToLocalTime method is called to convert that into the time of the local time zone.

The TiVo class contains two methods named GetNowPlayingListDocument and GetNowPlayingList which do the hard work of getting and parsing the XML data.

Polling for Data

The Poller class uses all of the above to poll every selected TiVo every 15 minutes for its Now Playing List data.  If new shows are found, they are tweeted to the specified Twitter account along with a link to Zap2it.com that gives further information.

The poller uses a simple Timer object to run a method every 15 minutes.  This method pulls the Now Playing List and determines if a recording is a suggestion.  If suggestions are disabled in the application, the recording is ignored.  If the show is not a suggestion, or suggestions are turned on, a Twitter string is created for upload, and posted to Twitter:

C#

foreach(TiVo t in TweeVoSettings.Default.TiVos.Values)
{
    if(t.Active)
    {
        // try to poll 3 times
        while(tries < 3 && !success)
        {
            try
            {
                List<NPLEntry> list = t.GetNowPlayingList();
                if(list != null)
                {
                    foreach(NPLEntry nplEntry in list)
                    {
                        // if the entry is valid, tweet it
                        if((!nplEntry.Suggestion || (nplEntry.Suggestion && TweeVoSettings.Default.Suggestions != SuggestionsType.NoShow)) &&
                            nplEntry.CaptureDate > t.LastPolled)
                        {
                            string tweet = CreateTwitterString(t.Machine, nplEntry);
                            Twitter.PostTwitterUpdate(tweet);
                        }
                    }
                }
                success = true;
                t.LastPolled = DateTime.Now;
                Logger.Log("Completed processing " + t.Machine, LoggerSeverity.Info);
            }
            catch(Exception ex)
            {
                Logger.Log("Exception on " + t.Machine + " with ex: " + ex, LoggerSeverity.Error);
                success = false;
                tries++;
 
                // if we've failed 3 times, notify somebody
                if(tries == 3)
                {
                    Logger.Log("3 retries on " + t.Machine + " with ex: " + ex, LoggerSeverity.Error);
                    (Application.Current as App).ShowBalloonTip("Error with " + t.Machine + ": " + ex.Message + "  If this keeps happening, you may want to disable TweeVo from communicating with this TiVo.");
                }
            }
        }
        tries = 0;
    }
    success = false;
}

VB

For Each t As TiVo In TweeVoSettings.Default.TiVos.Values
    If t.Active Then
        ' try to poll 3 times
        Do While tries < 3 AndAlso Not success
            Try
                Dim list As List(Of NPLEntry) = t.GetNowPlayingList()
                If list IsNot Nothing Then
                    For Each nplEntry As NPLEntry In list
                        ' if the entry is valid, tweet it
                        If ((Not nplEntry.Suggestion) OrElse (nplEntry.Suggestion AndAlso TweeVoSettings.Default.Suggestions <> SuggestionsType.NoShow)) AndAlso nplEntry.CaptureDate > t.LastPolled Then
                            Dim tweet As String = CreateTwitterString(t.Machine, nplEntry)
                            Twitter.PostTwitterUpdate(tweet)
                        End If
                    Next nplEntry
                End If
                success = True
                t.LastPolled = Date.Now
                Logger.Log("Completed processing " & t.Machine, LoggerSeverity.Info)
            Catch ex As Exception
                Logger.Log("Exception on " & t.Machine & " with ex: " & ex.ToString(), LoggerSeverity.Error)
                success = False
                tries += 1
 
                ' if we've failed 3 times, notify somebody
                If tries = 3 Then
                    Logger.Log("3 retries on " & t.Machine & " with ex: " & ex.ToString(), LoggerSeverity.Error)
                    TryCast(Application.Current, App).ShowBalloonTip("Error with " & t.Machine & ": " & ex.Message & "  If this keeps happening, you may want to disable TweeVo from communicating with this TiVo.")
                End If
            End Try
        Loop
        tries = 0
    End If
    success = False
Next t

The CreateTwitterString method builds out the text data to be posted to Twitter along with the Zap2It link.  The Zap2It link takes the form of:

http://tvlistings.zap2it.com/tv/x/<program ID>

The “x” can in fact be anything.  If you were to browse Zap2It's website, you would see that the “x” is actually a URL-formatted version of the episode title.  At the time of this writing, it appears that it is not actually used.  The program ID is the same program ID we get from the TiVo, but it needs to be padded to the correct length of 14 characters.

Tribune Media Services provides television programming guide data for a variety of devices and services.  They have uniquely identified every show that has aired or will air with a specific code.  The code is in the format of:

TTSSSSSSSSEEEE

TShow type
MV = movie
EP = TV show
SSeries ID
EEpisode number

As an example, the “Afternoon Delight” episode of the show “Arrested Development” has the ID EP005984700030.  Breaking it down, we have:

EPTV show
00598470Series ID
(all episodes of Arrested Development have this ID)
0030Episode number in the series

Browsing to http://tvlistings.zap2it.com/tv/x/EP005984700030 displays the page for the “Afternoon Delight” episode, while browsing to http://tvlistings.zap2it.com/tv/x/EP00598470 displays the global page for the “Arrested Development” series.

This link can be shorter.  There are many URL shortening services available, but perhaps the simplest one to use is tinyurl.com .  This site has an extremely simple API: send it a URL via a query string parameter and it will reply with a single, undecorated string that is the resulting shortened URL.  This can be done in a single line as shown here in the GetShortUrl method:

C#

private static string GetShortUrl(string url)
{
    // get a shortened URL using tinyurl.com
    return new WebClient().DownloadString("http://tinyurl.com/api-create.php?url=" + url);
}

VB

Private Shared Function GetShortUrl(ByVal url As String) As String
    ' get a shortened URL using tinyurl.com
    Return New WebClient().DownloadString("http://tinyurl.com/api-create.php?url=" & url)
End Function

The rest of the CreateTwitterString method puts together a string using the string.Format method with the URL created above and some properties of the NPLEntry object.

Posting to Twitter

The final task is to post the string to Twitter.  The Twitter class contains a method named PostTwitterUpdate that uses the Twitter API to POST the provided string to the Twitter user account specified in the TweeVo UI:

C#

public static void PostTwitterUpdate(string tweet)
{
    Logger.Log("Tweet: " + tweet, LoggerSeverity.Info);
 
    // turn the tweet into a byte array
    byte[] bytes = Encoding.ASCII.GetBytes("source=tweevo&status=" + tweet);
 
    HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://twitter.com/statuses/update.xml");
 
    // post that byte array up to the server
    request.Method = "POST";
    request.Credentials = new NetworkCredential(TweeVoSettings.Default.TwitterUsername.DecryptString(), TweeVoSettings.Default.TwitterPassword.DecryptString());
    request.ServicePoint.Expect100Continue = false;
    request.ContentType = "application/x-www-form-urlencoded";
 
    request.ContentLength = bytes.Length;
    
    Stream reqStream = request.GetRequestStream();
 
    reqStream.Write(bytes, 0, bytes.Length);
 
    reqStream.Close();
 
    HttpWebResponse resp = (HttpWebResponse)request.GetResponse();
    resp.Close();
}

VB

Public Shared Sub PostTwitterUpdate(ByVal tweet As String)
    Logger.Log("Tweet: " & tweet, LoggerSeverity.Info)
 
    ' turn the tweet into a byte array
    Dim bytes() As Byte = Encoding.ASCII.GetBytes("source=tweevo&status=" & tweet)
 
    Dim request As HttpWebRequest = CType(WebRequest.Create("http://twitter.com/statuses/update.xml"), HttpWebRequest)
 
    ' post that byte array up to the server
    request.Method = "POST"
    request.Credentials = New NetworkCredential(TweeVoSettings.Default.TwitterUsername.DecryptString(), TweeVoSettings.Default.TwitterPassword.DecryptString())
    request.ServicePoint.Expect100Continue = False
    request.ContentType = "application/x-www-form-urlencoded"
 
    request.ContentLength = bytes.Length
 
    Dim reqStream As Stream = request.GetRequestStream()
 
    reqStream.Write(bytes, 0, bytes.Length)
 
    reqStream.Close()
 
    Dim resp As HttpWebResponse = CType(request.GetResponse(), HttpWebResponse)
    resp.Close()
End Sub

This method takes the tweet text and turns it, along with some associated parameters, into a byte array.  A new NetworkCredential object is created with the supplied username/password combination and a POST request is made to the “update” API for Twitter.  The byte array is sent to the server as a url-encoded form and the connection is closed.

Conclusion

And there you have it: a simple little app that tweets what your TiVo is recording.  I plan to update this as people request features, find bugs, etc. and I will release new versions as necessary.  Enjoy!

Thanks

A very special thanks to Bill Pytlovany, Chris Miller and Mark Zaugg for testing out TweeVo during its (far longer than normal) development cycle, and Joey Buczek for creating the icon.

Bio

Brian is a Microsoft C# MVP who has been actively developing in .NET since its early betas in 2000, and who has been developing solutions using Microsoft technologies and platforms for even longer. Along with .NET, Brian is particularly skilled in the languages of C, C++ and assembly language for a variety of CPUs. He is also well-versed in a wide variety of technologies including web development, document imaging, GIS, graphics, game development, and hardware interfacing. Brian has a strong background in developing applications for the health-care industry, as well as developing solutions for portable devices, such as tablet PCs and PDAs. Additionally, Brian has co-authored the book "Coding4Fun: 10 .NET Programming Projects for Wiimote, YouTube, World of Warcraft, and More" published O'Reilly. He previously co-authored the book "Debugging ASP.NET" published by New Riders.  Brian is also an author for MSDN's Coding4Fun website.

Follow the Discussion

  • Henry TolinoHenry Tolino

    With some small modifications, you could modify this to Tweet your scheduled recordings from Windows 7 Media Center.  Just a thought...

  • Clint RutkasClint I'm a "developer"

    @David M that is because Twitter made their login system OAuth only.  This application does not leverage OAuth.  The app itself is open source and could be modified to accomidate this shift in Twitter's API model.

  • David MDavid M

    I'm not sure if there was an API change, but it seems in the past week or two, Twitter stopped accepting login requests from TweeVo.

  • Brian PeekBrian Peek

    A new version has been posted at the above link which fixes the authentication issues.

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.