1 ############################################################################
2 # Copyright (C) 2008 by RafaĆ Rzepecki #
3 # divided.mind@gmail.com #
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. #
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. #
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 ############################################################################
24 class OrderedHash < Hash
25 alias_method :retrieve, :[]
26 alias_method :store, :[]=
27 alias_method :each_pair, :each
49 @keys << key unless include? key
60 @keys.each { |k| yield k, self[k] }
64 @keys.each { |k| yield k }
68 @keys.each { |k| yield self[k] }
80 def dump_short(key = "TIT2")
82 tag = self.recode(2) if self["encoding"] == 0
83 [key, 0, tag.data].pack("a4ca#{TAG_LEN-5}")
90 COLLECTION_IDS = { "CONTENTTYPE" => 1, "ARTIST" => 2, "ALBUM" => 3}
91 CNTFILE = "04cntinf.dat"
93 attr_accessor :collections
94 attr_accessor :mount_point
96 def initialize(mount_point, parse = true)
97 @mount_point = mount_point
100 cnt = dbfile(CNTFILE)
101 @songs = cnt.objects[0].records.map { |cnfb|
103 song.tags.each { |key, tag|
104 song.tags[key] = tag.recode(0) if tag["encoding"] == 2
110 COLLECTION_IDS.keys.each { |collection|
111 collection_id = COLLECTION_IDS[collection]
112 @collections[collection] = Collection.new(collection_id, self)
117 COLLECTION_IDS.keys.each { |collection|
118 collection_id = COLLECTION_IDS[collection]
119 @collections[collection] = Collection.new(collection_id, self, false)
124 IMPORTANT_TAGS = [ 'ARTIST', 'TITLE', 'ALBUM', 'CONTENTTYPE' ]
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
140 # choose right tags up to 5 tags limit
142 song.tags.each do |k, v|
143 if IMPORTANT_TAGS.include? k
148 song.tags.each do |k, v|
149 if left > 0 and !IMPORTANT_TAGS.include? k
155 record.track_length = song.track_length
157 record.frame_count = song.frame_count
159 songs.records << record
161 cnt.objects['CNFB'] = songs
162 cntfile = File.new(dbfilename(CNTFILE), 'w')
165 @collections.values.each { |collection|
171 class Collection < OrderedHash
172 def initialize(id, db, load = true)
176 @tree_filename = sprintf("01tree%02x.dat", @id)
177 @ginf_filename = sprintf("03ginf%02x.dat", @id)
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] = [] }
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
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)]
200 super || (self[key] = [])
213 gplb.obj_size = 0x4010
219 self.keys.each {|key|
220 record = Record::Gpfb.new
222 gpfb.records << record
224 record = Record::Gplb.new
225 record.description_slot = didx
227 record.starts_on_list = sidx
228 sidx += self[key].length
229 gplb.records << record
231 record = Record::Tplb.new
233 tplb.records << record
236 ginf.objects['GPFB'] = gpfb
237 tree.objects['GPLB'] = gplb
238 tree.objects['TPLB'] = tplb
239 ginffile = File.new(@db.dbfilename(@ginf_filename), 'w')
242 treefile = File.new(@db.dbfilename(@tree_filename), 'w')
249 File.join([@mount_point, "omgaudio", name])
252 DbFile.new(File.open(dbfilename(name)){|file|file.read})
256 FRAME2SYMBOL = ID3::Framename2symbol["2.4.0"]
257 SYMBOL2FRAME = ID3::SUPPORTED_SYMBOLS["2.4.0"]
270 [SYMBOL2FRAME[self[:tag]], self[:encoding], Id3.encode(self[:encoding], self[:content])].
275 def Id3.frame(tag, content, encoding)
276 tag = FRAME2SYMBOL[tag] || tag
278 Frame.new :tag => tag, :content => Id3.decode(encoding, content), :encoding => encoding
280 def Id3.decode(encoding, content)
283 Iconv.conv('utf8', 'utf16be', content)
285 raise "Unknown id3 encoding (#{encoding})"
288 def Id3.encode(encoding, content)
291 Iconv.conv('utf16be', 'utf8', content)
293 raise "Unknown id3 encoding (#{encoding})"
301 if a.length == 1 and a[0].is_a?(String)
308 attr_accessor :song_slot
310 @song_slot = content.unpack("n")[0]
313 [@song_slot].pack("n")
323 [0, 0, 0x2eeeb, 1, 128].pack("N3n2") + ID3::Frame.new("ARTIST", "2.4.0", "\0#{@text}").recode(2).dump_short
326 frame = ID3::Frame.new("TITLE", "2.4.0", content.unpack("a21A*")[1])
327 @text = frame.recode(0)["text"]
335 attr_accessor :description_slot
336 attr_accessor :starts_on_list # index of where it starts in Tplb
338 @description_slot, _, @starts_on_list, _ = content.unpack("n4")
341 [@description_slot, 0x0100, @starts_on_list, 0x0000].pack("n4")
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 ]
354 def track_type=(new_type)
355 @track_type = new_type
356 @track_type.instance_eval do
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]
384 @tags[symbol] = ID3::Frame.new(symbol, "2.4.0", content)
389 s = [@track_type.to_s, @track_length, @frame_count, @tags.length, TAG_LEN].pack("a4NNnn@16") +
391 v.dump_short(ID3::SUPPORTED_SYMBOLS["2.4.0"][t])
395 16 + @tags.length * TAG_LEN
401 attr_accessor :magic, :records, :rec_size, :obj_size
405 if a.length == 2 and a[1].is_a?(String)
410 @magic, rec_count, @rec_size, contents = contents.unpack("a4nnx8a*")
412 Record.const_get(magic.capitalize)
416 (0...rec_count).each do
417 record, contents = contents.unpack("a#{rec_size}a*")
418 @records << record_klass.new(record)
422 @rec_size ||= @records[0].length if @records[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
427 [res].pack("a#{obj_size}")
434 class DbObjectPointer
435 attr_reader :magic, :range
437 if a.length == 1 and a[0].is_a?(String)
440 @magic, @range = a[0], (a[1]...(a[1]+a[2]))
444 @magic, offset, length = contents.unpack("a4NN")
445 @range = offset...(offset+length)
448 [magic, range.first, range.last - range.first].pack("a4NN@16")
453 attr_accessor :magic, :objects
455 @objects = OrderedHash.new
456 if a.length == 1 and a[0].is_a?(String)
461 @magic, numobjects = contents.unpack("a4x4C")
463 (1..numobjects).each do |objindex|
464 start = objindex * 16
465 fin = objindex * 16 + 16
466 objects << DbObjectPointer.new(contents[start...fin])
468 objects.each do |obj_pointer|
469 @objects[obj_pointer.magic] = DbObject.new(obj_pointer.magic, contents[obj_pointer.range])
473 header = [@magic, 1, 1, 0, 0, objects.length].pack("a4C4C@16")
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
483 header + pointers + objects