TweeVo: передавайте в свой блог на Twitter то, что записывает ваш TiVo


Опубликовано 19 февраля 2010 г. 23:14 | Coding4Fun

В этой статье Брайен Пик (Brian Peek) подробно расскажет о новом приложении Coding4Fun — TweeVo. TweeVo — это простая программа, работающая в фоновом режиме, которая опрашивает выбранные приставки TiVo в вашем доме и публикует то, что они записывают, по указанной учетной записи на Twitter. В блог передается ссылка на сайт www.zap2it.com, который предоставляет зрителям вашей потоковой передачи на Twitter больше информации о конкретном шоу и позволяет им настраивать запись на своих приставках TiVo.

Автор: Брайен Пик (Brian Peek)
Исходный код: загрузить
Попробуйте прямо сейчас: запустить приложение
Сложность: средняя
Необходимое время: 3 часа
Затраты: бесплатно! Но, естественно, вам понадобится приставка TiVo…
ПО: Visual Basic или Visual C# Express, проект WPF Themes на CodePlex для поддержки скина UI
Оборудование: нет!

Введение

Я большой любитель TiVo. Такая приставка у меня есть уже с июня 2000 года. Недавно TiVo добавила в свою платформу ряд сетевых сервисов, но некоторые пробелы все еще не заполнены. Один из сервисов, которые TiVo не подключила, — Twitter. У меня довольно много знакомых с устройствами TiVo, и мы часто обсуждаем шоу, которые смотрим, или будущие передачи, которые хотим записать, но было бы куда удобнее, если бы эти доступные нам передачи и другие вещи можно было бы смотреть автоматически. Вот здесь и пригодится TweeVo. TweeVo — это программа, значок которой помещается в системный лоток (секцию индикаторов на панели задач). Она опрашивает выбранные приставки TiVo в вашем доме, просматривает список Now Playing List («Сейчас воспроизводится»), чтобы определить, что нового появилось с момента последнего обновления, и транслирует новые передачи по указанной учетной записи на Twitter, попутно передавая информацию о канале и времени, а также ссылку на Zap2it.com; этот сайт позволяет зрителям получать более подробные сведения о программе или напрямую настраивать запись того же шоу на их приставках TiVo.

clip_image002

Само приложение довольно простое: оно выполняется в фоновом режиме, периодически опрашивает устройства TiVo и с помощью механизма LINQ to XML анализирует полученные списки воспроизведения. Когда обнаруживается новое шоу, создается текстовая строка, содержащая соответствующие подробности, а затем она публикуется на Twitter как простой HTTP-текст. Ну что ж, давайте покопаемся во внутренностях этой программы…

Приложение системного лотка

TweeVo является WPF-приложением, написанным как программа «системного лотка» (т.е. помещаемым в секцию системных индикаторов на панели задач), а это значит, что оно выполняется в фоне без взаимодействия с пользователем, но отображает значок в системном лотке рядом с часами. Увы, в WPF нет готовой поддержки создания таких приложений, поэтому нам придется вклинить в программу кое-что из WinForms, чтобы получить на экране значок для лотка. К счастью, это не слишком трудно.

Во-первых, добавьте ссылку на сборку System.Windows.Forms. Потом создайте обработчик события Startup в своем файле App.xaml, как показано ниже. Visual Studio должен автоматически создать за вас обработчик события, когда вы будете набирать имя события. Код этого обработчика выглядит следующим образом:

C#

private void App_Startup(object sender, StartupEventArgs e)
{
    // Создаем значок для лотка и настраиваем обработчик события так, чтобы он реагировал на двойной щелчок этого значка
    _notifyIcon = new NotifyIcon();
    _notifyIcon.Icon = TweeVo.Properties.Resources.Icon;
    _notifyIcon.Visible = true;
    _notifyIcon.DoubleClick += notifyIcon_DoubleClick;
 
    // Настраиваем две команды меню
    MenuItem[] items = new[]
    {
        new MenuItem("&Settings", Settings_Click) { DefaultItem = true } ,
        new MenuItem("-"),
        new MenuItem("&Exit", Exit_Click)
    };
    _notifyIcon.ContextMenu = new ContextMenu(items);
 
    // Создаем окно и показываем его, если программа не настроена
    _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)
    ' Создаем значок для лотка и настраиваем обработчик события так, чтобы он реагировал на двойной щелчок этого значка
    _notifyIcon = New NotifyIcon()
    _notifyIcon.Icon = My.Resources.Icon
    _notifyIcon.Visible = True
    AddHandler _notifyIcon.DoubleClick, AddressOf notifyIcon_DoubleClick
 
    ' Настраиваем две команды меню
    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)
 
    ' Создаем окно и показываем его, если программа не настроена
    _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

Этот код создает новый объект NotifyIcon, задает несколько свойств, устанавливает событие двойного щелчка и формирует всплывающее меню, которое будет появляться на экране при щелчке значка.

Вы наверняка заметили, что в качестве значка в этом коде используется ресурс Icon. Чтобы добавить такой ресурс, дважды щелкните файл Resources.resx в Visual Studio. Это приведет к его открытию в редакторе ресурсов. Выберите Icons из первого раскрывающегося меню и перетащите файл .ico в открытую область. Тем самым вы добавите ресурс значка в проект под указанным вами именем. Потом вы сможете ссылаться на этот значок, как показано в коде выше.

Значок не исчезает автоматически из системного лотка при завершении программы. Поэтому нужно добавить обработчик события Exit в файл App.xaml и реализовать его следующим образом:

C#

void App_Exit(object sender, ExitEventArgs e)
{
    // Удаляем значок из лотка при выходе
    _notifyIcon.Dispose();
}

VB

Private Sub App_Exit(ByVal sender As Object, ByVal e As ExitEventArgs)
    ' Удаляем значок из лотка при выходе
    _notifyIcon.Dispose()
End Sub

Теперь при завершении программы объект NotifyIcon будет объявляться недействительным и впоследствии удаляться из лотка.

Пользовательский интерфейс (UI)

Создав базовый каркас приложения для системного лотка, нужно сформировать UI, который позволил бы настраивать это приложение. Текущий UI выглядит так:

clip_image004

Я создал этот UI в WPF. Это простая разметка на основе Grid со стандартными TextBox, ComboBox и другими элементами управления. Элемент управления ListBox немного посложнее; в нем используется ItemTemplate, чтобы определять, как в него будут поступать данные TiVo.

Приставка TiVo в вашей домашней сети получает IP-адрес и имя. Чтобы легко получать эту информацию и узнавать, какая приставка TiVo была выбрана пользователем для опроса, я создал собственный ItemTemplate, который выводит информацию о каждой TiVo в таком формате:

[ ] TiVoName (IP-адрес)

Как только в сети обнаруживается устройство TiVo, добавляется новый элемент. Это позволяет выбирать, какие устройства TiVo включены, просто устанавливая флажки и используя информацию об их именах и/или IP-адресах.

Определение ItemTemplate хранится как ресурс в файле TweeVoWindow.xaml в виде DataTemplate. Соответствующий XAML-код показан ниже.

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

Этот XAML добавляет новый элемент в словарь Resources, который определяет, как будет выглядеть элемент в ListBox: как горизонтальный StackPanel, состоящий из CheckBox и TextBlock, связанных через механизм привязки данных со специфическими свойствами.

XAML-определение того, как ListBox ссылается на элементы, показано ниже.

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

В UI для TweeVo используется тема из проекта WPF Themes на сайте CodePlex. Применить эту тему несложно: перетащите в проект соответствующий файл Theme.xaml и добавьте MergedDictionary к словарю ресурсов приложения в App.xaml:

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

Пользовательские настройки, шифрование и связывание с данными

По умолчанию .NET-проекты позволяют определять пользовательские настройки, открывая файл Settings.Settings, который находится в папке Properties. К сожалению, редактор, применяемый на этапе разработки, несколько ограничен в том, какие типы объектов можно использовать для настроек и как они сохраняются в конфигурационном файле. В случае TweeVo я поддерживаю объект Dictionary — словарь устройств TiVo, индексируемых по уникальному идентификатору каждого устройства. Хранение данных таким способом дает возможность быстро проверять устройства TiVo по мере их обнаружения, не находятся ли они уже в основном списке (master list). Если бы я использовал редактор периода разработки, то не смог бы указать ни тип Dictionary, ни то, что этот объект нужно сериализовать в двоичный код, а не XML.

Для использования собственного класса настроек я создал новый класс, унаследовав его от ApplicationSettingsBase, и назвал его TweeVoSettings. Часть этого класса приведена ниже.

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

Этот класс создает статическое свойство, которое возвращает экземпляр самого себя, поэтому оно очень легко доступно на глобальном уровне. Именно это и сделал бы редактор периода разработки.

TiVos — единственное свойство, требующее особого внимания. Вы увидите, что его тип — Dictionary<string,TiVo> и он дополнен атрибутом:

[SettingsSerializeAs(SettingsSerializeAs.Binary)]

Это приводит к двоичной сериализации свойства, что и требовалось сделать, так как объект Dictionary не поддерживает XML-сериализацию.

Шифрование

Поскольку мы храним некоторые конфиденциальные данные — набор удостоверений для Twitter и пользовательский Media Access Key из TiVo, — эти параметры нужно шифровать перед сохранением в конфигурационный файл. Но для отображения в UI их надо расшифровывать.

Давайте сначала поговорим о методах шифрования. В классе Extensions есть два метода расширения для шифрования и дешифрования строки:

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

Эти методы используют класс ProtectedData, находящийся в пространстве имен System.Security.Cryptography. Данный класс является оболочкой Data Protection API (DPAPI), доступного со времен Windows 2000 и очень простого в использовании, так как у него всего два метода: Protect и Unprotect. Как и следовало бы ожидать, метод Protect шифрует данные, а метод Unprotect расшифровывает. Преобразуя строку в байтовый массив и передавая его этим методам, мы можем транслировать полученный байтовый массив обратно в строку и помещать ее в конфигурационный файл.

Связывание с данными

Связывание с данными можно было бы пропустить и вручную присваивать полям UI расшифрованные значения, но это не позволило бы нам задействовать преимущества привязки данных в WPF. Как бы мы передавали расшифрованные данные из конфигурационного файла UI-элементам? Ответ простой: нужно использовать IValueConverter — интерфейс, который можно реализовать и применять в WPF-механизме связывания с данными для автоматического преобразования одного типа данных в другой. Хороший пример — возможность указывать цвет в XAML строковым именем:

<TextBlock Foreground="White"/>

WPF предоставляет конвертер значений, который автоматически преобразует строку «Red» в соответствующее значение цвета для WPF-элемента управления и наоборот. В нашем случае конвертер значений будет использоваться для шифрования и дешифрования строковых данных с применением методов расширения. Класс EncryptionConverter представлен ниже.

C#

public class EncryptionConverter : IValueConverter
{
    // Дешифрование
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (value as string).DecryptString();
    }
 
    // Шифрование
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (value as string).EncryptString();
    }
}

VB

Public Class EncryptionConverter
    Implements IValueConverter
    ' Дешифрование
    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
 
    ' Шифрование
    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

Нужно реализовать два метода: Convert и ConvertBack. Метод Convert применяется, когда данные передаются UI-элементу, а ConvertBack — когда данные принимаются от UI-элемента. В любом случае для шифрования и дешифрования по мере необходимости мы используем наши простые методы расширения.

Чтобы задействовать этот преобразователь типов, мы должны добавить необходимый код в XAML-файл UI. В разделе ресурсов мы указываем этот преобразователь и назначаем ему ключ:

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

После этого мы можем указывать преобразователь типов по идентификатору ec. Параметр Text для элемента txtMAK (среди прочих) теперь может использовать этот преобразователь:

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

Решив использовать эти значения из класса TweeVoSettings, мы все равно должны самостоятельно вызывать метод расширения DecryptString, так как класс настроек сам хранит данные в зашифрованном виде.

Оповещения TiVo

В приложении необходим объект, представляющий TiVo. Его класс перечисляет несколько свойств, имеющихся у каждого устройства TiVo, и содержит методы, выполняющие операции с оборудованием TiVo. Некоторые из этих свойств определяются в период выполнения прослушиванием пакетов оповещения, которые посылаются всеми устройствами TiVo по сети примерно через каждые 60 секунд для идентификации себя перед другими устройствами TiVo. Мы можем слушать эти пакеты, разбирать их и создавать объекты TiVo, представляющие эти устройства в нашем приложении. Всю эту функциональность инкапсулирует класс TiVoBeaconListener.

Оповещения (beacons) посылаются по UDP в порт 2190. С помощью пространства имен System.Net.Sockets можно взаимодействовать с этим портом на уровне сокета и прослушивать пакеты:

C#

private static void DiscoverTiVos()
{
    MessageBoxResult dr = MessageBoxResult.None;
 
    do
    {
        try
        {
            // Связываем с портом 2190
            _listener = new UdpClient(2190);
        }
        catch(SocketException ex)
        {
            // Эта ошибка указывает, что данный порт прослушивается какой-то другой программой — скорее всего 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);
 
    // Через этот порт принимаем данные с любого IP-адреса
    IPEndPoint ep = new IPEndPoint(IPAddress.Any, 2190);
 
    while(_listener != null)
    {
        // Получаем пакет оповещения
        byte[] bytes = _listener.Receive(ref ep);
        string beacon = Encoding.ASCII.GetString(bytes);
 
        // Разбираем его
        TiVo t = ParseBeacon(beacon);
        // Назначаем IP входящих данных (т. е. IP-адрес TiVo)
        t.IpAddress = ep.Address;
    }
}

VB

Private Shared Sub DiscoverTiVos()
    Dim dr As MessageBoxResult = MessageBoxResult.None
 
    Do
        Try
            ' Связываем с портом 2190
            _listener = New UdpClient(2190)
        Catch ex As SocketException
            ' Эта ошибка указывает, что данный порт прослушивается какой-то другой программой — скорее всего 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
 
    ' Через этот порт принимаем данные с любого IP-адреса
    Dim ep As New IPEndPoint(IPAddress.Any, 2190)
 
    Do While _listener IsNot Nothing
        ' Получаем пакет оповещения
        Dim bytes() As Byte = _listener.Receive(ep)
        Dim beacon As String = Encoding.ASCII.GetString(bytes)
 
        ' Разбираем его
        Dim t As TiVo = ParseBeacon(beacon)
        ' Назначаем IP входящих данных (т. е. IP-адрес TiVo)
        t.IpAddress = ep.Address
    Loop
End Sub

Этот класс создает новый объект UdpClient и связывает его с портом 2190. Затем UdpClient создает конечную точку, которая прослушивает данные с любого IP-адреса, приходящие в этот порт. Далее клиент вызывает метод Receive, который блокируется, пока в указанном порту не появятся какие-либо данные. Как только они появляются, мы преобразуем байты в строку, разбираем ее согласно спецификации оповещений TiVo и создаем новый объект TiVo.

Пакет оповещения TiVo — список строк, разделяемый символами возврата каретки. Каждая строка представляет собой пару «имя-значение» в формате:

[Свойство]=[Значение]

и разбирается, как показано ниже:

C#

private static TiVo ParseBeacon(string beacon)
{
    TiVo t = new TiVo();
 
    // Разбираем пакет оповещения в свойства
    string[] lines = beacon.Split('\n');
    foreach(string line in lines)
    {
        // Разбираем пары "имя-значение"
        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()
 
    ' Разбираем пакет оповещения в свойства
    Dim lines() As String = beacon.Split(ControlChars.Lf)
    For Each line As String In lines
        ' Разбираем пары "имя-значение"
        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

Свойства, которые нас интересуют, — PLATFORM, MACHINE и IDENTITY. Они передаются и помещаются в наш объект TiVo, который потом добавляется в основной Dictionary объектов TiVo и индексируется по полю Identity — уникальному идентификатору каждого устройства TiVo. Если такой TiVo уже присутствует в словаре, он не включается в список.

Список «сейчас воспроизводится»

В каждом устройстве TiVo есть то, что называют Now Playing List (список «сейчас воспроизводится»). Это список записанных шоу, которые в настоящее время хранятся в TiVo. Пользователь обычно обращается к нему из главного меню TiVo, но он также доступен как XML-данные через веб-сервер, выполняемый на каждом устройстве TiVo.

Чтобы извлечь этот документ, вы можете перейти по IP-адресу своего TiVo и следующему пути:

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

Если вы обращаетесь к этому URL через веб-браузер, то получите предложение ввести удостоверения. На каждом TiVo именем пользователя является tivo, а паролем — ваш Media Access Key (MAK). Найти MAK на устройстве TiVo можно следующими командами:

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

Или найти его на веб-сайте TiVo, войдя под своей учетной записью и щелкнув «View Media Access Key» по следующему URL:

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

После ввода этих удостоверений вы увидите XML текущего Now Playing List. Вот сокращенный пример:

<?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>

При обращении по URL, показанному выше, вы получите весь Now Playing List с включенными Suggestions. Suggestions — это записи, сделанные TiVo автоматически на основе анализа ваших предпочтений. TweeVo позволяет помечать Suggestions в исходящей передаче на Twitter, поэтому нам нужно убедиться, что мы отметили эти записи. Самый простой способ сделать это (какой мне удалось найти) — извлекать элементы только из папки Suggestions, используя этот URL:

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

Он эквивалентен контейнеру «NowPlaying/0». Значение 0 — магический идентификатор папки Suggestions. Кому интересно, сообщу, что 1 соответствует магическому идентификатору папки HD Recordings на устройствах S3/HD TiVo.

Получив список записей Suggestions, мы можем сопоставить его элементы в полном Now Playing List с идентификаторами в папке Suggestions и соответственно отметить их.

Получение и разбор XML-данных

Для получения этих данных мы используем объект HttpWebRequest, передаем корректные удостоверения пользователя, а затем считываем данные ответа в объект XDocument, показанный ниже.

C#

private XDocument GetNowPlayingListDocument(string container, bool recurse)
{
    HttpWebRequest request;
    WebResponse response = null;
    XDocument doc;
 
    // Извлекаем 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());
        // Принимаем любой сертификат SSL
        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
 
    ' Извлекаем 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())
        ' Принимаем любой сертификат SSL
        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

Таким методом можно извлекать как полный Now Playing List, так и список Suggestions. Последний преобразуется в простой список целочисленных идентификаторов передач (Program ID):

C#

XNamespace ns = docNowPlaying.Root.Name.Namespace;
 
if(ns == null)
    return null;
 
// Получаем список ProgramId только для 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
 
' Получаем список ProgramId только для 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()

Сначала извлекается пространство имен документа. Оно дописывается к имени каждого элемента, чтобы корректно находить и разбирать элементы.

Полный Now Playing List немного сложнее. С помощью объекта XDocument мы можем разобрать XML-данные в список объектов NPLEntry:

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") &&    // Загруженных видеозаписей TiVo нет
                  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"),
                    // Если идентификатор передачи находится в списке Suggestions, помечаем его как предложение (suggestion)
                    Suggestion = suggestionIds.Exists(s => s == (string)details.Element(ns + "ProgramId"))
               };
 
List<NPLEntry> entries = query.ToList();

VB

Dim query = From entry In docNowPlaying.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 _
            Order By details.Element(ns + "CaptureDate").Value Ascending _
            Select New NPLEntry With {.Title = CStr(details.Element(ns + "Title")), _
            .EpisodeTitle = CStr(details.Element(ns + "EpisodeTitle")), _
            .SourceChannel = CStr(details.Element(ns + "SourceChannel")), _
            .SourceStation = CStr(details.Element(ns + "SourceStation")), _
            .CaptureDate = (details.Element(ns + "CaptureDate").Value).EpochToDateTime().RoundToNearestMinute(), _
            .ProgramID = CStr(details.Element(ns + "ProgramId")), _
            .Suggestion = suggestionIds.Exists(Function(s) s = CStr(details.Element(ns + "ProgramId")))}
                    ' Если идентификатор передачи находится в списке Suggestions, помечаем его как предложение (suggestion)

Dim entries As List(Of NPLEntry) = query.ToList()

Мы извлекаем данные из XML-схемы, которая соответствует указанному запросу, и это гарантирует, что мы получаем действительно записанные передачи, а не загруженные из Интернета видеоролики или другие мультимедийные файлы. Список идентификаторов предложений (Suggestion IDs) используется для установки булева свойства Suggestion в true, если в этом списке обнаружен идентификатор передачи (Program ID). Все эти свойства помещаются в объекты NPLEntry, преобразуются в список и возвращаются вызвавшему коду.

DateTime и Unix-время

Элементу CaptureDate нужно уделить особое внимание. Значение времени, возвращаемое в этом поле, представлено в Unix-формате «временная метка/век» (timestamp/epoch), и его требуется преобразовать в .NET-объект DateTime. Временная метка Unix — это целое число, которое представляет количество секунд, истекших с 1 января 1970 года в формате всемирного координированного времени — UTC (Coordinated Universal Time). Метод EpochToDateTime в классе Extensions, который является .NET-методом расширения, обрабатывает процесс преобразования:

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 создает новый объект DateTime с датой 1/1/1970. Затем используется метод AddSeconds объекта DateTime для добавления значения временной метки Unix. Наконец, вызывается метод ToLocalTime для преобразования этого значения во время в местном часовом поясе.

Класс TiVo содержит два метода — GetNowPlayingListDocument и GetNowPlayingList, — которые берут на себя всю черную работу по получению и разбору XML-данных.

Опрос данных

Класс Poller использует все перечисленное выше для того, чтобы опрашивать каждую выбранную приставку TiVo через каждые 15 минут и получать от нее данные из Now Playing List. Если обнаруживаются новые шоу, они передаются на указанную учетную запись Twitter вместе со ссылкой на Zap2it.com, который возвращает более подробную информацию.

Опросчик использует простой объект Timer, запускающий свой метод каждые 15 минут. Этот метод извлекает Now Playing List и определяет, является ли запись предложением (suggestion). Если поддержка Suggestions в приложении отключена, запись игнорируется. Если данное шоу не относится к предложению или если предложения включены, создается Twitter-строка для загрузки и публикации на Twitter:

C#

foreach(TiVo t in TweeVoSettings.Default.TiVos.Values)
{
    if(t.Active)
    {
        // Пытаемся опрашивать три раза подряд
        while(tries < 3 && !success)
        {
            try
            {
                List<NPLEntry> list = t.GetNowPlayingList();
                if(list != null)
                {
                    foreach(NPLEntry nplEntry in list)
                    {
                        // Если элемент допустим, передаем его на Twitter
                        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(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
        ' Пытаемся опрашивать три раза подряд
        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
                        ' Если элемент допустим, передаем его на Twitter
                        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 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

Метод CreateTwitterString формирует текстовые данные, публикуемые на Twitter вместе со ссылкой на Zap2It. Ссылка на Zap2It имеет такой формат:

http://tvlistings.zap2it.com/tv/x/<ID передачи>

Элемент «x» может принимать фактически что угодно. Если бы вы перешли на веб-сайт Zap2It, то увидели бы, что «x» — это на самом деле заголовок эпизода в формате URL. На момент написания этой статьи, похоже, что этот элемент реально не используется. «ID передачи» — тот же идентификатор передачи, который мы получаем от TiVo, но его нужно дополнить до корректной длины в 14 символов.

Tribune Media Services предоставляет данные о программах телепередач для самых разнообразных устройств и сервисов. Этот сервис присваивает уникальный идентификатор (код) каждой передаче, которая транслировалась в эфире или еще будет транслироваться. Этот код имеет следующий формат:

TTSSSSSSSSEEEE

T

Тип передачи

MV = кино

EP = ТВ-шоу

S

ID сериала

E

Номер эпизода

Например, эпизод «Afternoon Delight» в сериале «Arrested Development» имеет такой идентификатор: EP005984700030. Если его разобрать, получится вот что:

EP

ТВ-передача

00598470

ID сериала

(все эпизоды «Arrested Development» имеют этот ID)

0030

Номер эпизода в сериале

Пройдя по ссылке http://tvlistings.zap2it.com/tv/x/EP005984700030, вы увидите страницу эпизода «Afternoon Delight», а пройдя по ссылке http://tvlistings.zap2it.com/tv/x/EP00598470 — глобальную для всего сериала «Afternoon Delight» страницу.

Эту ссылку можно сократить. Существует уйма сервисов сокращения URL, но самый простой из них — tinyurl.com. У этого сайта крайне простой API: отправьте ему URL через параметр строки запроса, и он вернет единственную строку без всяких дополнений, которая и является сокращенным URL. Это можно сделать одной строкой кода в методе GetShortUrl:

C#

private static string GetShortUrl(string url)
{
    // Получаем сокращенный URL, используя 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
    ' Получаем сокращенный URL, используя tinyurl.com
    Return New WebClient().DownloadString("http://tinyurl.com/api-create.php?url=" & url)
End Function

Остальная часть метода CreateTwitterString формирует строку с помощью метода string.Format, используя ранее созданный URL и некоторые свойства объекта NPLEntry.

Публикация на Twitter

Последняя задача — публикация строки на Twitter. Класс Twitter содержит метод PostTwitterUpdate, применяющий Twitter API для отправки через POST предоставленной строки в блог на Twitter по учетной записи, указанной в TweeVo UI:

C#

public static void PostTwitterUpdate(string tweet)
{
    Logger.Log("Tweet: " + tweet, LoggerSeverity.Info);
 
    // // Преобразуем "твит" (строку, публикуемую на Twitter) в байтовый массив
    byte[] bytes = Encoding.ASCII.GetBytes("source=tweevo&status=" + tweet);
 
    HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://twitter.com/statuses/update.xml");
 
    // Отправляем этот байтовый массив на сервер
    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)
 
    ' Преобразуем "твит" (строку, публикуемую на Twitter) в байтовый массив
    Dim bytes() As Byte = Encoding.ASCII.GetBytes("source=tweevo&status=" & tweet)
 
    Dim request As HttpWebRequest = CType(WebRequest.Create("https://twitter.com/statuses/update.xml"), HttpWebRequest)
 
    ' Отправляем этот байтовый массив на сервер
    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

Этот метод принимает текст «твита» и преобразует его вместе с некоторыми другими связанными параметрами в байтовый массив. Создается новый объект NetworkCredential с заданной комбинацией «имя пользователя — пароль» и отправляется POST-запрос для «обновления» API для Twitter. Байтовый массив передается на сервер в виде кодированного URL, после чего соединение закрывается.

Заключение

Ну вот и все: мы создали простое приложение, которое публикует в блоге на Twitter то, что записывает ваша приставка TiVo. Я планирую обновлять это приложение по мере того, как мне будут предлагать ввести новые функции, исправить найденные ошибки и т.д. И я буду выпускать новые версии, если в том появится необходимость. Наслаждайтесь!

Благодарности

Особые благодарности Bill Pytlovany, Chris Miller и Mark Zaugg за тестирование TweeVo в течение этого (очень далекого от нормального) цикла разработки, а также Joey Buczek за создание значка.

Об авторе

Брайен является Microsoft C# MVP. Занимается активной разработкой для .NET со времен первых бета-версий, выпускавшихся аж в 2000 году, а создает решения с применением технологий и платформ Microsoft еще дольше. Помимо .NET, Брайен особенно хорошо знает языки C, C++ и ассемблеры для процессоров различных архитектур. Он также хорошо разбирается в широком спектре технологий, включая веб-разработку, сканирование документов, ГИС, графику, разработку игр и программирование с применением аппаратных интерфейсов. У него имеется большой опыт в создании приложений для здравоохранения, а также в разработке решений для портативных устройств, таких как планшетные ПК и КПК. Кроме того, Брайен является соавтором книги «Coding4Fun: 10 .NET Programming Projects for Wiimote, YouTube, World of Warcraft, and More», опубликованной издательством O'Reilly. Ранее был соавтором книги «Debugging ASP.NET», выпущенной New Riders. Автор множества статей на MSDN-сайте Coding4Fun.

Comments (0)

Skip to main content