From cfe1fb0106229d97c649630897f0856fafe500cd Mon Sep 17 00:00:00 2001 From: Vlad Bokov Date: Sun, 30 Sep 2018 14:04:53 +0700 Subject: [PATCH 1/6] Test on newer rubies --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1f98109..aafdbcb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,13 @@ rvm: - 2.0.0 - 2.1.0 - 2.1.2 + - 2.2 + - 2.3 + - 2.4 + - 2.5 - ruby-head - jruby - - rbx + - rbx-head matrix: allow_failures: - rvm: 1.8.7 From 7029569693390792d827e2f52dd55c6b8fcad5d3 Mon Sep 17 00:00:00 2001 From: Vlad Bokov Date: Mon, 1 Oct 2018 03:30:46 +0700 Subject: [PATCH 2/6] Initial support ID3v2.3 chapters --- .gitignore | 1 + lib/mp3info.rb | 1 + lib/mp3info/chapters_parser.rb | 86 ++++++++++++++++ lib/mp3info/frame.rb | 180 +++++++++++++++++++++++++++++++++ lib/mp3info/frame/apic.rb | 160 +++++++++++++++++++++++++++++ lib/mp3info/id3v2.rb | 125 +++++++++++++++++------ ruby-mp3info.gemspec | 2 + 7 files changed, 522 insertions(+), 33 deletions(-) create mode 100644 lib/mp3info/chapters_parser.rb create mode 100644 lib/mp3info/frame.rb create mode 100644 lib/mp3info/frame/apic.rb diff --git a/.gitignore b/.gitignore index eeba2bb..177bd67 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Session.vim pkg/ fusil/fusil coverage/ +Gemfile.lock diff --git a/lib/mp3info.rb b/lib/mp3info.rb index 1651486..ac181e6 100644 --- a/lib/mp3info.rb +++ b/lib/mp3info.rb @@ -6,6 +6,7 @@ require "stringio" require "mp3info/id3v2" require "mp3info/extension_modules" +require "mp3info/chapters_parser" # ruby -d to display debugging infos diff --git a/lib/mp3info/chapters_parser.rb b/lib/mp3info/chapters_parser.rb new file mode 100644 index 0000000..27340b2 --- /dev/null +++ b/lib/mp3info/chapters_parser.rb @@ -0,0 +1,86 @@ +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 + end + end +end diff --git a/lib/mp3info/frame.rb b/lib/mp3info/frame.rb new file mode 100644 index 0000000..de3fa70 --- /dev/null +++ b/lib/mp3info/frame.rb @@ -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. $00 (00) + # The actual text + 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 diff --git a/lib/mp3info/frame/apic.rb b/lib/mp3info/frame/apic.rb new file mode 100644 index 0000000..a356dbb --- /dev/null +++ b/lib/mp3info/frame/apic.rb @@ -0,0 +1,160 @@ +require 'bindata' + +class Mp3Info + class Frame + self.send(:remove_const, :BinImg) rescue nil + class BinImg < BinData::BasePrimitive + def value_to_binary_string(value) + end + + def read_and_return_value(io) + end + + def sensible_default + "" + end + end + + self.send(:remove_const, :SyncSafeInt) rescue nil + class SyncSafeInt < BinData::BasePrimitive + def value_to_binary_string(value) + a = value >> 21 + b = (value - (a << 24)) >> 14 + c = (value - (a << 24) - (b << 16)) >> 7 + d = value - (a << 24) - (b << 16) - (c << 8) + [a, b, c, d].pack('C*') + end + + def read_and_return_value(io) + bstr = io.readbytes(4) + (bstr.getbyte(0) << 21) | (bstr.getbyte(1) << 14) | (bstr.getbyte(2) << 7) | bstr.getbyte(3) + end + + def sensible_default + 0 + end + end + + self.send(:remove_const, :Stringz8859) rescue nil + class StringzToSym < BinData::Stringz # NOTE: this should decode ASCII only! + arg_processor :string + + def assign(val) + @value = val + end + + def snapshot + _value + end + + def value_to_binary_string(value) + super(value.to_s.encode('ASCII-8BIT')) + end + + def read_and_return_value(io) + super.chomp("\0").to_sym + end + end + + self.send(:remove_const, :String8859) rescue nil + class StringIso8859 < BinData::BasePrimitive + arg_processor :string + optional_parameters :read_length + + def value_to_binary_string(value) + text = Mp3Info::EncodingHelper.convert_to(value[:text], "utf-8", "iso-8859-1") + url = Mp3Info::EncodingHelper.convert_to(value[:url], "utf-8", "iso-8859-1") + [text, url].join("\x00") + end + + def read_and_return_value(io) + len = eval_parameter(:read_length) || 0 + bstr = io.readbytes(len) + text, url = Mp3Info::EncodingHelper.convert_from_iso_8859_1(bstr).split("\x00") + { :text => text, :url => url } + end + + def sensible_default + { :text => "", :url => "" } + end + end + + self.send(:remove_const, :StringUtf16) rescue nil + class StringUtf16 < BinData::BasePrimitive + arg_processor :string + optional_parameters :read_length + + def value_to_binary_string(value) + Mp3Info::EncodingHelper.convert_to(value, "utf-8", "utf-16") + end + + def read_and_return_value(io) + len = eval_parameter(:read_length) || 0 + bstr = io.readbytes(len) + Mp3Info::EncodingHelper.decode_utf16(bstr).encode("utf-8") + end + + def sensible_default + "" + end + + # fix upstream: should be default for string? + def do_num_bytes #:nodoc: + value_to_binary_string(_value).bytesize + end + end + + self.send(:remove_const, :Href) rescue nil + class Href < StringIso8859 + end + + self.send(:remove_const, :Link) rescue nil + class Link < BinData::Record # WXXX + # hide :flags + + default_parameter :flags => "\x00\x00" + default_parameter :encoding_index => 1 + + uint8 :encoding_index + href :href, :read_length => lambda { + require 'pry'; binding.pry if $DEBUG && $DEBUG_READ + + sub_frame_len - 1 } # 1 - encoding byte + end + + self.send(:remove_const, :Apic) rescue nil + class Apic < BinData::Record + hide :data #, :flags + + default_parameter :image_type => 0 + default_parameter :flags => "\x00\x00" + default_parameter :encoding_index => 1 + + ENCODING_SIZE = 1 + uint8 :encoding_index + stringz :mime + uint8 :image_type + stringz :description # FIXME: now ASCII-8BIT. Need iso8859z, utf16z, utf8z classes + string :data, :read_length => lambda { + require 'pry'; binding.pry if $DEBUG && $DEBUG_READ + + sub_frame_len - 1 - mime.num_bytes - 1 - description.num_bytes } # 1 - encoding byte + end + + self.send(:remove_const, :Tit2) rescue nil + class Tit2 < BinData::Record + # hide :len, :rest, :flags, :name, :encoding_index + default_parameter :encoding_index => 1 + + ENCODING_SIZE = 1 + uint8 :encoding_index + choice :title, :choices => { + 0 => [:string_iso8859, {:read_length => lambda { sub_frame_len - ENCODING_SIZE }}], + 1 => [:string_utf16, {:read_length => lambda { sub_frame_len - ENCODING_SIZE }}], + 2 => [:string_utf16, {:read_length => lambda { sub_frame_len - ENCODING_SIZE }}], + 3 => [:string, {:read_length => lambda { sub_frame_len - ENCODING_SIZE }}] + }, :selection => :encoding_index + end + + end +end diff --git a/lib/mp3info/id3v2.rb b/lib/mp3info/id3v2.rb index f2d5e4b..79be2b3 100644 --- a/lib/mp3info/id3v2.rb +++ b/lib/mp3info/id3v2.rb @@ -5,6 +5,7 @@ require "delegate" require "mp3info/extension_modules" +load "mp3info/frame.rb" class ID3v2Error < StandardError ; end @@ -12,13 +13,16 @@ class ID3v2Error < StandardError ; end # It works like a hash, where key represents the tag name as 3 or 4 upper case letters # (respectively related to 2.2 and 2.3+ tag) and value represented as array or raw value. # Written version is always 2.3. -class ID3v2 < DelegateClass(Hash) +Object.send(:remove_const, :ID3v2) rescue nil +class ID3v2 < DelegateClass(Hash) TAGS = { "AENC" => "Audio encryption", "APIC" => "Attached picture", + "CHAP" => "Chapters", "COMM" => "Comments", "COMR" => "Commercial frame", + "CTOC" => "Table of contents", "ENCR" => "Encryption method registration", "EQUA" => "Equalization", "ETCO" => "Event timing codes", @@ -261,6 +265,19 @@ def add_picture(data, opts = {}) self["APIC"] = header + data.force_encoding('BINARY') end + def add_chapters(chapters, mp3_length) + chapter_frames = Mp3Info::ChaptersParser.new((mp3_length * 1000).to_i, chapters) + + # TODO: multiple CTOC's; respect :ordered or not, add CHAPs outside of CTOC + ctoc = chapter_frames.ctocs[Mp3Info::ChaptersParser::TOC] + self['CTOC'] = ctoc.to_binary_s + + self['CHAP'] = ctoc[:children_ids].map do |child_id| + # FIXME: CHAP has chap_len+flags before + chapter_frames.chaps[child_id.to_sym].to_binary_s[6..-1] + end.compact + end + ### Returns an array of images: ### [ ["01_.jpg", "Image Data in Binary String"], ### ["02_.png", "Another Image in a String"] ] @@ -278,10 +295,10 @@ def pictures pic.force_encoding 'BINARY' picture = [] jpg_regexp = Regexp.new("jpg|JPG|jpeg|JPEG|jfif|JFIF".force_encoding("BINARY"), - Regexp::FIXEDENCODING ) + Regexp::FIXEDENCODING ) png_regexp = Regexp.new("png|PNG".force_encoding("BINARY"), - Regexp::FIXEDENCODING ) + Regexp::FIXEDENCODING ) header = pic.unpack('a120').first.force_encoding "BINARY" mime_pos = 0 @@ -290,7 +307,7 @@ def pictures mime = "jpg" mime_pos = header =~ jpg_regexp start = Regexp.new("\xFF\xD8".force_encoding("BINARY"), - Regexp::FIXEDENCODING ) + Regexp::FIXEDENCODING ) start_with_anchor = Regexp.new("^\xFF\xD8".force_encoding("BINARY"), Regexp::FIXEDENCODING ) end @@ -299,9 +316,9 @@ def pictures mime = "png" mime_pos = header =~ png_regexp start = Regexp.new("\x89PNG".force_encoding("BINARY"), - Regexp::FIXEDENCODING ) + Regexp::FIXEDENCODING ) start_with_anchor = Regexp.new("^\x89PNG".force_encoding("BINARY"), - Regexp::FIXEDENCODING ) + Regexp::FIXEDENCODING ) end puts "analysing image: #{header.inspect}..." if $DEBUG @@ -360,12 +377,12 @@ def from_io(io) @parsed = true begin case @version_maj - when 2 - read_id3v2_2_frames - when 3, 4 - # seek past extended header if present - @io.seek(@io.get_syncsafe - 4, IO::SEEK_CUR) if ext_header - read_id3v2_3_frames + when 2 + read_id3v2_2_frames + when 3, 4 + # seek past extended header if present + @io.seek(@io.get_syncsafe - 4, IO::SEEK_CUR) if ext_header + read_id3v2_3_frames end rescue ID3v2Error => e warn("warning: id3v2 tag not fully parsed: #{e.message}") @@ -433,19 +450,43 @@ def encode_tag(name, value) puts "encode #{name} lang: #{@options[:lang]}, value #{transcoded_value.inspect}" if $DEBUG s = [ 1, @options[:lang], "\xFE\xFF\x00\x00", transcoded_value].pack("ca3a*a*") return s - when /^T/ + when /^T/ transcoded_value.force_encoding("BINARY") - return "\x01" + transcoded_value - else - return value + return "\x01" + transcoded_value + else + return value + end + end + + # TODO: nested ctoc + def decode_ctoc(ctoc_frame_body) + toc = Mp3Info::Frame::Toc.read(StringIO.new(ctoc_frame_body)) + { toc[:id].to_sym => toc } + end + + def decode_chap(chap_frame_body, size) + # unread size because only chapter frame knows how many TiT2 subframes + chap = "" + chap << [size].pack("N") # chap size + chap << "\x00"*2 # flags + chap << chap_frame_body + chapter = if $DEBUG && $DEBUG_READ + BinData::trace_reading do + Mp3Info::Frame::Chapter.read(StringIO.new(chap)) + end + else + Mp3Info::Frame::Chapter.read(StringIO.new(chap)) end + + { chapter[:id].to_sym => chapter } end ### Read a tag from file and perform UNICODE translation if needed - def decode_tag(name, raw_value) - puts("decode_tag(#{name.inspect}, #{raw_value.inspect})") if $DEBUG + def decode_tag(name, raw_value, size) + # puts("decode_tag(#{name.inspect} of size=#{size}: #{raw_value.inspect})") if $DEBUG if name =~ /^(T|COM|USLT)/ + # Mp3Info::Frame::Frame.new.read(@io).string begin if name =~ /^(COM|USLT)/ encoding_index, lang, raw_tag = raw_value.unpack("ca3a*") @@ -470,13 +511,13 @@ def decode_tag(name, raw_value) return nil end end - puts "COM tag found. encoding: #{encoding_index} lang: #{lang} str: #{out.inspect}" if $DEBUG + puts "COM tag found. encoding: #{encoding_index} lang: #{lang} str: #{out.inspect}" if $DEBUG && $DEBUG_READ else encoding_index = raw_value.getbyte(0) # language encoding (see TEXT_ENCODINGS constant) out = raw_value[1..-1] end - # we need to convert the string in order to match - # the requested encoding + + # decode_prefixed_string!(encoding_index, out) if TEXT_ENCODINGS[encoding_index] if encoding_index == 1 out = Mp3Info::EncodingHelper.decode_utf16(out) @@ -495,11 +536,33 @@ def decode_tag(name, raw_value) warn "warning: cannot decode tag #{name} with raw value #{raw_value.inspect}: #{e}" return nil end + elsif name =~ /^(CTOC|CHAP)/ # return hashes with { element_id => hash } + begin + if name == 'CTOC' + return decode_ctoc(raw_value) + elsif name == 'CHAP' + return decode_chap(raw_value, size) + end + rescue => e + require 'pry'; binding.pry if $DEBUG + + warn "warning: cannot decode chapters #{name} with raw value #{raw_value.inspect}: #{e}" + return {} + end else return raw_value end end + def read_idv2_3_size(io) + puts "@version_maj = #{@version_maj}" if $DEBUG && $DEBUG_READ + if @version_maj == 4 + size = io.get_syncsafe + else + size = io.get32bits + end + end + ### reads id3 ver 2.3.x/2.4.x frames and adds the contents to @tag2 hash ### NOTE: the id3v2 header does not take padding zero's into consideration def read_id3v2_3_frames @@ -507,16 +570,12 @@ def read_id3v2_3_frames name = @io.read(4) if name.nil? || name.getbyte(0) == 0 || name == "MP3e" #bug caused by old tagging application "mp3ext" ( http://www.mutschler.de/mp3ext/ ) @io.seek(-4, IO::SEEK_CUR) # 1. find a padding zero, - seek_to_v2_end + seek_to_v2_end break else - if @version_maj == 4 - size = @io.get_syncsafe - else - size = @io.get32bits - end + size = read_idv2_3_size(@io) @io.seek(2, IO::SEEK_CUR) # skip flags - puts "name '#{name}' size #{size}" if $DEBUG + puts "name '#{name}' size #{size}" if $DEBUG && $DEBUG_READ add_value_to_tag2(name, size) end break if @io.pos >= @tag_length # 2. reach length from header @@ -534,7 +593,7 @@ def read_id3v2_2_frames break else size = (@io.getbyte << 16) + (@io.getbyte << 8) + @io.getbyte - add_value_to_tag2(name, size) + add_value_to_tag2(name, size) break if @io.pos >= @tag_length end end @@ -544,20 +603,20 @@ def read_id3v2_2_frames ### read lang_encoding, decode data if unicode and ### create an array if the key already exists in the tag def add_value_to_tag2(name, size) - puts "add_value_to_tag2" if $DEBUG - if size > 50_000_000 raise ID3v2Error, "tag size is > 50_000_000" end data_io = @io.read(size) - data = decode_tag(name, data_io) + data = decode_tag(name, data_io, size) if data && !data.empty? if self.keys.include?(name) if self[name].is_a?(Array) unless self[name].include?(data) self[name] << data end + elsif self[name].is_a?(Hash) + self[name].merge!(data) else self[name] = [ self[name], data ] end @@ -571,7 +630,7 @@ def add_value_to_tag2(name, size) end end - puts "self[#{name.inspect}] = #{self[name].inspect}" if $DEBUG + # puts "self[#{name.inspect}] = #{self[name].inspect}" if $DEBUG end ### runs thru @file one char at a time looking for best guess of first MPEG diff --git a/ruby-mp3info.gemspec b/ruby-mp3info.gemspec index 452b568..ebf6bde 100644 --- a/ruby-mp3info.gemspec +++ b/ruby-mp3info.gemspec @@ -20,6 +20,8 @@ Gem::Specification.new do |s| s.test_files = ["test/test_ruby-mp3info.rb"] s.license = 'GPL-3.0' + s.add_dependency(%q, ["~> 2.4.4"]) + if s.respond_to? :specification_version then s.specification_version = 3 From 7a8b777cd54c9c29a71cd0d4be224f3aafc860d3 Mon Sep 17 00:00:00 2001 From: Vlad Bokov Date: Fri, 1 Nov 2019 00:10:41 +0300 Subject: [PATCH 3/6] Add testing story --- .gitignore | 1 + Gemfile | 3 ++ lib/mp3info/chapters_parser.rb | 2 + test/silence.mp3 | Bin 0 -> 1186 bytes test/test_ruby-mp3info.rb | 69 +++++++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+) create mode 100644 test/silence.mp3 diff --git a/.gitignore b/.gitignore index 177bd67..2cdd72d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ pkg/ fusil/fusil coverage/ Gemfile.lock +test/silence_copy.mp3 diff --git a/Gemfile b/Gemfile index e9f29fa..281b2c4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,6 @@ source "https://rubygems.org" gem "minitest", "~> 5.4.1" +gem "pry-byebug" + +gemspec \ No newline at end of file diff --git a/lib/mp3info/chapters_parser.rb b/lib/mp3info/chapters_parser.rb index 27340b2..b5fe4ac 100644 --- a/lib/mp3info/chapters_parser.rb +++ b/lib/mp3info/chapters_parser.rb @@ -81,6 +81,8 @@ def top_ctoc 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 diff --git a/test/silence.mp3 b/test/silence.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..85a19c58a5f67c926f48f4600975461aba5c2a29 GIT binary patch literal 1186 zcmeZtF=l1}0!9tz5PxSNLx6#yBtMyfnJGD=z<>cr7&16}I0gV^Y#2aX1CVMk_zwjk zo*_m+0a>6S|No^iWHJ;n6f=}CC@>^5WH2N$6ad+&KryH(c3@Ksp=L0opqU~MHl-M- zBbgzeA&(&i!<_%WU5+p?A3vM&fuW(ScmCyHF#HX2#81IM z>xF`)r}Fj+q#c!+6WRAncj5tgHkKD%E4UhGxEJz>NIqHpD#xKmV8YTL#_fCUcf9r! zV0z@*5#sd!|NYH(f?ODd+Y1Iw7R()Z!TXn<^259 zApO@))Hdypv)wI|8XEic@-~4JPPeo}j$K)_!r9C1-?hE_POe^Z#>?9^Qa4?Q>f!-bKr1uD7L@*@aV8XP1APmj3?SdHx-j|DU+``T3jZT2Ii6*h6CCEd(VQQED1S9{y_$ /dev/null") +GOT_FFPROBE = system("which ffprobe > /dev/null") class Mp3InfoTest < TestCase TEMP_FILE = File.join(File.dirname(__FILE__), "test_mp3info.mp3") + FIXTURE_FILE = File.join(File.dirname(__FILE__), "silence.mp3") DUMMY_TAG2 = { "COMM" => "comments", @@ -343,6 +348,70 @@ def test_leading_char_gets_chopped end end + SILENCE_LENGTH = 0.1 + SILENCE_CHAPTERS = { + "CTOC" => { + :toc => { + :id => :toc, + :flags => {:top => true, :ordered => true}, + :children_count => 2, :children_ids => [:chp0, :chp1]}}, + "CHAP" => { + :chp0 => { + :chap_len => 60, + :flags => "\x00\x00", + :id => :chp0, + :start => 30, + :finish => 60, + :start_offset => 4294967295, + :finish_offset => 4294967295, + :sub_frames => [{:name => "TIT2", + :sub_frame_len => 29, + :flags => "\x00\x00", + :body => {:encoding_index => 1, :title => "first chapter"}}]}, + :chp1=>{ + :chap_len => 62, + :flags => "\x00\x00", + :id=>:chp1, + :start => 60, + :finish => 100, + :start_offset => 4294967295, + :finish_offset => 4294967295, + :sub_frames => [{:name => "TIT2", + :sub_frame_len => 31, + :flags => "\x00\x00", + :body => {:encoding_index => 1, :title => "second chapter"}}]}}} + + def test_id3v2_chapters_read + Mp3Info.open(FIXTURE_FILE) do |mp3| + assert_equal mp3.tag2, mp3.tag2 + end + end + + FFPROBE_EXPECTED_OUT = { + "chapters" => [ + {"id"=>0, + "time_base"=>"1/1000", + "start"=>50, + "start_time"=>"0.050000", + "end"=>100, + "end_time"=>"0.100000", + "tags"=>{"title"=>"single chapter"}}]} + + def test_id3v2_chapters_write + return unless GOT_FFPROBE + tmp = 'test/silence_copy.mp3' + FileUtils.cp(TEMP_FILE, tmp) + + begin + Mp3Info.open(tmp) do |mp3| + mp3.tag2.add_chapters([["00:00:00.05", "single chapter"]], SILENCE_LENGTH) + end + assert_equal FFPROBE_EXPECTED_OUT, JSON.parse(%x[ffprobe -v error -print_format json -show_chapters #{tmp}]) + ensure + File.delete(tmp) + end + end + def test_reading2_2_tags load_fixture_to_temp_file("2_2_tagged") From 088fc9c8f5571459d3e49ef4dbc18dcbe2ff4461 Mon Sep 17 00:00:00 2001 From: Vlad Bokov Date: Fri, 1 Nov 2019 00:13:59 +0300 Subject: [PATCH 4/6] Install no debug since it's very ruby version bound --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index 281b2c4..5c50016 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,5 @@ source "https://rubygems.org" gem "minitest", "~> 5.4.1" -gem "pry-byebug" gemspec \ No newline at end of file From e056cfad113da2e807bb250d3e72cab666e85367 Mon Sep 17 00:00:00 2001 From: Vlad Bokov Date: Fri, 1 Nov 2019 00:19:57 +0300 Subject: [PATCH 5/6] Test only on supported ruby versions --- .travis.yml | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index aafdbcb..1196ca8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,6 @@ language: ruby rvm: - - 1.8.7 - - 1.9.3 - - 2.0.0 - - 2.1.0 - - 2.1.2 - - 2.2 - - 2.3 - 2.4 - 2.5 - - ruby-head - - jruby - - rbx-head -matrix: - allow_failures: - - rvm: 1.8.7 + - 2.6 script: ruby test/test_ruby-mp3info.rb From f807ddbb11774ade8f54a0879d4df3079f5d89a4 Mon Sep 17 00:00:00 2001 From: Vlad Bokov Date: Fri, 1 Nov 2019 00:20:17 +0300 Subject: [PATCH 6/6] Revert "Install no debug since it's very ruby version bound" This reverts commit 088fc9c8f5571459d3e49ef4dbc18dcbe2ff4461. --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 5c50016..281b2c4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,6 @@ source "https://rubygems.org" gem "minitest", "~> 5.4.1" +gem "pry-byebug" gemspec \ No newline at end of file