Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ Session.vim
pkg/
fusil/fusil
coverage/
Gemfile.lock
test/silence_copy.mp3
14 changes: 3 additions & 11 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
language: ruby
rvm:
- 1.8.7
- 1.9.3
- 2.0.0
- 2.1.0
- 2.1.2
- ruby-head
- jruby
- rbx
matrix:
allow_failures:
- rvm: 1.8.7
- 2.4
- 2.5
- 2.6
script: ruby test/test_ruby-mp3info.rb
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
source "https://rubygems.org"

gem "minitest", "~> 5.4.1"
gem "pry-byebug"

gemspec
1 change: 1 addition & 0 deletions lib/mp3info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require "stringio"
require "mp3info/id3v2"
require "mp3info/extension_modules"
require "mp3info/chapters_parser"

# ruby -d to display debugging infos

Expand Down
88 changes: 88 additions & 0 deletions lib/mp3info/chapters_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require 'date'
require 'mp3info/frame'

class Mp3Info
# accepts list of
# [['00:00:00.000', 'text'], ..]
self.send(:remove_const, :ChaptersParser) rescue nil
class ChaptersParser

self.send(:remove_const, :Chapter) rescue nil
class Chapter
attr_reader :start, :finish, :title

def initialize(id, start, finish, title)
@id, @start, @finish, @title = id, start, finish, title
end

def chap
Mp3Info::Frame::Chapter.new(
:id => id,
:start => start,
:finish => finish,
:sub_frames => sub_frames
)
end

private

def id
Mp3Info::Frame::StringzToSym.new(@id)
end

def sub_frames
[tit2]
end

def tit2
Mp3Info::Frame::SubFrame.new(
:name => "TIT2".encode("ASCII-8BIT"),
:body => Mp3Info::Frame::Tit2.new(
:encoding_index => 1,
:title => Mp3Info::Frame::StringUtf16.new(title))
)
end
end

attr_reader :tlen, :chapters, :ctocs, :chaps
TOC = :toc

def initialize(tlen, chapters)
@tlen, @chapters = tlen.to_i, chapters
@chaps = {}
@ctocs = { TOC => top_ctoc }
parse!
end

private

def parse!
return unless @chaps.empty?
(chapters + [nil]).each_cons(2).each_with_index do |((start, title), (finish, _)), i|
id = "chp#{i}".to_sym
finish_ms = finish ? to_ms(finish) : tlen
chapter = Chapter.new(id, to_ms(start), finish_ms, title)
@chaps[id] = chapter.chap
add_to_top_ctoc!(id)
end
end

def add_to_top_ctoc!(sym_id)
@ctocs[TOC][:children_ids] << Mp3Info::Frame::StringzToSym.new(sym_id)
end

def top_ctoc
Mp3Info::Frame::Toc.new(
:id => Mp3Info::Frame::StringzToSym.new(TOC),
:flags => Mp3Info::Frame::TocFlags.new(:top => true, :ordered => true),
:children_ids => []
)
end

def to_ms(hh_mm_ss_ms)
DateTime.strptime("1970-01-01 #{hh_mm_ss_ms}", "%Y-%m-%d %H:%M:%S.%L").strftime("%Q").to_i
rescue => e
binding.pry
end
end
end
180 changes: 180 additions & 0 deletions lib/mp3info/frame.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
require 'bindata'
require 'mp3info/frame/apic'

class Mp3Info
# IDv3 standart has different types of strings with same structure of the body under some
# header tag or chapter subtag
#
# Text encoding $xx
# ... various fields. e.g. ULST/COMM (3 bytes for lang); TIT2 (0 bytes)
# Short content descrip. <text string according to encoding> $00 (00)
# The actual text <full text string according to encoding>
class Frame
self.send(:remove_const, :Mp3ChapterOffset) rescue nil
class Mp3ChapterOffset < BinData::Uint32be
def sensible_default
4294967295 # same as no offset, default for writes
end
end

self.send(:remove_const, :TocFlags) rescue nil
class TocFlags < BinData::Uint8
def assign(val)
@value = value
end

def value_to_binary_string(val)
super((val[:top] ? 1 : 0) + (val[:ordered] ? 2 : 0))
end

def read_and_return_value(io)
flags = super
top = flags & 1 == 1
ordered = (flags >> 1) & 1 == 1
{ :top => top, :ordered => ordered }
end

def sensible_default
{ :top => true, :ordered => true }
end
end

self.send(:remove_const, :SubFrame) rescue nil
class SubFrame < BinData::Record
# hide :sub_frame_len, :flags

string :name, :length => 4
uint32be :sub_frame_len, :value => lambda {
if reading?
@obj.instance_variable_get(:@value)
else
require 'pry'; binding.pry if $DEBUG && $DEBUG_WRITE
was = @obj.instance_variable_get(:@value)
now = body.num_bytes
if now != was
puts "SubFrame #{name} changed length #{was.inspect} -> #{now.inspect}" if $DEBUG_WRITE
end
now
end
}
string :flags, :length => 2
choice :body, :choices => {
'WXXX' => [:link, :read_length => :sub_frame_len],
'APIC' => [:apic, :read_length => :sub_frame_len],
'TIT2' => [:tit2, :read_length => :sub_frame_len]
}, :selection => :name
end

self.send(:remove_const, :SubFrames) rescue nil
class SubFrames < BinData::Array
optional_parameters :read_if
def initialize_shared_instance
super
if has_parameter?(:read_until)
extend ReadIfPlugin
end
end

module ReadIfPlugin
def do_read(io)
loop do
variables = { index: self.length - 1, element: self.last, array: self }
require 'pry'; binding.pry if $DEBUG && $DEBUG_READ
break unless eval_parameter(:read_if, variables)
element = append_new_element
element.do_read(io)
break if eval_parameter(:read_until, variables)
end
end
end
end

self.send(:remove_const, :Chapter) rescue nil
class Chapter < BinData::Record
# hide :chap_len, :flags, :start_offset, :finish_offset
# mandatory_parameters :id, :start, :finish # cannot parse with that o_O

uint32be :chap_len, :value => lambda {
if reading?
@obj.instance_variable_get(:@value)
else
require 'pry'; binding.pry if $DEBUG && $DEBUG_WRITE
# 131067
was = @obj.instance_variable_get(:@value)
now = id.num_bytes + start.num_bytes + finish.num_bytes + start_offset.num_bytes + finish_offset.num_bytes + sub_frames.map(&:num_bytes).reduce(:+).to_i
if now != was
puts "Chapter #{id} changed length #{was} -> #{now}" if $DEBUG_WRITE
end
now
end
}
string :flags, :length => 2

stringz_to_sym :id
uint32be :start
uint32be :finish
mp3_chapter_offset :start_offset
mp3_chapter_offset :finish_offset

# fix upstream: without :read_until it sets zero length
# https://github.com/dmendel/bindata/blob/v2.4.4/lib/bindata/array.rb#L278L281
sub_frames :sub_frames, :type => :sub_frame, :read_until => lambda {
require 'pry'; binding.pry if $DEBUG && $DEBUG_READ
bytes_left = (chap_len - id.num_bytes - start.num_bytes - finish.num_bytes - start_offset.num_bytes - finish_offset.num_bytes)
bytes_read = array.map(&:num_bytes).reduce(:+).to_i
bytes_read >= bytes_left
}, :read_if => lambda {
require 'pry'; binding.pry if $DEBUG && $DEBUG_READ
bytes_left = (chap_len - id.num_bytes - start.num_bytes - finish.num_bytes - start_offset.num_bytes - finish_offset.num_bytes)
# bytes_left = (chap_len - id.num_bytes - 16 - 4 - 4 - 2 - len)
bytes_read = array.map(&:num_bytes).reduce(:+).to_i
bytes_read < bytes_left
}
end

self.send(:remove_const, :Toc) rescue nil
class Toc < BinData::Record
stringz_to_sym :id
toc_flags :flags
uint8 :children_count, :value => lambda {
if reading?
@obj.instance_variable_get(:@value)
else
require 'pry'; binding.pry if $DEBUG && $DEBUG_WRITE
was = @obj.instance_variable_get(:@value)
now = children_ids.size
if now != was
puts "TOC #{id} changed length #{was.inspect} -> #{now.inspect}" if $DEBUG_WRITE
end
now
end
}
array :children_ids, type: :stringz_to_sym, :initial_length => :children_count
end

# main wrapper
self.send(:remove_const, :Frame) rescue nil
class Frame < BinData::Record
default_parameter :flags => "\x00\x00"

string :name, :length => 4 # TODO: must be ascii
uint32be :frame_len, :value => lambda {
if reading?
@obj.instance_variable_get(:@value)
else
require 'pry'; binding.pry if $DEBUG && $DEBUG_WRITE
was = @obj.instance_variable_get(:@value)
now = body.num_bytes
if now != was
puts "Frame #{name} changed length #{was.inspect} -> #{now.inspect}" if $DEBUG_WRITE
end
now
end
}
string :flags, :length => 2

# now for build/writes, TODO: use for reads
rest :body
end
end
end
Loading