id3tag 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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