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