30分で作るJetpack Feature (2) 一から作ろうとせずに既にあるソースからインスパイア

前の日記の続き
APIの使い方がちょっとわからなくてRSS使ったけど、API使うべきだった。
そしたらcategory_idを決め打ちしなくて済んだ。



(準備)試し試しやるために

  • about:jetpackのDevelopを使って、ソースを貼りつけた後「try out this code」を繰り返せば試せる

参考にするソース

  • Demoで使われているVideo Slide
jetpack.future.import("slideBar");
jetpack.slideBar.append({
  // Listen for click events on the icon
  onReady: function(slide) $(slide.icon).click(function() {
    let hasVideos = function() $(slide.doc).find("div.video").length > 1;

    // Find video elements to put in the SlideBar
    $(jetpack.tabs.focused.contentDocument).find("video, embed").each(function() {
      let paused = this.paused;

      // Move the video to the slidebar
      let video = $(slide.doc).find("proto > .video").clone();
      video.appendTo(slide.doc.body).prepend(this);

      // Add a control to remove the video
      video.find(".remove").click(function() {
        video.remove();
        // No more videos? Hide the slidebar
        if (!hasVideos())
          slide();
      });

      // Show built-in firefox video controls; we lose the site's custom ones
      this.controls = true;

      // Resume playing for seamless import
      if (!paused && this.play)
        this.play();
    });

    // Slide open to show videos, but only stay open if we have videos
    slide({ size: 206, persist: hasVideos() });
  }),
  
  width: 206,

  html: <>
    <style><![CDATA[
      body { margin: 0; }
      video, embed { max-height: 150px; width: 200px; }
      help { font-style: italic; }
      proto { display: none; }
      div.video { margin: 3px; min-height: 2em; position: relative; }
      div.video div.remove { background: #000; color: #fff; height: 1em; left: 3px; position: absolute; top: 3px; visibility: hidden; width: 1em; }
      div.video:hover div.remove { border: 1px solid #fff; -moz-border-radius: 1em; opacity: .6; text-align: center; visibility: visible; }
      div.video div.remove:hover { cursor: pointer; opacity: .8; }
    ]]></style>
    <body>
      <help>click icon when viewing videos</help>
      <proto>
        <div class="video">
          <div class="remove">X</div>
        </div>
      </proto>
    </body>
  </>
});
  1. スライドバーのアイコンがクリックされたら、
  2. 現在開いてるタブのHTMLからvideoタグまたはembedタグの集合を探し、
  3. slidebar用に用意してあるHTMLにそれらを加え、
  4. 動画を消す用のボタンにclickイベントを付け、
  5. 全ての動画タグの集合の処理を終えたら、
  6. slide(options)を使ってユーザが明示的にslidebarを閉じるようにするかどうか決める

作るものを決める

作る前にビデオ共有のRSSと動画の関係を調べる

  • 動画を再生するためのにはuidとcatalogIdとcategoryIdが必要
    • RSSのURLの「uid_<数字10桁>」の部分がuid
    • catalogIdはRSS内の一つ一つのrdf:about属性から取得できる
    • categoryIdがどうしようもない。仕方ないので動画を見て調べてみるとどうやら「エンターテイメント」を表す「19」のようなので決め打ちにする
  • 上のことを考慮してできあがるは以下のもの

<embed type="application/x-shockwave-flash" allowscriptaccess="always" wmode="transparent" src="http://dl.video.nifty.com/player.swf?dom=dl.video.nifty.com&amp;user_id=%%UID%%&amp;catalog_id=%%CATALOG_ID%%&amp;category_id=19"/>

    • %%UID%%%%CATALOG_ID%%をそれぞれuidとcatalogIdに置換すれば良い

元のソースを参考にしながらアルゴリズムを考える

  • RSSのURLからuidを抜き出す
    • uidが取得できなければ何もしない
var rssUrl = "http://video.nifty.com/cs/catalog/video_metadata/mylstrss/uid_0000012221/pgcnf_3/1.rdf";
var uid = (/uid_([0-9]{10})/.exec(rssUrl)) ? RegExp.$1 : '';
if (! uid) return;  // uidが取れなければ抜ける
  • slidebarのiconをクリックしたときにRSSを取得する
$(slide.icon).click(function() {
  $.get(rssUrl, function(xml){   // RSSを取得。xmlはRSSのdocument
    // RSSの処理
  });
}
  • RSS内にある全てのについて処理
$("item", xml).each(function() {
  var $item = $(this);   // e.g. <item rdf:about="http://video.nifty.com/cs/catalog/video_metadata/catalog_090707216609_1.htm"> ... </item>});
  // 以降$itemについて処理
});
  • の「リンク」部分からcatalogIdを抜き出す
var regExpForCatalogId = /catalog_([0-9]+)_/;   // e.g. <item rdf:about="http://video.nifty.com/cs/catalog/video_metadata/catalog_090707216609_1.htm">
var catalogId = (regExpForCatalogId.exec($item.attr('rdf:about'))) ? RegExp.$1 : '';
if (! catalogId) return true;   // catalogIdが取得できなければ次の<item>を処理する
  • uidとcatalogIdを元にを作りだし、slidebarのHTMLに付け加える
var embed_tmpl = '<embed type="application/x-shockwave-flash" allowscriptaccess="always" wmode="transparent" src="http://dl.video.nifty.com/player.swf?dom=dl.video.nifty.com&amp;user_id=%%UID%%&amp;catalog_id=%%CATALOG_ID%%&category_id=19"/>';
embed_tmpl.replace('%%UID%%', uid).replace('%%CATALOG_ID%%', catalogId)
var $video = $(slide.doc).find("proto > .video").clone();
$video
  .appendTo(slide.doc.body)
  .prepend(embed_tmpl.replace('%%UID%%', uid).replace('%%CATALOG_ID%%', catalogId));
ソース(動画リストを表示するところまで)
(function(){
jetpack.future.import("slideBar");

var rssUrl = "http://video.nifty.com/cs/catalog/video_metadata/mylstrss/uid_0000012221/pgcnf_3/1.rdf";
var uid = (/uid_([0-9]{10})/.exec(rssUrl)) ? RegExp.$1 : '';

if (! uid) return;  // uidが取れなければ抜ける

var embed_tmpl = '<embed type="application/x-shockwave-flash" allowscriptaccess="always" wmode="transparent" src="http://dl.video.nifty.com/player.swf?dom=dl.video.nifty.com&amp;user_id=%%UID%%&amp;catalog_id=%%CATALOG_ID%%&category_id=19"/>';
var regExpForCatalogId = /catalog_([0-9]+)_/;   // e.g. <item rdf:about="http://video.nifty.com/cs/catalog/video_metadata/catalog_090707216609_1.htm">

jetpack.slideBar.append({
  onReady: function(slide) {
    let hasVideos = function() $(slide.doc).find("div.video").length > 1; // iconがクリックされる度に定義する必要もなさそうなので外に出した

    $(slide.icon).click(function() {
      $.get(rssUrl, function(xml){   // RSSを取得。xmlはRSSのdocument
        $("item", xml).each(function() {
          var $item = $(this);   // e.g. <item rdf:about="http://video.nifty.com/cs/catalog/video_metadata/catalog_090707216609_1.htm"> ... </item>

          var catalogId = (regExpForCatalogId.exec($item.attr('rdf:about'))) ? RegExp.$1 : '';
          if (! catalogId) return true;   // catalogIdが取得できなければ次の<item>を処理する

          // Move the video to the slidebar
          var $video = $(slide.doc).find("proto > .video").clone();
          $video
            .appendTo(slide.doc.body)
            .prepend(embed_tmpl.replace('%%UID%%', uid).replace('%%CATALOG_ID%%', catalogId));

          // Add a control to remove the video
          $video.find(".remove").click(function() {
            $video.remove();
            // No more videos? Hide the slidebar
            if (!hasVideos())
              slide();
          });
        });
        // Slide open to show videos, but only stay open if we have videos
        slide({ size: 206, persist: hasVideos() });
      });
    });
  },
  
  width: 206,

  html: <>
    <style><![CDATA[
      body { margin: 0; }
      video, embed { max-height: 150px; width: 200px; }
      proto { display: none; }
      div.video { margin: 3px; min-height: 2em; position: relative; }
      div.video div.remove { background: #000; color: #fff; height: 1em; left: 3px; position: absolute; top: 3px; visibility: hidden; width: 1em; }
      div.video:hover div.remove { border: 1px solid #fff; -moz-border-radius: 1em; opacity: .6; text-align: center; visibility: visible; }
      div.video div.remove:hover { cursor: pointer; opacity: .8; }
    ]]></style>
    <body>
      <proto>
        <div class="video">
          <div class="remove">X</div>
        </div>
      </proto>
    </body>
  </>
});

})()

タイトルとリンクを付けてみる

  • 動画だけだと何かわからないので、せっかくだからタイトルと、それをクリックしたら動画ページに飛ぶリンクを付ける
  • RSSの<title>を抜き出し、<span class="title">タイトル</span>を$videoに付け加える
var title = $item.find('title').text();
var $title = $('<span/>').text(title).attr('class', 'title'); // <span class="title">タイトル</span>
$video
  .append($title);
  • RSSの<link>を抜き出し、タイトルをクリックした時に新しいタブでlinkを開くようにjetpack.tabs.open(link)を使う
var link = $item.find('link').text();
$video.find('.title').click(function(){
  jetpack.tabs.open(link);
  jetpack.tabs[ jetpack.tabs.length-1 ].focus();
});
  • ついでにiconをビデオ共有のものに変える
icon: "http://video.nifty.com/favicon.ico",
ソース(タイトルとリンクとアイコンの追加)
(function(){
jetpack.future.import("slideBar");

var rssUrl = "http://video.nifty.com/cs/catalog/video_metadata/mylstrss/uid_0000012221/pgcnf_3/1.rdf";
var uid = (/uid_([0-9]{10})/.exec(rssUrl)) ? RegExp.$1 : '';

if (! uid) return;  // uidが取れなければ抜ける

var embed_tmpl = '<embed type="application/x-shockwave-flash" allowscriptaccess="always" wmode="transparent" src="http://dl.video.nifty.com/player.swf?dom=dl.video.nifty.com&amp;user_id=%%UID%%&amp;catalog_id=%%CATALOG_ID%%&category_id=19"/>';
var regExpForCatalogId = /catalog_([0-9]+)_/;   // e.g. <item rdf:about="http://video.nifty.com/cs/catalog/video_metadata/catalog_090707216609_1.htm">

jetpack.slideBar.append({
  icon: "http://video.nifty.com/favicon.ico",
  onReady: function(slide) {
    let hasVideos = function() $(slide.doc).find("div.video").length > 1; // iconがクリックされる度に定義する必要もなさそうなので外に出した

    $(slide.icon).click(function() {
      $.get(rssUrl, function(xml){   // RSSを取得。xmlはRSSのdocument
        $("item", xml).each(function() {
          var $item = $(this);   // e.g. <item rdf:about="http://video.nifty.com/cs/catalog/video_metadata/catalog_090707216609_1.htm"> ... </item>

          var catalogId = (regExpForCatalogId.exec($item.attr('rdf:about'))) ? RegExp.$1 : '';
          if (! catalogId) return true;   // catalogIdが取得できなければ次の<item>を処理する

          var link = $item.find('link').text();
          var title = $item.find('title').text();
          var $title = $('<span/>').text(title).attr('class', 'title'); // <span class="title">タイトル</span>
            
          // Move the video to the slidebar
          var $video = $(slide.doc).find("proto > .video").clone();
          $video
            .appendTo(slide.doc.body)
            .prepend(embed_tmpl.replace('%%UID%%', uid).replace('%%CATALOG_ID%%', catalogId))
            .append($title);

          // Add a control to remove the video
          $video.find(".remove").click(function() {
            $video.remove();
            // No more videos? Hide the slidebar
            if (!hasVideos())
              slide();
          });
          $video.find('.title').click(function(){
            jetpack.tabs.open(link);
            jetpack.tabs[ jetpack.tabs.length-1 ].focus();
          });
            
        });

        // Slide open to show videos, but only stay open if we have videos
        slide({ size: 206, persist: hasVideos() });
      });
    });
  },
  
  width: 206,

  html: <>
    <style><![CDATA[
      body { margin: 0; }
      video, embed { max-height: 150px; width: 200px; }
      proto { display: none; }
      div.video { margin: 3px; min-height: 2em; position: relative; }
      div.video div.remove { background: #000; color: #fff; height: 1em; left: 3px; position: absolute; top: 3px; visibility: hidden; width: 1em; }
      div.video:hover div.remove { border: 1px solid #fff; -moz-border-radius: 1em; opacity: .6; text-align: center; visibility: visible; }
      div.video div.remove:hover { cursor: pointer; opacity: .8; }
    ]]></style>
    <body>
      <proto>
        <div class="video">
          <div class="remove">X</div>
        </div>
      </proto>
    </body>
  </>
});

})()

一度取得した動画を再度取得したくない

  • 今のままだとiconをクリックするたびに同じ動画を取得しに行ってしまう
    • 一度取得したリンクを保持しておき、既に取得していれば取りに行かないようにする。以下のようなリンクのURLをキーにしたハッシュがあれば良さそう
var isLoaded = {
  'http://example.com/1': 1,
  'http://example.com/2': 1
}
console.log(isLoaded['http://example.com/1']); // 真になる
console.log(isLoaded['http://example.com/99']); // 偽になる
  • 再起動後でも有効にしたいのでjetpack.storage.simpleを使う
jetpack.future.import("storage.simple");
  • storageからハッシュを取得し、linkを既に取得していれば何もしない、取得していなければハッシュに追加
var videoList = 'videoList';     // videoListという名前でデータをstorageに保存
jetpack.storage.simple.get(videoList, function(data, value) { // valueがハッシュ
  isLoaded = value;

  var link = $item.find('link').text();
  if (isLoaded && isLoaded[link]) {    // linkを既に持っていたら何もしない
    return;
  }
  else {            // 持ってなければリストに追加
    isLoaded[link] = 1;
    jetpack.storage.simple.set(videoList, isLoaded);
  }
});
ソース(二重に動画を取りにいかないようにする)
(function(){
jetpack.future.import("slideBar");
jetpack.future.import("storage.simple");

var videoList = 'videoList';     // videoListという名前でデータを保存
var rssUrl = "http://video.nifty.com/cs/catalog/video_metadata/mylstrss/uid_0000012221/pgcnf_3/1.rdf";
var uid = (/uid_([0-9]{10})/.exec(rssUrl)) ? RegExp.$1 : '';

if (! uid) return;  // uidが取れなければ抜ける

var embed_tmpl = '<embed type="application/x-shockwave-flash" allowscriptaccess="always" wmode="transparent" src="http://dl.video.nifty.com/player.swf?dom=dl.video.nifty.com&amp;user_id=%%UID%%&amp;catalog_id=%%CATALOG_ID%%&category_id=19"/>';
var regExpForCatalogId = /catalog_([0-9]+)_/;   // e.g. <item rdf:about="http://video.nifty.com/cs/catalog/video_metadata/catalog_090707216609_1.htm">

jetpack.slideBar.append({
  icon: "http://video.nifty.com/favicon.ico",
  onReady: function(slide) {
    let hasVideos = function() $(slide.doc).find("div.video").length > 1; // iconがクリックされる度に定義する必要もなさそうなので外に出した

    $(slide.icon).click(function() {
      $.get(rssUrl, function(xml){   // RSSを取得。xmlはRSSのdocument
        jetpack.storage.simple.get(videoList, function(data, value) { // valueがハッシュ
          var isLoaded = value || {};


          $("item", xml).each(function() {
            var $item = $(this);   // e.g. <item rdf:about="http://video.nifty.com/cs/catalog/video_metadata/catalog_090707216609_1.htm"> ... </item>

            var catalogId = (regExpForCatalogId.exec($item.attr('rdf:about'))) ? RegExp.$1 : '';
            if (! catalogId) return true;   // catalogIdが取得できなければ次の<item>を処理する

            var link = $item.find('link').text();
            if (isLoaded && isLoaded[link]) {    // linkを既に持っていたら何もせず次の<item>を処理する
              return true;
            }
            else {            // 持ってなければリストに追加
              isLoaded[link] = 1;
              jetpack.storage.simple.set(videoList, isLoaded);
            }
            
            var title = $item.find('title').text();
            var $title = $('<span/>').text(title).attr('class', 'title'); // <span class="title">タイトル</span>
            
            // Move the video to the slidebar
            var $video = $(slide.doc).find("proto > .video").clone();
            $video
              .appendTo(slide.doc.body)
              .prepend(embed_tmpl.replace('%%UID%%', uid).replace('%%CATALOG_ID%%', catalogId))
              .append($title);

            // Add a control to remove the video
            $video.find(".remove").click(function() {
            $video.remove();
              // No more videos? Hide the slidebar
              if (!hasVideos())
                slide();
            });
            $video.find('.title').click(function(){
              jetpack.tabs.open(link);
              jetpack.tabs[ jetpack.tabs.length-1 ].focus();
            });
            
          });

          // Slide open to show videos, but only stay open if we have videos
          slide({ size: 206, persist: hasVideos() });
        });
      });
    });
  },
  
  width: 206,

  html: <>
    <style><![CDATA[
      body { margin: 0; }
      video, embed { max-height: 150px; width: 200px; }
      proto { display: none; }
      div.video { margin: 3px; min-height: 2em; position: relative; }
      div.video div.remove { background: #000; color: #fff; height: 1em; left: 3px; position: absolute; top: 3px; visibility: hidden; width: 1em; }
      div.video:hover div.remove { border: 1px solid #fff; -moz-border-radius: 1em; opacity: .6; text-align: center; visibility: visible; }
      div.video div.remove:hover { cursor: pointer; opacity: .8; }
    ]]></style>
    <body>
      <proto>
        <div class="video">
          <div class="remove">X</div>
        </div>
      </proto>
    </body>
  </>
});

})()