2011/11/22

iTunes: MacRuby で重複する曲を削除

先日
MacRuby で iTunes から重複する曲を削除するものを作りました。
https://gist.github.com/1382271
(OS X 10.7.2 限定)

-- 参考にさせていただいたサイト

# ファイルを移動させる際の条件など大変参考になりましたし、
# MacRuby についての情報もいろいろありました。
iTunesに重複登録されている音楽ファイルを削除する - Watsonのメモ

# iTunes.app に下記のような機能があったことをこれまで知りませんでした。
iTunes ライブラリで重複した項目を検索し、削除する方法

-- 当初、最も悩んだこと

track.delete
を実行すると なぜか MacRuby がクラッシュしました。
MacRuby バージョンは

% macruby -v
MacRuby 0.11 (ruby 1.9.2) [universal-darwin11.0, x86_64]


重複する曲が 20 個以上あるとき
playlist.tracks の曲を each でまわし一定の条件を満たしたタイミングで
track.delete するとクラッシュしました。
こんな感じ。

myplaylist.tracks.each{|x|
  if ...
  # x がなにかの条件をみたしたら
  x.delete
  # この瞬間にクラーッシュ!
}


それで、オブジェクトを探ってみることに...

test.rb

framework "ScriptingBridge"

i = SBApplication.applicationWithBundleIdentifier("com.apple.iTunes")
a = i.sources[0].userPlaylists.find{|x| x.name ==  "ミュージック"}.tracks
p b = a[0]
p c = a[0]
p b.equal?(c)

#=>

% macruby test.rb
false

なんと false がかえってきました。
b, c は object としてはイコールでは『ない』とのこと。
a は1度しか定義していないにもかかわらず...。

例えば、Ruby 1.9 では

a = ["a","b","c"]
b = a[0]
c = a[0]
p b.equal?(c)

#=>

% ruby193 -v aaa.rb
ruby 1.9.3p0 (2011-10-30 revision 33570) [x86_64-darwin11.2.0]
true

true がかえってきます。

MacRuby のこの件について理解できておりませんが、ともかく
この事が原因でクラッシュをひきおこしているのかも? と考え
一意の曲オブジェクトを「確実」に指名する方法をとることにしました。
要は

track.databaseID

を使うことにしました。

こんな感じです。

条件をみたした 曲オブジェクトの databaseID を hash に溜め込んでから、
hash を each でまわしつつ
その都度 fileTracks から 指定した databaseID に基づいて曲オブジェクトを検索し
検出できたら
.delete
を実行...

      @h.each{|k,v|
        t = @filetracks.find{|x| x.databaseID == k}
        next unless t
        t.delete
      }


このようにしてみたところ .delete を実行しても
クラッシュしなくなりました。
# あくまでも私の環境下においてでは!です。

# ちなみに .fileTracks と .tracks の 2 種類ありますが
# 上記では .fileTracks を対象にしています。

2011/11/12

Tutorial MacRuby

Tutorial: OS X automation with MacRuby and the Scripting Bridge

One more for the road

以降にある iTunes のサンプルを実行すると

... `<main>': undefined method `playlists' for nil:NilClass (NoMethodError)


playlists がない?
なーんてことはなくて単に日本語システムだからだった。
要素名を日本語に変えればよいのだった。

...
  .find {|s| s.name == "ライブラリ"}
  .playlists.find {|p| p.name == "トップ 25"} #Top 25 Most Played"}
...


この内容を理解する為、基本的な AppleScript を動かす。

 tell application "iTunes"
  repeat with i in playlists
    if name of i is "ミュージック" then
      get tracks
    end if
  end repeat
end tell

AppleScript Editor.app のイベントログで
playlists の要素や tracks の属性を確認できた。
sources にアクセスしなくても、playlists を獲得できた。

MacRuby でも playlists 要素を目視したい為...

framework "ScriptingBridge"

i = SBApplication.applicationWithBundleIdentifier("com.apple.iTunes")
i.sources.select{|x|
  x.playlists.each{|y|
    puts y.name
    if y.name ==  "トップ 25" # "トップレート", "ミュージック"
      songs = y.tracks.sort{|t1,t2| t2.playedCount <=> t1.playedCount}
      # songs[0..2].map{|t| puts "#{t.artist} - #{t.name} (#{t.playedCount})"}
    end
  }
}

puts y.name でプレイリスト名一覧を確認できた。

ライブラリ
ミュージック
ムービー
テレビ番組
Podcast
iTunes U
ブック
iTunes DJ
90 年代ミュージック
クラシック音楽
トップ 25
トップレート
ミュージックビデオ
最近再生した項目
最近追加した項目
ラジオ


書き換えながらいろいろ試す。

framework "ScriptingBridge"

i = SBApplication.applicationWithBundleIdentifier("com.apple.iTunes")
ary = i.sources.map{|x| x.playlists}.flatten
top25 = ary.find{|i| i.name == "トップ 25"}
songs = top25.tracks.sort{|t1,t2| t2.playedCount <=> t1.playedCount}
songs.find_all{|t| t.rating > 2}.map{|t| puts "#{t.artist} - #{t.name} (#{t.playedCount})"}


Ruby らしい(つもりの)書き方へ。

framework "ScriptingBridge"

class MyiTunes
  def initialize(str)
    exit if (str.empty? or str.nil?)
    @str = str
    # iTunes Object
    i = SBApplication.applicationWithBundleIdentifier("com.apple.iTunes")
    # playlist Object
    @ary = i.sources.map{|x| x.playlists}.flatten
    @songs = @ary.find{|i| i.name == @str}.tracks
  end

  def debug
    @ary.each{|x| puts x.name}
  end

  def view
    print "-- #{@str}\n"
    find_all
    count_sort
    to_s
  end

  private
  def count_sort
    @songs = @songs.sort {|t1,t2| t2.playedCount <=> t1.playedCount}
  end

  def find_all
    @songs = @songs.find_all{|t| t.rating > 60}
  end

  def to_s
    @songs.each_with_index{|x,n|
      break if n > 2
      puts "#{x.artist} - #{x.name} (#{x.playedCount}) [#{x.rating}]"
    }
  end
end

MyiTunes.new("トップ 25").view
# MyiTunes.new("トップ 25").debug

実行結果

-- トップ 25
Glenn Gould - Bach: Goldberg Variations, BWV 988 - Aria (5) [100]
Glenn Gould - Bach: Goldberg Variations, BWV 988 - Var. 7 (5) [60]
Glenn Gould - Bach: Goldberg Variations, BWV 988 - Var. 5 (4) [80]
% 


MyiTunes クラスは
"トップ25" から、
星が3つ以上の曲を抽出し、
再生回数順にソートし
3行出力したら終わる。

.rating
に注意を払っていなかった。
puts 表示させてよくみてみると
星が 5つだと 100で、
星が 4つだと 80 で、
星が 3つだと 60 に、
なっていた。
星が3つ以上( > 60 )に変えた。
# t.rating > 60

iTunes.app 環境設定で表示する項目を減らして
MyiTunes.new("トップ 25").debug
を実行すると...正しく捕獲できた。

ライブラリ
ミュージック
ブック
90 年代ミュージック
クラシック音楽
トップ 25
トップレート
ミュージックビデオ
最近再生した項目
最近追加した項目
% 

MyiTunes.new("ミュージック")
でも OK だった。

-- 追記
OS X 10.7.2 で自力で MacRuby をインストールしていない為
/System/Library/PrivateFrameworks/MacRuby.framework/Versions/A/usr/bin/macruby
を使った。

最後に、~/Library/Logs/CrashReporter で
MacRuby がクラッシュしていないかみてみたが
大丈夫だった。

[2011-11-12]
最短?

i = SBApplication.applicationWithBundleIdentifier("com.apple.iTunes")
top25 = i.sources.flat_map{|x| x.playlists}.find{|t| t.name == "トップ 25"}
p top25.name
songs = top25.tracks
p songs.size
=>
"トップ 25"
25


[2011-11-22]
続き

Time.parse

はまった。

>> Time.parse("2011/11/01 am 10:00:00")
=> Thu Nov 10 01:00:00 +0900 2011
>> Time.parse("2011/11/01 am 00:30:00")
ArgumentError: argument out of range
  from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/time.rb:184:in `local'
  from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/time.rb:184:in `make_time'
  from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/time.rb:243:in `parse'
  from (irb):5
>> Time.parse("2011/11/01 am 01:30:00")
=> Tue Nov 01 01:00:00 +0900 2011
>> 


parse 様にいわせればユーザが am, pm 言うなんておかしいだろってこと?
そ、そうかも。

# 2011-11-04

2011/11/08

OS X 10.7.2: Mobile Processes

Lion になって新しく登場したプロセスにはどのようなものがあるだろうか?と

$ sudo launchctl list

を眺めつつ
% man xxx
しつつ

ふと

% apropos Mobile

を試してみたら...

mdmclient(1)             - MDM (Mobile Device Management) client
mtmd(8)                  - Mobile Time Machine snapshot daemon
mtmfs(8)                 - Mobile Time Machine file system daemon
special_file_handler(8)  - data provider for special files used by Mobile Documents
ubd(8)                   - ubiquity daemon, part of Mobile Documents
% 


Mobile 関連ぽいものがわかった。

#-------------------------
# MDM
#-------------------------
# 1. mdmclient(1) - MDM (Mobile Device Management) client
# 2. mtmd(8) - Mobile Time Machine snapshot daemon
# mtmfs(8) - Mobile Time Machine file system daemon
# 3. special_file_handler(8) - data provider for special files used by Mobile Documents
# 4. ubd(8)
#-------------------------

#-------------------------
# 1. mdmclient
#-------------------------
# man mdclient
# mdmclient -- MDM (Mobile Device Management) client
#
# $ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.mdmclient.daemon.plist
# % launchctl unload -w /System/Library/LaunchAgents/com.apple.mdmclient.agent.plist

#-------------------------
# 2. mtmd, mtmfs
#-------------------------
# $ man mtmd
# $ man mtmfs
# $ man tmutil
#
# $ sudo tmutil disable
# $ sudo tmutil disablelocal
# $ sudo launchctl unload -w System/Library/LaunchDaemons/com.apple.mtmd.plist
# $ sudo launchctl unload -w System/Library/LaunchDaemons/com.apple.mtmfs.plist

#-------------------------
# 3. special_file_handler
#-------------------------
# man special_file_handler
# special_file_handler -- data provider for special files used by Mobile Documents
#
# $ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.ubiquity.special_files.plist

#-------------------------
# 4. ubd
#-------------------------
# man ubd
# ubd -- ubiquity daemon, part of Mobile Documents
# ubd is the ubiquity server process. It is primarily used for "Mobile Documents".
# SEE ALSO
# special_file_handler(8)

# % launchctl unload -w /System/Library/LaunchAgents/com.apple.ubd.plist

#-------------------------
# findmymac
#-------------------------
# /System/Library/PrivateFrameworks/FindMyMac.framework/Resources/FindMyMacMessenger.app/Contents/MacOS/FindMyMacMessenger
#
# $ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.findmymac.plist
# $ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.findmymacmessenger.plist

# % launchctl unload -w /System/Library/LaunchAgents/com.apple.findmymacmessenger.plist

#-------------------------
# awacsd
#-------------------------
# man awacsd
# awacsd -- Apple Wide Area Connectivity Service daemon
# awacsd is an executable invoked by launchd to facilitate connections between devices using Back to My Mac.
#
# $ sudo launchctl unload -w /System/Library/LaunchDaemons/com.apple.awacsd.plist

#-------------------------
# talagent
#-------------------------
# man talagent
# The talagent tool provides services related to the Transparent App Lifecycle feature.
#
# % launchctl unload -w /System/Library/LaunchAgents/com.apple.talagent.plist
# % launchctl unload -w /System/Library/LaunchAgents/com.apple.TMLaunchAgent.plist

# [WARNING: 要注意]
# 上記に点在するコマンドを実行すると、iPhone, iPod, iPad などのデバイスと
# うまく連携できなくなるかもしれません。
# また、iTunes も起動できなくなるかもしれません。起動できたとしても
# 思い通りに動かなくなるかもしれません。
# iCloud と一切交信できなくてもよい人向け entry なのです。

-- 至らない説明。

$ で始まる行は管理者ユーザ用。
% で始まる行は一般ユーザ。
実行するといずれも、
/var/db/launchd.db/
にキャッシュされる。

/System/Library/LaunchDaemons/
配下には daemon plist が保存されており、
/System/Library/LaunchAgents/
配下には daemon に対する agent plist が保存されている。

特定の daemon を 起動時に読み込むか読み込まないかは、
sudo launchctl [load|unload] ...
で制御可能(管理者のみ)。
だが、unload されていない daemon
(イコール load していて sudo launchctl list に表示されるdaemon)
が率いる agent を一般ユーザ側が unload しても反映されない。場合がある。

反映しているかしていないかは
一般ユーザで
% launchctl list
を実行すればわかる。
保存された launchctl を確認したい時は
% cat /var/db/launchd.db/.../.../overrides.plist
で確認できる。

-- 私が実行してみたものはこちら。
github.com/1330318
gist.github.com/1330308

-- その他
anonymous.librariand

$ sudo defaults read /System/Library/LaunchAgents/com.apple.librariand

なんのことやら。

$ man librariand
No manual entry for librariand
$ 


-- 感想
Apple は Mac OS X を iCloud やデバイス中心の OS にしてしまった。
Mac OS X 10.7 全ユーザが iCloud にログインすることを前提にしている。
iCloud やデバイスと無関係でいたいマシンも
そのようなプロセスと無関係でいられなくなってしまった。
Mac OS X は Snow Leopard 以降、変わってしまった。

textutil

めったに使わないコマンド textutil
-- rtf file を plain text に変換する方法 --

# /Libray で rtf file をみかけたら適当な名前でホームにコピーする。
% cp /Library/Documentation/AirPort\ Acknowledgements.rtf ~/airport-doc.rtf

# そのファイルの information を表示する。

% textutil -info airport-doc.rtf
File:  airport-doc.rtf
  Type:  rich text format (RTF)
  Size:  43836 bytes
  Length:  39382 characters
  Contents:  Acknowledgments...
% 


# ヘルプで確認する。
% textutil -help

...

-convert fmt convert each input file to format (txt, rtf, rtfd,
html, doc, docx, odt, wordml, or webarchive)


# txt に変換。

% textutil  ~/airport-doc.rtf  -convert txt -output ~/000.txt


# txt ファイルを読む。

% more ~/000.txt

org.vim.MacVim

mymvim.rb
http://midorex.blogspot.com/2011/11/mymvimrb.html
のその後。(いろいろ試してわかったこと)

自分で -g に設定していたものを削除。

% defaults delete -g NSQuitAlwaysKeepsWindows
% defaults delete -g ApplePersistenceIgnoreState

% su admin
$ sudo defaults delete /Library/Preferences/.GlobalPreferences NSQuitAlwaysKeepsWindows
$ sudo defaults delete /Library/Preferences/.GlobalPreferences ApplePersistenceIgnoreState
$


org.vim.MacVim からも追加したものを削除

% defaults delete org.vim.MacVim NSQuitAlwaysKeepsWindows


MacVim の環境設定
[Preference General]
Open untitled window: never
Open files from applications: with a tab for each file
After last window closes: Quit MacVim

% defaults read org.vim.MacVim
{
    MMAutosaveColumns = 150;
    MMAutosaveRows = 45;
    MMCurrentPreferencePane = General;
    MMLastWindowClosedBehavior = 2;
    MMNativeFullScreen = 0;
    MMOpenInCurrentWindow = 1;
    MMTopLeftPoint = "{516, 746}";
    MMUntitledWindow = 0;
    MMUseInlineIm = 0;
    NSNavLastRootDirectory = "~";
    NSNavPanelExpandedSizeForOpenMode = "{585, 418}";
    NSQuotedKeystrokeBinding = "";
    NSRepeatCountBinding = "";
    SUCheckAtStartup = 0;
}


これで、MacVim が起動していない時に、
MacVim 付属の mvim を動かしても shell が終了しないなんてことはなくなった。
mvim の exec は何も悪くなかった!。
せっかく作った mymvim.rb は残念だけど不要だった。

% syslog -w
をみていても変なメッセージはでてこなくなった。

MacVim 起動すると、
~/Library/Saved Application State/org.vim.MacVim.savedState
が生成されるけれど、MacVim を終了すると同時に自動的に消えてなくなってくれるようにもなった!。

感想。
今後 defaults -g に write するのはやめよう。

2011/11/04

mymvim.rb

MacVim 付属のmvim はshell で書かれている。
mvim で存在しないファイルを指定すると新規ファイルを作成する。
Lion で MacVim を起動していない時にうっかり mvim を動かすと
下記のようなエラーが残り shell 終了しない。

... MacVim[451:507] ApplePersistenceIgnoreState: Existing state will not be touched. New state will be written to ....

Ctr + C で終了するはめになる。

defaults write -g ApplePersistenceIgnoreState YES

を実行しているけれど
念のため org.vim.MacVim にも指定。

% defaults write org.vim.MacVim ApplePersistenceIgnoreState YES
% defaults read org.vim.MacVim
{
    ApplePersistenceIgnoreState = YES;
    MMAutosaveColumns = 150;
    MMAutosaveRows = 45;
    MMCurrentPreferencePane = General;
    MMLastWindowClosedBehavior = 1;
    MMNativeFullScreen = 0;
    MMOpenInCurrentWindow = 1;
    MMTopLeftPoint = "{208, 723}";
    MMUntitledWindow = 0;
    MMUseInlineIm = 0;
    NSQuotedKeystrokeBinding = "";
    NSRepeatCountBinding = "";
    SUCheckAtStartup = 0;
}

が...むなしく変化なく shell 終わらず。
そりゃそうかも。
mvim の最後の行あたり exec が怪しいのかも...

それで
Ruby 1.9.3 で mymvim.rb を作るいい口実ができた。
mymvim.rb は
MacVim が起動していなかったら、たぶん?正しく起動し、指定したファイルを開く。
存在しないファイルを指定してしまっても、何もおこらない。
おもったよりも早く動く。

ところが mymvim.rb を実行すると...

% ps aux  |grep task

=>
...(略) grep --color=auto task
...(略) /usr/libexec/taskgated -p -s

必ず /usr/libexec/taskgated が発生する。
数分すると自動的に消える。これは一体なんなのだろう。
Snow Leopard の時もあったような気もする...。

taskgated を sudo で kill しつつ
よくよく試してみると mymvim.rb だけでなく
MacVim 付属の mvim を実行したあとにも taskgated が発生していた。
ということは、
どのような方法で MacVim を起動したかってことは関係なくて、
MacVim を起動したこと、そのこと自体が taskgated を引き起こしている
ようだ。

MacVim をインストールしている ~/Application と
同じ階層にある他の Application を起動してみると...
taskgated が発生した。

dmg をダウンロードしてインストールした
/usr/local/git/bin/git
を動かしてみると、やはり発生した。

% ruby19 -e "puts \"hello\""
hello
% 

を実行しただけでも発生した。

しかし Lion にデフォルトで入っていた ruby 1.8 を動かしても発生しなかった。

% ruby  -e "puts \"hello\""
hello
% 


/Application にある Safari.app などを起動してみると
taskgated は発生しなかった。

つまり
Apple によって署名されていない Application やプログラムを起動すると
taskgated process が発生するってことかな。?

署名されたソフトウエアと、そうじゃないものの違い....

mymvim.rb

# coding: utf-8
# snapshot MacVim 7.3
# OS X Lion 10.7.2
# ruby 1.9.3p0 (2011-10-30 revision 33570) [x86_64-darwin11.2.0]

module Mymvim
  class Mvim
    def initialize(f)
      @f = f
      @osa = '/usr/bin/osascript'
      macvim = "Tell application \"MacVim\""
      @s1 = "#{macvim} to open (POSIX file \"#{@f}\") as string"
      @s2 = "#{macvim} to activate"
    end
    def base
      run
    end
    def run
      begin
        e = system("#{@osa} -e '#{@s1}' -e '#{@s2}'")
        exit unless e
      ensure
        print "bye\n"
        exit
      end
    end
  end
end

f = ARGV[0]
 unless File.exist?(f)
  print "Not found : #{f}\n"
  exit
end
Mymvim::Mvim.new(f).base