WidgetでAmazon署名対応するには?

AmazonのWeb API(Product Advertising API)は8/15までに署名付きリクエストに対応しないといけないんだけど、Webアプリ系は対応したもののWidgetの対応がまだ残っていて一ヶ月切ったけどまだ終わらないぜ。

  1. forumにjavascriptのサンプル載っけてる人がいたので参考にしてみるか
  2. よし、Yahoo Widgetで署名付きリクエスト送信できたぜ!
  3. しかし、WidgetのJavascriptだと秘密キー全開だどうしよう。。。
  4. うーん、Google App Engineで署名つけてリダイレクトするサービスつくってみるか
  5. よっしゃ、Google App Engine経由でも動くようになった。俺天才!(ここまでMacで開発)
  6. Windowsで実行したら文字化けした・・・POSTで飛ばさないけないのか・・・文字化けについて
  7. 「青春ってこんなに険しいんですか?」←イマココ

ちなみに秘密キーをソフトウェアに含んでリリースすることに関してはこちらにAmazonの見解が載っていたりする。

秘密キーまたはパスワードの「公開」の範囲について
勝手にまとめると、

  • 推奨しないけど全部禁止にするのもかわいそうだから含んでリリースしてもいいよ
  • ただ秘密キーは簡単に取れないようにしときなさいよ
  • 秘密キーが抜かれて勝手に使われていることが判明したら直ちにアクセスキーをWebから変更しなさいよ
  • 以上を守れないとそのアクセスキーからのリクエスト全部止めるかもよ

Widgetのプラットフォームはいくつもあるけど、いずれもhtml(xml)とJavascriptから成るものを単にzip化しただけなので、せいぜい秘密キーを暗号化してたものをスクリプト内に記述して実行時に復号化するくらいしか思いつかない。復号化もjavascriptに入ってるので中を改変されて復号したあとのキーをprintなりalertなりで表示されたら終わりなんだけど。

Yahoo WidgetのAmazon系のWidgetはまだ誰も更新されてないっぽいんだけど、みんなどうするんだろう?

SongWidget 0.3.2をリリースしました(Yahoo! WidgetにおけるAmazonの文字化け)

ごめんなさい、ごめんなさい。半年ほったらかしにしていたSongWidgetのバグ修正版をリリースしました。(もっと放置してるwidgetもあるのは内緒)
http://makotokw.com/ja/portfolio/yahoowidget/songwidget
COMを同梱してwinamp対応とか考えてたんですが、影響がでかそうなので0.3.x系とtrunkに分岐して、とりあえず今起きてる不具合を修正して0.3.2としてリリースです。

でかい不具合としてAmazonのレスポンスが文字化けするという問題を修正しました。どうも、Yahoo Widget Engine側の修正の影響で発生するようになったようです。

結局、根本原因を調べるのはあきらめたんですが、調べてわかったことを書きます。

Yahoo! WidgetにおけるAWS(Amazon Web Service)文字化け問題
要因1:AWSにおいてResponseGroupをつけるとxml属性のencodingが省略される
http://webservices.amazon.co.jp/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=0F49XFHPQYWX91XK4K82&Operation=ItemLookup&ItemId=B0000AKI8I&ResponseGroup=Large
ただし、HTTPのレスポンスヘッダには
Content-Type:text/xml;charset=UTF-8
が入ってる。

ResponseGroupがないと何故かencoding属性は返ってくる。

http://webservices.amazon.co.jp/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=0F49XFHPQYWX91XK4K82&Operation=ItemLookup&ItemId=B0000AKI8I
&Versionで古いのを指定してもだめ。というかAmazonが突然仕様を返るとは考えにくい。

要因2:encodingが省略されるとXMLHttpRequestのresponseText/responseXMLが文字化けする
encodingが省略されるときにどの文字コードで処理しようとしてるのかはわからない。。。文字化けしたbyteコードを調べればわかったかもしれないが、どの文字コードで処理してるのではなくどうしたら文字化けしないのかばかり調べてたから。。。

要因3:SongWidetはレビューとかを取るためにReponseGroupを指定する
ResponseGroupを指定しなければencoding属性がついて結果として文字化けしないんだけど、SongWidgetではAmazonのレビューとかを取得するためにResponseGroupを省略するという選択肢がない。。。

最終的な対応
とりあえずforumになんかないかと思って調べてみたものの有用な情報は見つからなかった。こういう問題のときの英語圏のforumほど切ないものはないよ。。。

仕方がないので今度は日本のYahoo! WidgetでAmazon関連のwidgetを探してみたところ、文字化けを修正していた人がいたのでそれを参考にさせてもらった。

参考にしたWidget:
今日の新刊情報
# いぬさんありがとうございました!
結論から言うと、GETではなくPOSTを使い、リクエストをpostBodyに突っ込めばとencodingが返ってきて文字化けしない。

SongWidgetの場合、prototype.jsを経由しているの参考にならないが・・・対応コード

    var ajax = new Ajax.Request(url, {
method:'post',
postBody: postBody,
onComplete:this.onAjaxFetchContentComplete.bind(this),
onFailure:this.onAjaxFetchContentFailure.bind(this),
}
);

タイミング的にはYWE(Yahoo Widget Engine) 4.5で挙動が変わったと考えるのが自然なんだけど・・・確かYahoo.comで4.5がリリースされたときに動作確認した気がするんだけど・・・(YWEはなんか知らないがいつも日本はUSからかなり遅れてリリースされる)、それは気のせいなのか、日本のYWEだけ起きるのか今となってはわからないし、もうどっちでもいい。それが分かったところでYWEを直せないからさー。

SongWidget/Nico2Mobileはそのうち新バージョンリリース

連休終了。

休み中にやることはボリュームが多く、全部できないことはだいたい予想がついていたのでどれかに集中するより、バランスよくいろいろやることにした。

その結果、aTunesくらいしかまともにリリースできなかったのだが、家の整理やデータや開発環境の整理などもできたのでそれなりに有意義だったかと。

SongWidgetはamazonの文字化けを修正してwinampに対応してみたけどwidgetからcomをregsvr32する強引な仕様のためVista UAC ONの状態での検証をちゃんとやらないといけなくしばらくリリースできない。できるなら歌詞表示もしたいのだがバージョンを区切るのか検討中。

Nico2Mobileはニコニコムービーメーカーに対応しようとして苦戦中。予定通りに変換できずに困っている。こちらもWALKMAN対応とバージョンを区切るのか検討中。

今週末から来週末あたりにリリースするっす。

SongWidget 0.2.0でMoraが試聴できるようになってます

SongWidget 0.2.0をリリースしたわけですが、時間がなかったため説明とか全然できていません。ウジェットギャラリーのバージョンの説明なんて

0.2.0
まだ試作段階です。

と、やる気のかけらも感じさせないコメントです・・・順次、このblogでフォローしていきたいと思います。

SongWidgetのダウンロードはこちらから
http://widgets.yahoo.co.jp/gallery/detail.html?wid=10152
0.2.0ではあまり大きな声では言えませんがMoraの連続試聴機能があります。PCで音楽聞かないからコンテンツがPCにないよって人も、会社のPCだからコンテンツがないよって人もMoraの試聴なら楽しむことができます。

Moraの試聴をためすには
1) 右クリック -> ウジェットの環境設定…を選択
2) 左上の全般タブを選択
3) プレーヤー/ソースからMoraを選択
4) 保存を押す
でokです。

すると勝手にMoraの試聴が始まります。

この機能の仕組みですが、Mora winというサイトのTOP100のhtmlから試聴用のストリーミングファイルのURLを一生懸命ひっぱてきています。Mora winではコンテンツはWindows MediaフォーマットになっているのでこのMora試聴機能は内部的にはWindows Media Playerの機能を使って再生しています。というわけでもしWindows Media Playerが入ってなければインストールしてくださいまし。

Moraで試聴を聴きながらAmazonのレビューを見るって新鮮じゃないすか?個人的にもお気に入りの機能です。

SongWidget 0.2.0 リリースしますた

SongWidget 0.2.0をYahoo APIコンテスト向けに投稿しました。さすがに時間が全くなかったので機能的には全バージョンとほとんど変わっていません。

http://widgets.yahoo.co.jp/gallery/detail.html?wid=10152
ただしUI周り、特に機能の選択のしやすさが向上しています。前バージョンではPlayer/Webサービスの選択が設定ダイアログにあり、右クリック->設定をやらないとうまく使ってもらえなかったので起動時にplayer選択画面を出したり、メインパネルからPlayer/Webサーブスを切り替えられるようにしました。Playerは基本お気に入りのものを設定したら変更しないでしょうが、Webサービスはころころ切り替えるのが楽しくなったと思います。

あとAmazonの連携もlocaleが間違っていると全然アルバムがヒットしなくて使えない機能になってしまうのですが(しかもデフォルトはUS仕向け)、Widgetのlocaleに合わせて自動で選択できるようにしました。と言うわけで使い勝手としては格段にあがっていると思います。

あともう一つ大きな点としてCOMのEvnetを使うのを辞めました。というかWidget Engine 4になってからCOMの機能が書き直されたせいなのか落ちること落ちること。なんとなくメインスレッドで動作中に、Evnet元のスレッドからWidgetの機能が呼び出されるのがまずそうな気がしているのですが、Yahoo WidgetのReferenceをいくら呼んでもメインスレッドに通知する手段がわからず(そもそもスレッドを意識させていない)、Eventを使うのを辞めてTimerで地味に画面更新するようにしました。前バージョンで落ちると報告をくださった方の環境で直っていると幸いです。

さて、もともとSongWidgetのベースは二日くらいでやっつけでつくったもので、今回のバージョンもかなりのやっつけでリリースしたものなので正式リリースはもう少し整理した上で0.2.1としてリリースするつもりです。

というか、これが自分が書いたコードとは信じたくない。
書き直そ。

タイトル取得

Winamp SDKにはメタデータ取得のためのAPI(message)があり、これを駆使して再生中のコンテンツのタイトルやらアーティストやらを取ることが可能です。が、おそらくこれらのAPIは同一プロセス内のPlug-inから呼び出されることを最初は想定しているもの。で、SongWidgetのように外部プロセスからアクセスするにはメモリのプロセス間共有が必要です。
例えば再生中のfilepathを取得する場合には、Winamp領域のメモリを読む必要があります。ウインドウハンドルは分かっているので、GetWindowThreadProcessIdでProcessIDを取得し、OpenProcessでプロセスハンドルを取得、ReadProcessMemoryで指定のプロセスのメモリを読ませてもらいます。コードにすると以下のようになります。

CStringA str;
char szBuffer[_MAX_PATH] = {0};
DWORD dwPID;
GetWindowThreadProcessId(hWnd, &dwPID);
HANDLE hProcess = OpenProcess(PROCESS_VM_READ, FALSE, dwPID);
if (hProcess) {
  DWORD dwRead;
  BOOL bRet = ReadProcessMemory(hProcess, psz, szBuffer, sizeof(szBuffer), &dwRead);
  if (bRet) str = szBuffer;
  CloseHandle(hProcess);
}

さて、さらにアーティストやらメタを取る場合はさらに大変です。Winampプロセスにまずメモリを確保して、そこにメタデータを書いてもらい、さらに取得する。さらっと書くとこんな感じです。98/Meと2000/XPでメモリの確保の方法を変えないといけないのでちょっと面倒です。

typedef LPVOID (WINAPI *virtAllocEx)(
  HANDLE hProcess,  // process within which to allocate memory
  LPVOID lpAddress, // desired starting address of allocation
  DWORD dwSize,     // size, in bytes, of region to allocate
  DWORD flAllocationType,
  // type of allocation
  DWORD flProtect   // type of access protection
);
typedef BOOL (WINAPI *virtFreeEx)(
  HANDLE hProcess,  // process within which to free memory
  LPVOID lpAddress, // starting address of memory region to free
  DWORD dwSize,     // size, in bytes, of memory region to free
  DWORD dwFreeType  // type of free operation
);
CStringA getMetadataInfo(HWND hWnd, const CStringA& strFileName, const CStringA& strField)
{
  CStringA strMetadata;
  const int cbSize = 1024; // TBD
  OSVERSIONINFO osVersion;
  extendedFileInfoStruct extFileStruct;
  osVersion.dwOSVersionInfoSize = sizeof(osVersion);
  GetVersionEx(&osVersion);
  // for pointer in Winamp process
  extendedFileInfoStruct* pExtFileStructRemote = NULL;
  char* pszFileNameRemote = NULL;
  char* pszFieldRemote = NULL;
  char* pszBufferRemote = NULL;
  if (osVersion.dwPlatformId == VER_PLATFORM_WIN32_NT)
  {
    HINSTANCE hKernel = GetModuleHandle("kernel32.dll");
    if (!hKernel) return strMetadata;
    // Get handle over WinAmp process
    DWORD dwWinAmpProcId;
    GetWindowThreadProcessId(hWnd, &dwWinAmpProcId);
    HANDLE hWinampProc = OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ, FALSE, dwWinAmpProcId);
    if (!hWinampProc) return strMetadata;
    virtAllocEx allocEx = (virtAllocEx)GetProcAddress(hKernel, "VirtualAllocEx");
    virtFreeEx  freeEx  = (virtFreeEx)GetProcAddress(hKernel, "VirtualFreeEx");
    // Allocate the buffers in WinAmp address space
    pExtFileStructRemote = (extendedFileInfoStruct*)allocEx(hWinampProc, NULL, sizeof(extFileStruct), MEM_COMMIT,PAGE_READWRITE);
    pszFileNameRemote = (char*)allocEx(hWinampProc, NULL, strFileName.GetLength()+1, MEM_COMMIT,PAGE_READWRITE);
    pszFieldRemote    = (char*)allocEx(hWinampProc, NULL, strField.GetLength()+1, MEM_COMMIT,PAGE_READWRITE);
    pszBufferRemote   = (char*)allocEx(hWinampProc, NULL, cbSize, MEM_COMMIT, PAGE_READWRITE);
    // Fill the buffers allocated
    DWORD dwRet;
    WriteProcessMemory(hWinampProc, pszFileNameRemote, (void*)((LPCSTR)strFileName), strFileName.GetLength()+1, &dwRet);
    WriteProcessMemory(hWinampProc, pszFieldRemote, (void*)((LPCSTR)strField), strField.GetLength()+1, &dwRet);
    extFileStruct.filename  = pszFileNameRemote;
    extFileStruct.metadata  = pszFieldRemote;
    extFileStruct.ret   = pszBufferRemote;
    extFileStruct.retlen  = cbSize;
    WriteProcessMemory(hWinampProc, pExtFileStructRemote, &extFileStruct, sizeof(extFileStruct), &dwRet);
    // Send request to Winamp
    if ( SendMessage(hWnd, WM_WA_IPC, (WPARAM)pExtFileStructRemote, IPC_GET_EXTENDED_FILE_INFO) ) {
      char* pszBuffer = new char[cbSize+1];
      if (pszBuffer) {
        ReadProcessMemory(hWinampProc, pszBufferRemote, pszBuffer, cbSize, &dwRet);
        strMetadata = pszBuffer;
        delete [] pszBuffer;
      }
    }
    // Clean the Winamp address space
    freeEx(hWinampProc, pExtFileStructRemote, 0, MEM_DECOMMIT);
    freeEx(hWinampProc, pszFileNameRemote,    0, MEM_DECOMMIT);
    freeEx(hWinampProc, pszFieldRemote,     0, MEM_DECOMMIT);
    freeEx(hWinampProc, pszBufferRemote,    0, MEM_DECOMMIT);
    // Close the process
    CloseHandle(hWinampProc);
  }
  else if (osVersion.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
  {
    // Allocate the buffers in global address space
    HANDLE hFileExtFileStruct = CreateFileMapping((HANDLE)-1, NULL, PAGE_READWRITE|SEC_COMMIT, 0, sizeof(extFileStruct), NULL);
    HANDLE hFileFileName    = CreateFileMapping((HANDLE)-1, NULL, PAGE_READWRITE|SEC_COMMIT, 0, strFileName.GetLength()+1, NULL);
    HANDLE hFileField     = CreateFileMapping((HANDLE)-1, NULL, PAGE_READWRITE|SEC_COMMIT, 0, strField.GetLength()+1, NULL);
    HANDLE hFileBuffer      = CreateFileMapping((HANDLE)-1, NULL, PAGE_READWRITE|SEC_COMMIT, 0, cbSize, NULL);
    pExtFileStructRemote  = (extendedFileInfoStruct*)MapViewOfFile(hFileExtFileStruct, FILE_MAP_WRITE, 0, 0, 0);
    pszFileNameRemote   = (char*)MapViewOfFile(hFileFileName, FILE_MAP_WRITE, 0, 0, 0);
    pszFieldRemote      = (char*)MapViewOfFile(hFileField,    FILE_MAP_WRITE, 0, 0, 0);
    pszBufferRemote     = (char*)MapViewOfFile(hFileBuffer,   FILE_MAP_WRITE, 0, 0, 0);
    // Fill the buffers allocated
    strcpy(pszFileNameRemote, (LPCSTR)strFileName);
    strcpy(pszFieldRemote, (LPCSTR)strField);
    pExtFileStructRemote->filename  = pszFileNameRemote;
    pExtFileStructRemote->metadata  = pszFieldRemote;
    pExtFileStructRemote->ret   = pszBufferRemote;
    pExtFileStructRemote->retlen  = cbSize;
    // Send request to Winamp
    if (SendMessage(hWnd, WM_WA_IPC, (WPARAM)pExtFileStructRemote, IPC_GET_EXTENDED_FILE_INFO)) {
      char* pszBuffer = new char[cbSize+1];
      if (pszBuffer) {
        memcpy(pszBuffer, pszBufferRemote, cbSize);
        strMetadata = pszBuffer;
        delete [] pszBuffer;
      }
    }
    // Clean the global address space
    UnmapViewOfFile(pExtFileStructRemote);
    UnmapViewOfFile(pszFileNameRemote);
    UnmapViewOfFile(pszFieldRemote);
    UnmapViewOfFile(pszBufferRemote);
    CloseHandle(hFileExtFileStruct);
    CloseHandle(hFileFileName);
    CloseHandle(hFileField);
    CloseHandle(hFileBuffer);
  }
  return strMetadata;
}

天才すぎる

SongWidgetにてmora winのランキングページ(http://morawin.jp/rankingpage/rank_all.html)を解釈し、試聴用ファイルをWMPControlで再生し、同時にAmazonのReviewコメントを表示するような実装を入れてみた。かつてこれほどまでに新しい曲を探すためのアプリがあっただろうかと自画自賛
天才すぎる。(間違った日本語です)