搭配 YouTube Data API 開發 User Control:YouTube 影片讀取器

搭配 YouTube Data API 開發 User Control:YouTube 影片讀取器

YouTube Data API (以下簡稱 Data API) 是 YouTube APIs and Tools 裡的核心服務,用它開發可以做到大部分使用者在 YouTube 網站上能做的事。換句話說,想要在自己的網站上整合 YouTube 相關應用已經不再侷限於內嵌影片供觀看而已,現在可以開發更具互動性的應用,例如搜尋並取回相關饋送影片,若進一步提供身分驗證之後,還可以做到回應評論、加入標籤、上傳影片、修改撥放清單…等動作。

開發目標

儘管 Data API 全面開放多種互動功能,本篇內容先放在介紹讀取影片饋送資料 (Video Feeds),藉此熟悉與 Data API 互動的方式,未來有機會再考慮更進階的應用。既然如此,來寫一個 YouTube 影片讀取器,希望可以取得我們上傳的影片,由於預料到會有改版需求,因此得考慮將資料與呈現分離,一連串的動作最好包成一個 User Control,方便日後使用。

準備工作

取回影片饋送資料的方式就是對 Data API 送出請求,路徑會根據回應類型有所不同,可以參考:Developer's Guide: Data API Protocol – Video Feed Types,此外 Data API 可以接受查詢參數讓使用者微調回傳的結果,說明文件在:Developer's Guide: Data API Protocol – API Query Parameters,雖然目前尚未中文化,不過有心想要研究的人以上兩篇算是必讀吧…,假如一堆文字實在無法引起你的興趣,有一段說明如何送出搜尋的影片:YouTube APIs: Search Explained,對常用的用法做了重點介紹。

另外官方還提供一個網頁:Interactive YouTube API Demo Beta,類似查詢產生器的頁面,可以用設定的方式產出一組 Resulting URI(即 request URL),跟前面提到的文件對照使用可以幫助理解,比方說參考下圖設定,這是取回使用者上傳影片最簡單的方式(Step 1 - 4):

yt - request url setting 

有個小建議,不管你怎麼設定最終都要點 Submit 測試一下 (Step 4),若這組 URL 可以被 Data API 接受,就會收到如下圖的回應:

yt - request url set - 5

基本測試

由於剛剛設定輸出格式為 RSS 2.0,標準輸出會類似這樣:
<!-- 資料來源:W3C School - RSS Sample -->
<rss version="2.0">
  <channel>
    <title>W3Schools Home Page</title>
    <link>http://www.w3schools.com</link>
    <description>Free web building tutorials</description>
    <item>
      <title>RSS Tutorial</title>
      <link>http://www.w3schools.com/rss</link>
      <description>New RSS tutorial on W3Schools</description>
    </item>
    <item>
      <title>XML Tutorial</title>
      <link>http://www.w3schools.com/xml</link>
      <description>New XML tutorial on W3Schools</description>
    </item>
  </channel>
</rss>

以 Data API 真正回傳的 XML 檔來說,影片資料是包在 <description>…</description> 節點底下,內文是一串 HTML 標記,為確保後續開發工作能順利進行,先來簡單寫個測試程式,新增一支 aspx 內容如下:
<div>
    <asp:XmlDataSource 
        ID="XmlDataSource1" 
        runat="server" 
        DataFile="http://gdata.youtube.com/feeds/base/users/{userId}/uploads?max-results=5&alt=rss"
        XPath="/rss/channel/item"></asp:XmlDataSource>
    <asp:Repeater ID="Repeater1" runat="server" DataSourceID="XmlDataSource1">
        <ItemTemplate>
            <asp:Literal ID="content" runat="server" Text='<%# XPath("description") %>'></asp:Literal>
        </ItemTemplate>
    </asp:Repeater>
</div>
</form>

XmlDataSource 中的 XPath 運算式指定為 "/rss/channel/item",先篩選出所有 item 節點內容,再將 Repeater 樣板中的 Literal.Text 屬性繫結到 XPath("description") 運算式,底下為執行效果:

yt - test 

由測試雛型看來,我們已經可以順利跟 Data API 介接,可惜離完成還有一段距離,有幾個缺點必須改進:
  1. 接收到的 HTML,參雜了 Inline CSS,不利版面調整。
  2. 程式邏輯未元件化,缺乏重用性。
  3. 查詢參數若需變動,必須針對 XmlDatasource.DataFile 屬性異動,不利快速切換。

特別是第 1 點,非常不利未來版面更動,要改善勢必要對 HTML 做拆解…,此時我想到了小朱大大曾經介紹過的:HTML Agility Pack:簡單好用的快速 HTML Parser,當初拜讀完也還未曾用上,趁這次的機會用它來試試看吧!

拆解資料

看過朱大的教學文,發現這元件拆解 HTML 是用 XPath 運算式,雖然 HTML Agility Pack 本身有提供 HAP Explorer 程式輔助取得 XPath 語法,可惜不怎麼好用,如果大家有同樣的困擾,我推薦 XML Notepad 2007 這套工具,點部落另一位大大:星寂有介紹過(請看:Tools – XML Notepad);因為先天上 HTML 規範本身並不如 XML 來得嚴謹,因此借用 XML Notepad 巡覽 HTML 文件有一個小"眉角",你必須自行確認 HTML 內容所有標籤都已滿足閉合規則起始標籤一定要伴隨成對的結束標籤,空元素要在右角括號前面加一個斜線,如 <example />),否則嘗試用 XML Notepad 開啟會收到類似這樣的錯誤:

yt- xml load error 

遵照閉合規則修正即可,至於詳細取得 XPath 運算式過程就不提了,請自行參考星寂大大寫的介紹文,總之目的是取得影片連結、縮圖路徑、影片標題、觀看次數、時間長度等資料,轉存到 DataTable 裡,重點程式碼如下:
{
    string userId = "userId";     // 置換有效帳號
    int numberOfVideos = 5;         // 限定影片數量

    // 載入遠端 XML
    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.Load(
        String.Format(
            "http://gdata.youtube.com/feeds/base/users/{0}/uploads?max-results={1}&alt=rss",
            userId,
            numberOfVideos));

    // 取得節點清單
    XmlNodeList nodeList = xmlDoc.SelectNodes("/rss/channel/item/description");

    DataTable dt = new DataTable();
    dt.Columns.Add("vLink");        // 影片連結
    dt.Columns.Add("ImageUrl");     // 縮圖路徑
    dt.Columns.Add("Title");        // 影片標題
    dt.Columns.Add("Views");        // 觀看次數
    dt.Columns.Add("Time");         // 時間長度

    HtmlAgilityPack.HtmlDocument htmlDoc = new HtmlAgilityPack.HtmlDocument();
    string prefix = "/div/table/tbody";

    // 逐一拆解資料
    foreach (XmlNode node in nodeList)
    {
        htmlDoc.LoadHtml(node.InnerText);

        string vLink =
            htmlDoc.DocumentNode.SelectSingleNode(prefix + "/tr[1]/td[2]/div[1]/a").GetAttributeValue("href", "");
        string imageUrl =
            htmlDoc.DocumentNode.SelectSingleNode(prefix + "/tr[1]/td[1]/div/a/img").GetAttributeValue("src", "");
        string title =
            htmlDoc.DocumentNode.SelectSingleNode(prefix + "/tr[1]/td[2]/div[1]/a/text()").InnerText;
        string views =
            htmlDoc.DocumentNode.SelectSingleNode(prefix + "/tr[1]/td[3]/div[2]/text()").InnerText;
        string time =
            htmlDoc.DocumentNode.SelectSingleNode(prefix + "/tr[2]/td[1]/span[2]/text()").InnerText;

        DataRow dataRow = dt.NewRow();
        dataRow[0] = vLink;
        dataRow[1] = imageUrl;
        dataRow[2] = title;
        dataRow[3] = views;
        dataRow[4] = time;

        dt.Rows.Add(dataRow);
    }

    RepYouTube.DataSource = dt;
    RepYouTube.DataBind();

    nodeList = null;
    htmlDoc = null;
}

利用 HTML Agility Pack 拆解果然省事,不一會兒功夫就抓出需要的資料,再搭配資料繫結的技巧,基本上要怎麼呈現只是樣版在抽換而已。

封裝控件

前面提到的缺點目前暫時排除了第一項,為了提高可重用性及易用性,還必須做點努力。以下看圖說故事:

yt_reader_sections

假設呈現方式以每支影片為一個單位,每個單位還細分為三個區塊,分別為: - 縮圖區塊、 - 標題區塊、 - 描述區塊,除了每個單位可以給定 CssClass 屬性控制最外層 CSS 以外,每個區塊也有相對應的 ThumbnailCssClass、TitleCssClass、DescriptionCssClass 屬性可以進一步設定包含範圍的 CSS,加上 UserId、NumberOfVideos 兩個控制 Data API 要求路徑的屬性,總共要公開的屬性可以參考底下程式碼:
private string cssClass = null;

/// <summary>
/// 取得或設定 YouTube 影片讀取器所呈現的階層樣式表 (CSS)
/// </summary>
public string CssClass
{
    get { return cssClass; }
    set { cssClass = value; }
}

private string thumbnailCssClass = null;

/// <summary>
/// 取得或設定值,代表 YouTube 影片縮圖所呈現的階層樣式表 (CSS)
/// </summary>
public string ThumbnailCssClass
{
    get { return thumbnailCssClass; }
    set { thumbnailCssClass = value; }
}

private string titleCssClass = null;

/// <summary>
/// 取得或設定值,表示 YouTube 影片標題所呈現的階層樣式表 (CSS)
/// </summary>
public string TitleCssClass
{
    get { return titleCssClass; }
    set { titleCssClass = value; }
}

private string descriptionCssClass = null;

/// <summary>
/// 取得或設定值,表示 YouTube 影片描述所呈現的階層樣式表 (CSS)
/// </summary>
public string DescriptionCssClass
{
    get { return descriptionCssClass; }
    set { descriptionCssClass = value; }
}

private string userId = "userId";     // 置換有效帳號

/// <summary>
/// 取得或設定值,表示 YouTube 影片上傳用戶 ID
/// </summary>
public string UserId
{
    get
    {
        return userId;
    }
    set
    {
        userId = value;
    }
}

private int numberOfVideos = 5;

/// <summary>
/// 取得或設定值,表示要顯示的 YouTube 影片數量
/// </summary>
public int NumberOfVideos
{
    get { return numberOfVideos; }
    set { numberOfVideos = value; }
}

此時原本 Page_Load 事件底下前兩行程式碼要拿掉,改用 UserId、NumberOfVideos 屬性餵值,這麼一來透過設定的方式即可快速切換內容,最後整個改寫成 User Control 之後,使用範例類似這樣:
<%@ Register Src="UserCtrls/YoutubeReader.ascx" TagName="YoutubeReader" TagPrefix="uc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>YouTube 影片讀取器 Demo</title>
    <style type="text/css">
        .ytr
        {
            color: #000000;
            font-family: Arial, Helvetica, sans-serif;
            font-size: 12px;
            width: 300px;
        }
        .ytr-thumbnail
        {
            border: 3px double gray;
            width: 80px;
        }
        .ytr-title
        {
            font-size: 14px;
            font-weight: bold;
        }
        .ytr-description
        {
            color: #666666;
            font-size: 11px;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h3>
            YoutubeReader 使用實例:
        </h3>
        <uc1:YoutubeReader ID="YoutubeReader1" runat="server" 
            UserId="{userId}" 
            NumberOfVideos="3"
            CssClass="ytr" 
            TitleCssClass="ytr-title" 
            ThumbnailCssClass="ytr-thumbnail" 
            DescriptionCssClass="ytr-description" />
        <hr />
    </div>
    </form>
</body>
</html>

呈現的效果:

ytr-example

結語

跨系統整合一向都不是件輕鬆的事,尤其常常得要融合多項技術才能順利完成,好比本篇文章所載內容是我在開發實際需求的完整經歷,其中耗費在解決衍生問題的時間反而比較多…,好在一分耕耘一分收穫,以往對於類似的 XML 應用其實底子很薄弱,這次之後累積了不少經驗值,算是最值得慶幸的地方!

回過頭來檢視這個 YouTubeReader,個人認為還有改進空間,特別是拆解 HTML 那一段存在滿大的隱憂,由於 XPath 運算式都是寫死的,萬一 HTML 內容(結構)變更了就得回頭來修正…,雖然心裡大概有個解法,不過仍屬構想階段(事實上是很多細節仍未研究透徹XD),況且短期間內目前作法應該還能擋一陣子,就讓我苟且一下兼且賣個關子當做之後的發文素材吧!


範例下載



參考資料