GearsMonkey で Yahoo! カレンダーをオフライン化してみる

会社ではノート PC を使わせてもらっていて、ミーティング中もコード書いたりしてる僕ですが、無線 LAN がないため、超長い LAN ケーブルを持ち歩いています。ミーティングの予定は Yahoo! カレンダーに入れてたりするのですが、連続してあったりすると「次の部屋どこだっけ」「ちょっと待って、LAN につなぐから」なんつってケーブル刺して Y! カレンダー開いて。ああ面倒。

というわけで、Google Gears + Greasemonkey、いわゆる GearsMonkey を使って Y! カレンダーをオフライン化できれば、ケーブルつながなくても予定見られて便利なんじゃないのと思いまして、ちょっとやってみました。まだ全然途中で、エラー処理もなかったりしますが、コード書きながらブラウザが頻繁に落ちる現象に遭遇しましたので、気をつけないといけなかったところを書いておきます。環境は Mac OS X 10.5、Firefox 2.0.0.11、Google Gears 0.2.4.0 です。

コードはこんな感じ。Google Gears の Localserver を使用しています。

JavaScript:
  1. // ==UserScript==
  2. // @name           Yahoo Japan Offline Calendar
  3. // @namespace      http://www.forever5yearsold.net/
  4. // @include        http://calendar.yahoo.co.jp/*
  5. // @include        https://calendar.yahoo.co.jp/*
  6. // ==/UserScript==
  7.  
  8. //console.log(parseInt((new Date).getTime() / 1000));
  9.  
  10. (function() {
  11.     var PERIOD = 7;
  12.     var server = null;
  13.     var store = null;
  14.  
  15.     function initGears() {
  16.         if (!unsafeWindow.google) {
  17.             unsafeWindow.google = {};
  18.         }
  19.         if (!unsafeWindow.google.gears) {
  20.             unsafeWindow.google.gears = {
  21.                 factory: new GearsFactory()
  22.             };
  23.         } try {
  24.             server = unsafeWindow.google.gears.factory.create('beta.localserver', '1.1');
  25.         } catch (e) {
  26.             console.log(e);
  27.         }
  28.     }
  29.  
  30.     function triggerAllowDialog() {
  31.         window.addEventListener('load',
  32.                                 function() {
  33.                                     new GearsFactory().create('beta.localserver', '1.1');
  34.                                     return false;
  35.                                 },
  36.                                 true);
  37.     }
  38.  
  39.     initGears();
  40.     if (!server) {
  41.         triggerAllowDialog();
  42.     }
  43.     var imgRetrieve = document.createElement('img');
  44.     imgRetrieve.setAttribute('src', 'http://www.google.com/reader/ui/off-connected-synced.gif');
  45.     imgRetrieve.setAttribute('width', '11');
  46.     imgRetrieve.setAttribute('height', '11');
  47.     imgRetrieve.setAttribute('alt', '1週間分の予定を取得');
  48.     imgRetrieve.setAttribute('title', '1週間分の予定を取得');
  49.     imgRetrieve.setAttribute('style', 'display: block');
  50.     var imgRetrieving = document.createElement('img');
  51.     imgRetrieving.setAttribute('src', 'http://www.google.com/reader/ui/off-connected-syncing.gif');
  52.     imgRetrieving.setAttribute('width', '11');
  53.     imgRetrieving.setAttribute('height', '11');
  54.     imgRetrieving.setAttribute('alt', '1週間分の予定を取得中');
  55.     imgRetrieving.setAttribute('title', '1週間分の予定を取得中');
  56.     imgRetrieving.setAttribute('style', 'display: none');
  57.     var divRetrieve = document.createElement('div');
  58.     divRetrieve.setAttribute('id', 'yjcal_offline_retrieve');
  59.     divRetrieve.setAttribute('style', 'position: fixed; top: 3px; right: 3px');
  60.     divRetrieve.addEventListener('click',
  61.                                  function() {
  62.                                      imgRetrieve.setAttribute('style', 'display: none');
  63.                                      imgRetrieving.setAttribute('style', 'display: block');
  64.                                      server.removeStore('yjcal_offline');
  65.                                      store = server.createStore('yjcal_offline');
  66.                                      var today = new Date;
  67.                                      var now = parseInt(today.getTime() / 1000);
  68.                                      // 今から 1 週間分取得
  69.                                      var filesToCapture = new Array(PERIOD);
  70.                                      var fileNames = new Array();
  71.                                      for (var i = 0; i <filesToCapture.length; i++) {
  72.                                          filesToCapture[i] = '?v=0&t=' + (now + ((60 * 60 * 24) * i));
  73.                                          date = new Date(1900 + today.getYear(), today.getMonth(), today.getDate() + i);
  74.                                          y = (date.getYear() + 1900).toString();
  75.                                          m = date.getMonth() + 1;
  76.                                          m = m <10 ? '0' + m : m;
  77.                                          d = date.getDate();
  78.                                          d = d <10 ? '0' + d : d;
  79.                                          fileNames[filesToCapture[i]] = y + m + d + '.html';
  80.                                      }
  81.                                      var i = 0;
  82.                                      store.capture(filesToCapture, function(url, success, captureId) {
  83.                                          store.rename(url, location.protocol + '//' + location.hostname + '/' + fileNames[url]);
  84.                                          i++;
  85.                                          if (i == PERIOD - 1) {
  86.                                              imgRetrieve.setAttribute('style', 'display: block');
  87.                                              imgRetrieving.setAttribute('style', 'display: none');
  88.                                          }
  89.                                      });
  90.                                  },
  91.                                  true);
  92.     divRetrieve.appendChild(imgRetrieve);
  93.     divRetrieve.appendChild(imgRetrieving);
  94.     document.body.appendChild(divRetrieve);
  95. })();

Google Gears を初期化するあたりは Wikipedia をオフライン化するページを参考にしました。ほとんどそのままですが。

まず引っかかったのは 82 行目の store.capture() しているところ。上のコードだとデータ取得用のボタンがクリックされたイベントを受けて実行していますが、例えば以下のコードを 43 行目に挿入して実行すると、ブラウザが落ちます。ちなみに filesToCapture を空の配列にしても落ちます。

JavaScript:
  1. // 以下は失敗(クラッシュ)する
  2. // sa: http://www.lisbakken.com/gears/repro_gears/GreaseMonkey/badCapture/testcapture.html
  3. var filesToCapture = [
  4.     location.pathname
  5. ];
  6. store.capture(filesToCapture, function(url, success, captureId) {
  7.  
  8. });

コメント行のページにあるように、イベントハンドラで ResourceStore の capture() を実行しないとダメなようです。

次、83 行目で取得したページを YYYYMMDD.html な名前で保存していっています。Y! カレンダーはパーマリンクがないようなので、capture() したデータを日付名のファイルにリネームすることで、http://calendar.yahoo.co.jp/20080120.html のように、見たい日付を指定すればファイルを開けます。リネーム先が同じドメインであれば、URL で指定することでドメイン化のどこでも保存できるようです。ドメインが違えばブラウザが落ちます。

というように、動くべきコードを書けば動きますが、動かないコードを書けばガンガン落ちます。もうちょっと耐えてくれよって感じが猛烈にするので、動くととってもうれしいです。

簡単にまとめると、ここまでで気をつけないといけなかったのは、

  • ResourceStore::capture() はイベントハンドラ内で実行すること
  • ResourceStore::rename() の変更先を URL で指定する場合は同ドメインにすること

でした。

会社の人に意見を聞いてもうちょっと作り込もうと思います。


About this entry