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.
Files changed (62) hide show
  1. checksums.yaml +15 -0
  2. data/.travis.yml +7 -0
  3. data/Gemfile +9 -9
  4. data/Gemfile.lock +36 -14
  5. data/README.md +73 -0
  6. data/Rakefile +3 -2
  7. data/VERSION +1 -1
  8. data/id3tag.gemspec +75 -19
  9. data/lib/id3tag.rb +34 -0
  10. data/lib/id3tag/audio_file.rb +90 -0
  11. data/lib/id3tag/frame_id_advisor.rb +50 -0
  12. data/lib/id3tag/frames/util/genre_names.rb +140 -0
  13. data/lib/id3tag/frames/v1/comments_frame.rb +24 -0
  14. data/lib/id3tag/frames/v1/genre_frame.rb +19 -0
  15. data/lib/id3tag/frames/v1/text_frame.rb +27 -0
  16. data/lib/id3tag/frames/v1/track_nr_frame.rb +18 -0
  17. data/lib/id3tag/frames/v2/basic_frame.rb +26 -0
  18. data/lib/id3tag/frames/v2/comments_frame.rb +44 -0
  19. data/lib/id3tag/frames/v2/frame_fabricator.rb +36 -0
  20. data/lib/id3tag/frames/v2/genre_frame.rb +34 -0
  21. data/lib/id3tag/frames/v2/genre_frame/genre_parser.rb +36 -0
  22. data/lib/id3tag/frames/v2/genre_frame/genre_parser_24.rb +15 -0
  23. data/lib/id3tag/frames/v2/genre_frame/genre_parser_pre_24.rb +39 -0
  24. data/lib/id3tag/frames/v2/text_frame.rb +37 -0
  25. data/lib/id3tag/frames/v2/unique_file_id_frame.rb +25 -0
  26. data/lib/id3tag/id3_v1_frame_parser.rb +87 -0
  27. data/lib/id3tag/id3_v2_frame_parser.rb +72 -0
  28. data/lib/id3tag/id3_v2_tag_header.rb +61 -0
  29. data/lib/id3tag/number_util.rb +14 -0
  30. data/lib/id3tag/string_util.rb +7 -0
  31. data/lib/id3tag/synchsafe_integer.rb +28 -0
  32. data/lib/id3tag/tag.rb +121 -0
  33. data/spec/fixtures/id3v1_and_v2.mp3 +0 -0
  34. data/spec/fixtures/id3v1_with_track_nr.mp3 +0 -0
  35. data/spec/fixtures/id3v1_without_track_nr.mp3 +0 -0
  36. data/spec/fixtures/id3v2.mp3 +0 -0
  37. data/spec/lib/id3tag/audio_file_spec.rb +40 -0
  38. data/spec/lib/id3tag/frames/util/genre_name_by_id_finder_spec.rb +18 -0
  39. data/spec/lib/id3tag/frames/v1/comments_frame_spec.rb +31 -0
  40. data/spec/lib/id3tag/frames/v1/genre_frame_spec.rb +20 -0
  41. data/spec/lib/id3tag/frames/v1/text_frame_spec.rb +14 -0
  42. data/spec/lib/id3tag/frames/v1/track_nr_frame_spec.rb +19 -0
  43. data/spec/lib/id3tag/frames/v2/basic_frame_spec.rb +25 -0
  44. data/spec/lib/id3tag/frames/v2/comments_frame_spec.rb +45 -0
  45. data/spec/lib/id3tag/frames/v2/genre_frame/genre_parser_24_spec.rb +26 -0
  46. data/spec/lib/id3tag/frames/v2/genre_frame/genre_parser_pre_24_spec.rb +48 -0
  47. data/spec/lib/id3tag/frames/v2/genre_frame_spec.rb +44 -0
  48. data/spec/lib/id3tag/frames/v2/text_frame_spec.rb +59 -0
  49. data/spec/lib/id3tag/frames/v2/unique_file_id_frame_spec.rb +31 -0
  50. data/spec/lib/id3tag/id3_v1_frame_parser_spec.rb +67 -0
  51. data/spec/lib/id3tag/id3_v2_frame_parser_spec.rb +18 -0
  52. data/spec/lib/id3tag/id3_v2_tag_header_spec.rb +60 -0
  53. data/spec/lib/id3tag/id3tag_spec.rb +17 -0
  54. data/spec/lib/id3tag/number_util_spec.rb +24 -0
  55. data/spec/lib/id3tag/string_util_spec.rb +21 -0
  56. data/spec/lib/id3tag/synchsafe_integer_spec.rb +14 -0
  57. data/spec/lib/id3tag/tag_spec.rb +84 -0
  58. data/spec/spec_helper.rb +9 -1
  59. data/spec/support/mp3_fixtures.rb +4 -0
  60. metadata +102 -38
  61. data/README.rdoc +0 -19
  62. data/spec/id3tag_spec.rb +0 -7
@@ -0,0 +1,15 @@
1
+ module ID3Tag
2
+ module Frames
3
+ module V2
4
+ class GenreFrame
5
+ class GenreParser24 < GenreParser
6
+ def genres
7
+ genre_string.split("\x00").map do |genre|
8
+ expand_abbreviation(genre)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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,7 @@
1
+ module ID3Tag
2
+ module StringUtil
3
+ def self.blank?(string)
4
+ string !~ /[^[:space:]]/
5
+ end
6
+ end
7
+ 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