I like iPod

曲数の遷移

以下のグラフはiTunesのライブラリのxmlから生成した曲数の遷移です。

http://www2a.biglobe.ne.jp/~seki/ruby/songs0409.jpg

'03-11から始まっているのは、そのときにディスククラッシュがあったから。 過去半年の時速は 130曲/月らしい。

plistplist.rbを使ってこんなスクリプトで抽出。 元のデータはiTunesからライブラリをXML出力したファイルです。5MBくらいのファイルを処理させたら iBookG4で6分かかった。

お。[RAA:xmlscan]を使うバージョンscan_plist.rbでは、1分くらいだ。速い。

require 'plist'

def main
  plist = Plist.file_to_plist(ARGV.shift)

  bin = Hash.new(0)
  plist['Tracks'].each do |track, dict|
    key = dict['Date Added'].strftime('%Y-%m')
    bin[key] += 1     
  end

  bin.keys.sort.inject(0) do |sum, key|
    sum += bin[key]
    puts "#{key} #{sum}"
    sum
  end
end

main

ある期間でたくさん聴いたやつを調べたいなあ。

4/24 - 5/24

Play Time (song) (total: 111.3H)

1.0H The Right Time / Monday満ちる
0.8H Be Who You Are / Monday満ちる
0.8H Watertight (Napt Remix) / MJ Cole Feat. Laura Vane
0.7H Yikes! Peach Cut 5' 24"!! (by HANDSOMEBOY TECHNIQUE) / ピチカート・ファイヴ
0.7H Bodyswerve (Grant Nelson Remix) / M-Gee Feat. Mica Paris
0.7H Dig Deep / Monday満ちる
0.7H Jill / Paris Match
0.6H Saturday Night / Paris Match
0.6H Lovin' You More (That Big Track) / Steve Mac Vs. Mosquito Feat. Steve Smith
0.6H ボーイ フレンド / AYUSE KOZUE

Play Time (artist) (total: 111.3H)

7.6H Paris Match
7.1H Monday満ちる
3.0H Halfby
2.8H Pizzicato Five
2.6H スキマスイッチ
2.6H Infracom!
2.5H AYUSE KOZUE
2.4H [Re:Jazz]
2.4H Bonnie Pink
2.3H フジ子・ヘミング

Play Time (album) (total: 111.3H)

7.1H ROUTES
5.3H After Six
4.2H MOTIVATION3 compiled by TOWA TEI
3.1H Jazflora
3.0H Green Hours
2.6H [re:jazz]
2.4H [Re:Jazz] (Re:Mix)
2.2H Pizzicato Five CUT UP - Single
2.0H Modern Sounds From Italy 2
1.9H 憂愁のノクターン

集計スクリプト

何度も実行するとXMLを読み込む時間がバカにならないので、Marshalでキャッシュする。 iTunes Music Library.xmlを扱うクラスはどんなインターフェイスが良いんだろう。 もっとたくさんアプリを書かないと想像できないな。

以前(?)Marshalで保存した結果との再生回数の差分をとれるようにしてみた。(2005-06-04)

# itml.rb
require 'scan_plist'

class ITunesMusicLibrary
  def initialize
    setup_fname
  end

  def setup_fname
    @xml_fname = File.expand_path('~/Music/iTunes/iTunes Music Library.xml')
    @dump_fname = File.expand_path('~/iTML.dump')
  end

  def load_plist
    modified_xml = File.mtime(@xml_fname)
    modified_dump = File.mtime(@dump_fname) rescue nil

    if modified_dump && modified_xml < modified_dump
      return load_dump(@dump_fname)
    else
      if modified_dump
        suffix = modified_dump.strftime("-%y%m%d")
        File.rename(@dump_fname, @dump_fname + suffix)
      end
      plist = load_xml(@xml_fname)
      File.open(@dump_fname, 'w') { |fp| fp.write(Marshal.dump(plist)) }
      return plist
    end
  end

  def load_xml(fname)
    Plist.file_to_plist(fname)
  end

  def load_dump(fname)
    File.open(fname) { |fp| Marshal.load(fp) }
  end
end

class TopRateReporter
  def initialize(plist)
    @plist = plist
  end
  attr_reader :plist

  def subtract_playcount(old_plist)
    rev = {}
    old_plist['Tracks'].each do |k, v|
      pid = v['Persistent ID']
      rev[pid] = v
    end

    @plist['Tracks'].each do |k, v|
      track = rev[v['Persistent ID']]
      next unless track
      count = v['Play Count'].to_i
      p(v) unless v['Location'] == track['Location']

      old_count = track['Play Count'].to_i
      v['Play Count'] = count - old_count
    end
  end

  def report_play_time_by_song
    report_time(play_time(nil), 'song')
  end

  def report_play_count_by_song
    report_count(play_count(nil), 'song')
  end

  def report_play_time_by_artist
    report_time(play_time('Artist'), 'artist')
  end

  def report_play_time_by_album
    report_time(play_time('Album'), 'album')
  end

  def report_play_count_by_artist
    report_count(play_count('Artist'), 'artist')
  end

  def report_play_count_by_album
    report_count(play_count('Album'), 'album')
  end

  def play_time(key_name = 'Artist')
    make_bin(key_name) { |v| v['Total Time'].to_i * v['Play Count'].to_i }
  end

  def play_count(key_name = 'Artist')
    make_bin(key_name) { |v| v['Play Count'].to_i }
  end

  private
  class SongKey
    def initialize(track_id, value)
      @track_id = track_id
      @value = value
    end

    def hash
      @track_id.hash
    end

    def to_s
      [@value['Name'], @value['Artist']].join(' / ')
    end
  end

  def make_bin(key_name = 'Artist')
    @plist['Tracks'].inject({:all => 0}) { |bin, kv|
      v = kv[1]
      if key_name
        key = v[key_name]
      else
        key = SongKey.new(kv[0], kv[1])
      end
      bin[key] ||= 0
      value = yield(v)
      bin[key] += value
      bin[:all] += value
      bin
    }.sort_by { |kv| -1 * kv[1] }
  end

  def report_time(result, caption)
    printf("=== Play Time (%s) (total: %6.1fH)", caption, result[0][1] / 3600000.0)
    puts
    result[1, 20].each { |v|
      hour = v[1] / (3600000.0)
      printf("%6.1fH %s", hour, v[0])
      puts
    }
    puts
  end

  def report_count(result, caption)
    printf("=== Play Count (%s) (total: %d)", caption, result[0][1])
    puts
    result[1, 20].each { |v|
      printf(" %6d %s", v[1], v[0])
      puts
    }
    puts
  end
end

def main(diff = nil)
  iml = ITunesMusicLibrary.new
  app = TopRateReporter.new(iml.load_plist)

  app.report_play_time_by_song
  app.report_play_time_by_artist
  app.report_play_time_by_album
  app.report_play_count_by_song
  app.report_play_count_by_artist
  app.report_play_count_by_album

  return unless diff

  a = iml.load_dump(File.expand_path(diff))
  app.subtract_playcount(a)

  app.report_play_time_by_song
  app.report_play_time_by_artist
  app.report_play_time_by_album
  app.report_play_count_by_song
  app.report_play_count_by_artist
  app.report_play_count_by_album
end

main(ARGV.shift)