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



