Safari のウィンドウとドキュメント

Safari がタブに対応したのはいつでしたでしょうか?タブに対応したのはともかく、AppleScript からタブを操作できるようになったのはいつからでしたでしょうか。こういう時に参考になるのが AS Hole(AppleScriptの穴)。ここを見ると AppleScript でタブがまともに利用できるのは Mac OS X 10.5 以降、Safari 3 以降のようです(Safariの最前面のウィンドウ内のタブを順々に表示)。

何で、今更タブのことなんか?

Safari の AppleScript 対応度というのは Finder などに比べると貧弱なものです。それでも Safari の自動化に何らししょうがないのは AppleScript 内から Safari に JavaScript が実行させることができるからです。

それは、アプリケーションに AppleScript をどのように対応させるか?といった問題へのシンプルな答えでもあります。「ウェブブラウザの自動化や処理がどういう時に必要なのか?」といったことを考えた時、JavaScript を実行できることが何よりも問題の解決に役立ってくれるでしょう。

AppleScript への対応というのは、このようにキモとなる部分さえ外していなければ、Safari の do JavaScript 命令の実装だけで解決してしまうものなのです。これは他のアプリケーションでも同様です。そのため用語辞書を見るとアプリケーション作成者の AppleScript の理解度が分かったりできて面白かったりします。

閑話休題。

ようするに do JavaScript 命令は古くは document オブジェクトを引数に指定していたのですが、タブを操作できるようになると、tab オブジェクトを引数に渡すように変更されたのです。そして、tab オブジェクトは window の要素なのです。

この変更により、古いスクリプトは修正が必要になりました。

また、少し話は変わりますが、Safariで開いているページの一時保存・リストアで紹介されているスクリプト。重宝しています。

重宝しているのだけど、時々エラーになってしまうことがあるようです。どういう時にエラーになるかというと、ダウンロードしている項目を表示する「ダウンロード」というウィンドウや環境設定のウィンドウなどが前面にある時です。要するに document を持たないウィンドウが前面にある時です。

現在の Mac のアプリケーションの多くは Cocoa/Objective-C で作成されています。これらのアプリケーションは多くの window を利用しますが、ドキュメントベースのアプリケーション(書類の編集などができるアプリケーション)は window の中に document というオブジェクトを持っています。細かいことは省きますが、document を持っている window と持っていない window(設定パネルやダイアログ等)の 2 種類があるのです。

AppleScript で document というと、多くの場合この document を持った window のことを指します。例えば、TextEdit などで Book Review.rtf という書類を開いており、最前面に環境設定を開いていたとします。

tell application "TextEdit"
    name of window 1
    --> 環境設定
    name of document 1
    --> "Book Review.rtf"
    name of windows
    --> {"Book Review.rtf", "環境設定"}
end tell

windows として複数の window オブジェクトを取得すると全てのウィンドウが取得できます(document も含む)。そして、上記のスクリプトでは Book Review.rtf を最前面にすると window 1 は Book Review.rtf になります。また、document を持たない環境設定などの window は閉じても AppleScript から取得できます(これらの window の属性 visible が false になるだけで、存在自体が消滅するわけではない)。

これらは AppleScript で window と document を操作する時の注意点ですが...何が問題かというと、Safari の場合、JavaScript を実行させるためにタブを持ったウィンドウを取得しなければならないのだけど、最前面の document が必ずしも最前面の window だとは限らない、ということです。

document を持った window で最前面のものを取得する必要があります。

こういうことを知ってかどうかは分かりませんが、window の属性に document というものがあります。少し前に追加されたものだと思うのですが。また、以前はきちんと動作しなかったような...。この属性を利用して document を持った最前面の window を取得することができます。

Script Editor で開く

tell application "Safari"
    set front_window to my get_front_window()
    if front_window is missing value then return

    tell current tab of front_window
        do JavaScript "new Date();" in it
    end tell
end tell

on get_front_window()
    tell application "Safari"
        if not (front document exists) then return missing value
        set the_document to front document
        set window_ref to a reference to (windows whose document of it is not missing value)
        repeat with this_window in window_ref
            if document of this_window is the_document then return contents of this_window
        end repeat
        return missing value
    end tell
end get_front_window

tab オブジェクトが document の要素だったら話は簡単なんだけど、そうはいかないんでしょうね。この window の document 属性を使って前面の document オブジェクトを正確に指定する...という方法は、他のドキュメントベースのアプリケーションでも利用できるものなのですが、Script Editor(現在では AppleScript Editor)では、用語辞書を表示する window も document を持っている window オブジェクトなので注意が必要だったりします(つまり、document 1 という参照で用語辞書を表示している window を指してしまうこともある)。

Script Editor で開く

tell application id "com.apple.ScriptEditor2"
    name of windows whose document of it is not missing value
    --> {"名称未設定", "StandardAdditions.sdef"}
end tell

document を持っている window はアプリケーションにより異なる...と。System Events を使って GUI Scripting を利用すれば、各アプリケーションで共通して利用できるハンドラも作成できると思うのですが、System Events はどうも処理が遅くて...乗り気になれなかったり(必要があればもちろん使います)。

「iTunes に自動的に追加」と AppleScript

全く知らなかったのですが、iTunes 9 になって iTunes Music フォルダ(現行バージョンでは iTunes Media に名称が変更されている)に「iTunes に自動的に追加(英語環境では Automatically Add to iTunes)」というフォルダが追加されているのですね。このフォルダに iTunes が取り扱えるファイルを放り込んでおくと、自動的に iTunes に取り込んでくれる...そういうフォルダなのだそうです。

どうも、このような機能は待ち望まれていたもののようです。検索を行うと評判はいいようで、関連する記事も散見されます。例えば、ライフハッカー[日本版]の「「iTunes 9」でウォッチフォルダから新曲自動追加ができるようになった : ライフハッカー[日本版]」という記事。

このような「特定のフォルダを監視し、特定のファイルがフォルダに加えられると何らかの処理を行う...」ということをしたい時に、すぐに思いつくのはフォルダアクションです。あまり、利用されていない...のかな、もしかすると。

ライフハッカー[日本版]と同じことをするのは面白くないので、フォルダアクションで同じような機能を実装してみました。

以下のスクリプトを保存しておき、監視しておきたいフォルダにスクリプトを関連づけておくと、iTunes にファイルを自動的に追加することはできます。

Script Editor で開く

property file_extensions : {".mp3"}

on run -- debug
    set file_list to choose file with multiple selections allowed without invisibles
    set the_folder to path to home folder -- dummy
    adding folder items to the_folder after receiving file_list
end run

on adding folder items to this_folder after receiving these_items
    set music_files to {}

    repeat with this_item in these_items
        set {fine_name, ext} to my splitext(this_item as text)
        if ext is in file_extensions then
            set end of music_files to contents of this_item
        end if
    end repeat

    if music_files is not {} then
        set playlist_name to "Added: " & (short date string of (current date))
        tell application "iTunes"
            activate
            set added_tracks to add music_files

            if class of added_tracks is file track then
                set added_tracks to {added_tracks}
            end if

            if not (playlist playlist_name exists) then
                set the_playlist to make new playlist with properties {name:playlist_name}
            else
                set the_playlist to playlist playlist_name
            end if

            repeat with this_track in added_tracks
                duplicate this_track to the_playlist
            end repeat
        end tell
    end if
end adding folder items to

on splitext(file_name)
    set reversed_name to (reverse of (characters of file_name)) as text
    set num to offset of "." in reversed_name
    if num is 0 then
        set ext to ""
    else
        set reversed_ext to text 1 thru num of reversed_name
        set ext to (reverse of (characters of reversed_ext)) as text
        set ext_num to count ext
        set name_length to count file_name
        set file_name to text 1 thru (name_length - ext_num) of file_name
    end if
    return {file_name, ext}
end splitext

フォルダアクションの設定の仕方は...Mac OS X 10.5 と Mac OS X 10.6 では若干異なっているのでしたね。Mac OS X 10.5 の場合は「シゴタノ! —    フォルダアクションと Automator で時間をセーブする」を()、Mac OS X 10.6 の場合は「わかばマークのMacの備忘録 : setWeblocThumb」を参考にしてみてください。

これで監視しているフォルダに MP3 ファイルが追加されたら iTunes に自動的に取り込んでくれます。

しかし、試してみると分かると思うのですが、iTunes の add 命令はファイルの複製なのです。オリジナルのファイルはそのまま残ります。また、フォルダアクションは反応がよくて(よすぎて)、例えば、Safari でダウンロードしているファイルなんかだったりした場合、ダウンロード完了まで待ってくれません。だから、上記のスクリプトでは目的が達成できません(Mac OS X 10.6 のフォルダアクションは項目がコピーされている時は処理を待機するようですが...ダウンロードしている項目にも適用されるのかは分かりません)。

上記のフォルダアクションでもいいのですが、何となく釈然としない部分があります。フォルダの更新を監視するなら、launchd があるな...。launchd と AppleScript の組み合わせでいいじゃないか、と。しかし、この思いつきがハマる原因になるとは。

ハマるなどとはつゆとも思わず、とりあえず作り始めました。最初に書いたように「iTunes に自動的に追加」フォルダの名称は言語環境で異なるようで、英語環境では「Automatically Add to iTunes」になります。この辺りの問題に対応するため、バンドル形式のアプリケーションかスクリプトバンドルで保存し、localized string 命令を利用します。

そして、作成したスクリプトを launchd で実行するプログラムに指定します。プログラムの実行は osascript を利用するのですが...ここでハマる。

osascript でスクリプトファイルを指定して実行すると、どうも言語環境を考慮しないようで localized string 命令が英語環境の文字列を返します。まさか、このような部分に問題があるとも思わず、他の部分をいろいろ調べていました。

もちろん、launchd でアプリケーションを直接起動させてもいいのですが...この場合、アプリケーション内のプログラムの実態そのものを指定する必要があります。AppleScript のバンドル形式のアプリケーションの場合は、パッケージ内の /Contents/MacOS/applet がそれにあたります。が、これを起動させても目的の動作は行われません。他の Cocoa アプリケーションのようにこの方法も利用できない。バンドル形式ではないアプレットで保存すると問題はないのですが、そうなると localized string が利用できません。

osascript で実行すると localized string で問題があり、アプリケーションの指定にも問題がある...。どうしたものかと思いましたが、バンドル形式のアプリケーションで保存し、osascript でアプリケーションを起動させてしまえばいいじゃないかと思いつきました。

具体的な launchd の記述は以下のようになります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.charan.AutomaticallyAddiTunes</string>
    <key>Program</key>
    <string>/usr/bin/osascript</string>
    <key>ProgramArguments</key>
    <array>
        <string>osascript</string>
        <string>-e</string>
        <string>tell application "Move to iTunes" to activate</string>
    </array>
    <key>WatchPaths</key>
    <array>
        <string>/Users/your_name/Downloads</string>
    </array>
</dict>
</plist>

これで、かなり満足したものができました。必要なファイルは以下からダウンロードできます。利用方法は同梱の書類を参照してみてください。まぁ、たいていの場合と同じように「ご利用はご自身の責任で」行ってください。

iTunes Music Library.xml を見つけ出す

ちょっと必要があって iTunes Music Library.xml の場所がどこにあるかを AppleScript で探し出したいんだけど...。目的はそれだけなんです。

iTunes Music Library.xml は iMovie や iPhoto、iDVD、Keynote など他のアプリケーションが利用するファイルです。iTunes が管理しているファイルやプレイリストなどの情報が記述されています。拡張子は XML ですが、中身は Apple の Property List 形式で記述されています。

なんで iTunes Music Library.xml を探し出したいかというと、iTunes が利用している iTunes Music フォルダの場所を参照したいからです。その情報が iTunes Music Library.xml に記述されているのです。

通常、iTunes はユーザーの Music フォルダ(~/Music)に iTunes というフォルダを作成し、そこで各ファイルの管理をするのですが、このフォルダの場所はユーザーが変更可能です。ですから、iTunes Music Library.xml が ~/Music/iTunes 以下に存在すると決めつけることはできません。

ところで、iTunes 9 になってファイルの整理方式が変更になったんですね。知りませんでした。うちの iTunes フォルダは古くからのもので、OS のバージョンアップの度に外付け HDD に退避し、バージョンアップ後に戻す...ということを繰り返しているので、現在では利用されていないファイルやフォルダが複数散見されます。このことが余計に事態を複雑にしてくれました。

他のアプリケーションが参照するファイルなんだから、iTunes Music Library.xml の場所を調べる方法が何かあるはず...という想定のもと調べてみると、com.apple.iApps.plist というファイルが見つかりました。ここに iTunes Music Library.xml の場所が記述されているのでした。

ユーザーの Preferences フォルダにある com.apple.iApps.plist は iTunes、もしくは iPhoto の起動時に作成されます。とりあえずこのファイルを見てみれば iTunes Music Library.xml の場所が分かります(まぁ、問題点がないわけでもないようですが)。

以下、iTunes Music Library.xml の場所を調べ、iTunes Music フォルダの場所を調べるスクリプト。エラー処理はしていないので、その辺りは大人の対応で。

Script Editor で開く

set music_folder to find_iTunes_folder()

on find_iTunes_database()
    set prefs_folder to path to preferences folder from user domain as text
    set prefs_file to POSIX path of (prefs_folder & "com.apple.iApps.plist")

    tell application "System Events"
        tell property list file prefs_file
            set file_list to value of property list item "iTunesRecentDatabasePaths"
            return item 1 of file_list
        end tell
    end tell
end find_iTunes_database

on find_iTunes_folder()
    set xml_file to find_iTunes_database()
    tell application "System Events"
        tell property list file xml_file
            set music_folder to value of property list item "Music Folder"
        end tell
    end tell

    return do shell script "python -c \"import sys, urlparse, urllib; print urllib.unquote(urlparse.urlparse(sys.argv[1]).path)\" " & quoted form of music_folder
end find_iTunes_folder