Allow inserting mp3 without tags.
[amarok_sonynw.git] / walkgirl / db.rb
blob8b604b02f32d1dfbebed748cd1c41b4db5ccc95a
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)
83       begin
84         [key, 0, tag.data].pack("a4ca#{TAG_LEN-5}")
85       rescue
86         [key, 0, ""].pack("a4ca#{TAG_LEN-5}")
87       end
88     end
89   end
90 end
92 module NetworkWalkman
93   module Song
94     attr_accessor :slot
95     def path
96       File.join(['omgaudio', sprintf('10f%02x', slot >> 8), sprintf('1%07x.oma', slot & 0xff)])
97     end
98   end
99   class Db
100     COLLECTION_IDS = { "CONTENTGROUP" => 1, "ARTIST" => 2, "ALBUM" => 3, "CONTENTTYPE" => 4}
101     CNTFILE = "04cntinf.dat"
102     attr_accessor :songs
103     attr_accessor :collections
104     attr_accessor :mount_point
105     
106     def initialize(mount_point, parse = true)
107       @mount_point = mount_point
109       if parse
110         cnt = dbfile(CNTFILE)
111         slot = 1
112         @songs = cnt.objects[0].records.map { |cnfb|
113           song = cnfb
114           song.tags.each { |key, tag|
115             song.tags[key] = tag.recode(3)
116           }
117           song.extend Song
118           song.slot = slot
119           slot += 1
120           song
121         }
123         @collections = {}
124         COLLECTION_IDS.keys.each { |collection|
125           collection_id = COLLECTION_IDS[collection]
126           @collections[collection] = Collection.new(collection_id, self)
127         }
128       else
129         @songs = []
130         @collections = {}
131         COLLECTION_IDS.keys.each { |collection|
132           collection_id = COLLECTION_IDS[collection]
133           @collections[collection] = Collection.new(collection_id, self, false)
134         }
135       end
136     end
138     def remove(song)
139       # first remove it from collections
140       collections.each { |_, collection|
141         collection.each { |key, songs|
142           songs.delete song
143           collection.delete key if songs.empty?
144         }
145       }
147       # then blank all data fields
148       song.tags = ID3::Tag2.new
149       song.track_length = 0
150       song.frame_count = 0
152       # finally delete the file
153       File.delete(File.join([mount_point, song.path]))
154     end
156     def really_exists(song)
157       File.exists?(File.join([mount_point, song.path]))
158     end
160     def add(song)
161       songs[song.slot-1] = song
162       collections.each { |k, c|
163         key = song.tags[k]["text"] if song.tags[k] && song.tags[k]["text"]
164         key ||= "Other"
165         c[key] << song
166       }
167     end
169     IMPORTANT_TAGS = [ 'ARTIST', 'TITLE', 'ALBUM', 'CONTENTTYPE' ]
170     def save
171       cnt = DbFile.new
172       cnt.magic = 'CNIF'
173       songs = DbObject.new
174       songs.magic = 'CNFB'
175       songs.rec_size = 656
176       @songs.each { |song|
177         record = Record::Cnfb.new
178         record.track_type = Record::Cnfb::OBFUSCATED_MP3
179         record.track_length = 0
180         record.tags = ID3::Tag2.new
181         record.frame_count = 0
182         if song
183           tag = ID3::Tag2.new
185           # choose right tags up to 5 tags limit
186           left = 5
187           song.tags.each do |k, v|
188             if IMPORTANT_TAGS.include? k
189               tag[k] = v
190               left -= 1
191             end
192           end
193           song.tags.each do |k, v|
194             if left > 0 and !IMPORTANT_TAGS.include? k
195               tag[k] = v
196               left -= 1
197             end
198           end
200           record.track_length = song.track_length
201           record.tags = tag
202           record.frame_count = song.frame_count
203         end
204         songs.records << record
205       }
206       cnt.objects['CNFB'] = songs
207       cntfile = File.new(dbfilename(CNTFILE), 'w')
208       cntfile << cnt
209       cntfile.close
210       @collections.values.each { |collection|
211         collection.save
212       }
213       nil
214     end
216     class Collection < OrderedHash
217       def initialize(id, db, load = true)
218         super()
219         @db = db
220         @id = id
221         @tree_filename = sprintf("01tree%02x.dat", @id)
222         @ginf_filename = sprintf("03ginf%02x.dat", @id)
224         if load
225           tree = @db.dbfile(@tree_filename)
226           gplb = tree.objects['GPLB']
227           tplb = tree.objects['TPLB']
228           ginf = @db.dbfile(@ginf_filename)
229           gpfb = ginf.objects['GPFB']
231           gpfb.records.each { |r| self[r.text] = [] }
232           
233           starts = OrderedHash.new
234           gplb.records.sort_by{|r|r.starts_on_list}.each{ |r|             starts[self.keys[r.description_slot-1]] = r.starts_on_list
235           }
236           starts
237           (0...starts.length).each{ |i|
238             last = starts[i+1] || (tplb.records.length + 1)
239             self[starts.keys[i]] = tplb.records.map{|r|r.song_slot}[(starts[i]-1)...(last-1)].map{|slot| @db.songs[slot-1] }
240           }
241         end
242       end
244       def [](key)
245         super || (self[key] = [])
246       end
248       def save
249         ginf = DbFile.new
250         ginf.magic = 'GPIF'
251         gpfb = DbObject.new
252         gpfb.magic = 'GPFB'
254         tree = DbFile.new
255         tree.magic = 'TREE'
256         gplb = DbObject.new
257         gplb.magic = 'GPLB'
258         gplb.obj_size = 0x4010
259         tplb = DbObject.new
260         tplb.magic = 'TPLB'
262         didx = 1
263         sidx = 1
264         self.keys.each {|key|
265           record = Record::Gpfb.new
266           record.text = key
267           gpfb.records << record
269           record = Record::Gplb.new
270           record.description_slot = didx
271           didx += 1
272           record.starts_on_list = sidx
273           sidx += self[key].length
274           gplb.records << record
275           self[key].each{ |s|
276             record = Record::Tplb.new
277             record.song_slot = s.slot
278             tplb.records << record
279           }
280         }
281         ginf.objects['GPFB'] = gpfb
282         tree.objects['GPLB'] = gplb
283         tree.objects['TPLB'] = tplb
284         ginffile = File.new(@db.dbfilename(@ginf_filename), 'w')
285         ginffile << ginf
286         ginffile.close
287         treefile = File.new(@db.dbfilename(@tree_filename), 'w')
288         treefile << tree
289         treefile.close
290       end
291     end
293     def dbfilename(name)
294       File.join([@mount_point, "omgaudio", name])
295     end
296     def dbfile(name)
297       DbFile.new(File.open(dbfilename(name)){|file|file.read})
298     end
300     module Id3
301       FRAME2SYMBOL = ID3::Framename2symbol["2.4.0"]
302       SYMBOL2FRAME = ID3::SUPPORTED_SYMBOLS["2.4.0"]
304       class Frame < Hash
305         def initialize(*a)
306           if a[0].is_a?(Hash)
307             self.merge! a[0]
308           end
309         end
310         def to_s(*a)
311           limit = ""
312           if a[0]
313             limit = a[0] - 6
314           end
315           [SYMBOL2FRAME[self[:tag]], self[:encoding], Id3.encode(self[:encoding], self[:content])].
316           pack "a4nZ#{limit}"
317         end
318       end
319       
320       def Id3.frame(tag, content, encoding)
321         tag = FRAME2SYMBOL[tag] || tag
322         content = content
323         Frame.new :tag => tag, :content => Id3.decode(encoding, content), :encoding => encoding
324       end
325       def Id3.decode(encoding, content)
326         case encoding
327         when 2: # UTF-16BE
328           Iconv.conv('utf8', 'utf16be', content)
329         else
330           raise "Unknown id3 encoding (#{encoding})"
331         end
332       end
333       def Id3.encode(encoding, content)
334         case encoding
335         when 2: # UTF-16BE
336           Iconv.conv('utf16be', 'utf8', content)
337         else
338           raise "Unknown id3 encoding (#{encoding})"
339         end
340       end
341     end
342     
343     module Record
344       class Record
345         def initialize(*a)
346           if a.length == 1 and a[0].is_a?(String)
347             parse a[0]
348           end
349         end
350       end
352       class Tplb < Record
353         attr_accessor :song_slot
354         def parse(content)
355           @song_slot = content.unpack("n")[0]
356         end
357         def to_s
358           [@song_slot].pack("n")
359         end
360         def length
361           2
362         end
363       end
365       class Gpfb < Record
366         attr_accessor :text
367         def to_s
368           [0, 0, 0x2eeeb, 1, 128].pack("N3n2") + ID3::Frame.new("ARTIST", "2.4.0", "\3#{@text}").recode(2).dump_short
369         end
370         def parse(content)
371           frame = ID3::Frame.new("TITLE", "2.4.0", content.unpack("a21A*")[1])
372           @text = frame.recode(0)["text"]
373         end
374         def length
375           16 + 128
376         end
377       end
379       class Gplb < Record
380         attr_accessor :description_slot
381         attr_accessor :starts_on_list # index of where it starts in Tplb
382         def parse(content)
383           @description_slot, _, @starts_on_list, _ = content.unpack("n4")
384         end
385         def to_s
386           [@description_slot, 0x0100, @starts_on_list, 0x0000].pack("n4")
387         end
388         def length
389           8
390         end
391       end
393       class Cnfb < Record
394         attr_accessor :track_length, :frame_count, :tags, :content
395         attr_reader :track_type
396         OBFUSCATED_MP3 = [ 0, 0, 0xFF, 0xFE ]
397         PLAIN_MP3 = [ 0, 0, 0xFF, 0xFF ]
398         TAG_LEN = 128
399         def track_type=(new_type)
400           @track_type = new_type
401           @track_type.instance_eval do
402             def inspect
403               case self
404               when OBFUSCATED_MP3:
405                 "OBFUSCATED_MP3"
406               when PLAIN_MP3:
407                 "PLAIN_MP3"
408               else
409                 super
410               end
411             end
412             def to_s
413               pack("C4")
414             end
415           end
416         end
417         def parse(contents)
418           track_type, @track_length, @frame_count, num_tags, tag_size, contents =
419           contents.unpack("a4NNnna*")
421           self.track_type = track_type.unpack("C4")
423           @tags = ID3::Tag2.new
424           (0...num_tags).each do
425             tag, contents = contents.unpack("a#{tag_size}a*")
426             tag, _, content = tag.unpack("a4aA*")
427             symbol = ID3::Framename2symbol["2.4.0"][tag]
428             if symbol
429               @tags[symbol] = ID3::Frame.new(symbol, "2.4.0", content)
430             end
431           end
432         end
433         def to_s
434           s = [@track_type.to_s, @track_length, @frame_count, @tags.length, TAG_LEN].pack("a4NNnn@16") +
435           @tags.map do |t,v|
436             v.dump_short(ID3::SUPPORTED_SYMBOLS["2.4.0"][t])
437           end.join
438         end
439         def length
440           16 + @tags.length * TAG_LEN
441         end
442       end
443     end
445     class DbObject
446       attr_accessor :magic, :records, :rec_size, :obj_size
447       def initialize(*a)
448         @records = []
449         @magic = "CAFE"
450         if a.length == 2 and a[1].is_a?(String)
451           parse a[1]
452         end
453       end
454       def parse(contents)
455         @magic, rec_count, @rec_size, contents = contents.unpack("a4nnx8a*")
456         record_klass = begin
457           Record.const_get(magic.capitalize)
458         rescue NameError
459           String
460         end
461         (0...rec_count).each do
462           record, contents = contents.unpack("a#{rec_size}a*")
463           @records << record_klass.new(record)
464         end
465       end
466       def to_s
467         @rec_size ||= @records[0].length if @records[0]
468         @rec_size ||= 0
469         res = [@magic, @records.length, rec_size, @records.length].pack("a4nnN@16") +
470           @records.map{|r|[r.to_s].pack("Z#{rec_size}")}.join
471         if obj_size
472           [res].pack("a#{obj_size}")
473         else
474           res
475         end
476       end
477     end
479     class DbObjectPointer
480       attr_reader :magic, :range
481       def initialize(*a)
482         if a.length == 1 and a[0].is_a?(String)
483           parse a[0]
484         elsif a.length == 3
485           @magic, @range = a[0], (a[1]...(a[1]+a[2]))
486         end
487       end
488       def parse(contents)
489         @magic, offset, length = contents.unpack("a4NN")
490         @range = offset...(offset+length)
491       end
492       def to_s
493         [magic, range.first, range.last - range.first].pack("a4NN@16")
494       end
495     end
497     class DbFile
498       attr_accessor :magic, :objects
499       def initialize(*a)
500         @objects = OrderedHash.new
501         if a.length == 1 and a[0].is_a?(String)
502           parse a[0]
503         end
504       end
505       def parse(contents)
506         @magic, numobjects = contents.unpack("a4x4C")
507         objects = []
508         (1..numobjects).each do |objindex|
509           start = objindex * 16
510           fin = objindex * 16 + 16
511           objects << DbObjectPointer.new(contents[start...fin])
512         end
513         objects.each do |obj_pointer|
514           @objects[obj_pointer.magic] = DbObject.new(obj_pointer.magic, contents[obj_pointer.range])
515         end
516       end
517       def to_s
518         header = [@magic, 1, 1, 0, 0, objects.length].pack("a4C4C@16")
519         pointers = ""
520         objects = ""
521         offset = 16 + @objects.length * 16
522         @objects.each_value do |object|
523           object_raw = object.to_s
524           pointers += DbObjectPointer.new(object.magic, offset, object_raw.length).to_s
525           offset += object_raw.length
526           objects += object_raw
527         end
528         header + pointers + objects
529       end
530     end
531   end