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")
84 [key, 0, tag.data].pack("a4ca#{TAG_LEN-5}")
86 [key, 0, ""].pack("a4ca#{TAG_LEN-5}")
96 File.join(['omgaudio', sprintf('10f%02x', slot >> 8), sprintf('1%07x.oma', slot & 0xff)])
100 COLLECTION_IDS = { "CONTENTGROUP" => 1, "ARTIST" => 2, "ALBUM" => 3, "CONTENTTYPE" => 4}
101 CNTFILE = "04cntinf.dat"
103 attr_accessor :collections
104 attr_accessor :mount_point
106 def initialize(mount_point, parse = true)
107 @mount_point = mount_point
110 cnt = dbfile(CNTFILE)
112 @songs = cnt.objects[0].records.map { |cnfb|
114 song.tags.each { |key, tag|
115 song.tags[key] = tag.recode(3)
124 COLLECTION_IDS.keys.each { |collection|
125 collection_id = COLLECTION_IDS[collection]
126 @collections[collection] = Collection.new(collection_id, self)
131 COLLECTION_IDS.keys.each { |collection|
132 collection_id = COLLECTION_IDS[collection]
133 @collections[collection] = Collection.new(collection_id, self, false)
139 # first remove it from collections
140 collections.each { |_, collection|
141 collection.each { |key, songs|
143 collection.delete key if songs.empty?
147 # then blank all data fields
148 song.tags = ID3::Tag2.new
149 song.track_length = 0
152 # finally delete the file
153 File.delete(File.join([mount_point, song.path]))
156 def really_exists(song)
157 File.exists?(File.join([mount_point, song.path]))
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"]
169 IMPORTANT_TAGS = [ 'ARTIST', 'TITLE', 'ALBUM', 'CONTENTTYPE' ]
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
185 # choose right tags up to 5 tags limit
187 song.tags.each do |k, v|
188 if IMPORTANT_TAGS.include? k
193 song.tags.each do |k, v|
194 if left > 0 and !IMPORTANT_TAGS.include? k
200 record.track_length = song.track_length
202 record.frame_count = song.frame_count
204 songs.records << record
206 cnt.objects['CNFB'] = songs
207 cntfile = File.new(dbfilename(CNTFILE), 'w')
210 @collections.values.each { |collection|
216 class Collection < OrderedHash
217 def initialize(id, db, load = true)
221 @tree_filename = sprintf("01tree%02x.dat", @id)
222 @ginf_filename = sprintf("03ginf%02x.dat", @id)
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] = [] }
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
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] }
245 super || (self[key] = [])
258 gplb.obj_size = 0x4010
264 self.keys.each {|key|
265 record = Record::Gpfb.new
267 gpfb.records << record
269 record = Record::Gplb.new
270 record.description_slot = didx
272 record.starts_on_list = sidx
273 sidx += self[key].length
274 gplb.records << record
276 record = Record::Tplb.new
277 record.song_slot = s.slot
278 tplb.records << record
281 ginf.objects['GPFB'] = gpfb
282 tree.objects['GPLB'] = gplb
283 tree.objects['TPLB'] = tplb
284 ginffile = File.new(@db.dbfilename(@ginf_filename), 'w')
287 treefile = File.new(@db.dbfilename(@tree_filename), 'w')
294 File.join([@mount_point, "omgaudio", name])
297 DbFile.new(File.open(dbfilename(name)){|file|file.read})
301 FRAME2SYMBOL = ID3::Framename2symbol["2.4.0"]
302 SYMBOL2FRAME = ID3::SUPPORTED_SYMBOLS["2.4.0"]
315 [SYMBOL2FRAME[self[:tag]], self[:encoding], Id3.encode(self[:encoding], self[:content])].
320 def Id3.frame(tag, content, encoding)
321 tag = FRAME2SYMBOL[tag] || tag
323 Frame.new :tag => tag, :content => Id3.decode(encoding, content), :encoding => encoding
325 def Id3.decode(encoding, content)
328 Iconv.conv('utf8', 'utf16be', content)
330 raise "Unknown id3 encoding (#{encoding})"
333 def Id3.encode(encoding, content)
336 Iconv.conv('utf16be', 'utf8', content)
338 raise "Unknown id3 encoding (#{encoding})"
346 if a.length == 1 and a[0].is_a?(String)
353 attr_accessor :song_slot
355 @song_slot = content.unpack("n")[0]
358 [@song_slot].pack("n")
368 [0, 0, 0x2eeeb, 1, 128].pack("N3n2") + ID3::Frame.new("ARTIST", "2.4.0", "\3#{@text}").recode(2).dump_short
371 frame = ID3::Frame.new("TITLE", "2.4.0", content.unpack("a21A*")[1])
372 @text = frame.recode(0)["text"]
380 attr_accessor :description_slot
381 attr_accessor :starts_on_list # index of where it starts in Tplb
383 @description_slot, _, @starts_on_list, _ = content.unpack("n4")
386 [@description_slot, 0x0100, @starts_on_list, 0x0000].pack("n4")
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 ]
399 def track_type=(new_type)
400 @track_type = new_type
401 @track_type.instance_eval do
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]
429 @tags[symbol] = ID3::Frame.new(symbol, "2.4.0", content)
434 s = [@track_type.to_s, @track_length, @frame_count, @tags.length, TAG_LEN].pack("a4NNnn@16") +
436 v.dump_short(ID3::SUPPORTED_SYMBOLS["2.4.0"][t])
440 16 + @tags.length * TAG_LEN
446 attr_accessor :magic, :records, :rec_size, :obj_size
450 if a.length == 2 and a[1].is_a?(String)
455 @magic, rec_count, @rec_size, contents = contents.unpack("a4nnx8a*")
457 Record.const_get(magic.capitalize)
461 (0...rec_count).each do
462 record, contents = contents.unpack("a#{rec_size}a*")
463 @records << record_klass.new(record)
467 @rec_size ||= @records[0].length if @records[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
472 [res].pack("a#{obj_size}")
479 class DbObjectPointer
480 attr_reader :magic, :range
482 if a.length == 1 and a[0].is_a?(String)
485 @magic, @range = a[0], (a[1]...(a[1]+a[2]))
489 @magic, offset, length = contents.unpack("a4NN")
490 @range = offset...(offset+length)
493 [magic, range.first, range.last - range.first].pack("a4NN@16")
498 attr_accessor :magic, :objects
500 @objects = OrderedHash.new
501 if a.length == 1 and a[0].is_a?(String)
506 @magic, numobjects = contents.unpack("a4x4C")
508 (1..numobjects).each do |objindex|
509 start = objindex * 16
510 fin = objindex * 16 + 16
511 objects << DbObjectPointer.new(contents[start...fin])
513 objects.each do |obj_pointer|
514 @objects[obj_pointer.magic] = DbObject.new(obj_pointer.magic, contents[obj_pointer.range])
518 header = [@magic, 1, 1, 0, 0, objects.length].pack("a4C4C@16")
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
528 header + pointers + objects