DATE : 2009/11/05 (Thu)
※ 本記事は、北陸アンカンファレンス2009で発表した内容を文章にまとめたものです。
はじめに
本記事では、Windows上でiTunesをスクリプトから操作できるように、以下の情報を提供します。
- スクリプトからiTunesに対してどのような操作ができるのか
- iTunesをスクリプトから操作するために必要となるもの
- スクリプトの例
- 「iTunes COM Interface Documentation」の見方
- スクリプトを書く上での注意事項
なお本記事の対象はWindowsで、対応する「iTunes COM Interface Documentation」のバージョンは8.1.0.52 です。MacOSに関しては扱いません。なおMacOSではOSA(Open Scripting Architecture)に対応したスクリプト言語からiTunesを操作できるようです。
スクリプトからiTunesに対してどのような操作ができるのか
以下の操作が、スクリプトから可能です。
- プレイヤーの状態の取得(再生中か否か、選択しているトラックなど)
- プレイヤーの操作(再生開始、停止、スキップなど)
- (音楽、ビデオなどにかかわらず)ライブラリの編集(トラックの情報の編集、プレイリストの作成など)
- Podcastの購読・更新
アプリケーション関係やGenius関係以外はだいたい操作できると思います。
iTunesをスクリプトから操作するために必要となるもの。
iTunesをスクリプトから操作するには、以下のものが必要です。
- iTunes
- スクリプトの実行環境
- iTunes COM for Windows SDK
COM(Component Object Model)にアクセス可能であれば、スクリプトの言語は問いません。
iTunes COM for Windows SDKは、以下の手順で入手できます(Apple IDが必要です)。
- Apple Developer Connectionにログインする。
- 「Downloads」を開く。
- 「Developer Tools」を開く。
- 「iTunes COM for Windows SDK」をダウンロード。
スクリプトの例
ここでは、ライブラリ内の曲の情報をコンソールに出力する例を示します。
以下の手順で、ライブラリから曲の情報を取り出します。
- iTunesのCOMオブジェクトを取得する。
- iTunesからライブラリを取得する。
- ライブラリ内の1曲ごとに
- 曲の情報を1行に出力する。
この手順をPython(Python 2.6)で表現すると以下のようになります(;´∀`)Pythonなのは、ただ単に自分がいつも使っている言語だからです。なおPythonでCOMを取得するには、Pythonの実行環境にくわえて「Python for Windows extensions」も必要です。
# iTunesのCOMオブジェクトを取得する。 import win32com.client itunes = win32com.client.Dispatch("iTunes.Application") # iTunesからライブラリを取得する。 musicLibrary = itunes.LibraryPlaylist # ライブラリ中の1曲ごとに for aTrack in musicLibrary.Tracks: # 曲の情報を1行に出力する。 print "%s, %s, %s" % (aTrack.Artist, aTrack.Name, aTrack.Album)
上記のスクリプトで最も重要なのは以下の部分です。
itunes = win32com.client.Dispatch("iTunes.Application")
(※ 以下では、「itunes」変数を、iTunesから初めに取得したCOMオブジェクトが入っている変数とします)
この部分は、iTunesからCOMを取り出す処理を行っています。つまりここからすべてが始まると言えます。「win32com.client.Dispatch」の部分はスクリプト言語によって変わりますが、「iTunes.Application」の部分はどのスクリプト言語であっても変わりません。
さて、iTunesからCOMを取り出す部分がすべての始まりだと言うことはわかりました。それでは、それから様々な操作を行うには、どのようにすればよいのでしょうか。その答えが「iTunes COM Interface Documentation」に書かれています。
「iTunes COM Interface Documentation」の見方
iTunes COM Interface Documentationを開くと、まず目に飛び込んでくるのがインタフェースやクラスの一覧です。
この中にある、「IiTunes」が、iTunesから初めに取得できるCOMオブジェクトとなります。つまり、まず「IiTunes」インタフェースからドキュメントを読んでいくと、スクリプトを使ってiTunesで何ができるのかわかります。
ところが、このiTunes COM Interface DocumentationはC言語向けに書かれており、スクリプトから使用する場合には若干理解しづらい書き方がされています。例えば、プレイリストを表すオブジェクトを生成する、IiTunesインタフェースのCreatePlaylistメソッドは以下のように記述されています。
HRESULT CreatePlaylist ([in] BSTR playlistName, [out, retval] IITPlaylist **iPlaylist)
スクリプトから使う際には、以下のように読むと使い方が理解しやすくなります。(ただし、スクリプトによっては以下のような理解では使えないものもあるかもしれません。しかし少なくとも、VBAやJScript、Ruby、Pythonでは以下のように理解しても問題はありません)。
- 「HRESULT」は無視する。
- 「CreatePlaylist」はメソッド名。
- 「[in]」はメソッドの引数。
- 「BSTR」は文字列型。
- 「[out, retval]」はメソッドの戻り値。
- 「IITPlaylist」はIITPlaylistインタフェース
つまり、プレイリストを生成するには以下のようにメソッド呼び出しを行います。
createdPlaylist = itunes.CreatePlaylist("New Playlist Name")
なお「BSTR」は文字列型を表しています。他にも「VARIANT_BOOL」や「long」、「DATE」などの型があります。このあたりは、スクリプト言語のどの型に相当するのかその名前から想像が付くと思います。
トラックのコレクションを表すIITTrackCollectionなどのxxxxCollectionインタフェースにも注意が必要です(例えば、「itunes.LibraryPlaylist.Tracks」で取得できるオブジェクトはIITTrackCollectionインタフェースを持っています)。言語によっては、これらxxxxCollectionインタフェースがそのスクリプト言語内でのコレクションにマッピングされることがあります。スクリプト言語内のコレクションにマッピングされた場合、xxxxCollectionインタフェースの、指定したインデックスの要素を返す「Item」プロパティは読み替えが必要です。具体的には次のようになります。
# ライブラリから初めの1曲を取り出すコード例 # IITTackCollectionがコレクションオブジェクトにマッピングされる言語の場合、 # このコードはうまく動作しない aFirstTrack = itunes.LibraryPlaylist.Tracks.Item(1) # 以下のように読み替えなければならない aFirstTrack = itunes.LibraryPlaylist.Tracks[0]
スクリプトを書く上での注意事項
iTunesのCOMでは、インデックスが「1」から始まる点に注意が必要です。
ところが、処理系によってはそのスクリプト言語内のコレクションにマッピングする際に、インデックスを補正して0始まりに修正する場合があります(例えばこれまでのコード例のように、Pythonでは0始まりとして扱えます)。それはそれで便利なのですが、気をつけなければならない点があります。コレクションにマッピングされる「Item」プロパティはインデックスが0始まりとして扱えますが、それ以外は1始まりとして扱わなければなりません。例えば、IITTrackCollectionには「ItemByPlayOrder」という指定したインデックスで再生順にトラックを返すメソッドがあります。このメソッドはItemプロパティとは異なり、コレクションにマッピングされる対象外のためインデックスは1始まりとなります。具体的には次のようになります。
# ライブラリから初めの1曲を取り出すコード例 # IITTackCollectionがコレクションオブジェクトにマッピングされる言語の場合、 # このコードはうまく動作するが aFirstTrack = itunes.LibraryPlaylist.Tracks[0] # 次のコードはうまく動作しない aFirstTrack = itunes.LibraryPlaylist.Tracks.ItemByPlayOrder(0) # コレクションにマッピングされないプロパティは、インデックスが1から始まる aFirstTrack = itunes.LibraryPlaylist.Tracks.ItemByPlayOrder(1)
つまり、コレクションへのマッピングの際にインデックスを0始まりに補正するスクリプト処理系でメソッドの引数にインデックスを渡す必要がある場合は、インデックスが1始まりであることを念頭に置いて処理しなければなりません。
最後に
以上、iTunesをスクリプトから操作する方法について簡単に説明してきました。iTunes自体のUIはとても良くできており、手動での操作でもかなりのことができます。ところが再生回数の変更やレートの一括変更など、細かいところではスクリプトの得意とする面もあります。またiTunes外からiTunesを操作するアプリケーションも数多く存在します。そう言った面では、アイディア次第でiTunesをさらに面白く活用することも可能ではないかと思います。
謝辞
北陸アンカンファレンス2009にて、本セッションを聞きに来てくださった方々、つたない説明ながらも熱心にお聞きくださり、誠にありがとうございました。
DATE : 2009/11/05 (Thu)
11月1日に「北陸アンカンファレンス2009」に行ってきました。「アンカンファレンス」とは、セッションの内容が事前に決まっておらず、参加者が自由に発表や議題を持ち込んでセッションを開くというスタイルのカンファレンスです。
どのようなセッションが開かれるのかわからず、高度な内容が多そうで初めは発表する気はありませんでした。しかしアンカンファレンスという参加者が自由に発表を持ち込む形式であること、また部屋2つ(当日は3つに増えました)で並行してセッションが開かれると言うことで、奮起して発表を持ち込みました。
とにかくバラエティに富んだセッションが多くて面白かったです。テーマが絞られていないので、本当に何が出てくるのかわからない。IT系のものもあればそうでないものもある、制作したものを発表している方もいれば、ツールの使い方を発表している方、iPhoneにインストールしているアプリを紹介している方もいれば、ディスプレイモニタの解体ショーを実演している方もいる。午前11時から午後5時までの長丁場だったにもかかわらず、どのセッションに参加するかとても悩ましく、時間があっという間に過ぎていきました。自分がふだん情報収集していても気付かない情報が大量にあって、刺激的で見聞がとても広がりました。久々にHudsonに本腰を入れたくなったり、jetpackにも興味を持ちました。非常に勉強熱心な方を見て、ライバル心にも似た気持ちが燃え上がったりもしました。
自分自身の発表「スクリプトからiTunesを操作する(Windowsで)」も、ところどころ詰まりながらもなんとか終えることができました。セッションへ参加された皆さんは熱心な表情でお聞くださりとてもありがたく、楽しかったです。このような機会があれば、またネタを持ち込んで発表したいと思いました。
以下、当日に思ったことをつらづらと書き連ねます。
- 自分自身の発表資料は、前日の夜遅くまでOpenOffice.org Impressで作成していました。ところが、当日の発表の中にはテキストエディタ中の文字を大きく表示させてスライド代わりにされている方もいました。発表したくなったら空いている時間に自分のセッションを入れても良いという点もアンカンファレンスの魅力なので、テキストエディタを使って即席で資料を作って発表したり、ホワイトボードを使ってアドリブで発表してしまうというのも十分にアリだと感じました。むしろ、それぐらい肩から力を抜いて参加した方が自分自身よかったのかもしれません(;´∀`)本当は、発表ネタを2つ3つ用意していたのですが、1個目のスライド作りだけで力尽きてしまったので……。
- 1セッション15分でセッション間が途切れなく進むので、実際に発表できる目安は10分ほどでした。ところが自分自身内容を詰め込みすぎて15分の発表資料になってしまい、セッション中は余裕がなく慌ただしく進めざるを得なくなってしまいました。次はもう少し時間に余裕を見て発表資料をまとめようと思いました。
- 1セッション15分で、並行してセッションが別々の部屋で3つあるため、15分ごとに移動することもたびたびありました。ノートPCの電源の確保が大変でしたし、移動の際には荷物を素早く片付けなければなりませんでした。そのため、アンカンファレンスにはネットブックなどの電源効率が良くて持ち運びしやすいノートPCが必須だと感じました。(;´∀`)いちおう自分はネットブックを持っていったのでなんとかなりました。
一参加者として、北陸アンカンファレンス2009はとても楽しく、刺激的な会となりました。このようなイベントがまたあれば、ぜひ参加して、発表したいと思います。
参考文献
DATE : 2009/06/21 (Sun)
Mapから値を取り出すと同時にオートアンボクシングを使用する場合は、nullが出てくる可能性を考えなければなりません。
Mapからboolean値を取り出してif文で条件分岐をさせるコードを書いたら、NullPointerExceptionが発生してしまいました。NullPointerExceptionが発生した箇所は下記の通りです。
private void function(String key, Map<String, Boolean> map) { ... if (map.get(key)) { // NullPointerException発生 .... } ... }
初めは、MapがnullだったためNullPointerExceptionが発生したのかと思いました。しかし動作を調べてみても、Mapにはきちんとインスタンスが割り当てられていました。
実は、NullPointerExceptionが発生していたのは、ラッパークラスのBooleanオブジェクトからプリミティブ値のboolean型へと変換を行うオートアンボクシングの部分でした。NullPointerExceptionが発生した箇所の処理を細かく見ていくと、以下のようになります。
- 「map.get(key)」でBooleanオブジェクトを取り出す。
- if文の評価にはプリミティブ値のboolean値が必要。
- そのため、取り出されたBooleanオブジェクトをプリミティブ値のboolean値にオートアンボクシングする。
問題だったのは、Mapから取り出したBooleanオブジェクトがnullになっていたことでした。キーに対応づく値がMapに存在しなかったためです。nullオブジェクトをアンボクシングしようとすると、NullPointerExceptionが発生します。だから上記の箇所でNullPointerExceptionが発生していたのです。
こういう場合、オートアンボクシングでNullPointerExceptionが発生しないようにするには、Mapから値を取り出す際にはオートアンボクシングせず、取り出した値がnullではないか確かめるほうが安全です。
(;^ω^)問題が発生した書き方だとコードがやけにすっきりするので、nullのことをつい忘れてしまいました。
参考文献
DATE : 2009/02/15 (Sun)
Skype 4.0以上ではsecondaryオプションを付けることで、Skypeがすでに立ち上がっている状態でもさらにSkypeを追加して起動できます。しかし「Skypeがすでに立ち上がっている状態」というのが曲者で、単純に次のようなバッチスクリプトを書いてもSkypeは1つ目のものしか起動しません。なお、本記事ではSkypeは「C:\Program Files\Skype」にインストールされているものとします。その他のフォルダにインストールされている場合は、SKYPE_EXE_DIR変数を、Skype.exeのあるフォルダに書き換えてください。
setlocal set SKYPE_EXE_DIR="C:\Program Files\Skype\Phone" start /d %SKYPE_EXE_DIR% Skype.exe start /d %SKYPE_EXE_DIR% Skype.exe /secondary endlocal
そこで、1つ目のSkypeを起動したあとにスクリプトの実行をしばらく待機させて、1つ目のSkypeが起動しきってから2つ目のSkypeを立ち上げるようにしました。ただし、指定した時間分待機するようなコマンドはコマンドプロンプトには存在しません。そこで代わりにpingコマンドを使用しました。
setlocal set SKYPE_EXE_DIR="C:\Program Files\Skype\Phone" set PAUSE_SEC=60 start /d %SKYPE_EXE_DIR% Skype.exe REM 1つ目のSkypeの起動を待つため待機 ping -n %PAUSE_SEC% localhost start /d %SKYPE_EXE_DIR% Skype.exe /secondary endlocal
このバッチスクリプトをスタートアップに入れておけば、Windowsの起動時にSkypeを同時に2つ立ち上げることができます。なお、上のバッチファイルでは1つ目のSkypeを立ち上げた後に60秒待機してから2つ目のSkypeの起動を行っています。お使いの環境によっては、それが長すぎたり短すぎたりすることもあるので、PAUSE_SEC変数の部分をほどよい秒数に調節してください。
参考文献
DATE : 2008/11/22 (Sat)
Firefox 拡張で、特定の URL が読み込まれた際に他のページへ飛ばすには、nsIWebProgressListener インタフェースを実装したオブジェクトを作ります。
function MyListener() { } MyListener.prototype = { /* * nsIWebProgressListener の実装 */ QueryInterface : function(aIID) { if (aIID.equals(Components.interfaces.nsIWebProgressListener) || aIID.equals(Components.interfaces.nsISupportsWeakReference) || aIID.equals(Components.interfaces.nsISupports)) { return this; } throw Components.results.NS_NOINSTANCE; }, onStateChange : function(aWebProgress, aRequest, aFlag, aStatus) { }, onLocationChange : function(aProgress, aRequest, aURI) { }, onProgressChange : function(aProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) { }, onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) { }, onSecurityChange : function(aWebProgress, aRequest, aState) { } }
nsIWebProgressListener インタフェースを実装したオブジェクトは、ページへのリクエストを監視できます。ページへのリクエストが発生すると onStateChange メソッドが呼ばれるので、このメソッド内に、移動したいページの URL へのリクエストを監視するコードを書きます。
onStateChange : function(aWebProgress, aRequest, aFlag, aStatus) { var url = aRequest.name; if (aFlag & Components.interfaces.nsIWebProgressListener.STATE_START) { // url のページへのリクエストが発生した際に到達する } },
リクエストされた URL を取得するには、引数 aRequest の型である nsIRequest の name プロパティを使用します。また、リクエストの開始かどうかは、引数 aFlag のビット列を見ることでわかります。
引数 aRequest の name プロパティの中身が予期していた URL と同じであれば、別のページへ飛ばすようなコードを書きます。ここで、SOURCE_URL は別ページへ飛ばす対象となる URL、DESTINATION_URL は、移動先の URL を表します。
onStateChange : function(aWebProgress, aRequest, aFlag, aStatus) { var url = aRequest.name; if (aFlag & Components.interfaces.nsIWebProgressListener.STATE_START) { if (url == SOURCE_URL) { aWebProgress.DOMWindow.location.replace(DESTINATION_URL); } } },
nsIWebProgress 型である引数 aWebProgress から、現在のページを表示している nsIDOMWindow 型のオブジェクトを取得し、そのページの場所を表す Location オブジェクトを使用して、移動先のページに置き換えます。Location オブジェクトでは、assign メソッドを使用してページを置き換える方法もありますが、上記のコードでは replace メソッドを使用しました。assing メソッドでは置き換えた元のページが履歴に残りますが、replace では残りません。
現在のページの URL は、 nsIDOMWindow 型のオブジェクトから取得した Location オブジェクトを使用しても取得できますが、今回はその方法が使えません。Location オブジェクトの表す URL はすでに読み込みが完了した後のページのものなので、上記のコードで新しくリクエストされた URL を Location オブジェクトから取得しようとしても、現在表示されているページの URL が取得されてしまいます。replace メソッドを使用してページを置き換えると新しいリクエストが発生するので、無限ループになる場合もあります。そのため、リクエストされた URL を使用する際には、必ず引数 aRequest の name プロパティから取得するようにしてください。
(;^ω^)これで長時間つまりました。
あとは、このリスナーオブジェクトを Firefox に登録します。
var myListener = new MyListener(); function load() { gBrowser.addProgressListener( myListener, Components.interfaces.nsIWebProgress.NOTIFY_STATE_DOCUMENT); } function unload() { gBrowser.removeProgressListener(myListener); } window.addEventListener("load", load, false); window.addEventListener("unload", unload, false);