TweeVo: Tweet What Your TiVo is Recording
- Posted: Feb 19, 2010 at 8:14 PM
- 1,572 Views
- 4 Comments
Loading User Information from Channel 9
Something went wrong getting user information from Channel 9
Loading User Information from MSDN
Something went wrong getting user information from MSDN
Loading Visual Studio Achievements
Something went wrong getting the Visual Studio Achievements
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.
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.
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…
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 SubThis disposes the NotifyIcon object as the application terminates, causing it to be removed from the tray.
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:
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>
.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 ApplicationSettingsBasePrivate Shared defaultInstance As TweeVoSettings = (CType(Synchronized(New TweeVoSettings()), TweeVoSettings))
Public Shared ReadOnly Property [Default]() As TweeVoSettings
Get Return defaultInstanceEnd Get
End Property
<UserScopedSettingAttribute(), DebuggerNonUserCodeAttribute(), DefaultSettingValueAttribute(""), SettingsSerializeAs(SettingsSerializeAs.Binary)> _Public Property TiVos() As Dictionary(Of String,TiVo)
GetReturn (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
GetReturn (CStr(Me("TwitterUsername")))
End Get
Set(ByVal value As String)
Me("TwitterUsername") = value
End Set
End Property
End ClassThis 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.
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 FunctionThese 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.
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
{ // decryptpublic object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{return (value as string).DecryptString();
}
// encryptpublic object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{return (value as string).EncryptString();
}
}
VB
Public Class EncryptionConverter
Implements IValueConverter ' decryptPublic 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
' encryptPublic 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 ClassTwo 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.
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 DesktopIf 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
ReturnEnd If
Else ThrowEnd If
End Try
Loop While dr = MessageBoxResult.Yes
' receive data from any IP on this portDim ep As New IPEndPoint(IPAddress.Any, 2190)
Do While _listener IsNot Nothing
' get a beacon packetDim bytes() As Byte = _listener.Receive(ep)
Dim beacon As String = Encoding.ASCII.GetString(bytes)
' parse it outDim t As TiVo = ParseBeacon(beacon)
' assign the IP of the incoming data (that's the TiVo's IP)t.IpAddress = ep.Address
LoopEnd SubThis 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 propertiesstring[] lines = beacon.Split('\n');
foreach(string line in lines)
{ // parse the name/value pairsstring[] 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 propertiesstring[] lines = beacon.Split('\n');
foreach(string line in lines)
{ // parse the name/value pairsstring[] 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 propertiesDim lines() As String = beacon.Split(ControlChars.Lf)
For Each line As String In lines
' parse the name/value pairsDim 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 tEnd 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.
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&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.
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 NPLstring 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 certificateServicePointManager.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 NPLDim 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 certificateServicePointManager.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)
FinallyIf response IsNot Nothing Then
response.Close()
End If
End Try
Return docEnd 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 suggestionsvar 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 suggestionsDim 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 suggestionSuggestion = 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 suggestionsDim 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.
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 FunctionEpochToDateTime 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.
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 timesDo While tries < 3 AndAlso Not success
TryDim 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 itIf ((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 nplEntryEnd 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 = Falsetries += 1
' if we've failed 3 times, notify somebodyIf 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
Looptries = 0
End If
success = FalseNext tThe 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
| T | Show type MV = movie EP = TV show |
| S | Series ID |
| E | Episode number |
As an example, the “Afternoon Delight” episode of the show “Arrested Development” has the ID EP005984700030. Breaking it down, we have:
| EP | TV show |
| 00598470 | Series ID (all episodes of Arrested Development have this ID) |
| 0030 | Episode 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.comreturn 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.comReturn New WebClient().DownloadString("http://tinyurl.com/api-create.php?url=" & url)
End FunctionThe 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.
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 arraybyte[] 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 arrayDim 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 SubThis 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.
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!
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.
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.
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.
Follow the Discussion
Oops, something didn't work.
What does this mean?
Following an item on Channel 9 allows you to watch for new content and comments that you are interested in. You need to be signed in to Channel 9 to use this feature.What does this mean?
Following an item on Channel 9 allows you to watch for new content and comments that you are interested in and view them all on your notifications page.sign up for email notifications?
With some small modifications, you could modify this to Tweet your scheduled recordings from Windows 7 Media Center. Just a thought...
@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.
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.
A new version has been posted at the above link which fixes the authentication issues.
Remove this comment
Remove this thread
close