plist
MacOS Xではplistというファイル形式でちょっとした永続化を行うことが多い。 iTunesのプレイリストもその一つ。
OSXのplist
iTunesの出力するプレイリストから、曲名とアーティストを印字したかったので ちょっと書いてみた。須藤さんのページを眺めながらREXML初挑戦。
そういえばplistのdtdを見つけた。
dataを実装してないから、そのうち書こう。
iTunesのライブラリを全て処理すると時間がかかるのでXMLScanを 使った実装(scan_plist.rb)も用意してみた。こっちは3倍くらい速い。
plist.rb
plistを解釈するライブラリって既にありそう。 今回利用しないdateはめんどくさかったのでFIXME。
- レシピ本を読んだらTime.parseを見つけたのでとりあえずそれを使うようにした。
- Peter McMasterさんからコメントをもらったので、ちょっと反映した。
# plist.rb require 'rexml/document' require 'time' class Plist def self.file_to_plist(fname) File.open(fname) do |fp| doc = REXML::Document.new(fp) return self.new.visit(REXML::XPath.match(doc, '/plist/')[0]) end end def initialize setup_method_table end def visit(node) visit_one(node.elements[1]) end def visit_one(node) choose_method(node.name).call(node) end def visit_null(node) p node if $DEBUG nil end def visit_dict(node) dict = {} es = node.elements.to_a while key = es.shift next unless key.name == 'key' dict[key.text] = visit_one(es.shift) end dict end def visit_array(node) node.elements.collect do |x| visit_one(x) end end def visit_integer(node) node.text.to_i end def visit_real(node) node.text.to_f end def visit_string(node) node.text.to_s end def visit_date(node) Time.parse(node.text.to_s) end def visit_true(node) true end def visit_false(node) false end private def choose_method(name) @method.fetch(name, method(:visit_null)) end def setup_method_table @method = {} @method['dict'] = method(:visit_dict) @method['integer'] = method(:visit_integer) @method['real'] = method(:visit_real) @method['string'] = method(:visit_string) @method['date'] = method(:visit_date) @method['true'] = method(:visit_true) @method['false'] = method(:visit_false) @method['array'] = method(:visit_array) end end
playlist.rb
MacOSXのplistをHash, ArrayなどRubyのオブジェクトで表現してから プレイリストを取り出すことにした。
# playlist.rb require 'plist' class MyITunesLibrary Song = Struct.new(:track_id, :name, :artist, :album) class Song def self.new_with_dict(dict) self.new(dict['Track ID'], dict['Name'], dict['Artist'], dict['Album']) end end class Playlist def self.new_with_dict(library, dict) playlist = self.new(dict['Name']) dict['Playlist Items'].each do |track| track_id = track['Track ID'] playlist.add_track(library[track_id]) end playlist end def initialize(name) @name = name @track = [] end attr_reader :name, :track def add_track(song) @track << song end end def initialize @song = Hash.new @playlist = Hash.new end attr_reader :song, :playlist def [](track_id) @song[track_id] end def add_song(song) @song[song.track_id] = song end def add_playlist(playlist) @playlist[playlist.name] = playlist end def import_plist(plist) plist['Tracks'].each do | track, dict | add_song(Song.new_with_dict(dict)) end plist['Playlists'].each do | dict | add_playlist(Playlist.new_with_dict(self, dict)) end end end def main plist = Plist.file_to_plist(ARGV.shift) lib = MyITunesLibrary.new lib.import_plist(plist) lib.playlist.each do |name, it| puts name it.track.each_with_index do |x, idx| printf("%3d. %s / %s", idx + 1, x.name, x.artist) puts end end end main
scan_plist.rb
iTunesのライブラリを読み込むのに数分かかる。 REXMLの代わりに[RAA:XMLScan]を使って時間を測ってみた
# scan_plist.rb require 'xmlscan/parser' require 'time' class Plist class Visitor include XMLScan::Visitor def initialize @stack = [] @preserve = false end attr_reader :stack def on_stag(name) sym = name.intern push(sym) @preserve = [:string, :key, :data].include?(sym) end def on_chardata(str) return if !@preserve && /^\s*$/ =~ str if tail.class == String str = pop + str end push(str) end def on_stag_end_empty(name) @preserve = false case name when 'true' eval_true when 'false' eval_true end end def on_etag(name) @preserve = false case name when 'dict' eval_dict when 'integer' eval_integer when 'real' eval_real when 'string' eval_string when 'date' eval_date when 'array' eval_array when 'key' eval_key when 'plist' eval_plist end end def eval_integer value = pop.to_i pop_stag(:integer) push(value) end def eval_real value = pop.to_f pop_stag(:real) push(value) end def eval_string value = pop_str pop_stag(:string) push(value) end def eval_key value = pop pop_stag(:key) push(value) end def eval_date value = Time.parse(pop) pop_stag(:date) push(value) end def eval_true pop_stag(:true) push(true) end def eval_false pop_stag(:false) push(false) end def eval_array value = [] while v = pop break if v == :array value.push(v) end push(value) end def eval_dict value = {} while v = pop break if v == :dict k = pop break if k == :dict #FIXME value[k] = v end push(value) end def eval_plist value = pop pop_stag(:plist) push(value) end def push(obj) @stack.push(obj) end def pop @stack.pop end def pop_str (tail.class == String) ? pop : '' end def pop_stag(sym) return pop unless $DEBUG v = pop raise([sym, v].inspect) unless v == sym return v end def tail @stack[-1] end end def self.file_to_plist(fname) File.open(fname) do |f| visitor = Visitor.new parser = XMLScan::XMLParser.new(visitor) parser.parse(f) visitor.tail end end end