Correctly writes out the db.
[amarok_sonynw.git] / walkgirl / db.rb
blob7ee8552859669525c6898c1083f4988f42e60da3
1 ############################################################################
2 #    Copyright (C) 2008 by RafaƂ Rzepecki   #
3 #    divided.mind@gmail.com   #
4 #                                                                          #
5 #    This program is free software; you can redistribute it and#or modify  #
6 #    it under the terms of the GNU General Public License as published by  #
7 #    the Free Software Foundation; either version 2 of the License, or     #
8 #    (at your option) any later version.                                   #
9 #                                                                          #
10 #    This program is distributed in the hope that it will be useful,       #
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of        #
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         #
13 #    GNU General Public License for more details.                          #
14 #                                                                          #
15 #    You should have received a copy of the GNU General Public License     #
16 #    along with this program; if not, write to the                         #
17 #    Free Software Foundation, Inc.,                                       #
18 #    59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             #
19 ############################################################################
21 require 'iconv'
22 require 'id3'
24 class OrderedHash < Hash
25   alias_method :retrieve, :[]
26   alias_method :store, :[]=
27   alias_method :each_pair, :each
29   def initialize
30     @keys = []
31   end
33   def keys
34     @keys
35   end
37   def [](key)
38     if key.is_a? Integer
39       retrieve @keys[key]
40     else
41       super
42     end
43   end
45   def []=(key, val)
46     if key.is_a? Integer
47       store @keys[key], val
48     else
49       @keys << key unless include? key
50       super
51     end
52   end
54   def delete(key)
55     @keys.delete(key)
56     super
57   end
59   def each
60     @keys.each { |k| yield k, self[k] }
61   end
63   def each_key
64     @keys.each { |k| yield k }
65   end
67   def each_value
68     @keys.each { |k| yield self[k] }
69   end
71   def clear
72     @keys.clear
73     super
74   end
75 end
77 module ID3
78   TAG_LEN = 128
79   class Frame
80     def dump_short(key = "TIT2")
81       tag = self
82       tag = self.recode(2) if self["encoding"] == 0
83       [key, 0, tag.data].pack("a4ca#{TAG_LEN-5}")
84     end
85   end
86 end
88 module NetworkWalkman
89   class Db
90     COLLECTION_IDS = { "CONTENTTYPE" => 1, "ARTIST" => 2, "ALBUM" => 3}
91     CNTFILE = "04cntinf.dat"
92     attr_accessor :songs
93     attr_accessor :collections
94     attr_accessor :mount_point
95     
96     def initialize(mount_point, parse = true)
97       @mount_point = mount_point
99       if parse
100         cnt = dbfile(CNTFILE)
101         @songs = cnt.objects[0].records.map { |cnfb|
102           song = cnfb
103           song.tags.each { |key, tag|
104             song.tags[key] = tag.recode(0) if tag["encoding"] == 2
105           }
106           song
107         }
109         @collections = {}
110         COLLECTION_IDS.keys.each { |collection|
111           collection_id = COLLECTION_IDS[collection]
112           @collections[collection] = Collection.new(collection_id, self)
113         }
114       else
115         @songs = []
116         @collections = {}
117         COLLECTION_IDS.keys.each { |collection|
118           collection_id = COLLECTION_IDS[collection]
119           @collections[collection] = Collection.new(collection_id, self, false)
120         }
121       end
122     end
124     IMPORTANT_TAGS = [ 'ARTIST', 'TITLE', 'ALBUM', 'CONTENTTYPE' ]
125     def save
126       cnt = DbFile.new
127       cnt.magic = 'CNIF'
128       songs = DbObject.new
129       songs.magic = 'CNFB'
130       songs.rec_size = 656
131       @songs.each { |song|
132         record = Record::Cnfb.new
133         record.track_type = Record::Cnfb::OBFUSCATED_MP3
134         record.track_length = 0
135         record.tags = ID3::Tag2.new
136         record.frame_count = 0
137         if song
138           tag = ID3::Tag2.new
140           # choose right tags up to 5 tags limit
141           left = 5
142           song.tags.each do |k, v|
143             if IMPORTANT_TAGS.include? k
144               tag[k] = v
145               left -= 1
146             end
147           end
148           song.tags.each do |k, v|
149             if left > 0 and !IMPORTANT_TAGS.include? k
150               tag[k] = v
151               left -= 1
152             end
153           end
155           record.track_length = song.track_length
156           record.tags = tag
157           record.frame_count = song.frame_count
158         end
159         songs.records << record
160       }
161       cnt.objects['CNFB'] = songs
162       cntfile = File.new(dbfilename(CNTFILE), 'w')
163       cntfile << cnt
164       cntfile.close
165       @collections.values.each { |collection|
166         collection.save
167       }
168       0
169     end
171     class Collection < OrderedHash
172       def initialize(id, db, load = true)
173         super()
174         @db = db
175         @id = id
176         @tree_filename = sprintf("01tree%02x.dat", @id)
177         @ginf_filename = sprintf("03ginf%02x.dat", @id)
179         if load
180           tree = @db.dbfile(@tree_filename)
181           gplb = tree.objects['GPLB']
182           tplb = tree.objects['TPLB']
183           ginf = @db.dbfile(@ginf_filename)
184           gpfb = ginf.objects['GPFB']
186           gpfb.records.each { |r| p r; self[r.text] = [] }
187           
188           starts = OrderedHash.new
189           gplb.records.sort_by{|r|r.starts_on_list}.each{ |r| p r
190             starts[self.keys[r.description_slot]] = r.starts_on_list
191           }
192           (0...starts.length).each{ |i|
193             last = starts[i+1] || (tplb.records.length + 1)
194             self[starts.keys[i]] = tplb.records.map{|r|r.song_slot}[(starts[i]-1)...(last-1)]
195           }
196         end
197       end
199       def [](key)
200         super || (self[key] = [])
201       end
203       def save
204         ginf = DbFile.new
205         ginf.magic = 'GPIF'
206         gpfb = DbObject.new
207         gpfb.magic = 'GPFB'
209         tree = DbFile.new
210         tree.magic = 'TREE'
211         gplb = DbObject.new
212         gplb.magic = 'GPLB'
213         gplb.obj_size = 0x4010
214         tplb = DbObject.new
215         tplb.magic = 'TPLB'
217         didx = 1
218         sidx = 1
219         self.keys.each {|key|
220           record = Record::Gpfb.new
221           record.text = key
222           gpfb.records << record
224           record = Record::Gplb.new
225           record.description_slot = didx
226           didx += 1
227           record.starts_on_list = sidx
228           sidx += self[key].length
229           gplb.records << record
230           self[key].each{ |s|
231             record = Record::Tplb.new
232             record.song_slot = s
233             tplb.records << record
234           }
235         }
236         ginf.objects['GPFB'] = gpfb
237         tree.objects['GPLB'] = gplb
238         tree.objects['TPLB'] = tplb
239         ginffile = File.new(@db.dbfilename(@ginf_filename), 'w')
240         ginffile << ginf
241         ginffile.close
242         treefile = File.new(@db.dbfilename(@tree_filename), 'w')
243         treefile << tree
244         treefile.close
245       end
246     end
248     def dbfilename(name)
249       File.join([@mount_point, "omgaudio", name])
250     end
251     def dbfile(name)
252       DbFile.new(File.open(dbfilename(name)){|file|file.read})
253     end
255     module Id3
256       FRAME2SYMBOL = ID3::Framename2symbol["2.4.0"]
257       SYMBOL2FRAME = ID3::SUPPORTED_SYMBOLS["2.4.0"]
259       class Frame < Hash
260         def initialize(*a)
261           if a[0].is_a?(Hash)
262             self.merge! a[0]
263           end
264         end
265         def to_s(*a)
266           limit = ""
267           if a[0]
268             limit = a[0] - 6
269           end
270           [SYMBOL2FRAME[self[:tag]], self[:encoding], Id3.encode(self[:encoding], self[:content])].
271           pack "a4nZ#{limit}"
272         end
273       end
274       
275       def Id3.frame(tag, content, encoding)
276         tag = FRAME2SYMBOL[tag] || tag
277         content = content
278         Frame.new :tag => tag, :content => Id3.decode(encoding, content), :encoding => encoding
279       end
280       def Id3.decode(encoding, content)
281         case encoding
282         when 2: # UTF-16BE
283           Iconv.conv('utf8', 'utf16be', content)
284         else
285           raise "Unknown id3 encoding (#{encoding})"
286         end
287       end
288       def Id3.encode(encoding, content)
289         case encoding
290         when 2: # UTF-16BE
291           Iconv.conv('utf16be', 'utf8', content)
292         else
293           raise "Unknown id3 encoding (#{encoding})"
294         end
295       end
296     end
297     
298     module Record
299       class Record
300         def initialize(*a)
301           if a.length == 1 and a[0].is_a?(String)
302             parse a[0]
303           end
304         end
305       end
307       class Tplb < Record
308         attr_accessor :song_slot
309         def parse(content)
310           @song_slot = content.unpack("n")[0]
311         end
312         def to_s
313           [@song_slot].pack("n")
314         end
315         def length
316           2
317         end
318       end
320       class Gpfb < Record
321         attr_accessor :text
322         def to_s
323           [0, 0, 0x2eeeb, 1, 128].pack("N3n2") + ID3::Frame.new("ARTIST", "2.4.0", "\0#{@text}").recode(2).dump_short
324         end
325         def parse(content)
326           frame = ID3::Frame.new("TITLE", "2.4.0", content.unpack("a21A*")[1])
327           @text = frame.recode(0)["text"]
328         end
329         def length
330           16 + 128
331         end
332       end
334       class Gplb < Record
335         attr_accessor :description_slot
336         attr_accessor :starts_on_list # index of where it starts in Tplb
337         def parse(content)
338           @description_slot, _, @starts_on_list, _ = content.unpack("n4")
339         end
340         def to_s
341           [@description_slot, 0x0100, @starts_on_list, 0x0000].pack("n4")
342         end
343         def length
344           8
345         end
346       end
348       class Cnfb < Record
349         attr_accessor :track_length, :frame_count, :tags, :content
350         attr_reader :track_type
351         OBFUSCATED_MP3 = [ 0, 0, 0xFF, 0xFE ]
352         PLAIN_MP3 = [ 0, 0, 0xFF, 0xFF ]
353         TAG_LEN = 128
354         def track_type=(new_type)
355           @track_type = new_type
356           @track_type.instance_eval do
357             def inspect
358               case self
359               when OBFUSCATED_MP3:
360                 "OBFUSCATED_MP3"
361               when PLAIN_MP3:
362                 "PLAIN_MP3"
363               else
364                 super
365               end
366             end
367             def to_s
368               pack("C4")
369             end
370           end
371         end
372         def parse(contents)
373           track_type, @track_length, @frame_count, num_tags, tag_size, contents =
374           contents.unpack("a4NNnna*")
376           self.track_type = track_type.unpack("C4")
378           @tags = ID3::Tag2.new
379           (0...num_tags).each do
380             tag, contents = contents.unpack("a#{tag_size}a*")
381             tag, _, content = tag.unpack("a4aA*")
382             symbol = ID3::Framename2symbol["2.4.0"][tag]
383             if symbol
384               @tags[symbol] = ID3::Frame.new(symbol, "2.4.0", content)
385             end
386           end
387         end
388         def to_s
389           s = [@track_type.to_s, @track_length, @frame_count, @tags.length, TAG_LEN].pack("a4NNnn@16") +
390           @tags.map do |t,v|
391             v.dump_short(ID3::SUPPORTED_SYMBOLS["2.4.0"][t])
392           end.join
393         end
394         def length
395           16 + @tags.length * TAG_LEN
396         end
397       end
398     end
400     class DbObject
401       attr_accessor :magic, :records, :rec_size, :obj_size
402       def initialize(*a)
403         @records = []
404         @magic = "CAFE"
405         if a.length == 2 and a[1].is_a?(String)
406           parse a[1]
407         end
408       end
409       def parse(contents)
410         @magic, rec_count, @rec_size, contents = contents.unpack("a4nnx8a*")
411         record_klass = begin
412           Record.const_get(magic.capitalize)
413         rescue NameError
414           String
415         end
416         (0...rec_count).each do
417           record, contents = contents.unpack("a#{rec_size}a*")
418           @records << record_klass.new(record)
419         end
420       end
421       def to_s
422         @rec_size ||= @records[0].length if @records[0]
423         @rec_size ||= 0
424         res = [@magic, @records.length, rec_size, @records.length].pack("a4nnN@16") +
425           @records.map{|r|[r.to_s].pack("Z#{rec_size}")}.join
426         if obj_size
427           [res].pack("a#{obj_size}")
428         else
429           res
430         end
431       end
432     end
434     class DbObjectPointer
435       attr_reader :magic, :range
436       def initialize(*a)
437         if a.length == 1 and a[0].is_a?(String)
438           parse a[0]
439         elsif a.length == 3
440           @magic, @range = a[0], (a[1]...(a[1]+a[2]))
441         end
442       end
443       def parse(contents)
444         @magic, offset, length = contents.unpack("a4NN")
445         @range = offset...(offset+length)
446       end
447       def to_s
448         [magic, range.first, range.last - range.first].pack("a4NN@16")
449       end
450     end
452     class DbFile
453       attr_accessor :magic, :objects
454       def initialize(*a)
455         @objects = OrderedHash.new
456         if a.length == 1 and a[0].is_a?(String)
457           parse a[0]
458         end
459       end
460       def parse(contents)
461         @magic, numobjects = contents.unpack("a4x4C")
462         objects = []
463         (1..numobjects).each do |objindex|
464           start = objindex * 16
465           fin = objindex * 16 + 16
466           objects << DbObjectPointer.new(contents[start...fin])
467         end
468         objects.each do |obj_pointer|
469           @objects[obj_pointer.magic] = DbObject.new(obj_pointer.magic, contents[obj_pointer.range])
470         end
471       end
472       def to_s
473         header = [@magic, 1, 1, 0, 0, objects.length].pack("a4C4C@16")
474         pointers = ""
475         objects = ""
476         offset = 16 + @objects.length * 16
477         @objects.each_value do |object|
478           object_raw = object.to_s
479           pointers += DbObjectPointer.new(object.magic, offset, object_raw.length).to_s
480           offset += object_raw.length
481           objects += object_raw
482         end
483         header + pointers + objects
484       end
485     end
486   end