id3tag 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.travis.yml +7 -0
- data/Gemfile +9 -9
- data/Gemfile.lock +36 -14
- data/README.md +73 -0
- data/Rakefile +3 -2
- data/VERSION +1 -1
- data/id3tag.gemspec +75 -19
- data/lib/id3tag.rb +34 -0
- data/lib/id3tag/audio_file.rb +90 -0
- data/lib/id3tag/frame_id_advisor.rb +50 -0
- data/lib/id3tag/frames/util/genre_names.rb +140 -0
- data/lib/id3tag/frames/v1/comments_frame.rb +24 -0
- data/lib/id3tag/frames/v1/genre_frame.rb +19 -0
- data/lib/id3tag/frames/v1/text_frame.rb +27 -0
- data/lib/id3tag/frames/v1/track_nr_frame.rb +18 -0
- data/lib/id3tag/frames/v2/basic_frame.rb +26 -0
- data/lib/id3tag/frames/v2/comments_frame.rb +44 -0
- data/lib/id3tag/frames/v2/frame_fabricator.rb +36 -0
- data/lib/id3tag/frames/v2/genre_frame.rb +34 -0
- data/lib/id3tag/frames/v2/genre_frame/genre_parser.rb +36 -0
- data/lib/id3tag/frames/v2/genre_frame/genre_parser_24.rb +15 -0
- data/lib/id3tag/frames/v2/genre_frame/genre_parser_pre_24.rb +39 -0
- data/lib/id3tag/frames/v2/text_frame.rb +37 -0
- data/lib/id3tag/frames/v2/unique_file_id_frame.rb +25 -0
- data/lib/id3tag/id3_v1_frame_parser.rb +87 -0
- data/lib/id3tag/id3_v2_frame_parser.rb +72 -0
- data/lib/id3tag/id3_v2_tag_header.rb +61 -0
- data/lib/id3tag/number_util.rb +14 -0
- data/lib/id3tag/string_util.rb +7 -0
- data/lib/id3tag/synchsafe_integer.rb +28 -0
- data/lib/id3tag/tag.rb +121 -0
- data/spec/fixtures/id3v1_and_v2.mp3 +0 -0
- data/spec/fixtures/id3v1_with_track_nr.mp3 +0 -0
- data/spec/fixtures/id3v1_without_track_nr.mp3 +0 -0
- data/spec/fixtures/id3v2.mp3 +0 -0
- data/spec/lib/id3tag/audio_file_spec.rb +40 -0
- data/spec/lib/id3tag/frames/util/genre_name_by_id_finder_spec.rb +18 -0
- data/spec/lib/id3tag/frames/v1/comments_frame_spec.rb +31 -0
- data/spec/lib/id3tag/frames/v1/genre_frame_spec.rb +20 -0
- data/spec/lib/id3tag/frames/v1/text_frame_spec.rb +14 -0
- data/spec/lib/id3tag/frames/v1/track_nr_frame_spec.rb +19 -0
- data/spec/lib/id3tag/frames/v2/basic_frame_spec.rb +25 -0
- data/spec/lib/id3tag/frames/v2/comments_frame_spec.rb +45 -0
- data/spec/lib/id3tag/frames/v2/genre_frame/genre_parser_24_spec.rb +26 -0
- data/spec/lib/id3tag/frames/v2/genre_frame/genre_parser_pre_24_spec.rb +48 -0
- data/spec/lib/id3tag/frames/v2/genre_frame_spec.rb +44 -0
- data/spec/lib/id3tag/frames/v2/text_frame_spec.rb +59 -0
- data/spec/lib/id3tag/frames/v2/unique_file_id_frame_spec.rb +31 -0
- data/spec/lib/id3tag/id3_v1_frame_parser_spec.rb +67 -0
- data/spec/lib/id3tag/id3_v2_frame_parser_spec.rb +18 -0
- data/spec/lib/id3tag/id3_v2_tag_header_spec.rb +60 -0
- data/spec/lib/id3tag/id3tag_spec.rb +17 -0
- data/spec/lib/id3tag/number_util_spec.rb +24 -0
- data/spec/lib/id3tag/string_util_spec.rb +21 -0
- data/spec/lib/id3tag/synchsafe_integer_spec.rb +14 -0
- data/spec/lib/id3tag/tag_spec.rb +84 -0
- data/spec/spec_helper.rb +9 -1
- data/spec/support/mp3_fixtures.rb +4 -0
- metadata +102 -38
- data/README.rdoc +0 -19
- data/spec/id3tag_spec.rb +0 -7
@@ -0,0 +1,39 @@
|
|
1
|
+
module ID3Tag
|
2
|
+
module Frames
|
3
|
+
module V2
|
4
|
+
class GenreFrame
|
5
|
+
class GenreParserPre24 < GenreParser
|
6
|
+
REFINEMENT_IN_PARENTHESES = /(\(\([^()]+\))/
|
7
|
+
GENRE_NUMBERS_OR_NAMES = /\(([^)]+)\)/
|
8
|
+
REFINEMENTS = /([)](?<blank>)[(][^(])|(\)(?<regular>[^()]+)[)]*)|(\((?<in_parentheses>\([^()]+\)))/
|
9
|
+
|
10
|
+
def genres
|
11
|
+
result = []
|
12
|
+
just_genres.each_with_index do |genre, index|
|
13
|
+
if ID3Tag::StringUtil.blank?(just_requirements[index])
|
14
|
+
result << expand_abbreviation(genre)
|
15
|
+
else
|
16
|
+
result << just_requirements[index]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
result
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def just_genres
|
25
|
+
genre_string.gsub(REFINEMENT_IN_PARENTHESES, '').scan(GENRE_NUMBERS_OR_NAMES).flatten
|
26
|
+
end
|
27
|
+
|
28
|
+
def just_requirements
|
29
|
+
result = []
|
30
|
+
genre_string.scan(REFINEMENTS) do |blank, regular, in_parentheses|
|
31
|
+
result << (regular || in_parentheses || blank)
|
32
|
+
end
|
33
|
+
result
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ID3Tag
|
2
|
+
module Frames
|
3
|
+
module V2
|
4
|
+
class TextFrame < BasicFrame
|
5
|
+
class UnsupportedTextEncoding < StandardError; end
|
6
|
+
ENCODING_MAP = {
|
7
|
+
0b0 => Encoding::ISO8859_1,
|
8
|
+
0b1 => Encoding::UTF_16,
|
9
|
+
0b10 => Encoding::UTF_16BE,
|
10
|
+
0b11 => Encoding::UTF_8
|
11
|
+
}
|
12
|
+
|
13
|
+
def content
|
14
|
+
@content ||= content_without_encoding_byte.encode(destination_encoding, source_encoding)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def source_encoding
|
20
|
+
ENCODING_MAP[get_encoding_byte] || raise(UnsupportedTextEncoding)
|
21
|
+
end
|
22
|
+
|
23
|
+
def destination_encoding
|
24
|
+
Encoding::UTF_8
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_encoding_byte
|
28
|
+
@raw_content.getbyte(0)
|
29
|
+
end
|
30
|
+
|
31
|
+
def content_without_encoding_byte
|
32
|
+
@raw_content.byteslice(1, @raw_content.bytesize - 1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ID3Tag
|
2
|
+
module Frames
|
3
|
+
module V2
|
4
|
+
class UniqueFileIdFrame < BasicFrame
|
5
|
+
def owner_identifier
|
6
|
+
content_split_apart_by_null_byte.first
|
7
|
+
end
|
8
|
+
|
9
|
+
def content
|
10
|
+
content_split_apart_by_null_byte.last
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def content_split_apart_by_null_byte
|
16
|
+
@raw_content.split("\x00", 2)
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect_content
|
20
|
+
"#{owner_identifier}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module ID3Tag
|
2
|
+
class ID3V1FrameParser
|
3
|
+
|
4
|
+
def initialize(input)
|
5
|
+
@input = StringIO.new(input)
|
6
|
+
end
|
7
|
+
|
8
|
+
def frames
|
9
|
+
@frames ||= get_frames
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def get_frames
|
15
|
+
frames = Set.new
|
16
|
+
frames << title_frame
|
17
|
+
frames << artist_frame
|
18
|
+
frames << album_frame
|
19
|
+
frames << year_frame
|
20
|
+
frames << comment_frame
|
21
|
+
frames << genre_frame
|
22
|
+
frames << track_nr_frame if tag_version_1_1?
|
23
|
+
frames
|
24
|
+
end
|
25
|
+
|
26
|
+
def title_frame
|
27
|
+
Frames::V1::TextFrame.new(:title, read_from_pos_till_null_byte_or_limit(0, 30))
|
28
|
+
end
|
29
|
+
|
30
|
+
def artist_frame
|
31
|
+
Frames::V1::TextFrame.new(:artist, read_from_pos_till_null_byte_or_limit(30, 30))
|
32
|
+
end
|
33
|
+
|
34
|
+
def album_frame
|
35
|
+
Frames::V1::TextFrame.new(:album, read_from_pos_till_null_byte_or_limit(60, 30))
|
36
|
+
end
|
37
|
+
|
38
|
+
def year_frame
|
39
|
+
Frames::V1::TextFrame.new(:year, read_from_pos_till_null_byte_or_limit(90, 4))
|
40
|
+
end
|
41
|
+
|
42
|
+
def comment_frame
|
43
|
+
Frames::V1::CommentsFrame.new(:comments, read_from_pos_till_null_byte_or_limit(94, comment_frame_size))
|
44
|
+
end
|
45
|
+
|
46
|
+
def genre_frame
|
47
|
+
Frames::V1::GenreFrame.new(:genre, read_from_pos_till_limit(124, 1))
|
48
|
+
end
|
49
|
+
|
50
|
+
def track_nr_frame
|
51
|
+
Frames::V1::TrackNrFrame.new(:track_nr, read_from_pos_till_null_byte_or_limit(123, 1))
|
52
|
+
end
|
53
|
+
|
54
|
+
def comment_frame_size
|
55
|
+
if tag_version_1_1?
|
56
|
+
28
|
57
|
+
else
|
58
|
+
30
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def tag_version_1_1?
|
63
|
+
@tag_version_1_1 ||= get_tag_version_1_1?
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_tag_version_1_1?
|
67
|
+
second_last_comment_byte, last_comment_byte = read_from_pos_till_limit(122, 2).bytes.to_a
|
68
|
+
(second_last_comment_byte == 0) && (last_comment_byte != 0)
|
69
|
+
end
|
70
|
+
|
71
|
+
def read_from_pos_till_null_byte_or_limit(position, limit)
|
72
|
+
@input.seek(position)
|
73
|
+
value = ""
|
74
|
+
limit.times do
|
75
|
+
char = @input.getc
|
76
|
+
break if char == "\x00"
|
77
|
+
value << char
|
78
|
+
end
|
79
|
+
value
|
80
|
+
end
|
81
|
+
|
82
|
+
def read_from_pos_till_limit(position, limit)
|
83
|
+
@input.seek(position)
|
84
|
+
@input.read(limit)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module ID3Tag
|
2
|
+
class ID3V2FrameParser
|
3
|
+
def initialize(input, major_version_number)
|
4
|
+
@input, @major_version_number = StringIO.new(input), major_version_number
|
5
|
+
end
|
6
|
+
|
7
|
+
def frames
|
8
|
+
@frames ||= get_frames
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def get_frames
|
14
|
+
frames = []
|
15
|
+
rewind_input
|
16
|
+
loop do
|
17
|
+
frame_id = read_next_frame_id
|
18
|
+
frame_size = read_next_frame_size
|
19
|
+
frame_flags = read_next_frame_flags
|
20
|
+
frame_content = read_next_bytes(frame_size)
|
21
|
+
frames << Frames::V2::FrameFabricator.fabricate(frame_id, frame_content, frame_flags, @major_version_number)
|
22
|
+
break if padding_or_eof_reached?
|
23
|
+
end
|
24
|
+
frames
|
25
|
+
end
|
26
|
+
|
27
|
+
def read_next_frame_id
|
28
|
+
read_next_bytes(frame_id_length)
|
29
|
+
end
|
30
|
+
|
31
|
+
def read_next_frame_size
|
32
|
+
size_bytes = case frame_size_length
|
33
|
+
when 4
|
34
|
+
read_next_bytes(frame_size_length)
|
35
|
+
when 3
|
36
|
+
"\x00" + read_next_bytes(frame_size_length)
|
37
|
+
end
|
38
|
+
SynchsafeInteger.decode(NumberUtil.convert_string_to_32bit_integer(size_bytes))
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_next_frame_flags
|
42
|
+
frames_has_flags? ? read_next_bytes(2) : nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def frame_id_length
|
46
|
+
@major_version_number <= 2 ? 3 : 4
|
47
|
+
end
|
48
|
+
|
49
|
+
def frames_has_flags?
|
50
|
+
@major_version_number > 2
|
51
|
+
end
|
52
|
+
|
53
|
+
def frame_size_length
|
54
|
+
@major_version_number <= 2 ? 3 : 4
|
55
|
+
end
|
56
|
+
|
57
|
+
def read_next_bytes(limit)
|
58
|
+
@input.read(limit)
|
59
|
+
end
|
60
|
+
|
61
|
+
def rewind_input
|
62
|
+
@input.rewind
|
63
|
+
end
|
64
|
+
|
65
|
+
def padding_or_eof_reached?
|
66
|
+
current_position = @input.pos
|
67
|
+
next_byte = @input.getbyte
|
68
|
+
@input.pos = current_position
|
69
|
+
(next_byte == 0) || @input.eof?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module ID3Tag
|
2
|
+
class ID3v2TagHeader
|
3
|
+
def initialize(header_chunk)
|
4
|
+
@content = StringIO.new(header_chunk)
|
5
|
+
end
|
6
|
+
|
7
|
+
def major_version_number
|
8
|
+
@content.seek(3)
|
9
|
+
@content.readbyte
|
10
|
+
end
|
11
|
+
|
12
|
+
def minor_version_number
|
13
|
+
@content.seek(4)
|
14
|
+
@content.readbyte
|
15
|
+
end
|
16
|
+
|
17
|
+
def version_number
|
18
|
+
sprintf("%s.%s", major_version_number, minor_version_number)
|
19
|
+
end
|
20
|
+
|
21
|
+
def unsynchronisation?
|
22
|
+
flags_byte[7] == 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def extended_header?
|
26
|
+
flags_byte[6] == 1
|
27
|
+
end
|
28
|
+
|
29
|
+
def experimental?
|
30
|
+
flags_byte[5] == 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def footer_present?
|
34
|
+
flags_byte[4] == 1
|
35
|
+
end
|
36
|
+
|
37
|
+
def tag_size
|
38
|
+
@tag_size ||= get_tag_size
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def flags_byte
|
44
|
+
@flags_byte ||= get_flags_byte
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_flags_byte
|
48
|
+
@content.seek(5)
|
49
|
+
@content.readbyte
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_tag_size
|
53
|
+
SynchsafeInteger.decode(NumberUtil.convert_string_to_32bit_integer(tag_size_bytes))
|
54
|
+
end
|
55
|
+
|
56
|
+
def tag_size_bytes
|
57
|
+
@content.seek(6)
|
58
|
+
@content.read(4)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ID3Tag
|
2
|
+
module NumberUtil
|
3
|
+
FORMAT_FOR_8_BIT_SIGNED_INTEGER = 'c'
|
4
|
+
FORMAT_FOR_32BIT_INTEGER = 'N'
|
5
|
+
def self.convert_string_to_32bit_integer(string)
|
6
|
+
integers = string.unpack(FORMAT_FOR_32BIT_INTEGER)
|
7
|
+
integers.first || raise(ArgumentError, "String: '#{string}' could not be decoded as 32-bit integer")
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.convert_32bit_integer_to_string(integer)
|
11
|
+
[integer].pack(FORMAT_FOR_32BIT_INTEGER)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ID3Tag
|
2
|
+
class SynchsafeInteger
|
3
|
+
class << self
|
4
|
+
def encode(int)
|
5
|
+
mask = 0x7F
|
6
|
+
while mask <= 0x7FFFFFFF do
|
7
|
+
out = int & ~mask
|
8
|
+
out = out << 1
|
9
|
+
out = out | (int & mask)
|
10
|
+
mask = ((mask + 1) << 8) - 1
|
11
|
+
int = out
|
12
|
+
end
|
13
|
+
out
|
14
|
+
end
|
15
|
+
|
16
|
+
def decode(synchsafe_int)
|
17
|
+
out = 0
|
18
|
+
mask = 0x7F000000
|
19
|
+
while mask > 0 do
|
20
|
+
out = out >> 1
|
21
|
+
out = out | (synchsafe_int & mask)
|
22
|
+
mask = mask >> 8
|
23
|
+
end
|
24
|
+
out
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/id3tag/tag.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
module ID3Tag
|
2
|
+
class Tag
|
3
|
+
class MultipleFrameError < StandardError; end
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def read(source, version = nil)
|
7
|
+
new(source, version)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(source, version = nil)
|
12
|
+
@source, @version = source, version
|
13
|
+
end
|
14
|
+
|
15
|
+
def artist
|
16
|
+
get_frame_content(frame_id_by_name(:artist))
|
17
|
+
end
|
18
|
+
|
19
|
+
def title
|
20
|
+
get_frame_content(frame_id_by_name(:title))
|
21
|
+
end
|
22
|
+
|
23
|
+
def album
|
24
|
+
get_frame_content(frame_id_by_name(:album))
|
25
|
+
end
|
26
|
+
|
27
|
+
def year
|
28
|
+
get_frame_content(frame_id_by_name(:year))
|
29
|
+
end
|
30
|
+
|
31
|
+
def comments(language = nil)
|
32
|
+
all_comments_frames = get_frames(frame_id_by_name(:comments))
|
33
|
+
comments_frame = if language
|
34
|
+
all_comments_frames.select { |frame| frame.language == language.to_s.downcase }.first
|
35
|
+
else
|
36
|
+
in_english = all_comments_frames.select { |frame| frame.language == 'eng' }
|
37
|
+
in_english.first || all_comments_frames.first
|
38
|
+
end
|
39
|
+
comments_frame && comments_frame.content
|
40
|
+
end
|
41
|
+
|
42
|
+
def track_nr
|
43
|
+
get_frame_content(frame_id_by_name(:track_nr))
|
44
|
+
end
|
45
|
+
|
46
|
+
def genre
|
47
|
+
get_frame_content(frame_id_by_name(:genre))
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_frame(frame_id)
|
51
|
+
frames = get_frames(frame_id)
|
52
|
+
if frames.count > 1
|
53
|
+
raise MultipleFrameError, "Could not return only one frame with id: #{frame_id}. Tag has #{frames.count} of them"
|
54
|
+
else
|
55
|
+
frames.first
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_frames(frame_id)
|
60
|
+
frames.select { |frame| frame.id == frame_id }
|
61
|
+
end
|
62
|
+
|
63
|
+
def frame_ids
|
64
|
+
frames.map { |frame| frame.id }
|
65
|
+
end
|
66
|
+
|
67
|
+
def frames
|
68
|
+
@frames ||= parse_frames
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_frame_content(frame_id)
|
72
|
+
frame = get_frame(frame_id)
|
73
|
+
frame && frame.content
|
74
|
+
end
|
75
|
+
|
76
|
+
def parsable_version
|
77
|
+
@parsable_version ||= calc_parsable_version
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def frame_id_by_name(name)
|
83
|
+
case parsable_version
|
84
|
+
when 2
|
85
|
+
FrameIdAdvisor.new(2, audio_file.v2_tag_major_version_number).advise(name)
|
86
|
+
when 1
|
87
|
+
FrameIdAdvisor.new(1, 'x').advise(name)
|
88
|
+
else
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse_frames
|
94
|
+
case parsable_version
|
95
|
+
when 2
|
96
|
+
ID3V2FrameParser.new(audio_file.v2_tag_body, audio_file.v2_tag_major_version_number).frames
|
97
|
+
when 1
|
98
|
+
ID3V1FrameParser.new(audio_file.v1_tag_body).frames
|
99
|
+
else
|
100
|
+
[]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def calc_parsable_version
|
105
|
+
if @version
|
106
|
+
method = "v#{@version}_tag_present?"
|
107
|
+
if audio_file.respond_to?(method) && audio_file.send(method)
|
108
|
+
@version
|
109
|
+
else
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
else
|
113
|
+
audio_file.greatest_tag_version
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def audio_file
|
118
|
+
@audio_file ||= AudioFile.new(@source)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|