id3_tags 0.0.1

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.
data/lib/id3_tags.rb ADDED
@@ -0,0 +1,182 @@
1
+ require 'taglib'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ # Provides two methods to read and write ID3 metadata from an MP3 file
5
+ module Id3Tags
6
+
7
+ # Returns a Hash of ID3 attributes stored in the file located at +file_path+.
8
+ #
9
+ # @param [String] file_path Local path to an MP3 file
10
+ # @return [Hash] the ID3 attributes stored in the file
11
+ #
12
+ # @example Read tags from an MP3 full of metadata
13
+ # Id3Tags.read_tags_from("all_id3.mp3")
14
+ #
15
+ # # => {title: "Sample track", album: "Sample album", artist: Sample
16
+ # artist", :comment=>"Sample comments", genre: "Sample Genre",
17
+ # year: 1979, bitrate: 128, channels: 2, length: 38, samplerate:
18
+ # 44100, bpm: 110, lyrics: "Sample lyrics line 1\rand line 2",
19
+ # composer: "Sample composer", grouping: "Sample group",
20
+ # album_artist: "Sample album artist", compilation: true, track:
21
+ # {number: 3, count: 12}, disk: {number: 1, count: 2}, cover_art:
22
+ # {mime_type: "image/png", data: "\x89PNG\r\n\x1A[...]"}}
23
+ def self.read_tags_from(file_path)
24
+ attrs = {}
25
+ TagLib::MPEG::File.open(file_path) do |file|
26
+ tag_fields.each do |field, opts|
27
+ value = file.tag.send opts[:method]
28
+ assign! attrs, field, value, opts[:type]
29
+ end unless file.tag.nil?
30
+
31
+ audio_properties_fields.each do |field, opts|
32
+ value = file.audio_properties.send opts[:method]
33
+ assign! attrs, field, value, opts[:type]
34
+ end unless file.audio_properties.nil?
35
+
36
+ id3v2_tag_fields.each do |field, opts|
37
+ value = file.id3v2_tag.frame_list(opts[:frame_id]).first
38
+ value = value.to_string if value and opts[:type] != :image
39
+ assign! attrs, field, value, opts[:type]
40
+ end unless file.id3v2_tag.nil?
41
+ end
42
+
43
+ attrs.symbolize_keys
44
+ end
45
+
46
+ # Stores the +attrs+ Hash of ID3 attributes into the file at +file_path+.
47
+ #
48
+ # @param [String] file_path Local path to an MP3 file
49
+ # @param [Hash] attrs the ID3 attributes stored in the file
50
+ # @return [Boolean] true (the file gets changed)
51
+ #
52
+ # @example Write ID3 tags to an MP3 file
53
+ # Id3Tags.write_tags_to("no_id3.mp3", {title: "Sample track", album:
54
+ # "Sample album", artist: Sample artist", :comment=>"Sample comments",
55
+ # genre: "Sample Genre", year: 1979, bitrate: 128, channels: 2,
56
+ # length: 38, samplerate: 44100, bpm: 110, lyrics: "Sample lyrics
57
+ # line 1\rand line 2", composer: "Sample composer", grouping: "Sample
58
+ # group", album_artist: "Sample album artist", compilation: true,
59
+ # track: {number: 3, count: 12}, disk: {number: 1, count: 2},
60
+ # cover_art: {mime_type: "image/png", data: "\x89PNG\r\n\x1A[...]"}}
61
+ #
62
+ # # => true
63
+ def self.write_tags_to(file_path, attrs = {})
64
+ attrs.symbolize_keys!
65
+
66
+ TagLib::MPEG::File.open(file_path) do |file|
67
+ tag_fields.each do |field, opts|
68
+ file.tag.send "#{opts[:method]}=", (attrs[field] || opts[:default])
69
+ end unless file.tag.nil?
70
+
71
+ id3v2_tag_fields.each do |field, opts|
72
+ file.id3v2_tag.remove_frames opts[:frame_id]
73
+ frame = new_frame_for attrs[field], opts[:frame_id], opts[:type]
74
+ file.id3v2_tag.add_frame frame if frame
75
+ end unless file.id3v2_tag.nil?
76
+
77
+ file.save
78
+ end
79
+ end
80
+
81
+ # @private
82
+ def self.fields
83
+ {
84
+ title: {on: :tag, method: :title, type: :string},
85
+ album: {on: :tag, method: :album, type: :string},
86
+ artist: {on: :tag, method: :artist, type: :string},
87
+ comment: {on: :tag, method: :comment, type: :string},
88
+ genre: {on: :tag, method: :genre, type: :string},
89
+ year: {on: :tag, method: :year, type: :integer, default: 0},
90
+ bitrate: {on: :audio_properties, method: :bitrate, type: :integer},
91
+ channels: {on: :audio_properties, method: :channels, type: :integer},
92
+ length: {on: :audio_properties, method: :length, type: :integer},
93
+ samplerate: {on: :audio_properties, method: :sample_rate, type: :integer},
94
+ bpm: {on: :id3v2_tag, frame_id: 'TBPM', type: :integer},
95
+ lyrics: {on: :id3v2_tag, frame_id: 'USLT', type: :text},
96
+ composer: {on: :id3v2_tag, frame_id: 'TCOM', type: :string},
97
+ grouping: {on: :id3v2_tag, frame_id: 'TIT1', type: :string},
98
+ album_artist:{on: :id3v2_tag, frame_id: 'TPE2', type: :string},
99
+ compilation: {on: :id3v2_tag, frame_id: 'TCMP', type: :boolean},
100
+ track: {on: :id3v2_tag, frame_id: 'TRCK', type: :pair},
101
+ disk: {on: :id3v2_tag, frame_id: 'TPOS', type: :pair},
102
+ cover_art: {on: :id3v2_tag, frame_id: 'APIC', type: :image},
103
+ }
104
+ end
105
+
106
+ # @private
107
+ def self.tag_fields
108
+ fields.select{|k,v| v[:on] == :tag}
109
+ end
110
+
111
+ # @private
112
+ def self.audio_properties_fields
113
+ fields.select{|k,v| v[:on] == :audio_properties }
114
+ end
115
+
116
+ # @private
117
+ def self.id3v2_tag_fields
118
+ fields.select{|k,v| v[:on] == :id3v2_tag }
119
+ end
120
+
121
+ # @private
122
+ def self.assign!(attrs, field, value, type)
123
+ case type
124
+ when :string, :text
125
+ attrs[field] = value && value.to_s
126
+ when :integer
127
+ attrs[field] = value && value.to_i
128
+ when :boolean
129
+ attrs[field] = value.present? && value.eql?('1')
130
+ when :pair
131
+ pair = value ? value.split('/').map(&:to_i) : [nil, nil]
132
+ attrs[field] = {number: pair[0], count: pair[1]}
133
+ when :image
134
+ pair = [value && value.mime_type, value && value.picture].map(&:presence)
135
+ attrs[field] = {mime_type: pair[0], data: pair[1]}
136
+ end
137
+ end
138
+
139
+ # @private
140
+ def self.new_frame_for(content, frame_id, type)
141
+ return if content.nil?
142
+ case type
143
+ when :string, :integer
144
+ frame = new_string_frame(frame_id)
145
+ frame.text = content.to_s
146
+ when :text
147
+ frame = new_text_frame(frame_id)
148
+ frame.text = content.to_s
149
+ when :boolean
150
+ return unless content.eql?(true)
151
+ frame = new_string_frame(frame_id)
152
+ frame.text = '1'
153
+ when :pair
154
+ return unless content.has_key? :number
155
+ frame = new_string_frame(frame_id)
156
+ frame.text = content.values_at(:number, :count).compact.join '/'
157
+ when :image
158
+ return unless content.has_key? :data
159
+ frame = new_image_frame(frame_id)
160
+ frame.description = 'Cover'
161
+ frame.type = TagLib::ID3v2::AttachedPictureFrame::FrontCover
162
+ frame.mime_type = content[:mime_type]
163
+ frame.picture = content[:data]
164
+ end
165
+ frame
166
+ end
167
+
168
+ # @private
169
+ def self.new_string_frame(frame_id)
170
+ TagLib::ID3v2::TextIdentificationFrame.new frame_id, TagLib::String::UTF8
171
+ end
172
+
173
+ # @private
174
+ def self.new_text_frame(frame_id)
175
+ TagLib::ID3v2::UnsynchronizedLyricsFrame.new frame_id
176
+ end
177
+
178
+ # @private
179
+ def self.new_image_frame(frame_id)
180
+ TagLib::ID3v2::AttachedPictureFrame.new frame_id
181
+ end
182
+ end
Binary file
Binary file
Binary file
@@ -0,0 +1,11 @@
1
+ require 'tmpdir'
2
+
3
+ def with_duplicate_file_of(original)
4
+ duplicate = File.join Dir.tmpdir, File.basename(original)
5
+ begin
6
+ FileUtils.cp original, duplicate
7
+ yield(duplicate)
8
+ ensure
9
+ FileUtils.rm duplicate
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ require 'id3_tags'
2
+ require_relative '../helpers/duplicate_file_helper'
3
+
4
+ describe Id3Tags do
5
+ it 'does not alter tracks after reading AND writing the metadata' do
6
+ original = File.join File.dirname(__FILE__), '../assets/all_id3.mp3'
7
+ original_id3_tags = Id3Tags.read_tags_from(original)
8
+ with_duplicate_file_of(original) do |duplicate|
9
+ Id3Tags.write_tags_to(duplicate, original_id3_tags)
10
+ Id3Tags.read_tags_from(duplicate).should == original_id3_tags
11
+ FileUtils.identical?(original, duplicate).should be_true
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,72 @@
1
+ require 'id3_tags'
2
+
3
+ describe 'Id3Tags.read_tags_from' do
4
+ context 'given a track with all the ID3 metadata' do
5
+ let(:track_with_metadata) {File.join File.dirname(__FILE__), '../assets/all_id3.mp3'}
6
+
7
+ it 'returns all the ID3 tag with the right value' do
8
+ id3_tags = Id3Tags.read_tags_from track_with_metadata
9
+
10
+ id3_tags.keys.should =~ [:album, :artist, :comment, :genre, :samplerate,
11
+ :track, :disk, :title, :year, :bitrate, :channels, :length, :grouping,
12
+ :album_artist, :composer, :bpm, :lyrics, :compilation, :cover_art]
13
+ id3_tags[:title].should == 'Sample track'
14
+ id3_tags[:album].should == 'Sample album'
15
+ id3_tags[:artist].should == 'Sample artist'
16
+ id3_tags[:comment].should == 'Sample comments'
17
+ id3_tags[:genre].should == 'Sample Genre'
18
+ id3_tags[:year].should == 1979
19
+ id3_tags[:track][:number].should == 3
20
+ id3_tags[:track][:count].should == 12
21
+ id3_tags[:disk][:number].should == 1
22
+ id3_tags[:disk][:count].should == 2
23
+ id3_tags[:bitrate].should == 128
24
+ id3_tags[:channels].should == 2
25
+ id3_tags[:length].should == 38
26
+ id3_tags[:samplerate].should == 44100
27
+ id3_tags[:album_artist].should == 'Sample album artist'
28
+ id3_tags[:composer].should == 'Sample composer'
29
+ id3_tags[:grouping].should == 'Sample group'
30
+ id3_tags[:bpm].should == 110
31
+ id3_tags[:lyrics].should == "Sample lyrics line 1\rand line 2"
32
+ id3_tags[:compilation].should be_true
33
+ id3_tags[:cover_art][:mime_type].should == 'image/png'
34
+ id3_tags[:cover_art][:data].length.should == 3368
35
+ end
36
+ end
37
+
38
+ context 'given a track with no ID3 metadata' do
39
+ let(:track_with_metadata) {File.join File.dirname(__FILE__), '../assets/no_id3.mp3'}
40
+
41
+ it 'returns all the ID3 tag with a nil value' do
42
+ id3_tags = Id3Tags.read_tags_from track_with_metadata
43
+
44
+ id3_tags.keys.should =~ [:album, :artist, :comment, :genre, :samplerate,
45
+ :track, :disk, :title, :year, :bitrate, :channels, :length, :grouping,
46
+ :album_artist, :composer, :bpm, :lyrics, :compilation, :cover_art]
47
+
48
+ id3_tags[:title].should == ' ' # can never be nil
49
+ id3_tags[:album].should be_nil
50
+ id3_tags[:artist].should be_nil
51
+ id3_tags[:comment].should be_nil
52
+ id3_tags[:genre].should be_nil
53
+ id3_tags[:year].should == 0 # can never be nil
54
+ id3_tags[:track][:number].should be_nil
55
+ id3_tags[:track][:count].should be_nil
56
+ id3_tags[:disk][:number].should be_nil
57
+ id3_tags[:disk][:count].should be_nil
58
+ id3_tags[:bitrate].should == 128 # can never be nil
59
+ id3_tags[:channels].should == 2 # can never be nil
60
+ id3_tags[:length].should == 38 # can never be nil
61
+ id3_tags[:samplerate].should == 44100 # can never be nil
62
+ id3_tags[:album_artist].should be_nil
63
+ id3_tags[:composer].should be_nil
64
+ id3_tags[:grouping].should be_nil
65
+ id3_tags[:bpm].should be_nil
66
+ id3_tags[:lyrics].should be_nil
67
+ id3_tags[:compilation].should be_false
68
+ id3_tags[:cover_art][:mime_type].should be_nil
69
+ id3_tags[:cover_art][:data].should be_nil
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,67 @@
1
+ require 'id3_tags'
2
+ require_relative '../helpers/duplicate_file_helper'
3
+
4
+ describe 'Id3Tags.write_tags_to' do
5
+ context 'given a track with all the ID3 metadata and a Hash of empty ID3' do
6
+ let(:original_track) {File.join File.dirname(__FILE__), '../assets/all_id3.mp3'}
7
+ let(:new_metadata) { {} }
8
+
9
+ it 'resets all the editable ID3 tags of the track to nil' do
10
+ with_duplicate_file_of(original_track) do |duplicate|
11
+ Id3Tags.write_tags_to(duplicate, new_metadata)
12
+ id3_tags = Id3Tags.read_tags_from(duplicate)
13
+ [:album, :artist, :comment, :genre, :title, :grouping, :lyrics,
14
+ :album_artist, :composer, :bpm].each do |field|
15
+ id3_tags[field].should be_nil
16
+ end
17
+ id3_tags[:year].should == 0
18
+ id3_tags[:compilation].should be_false
19
+ # NOTE: Taglib does NOT allow the track_number to be nil; the old
20
+ # value is persisted in this case, so we cannot test for nil?
21
+ #id3_tags[:track][:number].should be_nil
22
+ id3_tags[:track][:count].should be_nil
23
+ id3_tags[:disk][:number].should be_nil
24
+ id3_tags[:disk][:count].should be_nil
25
+ id3_tags[:cover_art][:mime_type].should be_nil
26
+ id3_tags[:cover_art][:data].should be_nil
27
+ end
28
+ end
29
+ end
30
+
31
+ context 'given a track with no metadata and a Hash full of ID3' do
32
+ let(:original_track) {File.join File.dirname(__FILE__), '../assets/no_id3.mp3'}
33
+ let(:new_cover_art) {File.join File.dirname(__FILE__), '../assets/black_pixel.png'}
34
+ let(:new_cover_art_data) {File.open(new_cover_art, 'rb') {|io| io.read}}
35
+ let(:new_metadata) { {title: "A track", album: "An album", artist:
36
+ "An artist", year: 2012, comment: "A comment", genre: "A genre", bpm: 68,
37
+ composer: "A composer", lyrics: "A lyrics line 1\rand line 2",
38
+ album_artist: "An album artist", grouping: "A group", compilation: true,
39
+ track: {number: 1, count: 24}, disk: {number: 1, count: 2}, cover_art:
40
+ {mime_type: "image/png", data: new_cover_art_data}}
41
+ }
42
+
43
+ it 'sets all the editable ID3 tags of a track' do
44
+ with_duplicate_file_of(original_track) do |duplicate|
45
+ Id3Tags.write_tags_to(duplicate, new_metadata)
46
+ id3_tags = Id3Tags.read_tags_from(duplicate)
47
+ id3_tags[:album].should == 'An album'
48
+ id3_tags[:artist].should == 'An artist'
49
+ id3_tags[:comment].should == 'A comment'
50
+ id3_tags[:genre].should == 'A genre'
51
+ id3_tags[:title].should == 'A track'
52
+ id3_tags[:grouping].should == 'A group'
53
+ id3_tags[:lyrics].should == "A lyrics line 1\rand line 2"
54
+ id3_tags[:album_artist].should == 'An album artist'
55
+ id3_tags[:composer].should == 'A composer'
56
+ id3_tags[:bpm].should == 68
57
+ id3_tags[:compilation].should be_true
58
+ id3_tags[:track][:number].should == 1
59
+ id3_tags[:track][:count].should == 24
60
+ id3_tags[:disk][:number].should == 1
61
+ id3_tags[:disk][:count].should == 2
62
+ id3_tags[:cover_art][:mime_type].should == 'image/png'
63
+ id3_tags[:cover_art][:data].should == new_cover_art_data
64
+ end
65
+ end
66
+ end
67
+ end
metadata ADDED
@@ -0,0 +1,207 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: id3_tags
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Claudio Baccigalupo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: taglib-ruby
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: redcarpet
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: yard
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: Read and write ID3 metadata from/to MP3 files
127
+ email:
128
+ - claudio@topspinmedia.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - .rspec
135
+ - Gemfile
136
+ - HISTORY.md
137
+ - MIT-LICENSE
138
+ - README.md
139
+ - Rakefile
140
+ - TODO.md
141
+ - doc/Id3Tags.html
142
+ - doc/_index.html
143
+ - doc/class_list.html
144
+ - doc/css/common.css
145
+ - doc/css/full_list.css
146
+ - doc/css/style.css
147
+ - doc/file.README.html
148
+ - doc/file_list.html
149
+ - doc/frames.html
150
+ - doc/index.html
151
+ - doc/js/app.js
152
+ - doc/js/full_list.js
153
+ - doc/js/jquery.js
154
+ - doc/method_list.html
155
+ - doc/top-level-namespace.html
156
+ - id3_tags.gemspec
157
+ - lib/id3_tags.rb
158
+ - lib/id3_tags/version.rb
159
+ - spec/assets/all_id3.mp3
160
+ - spec/assets/black_pixel.png
161
+ - spec/assets/no_id3.mp3
162
+ - spec/helpers/duplicate_file_helper.rb
163
+ - spec/lib/idempotency_spec.rb
164
+ - spec/lib/read_id3_tags_spec.rb
165
+ - spec/lib/write_id3_tags_spec.rb
166
+ homepage: https://github.com/topspin/id3_tags
167
+ licenses:
168
+ - MIT
169
+ post_install_message:
170
+ rdoc_options: []
171
+ require_paths:
172
+ - lib
173
+ required_ruby_version: !ruby/object:Gem::Requirement
174
+ none: false
175
+ requirements:
176
+ - - ! '>='
177
+ - !ruby/object:Gem::Version
178
+ version: '0'
179
+ segments:
180
+ - 0
181
+ hash: -1313956793775071095
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ none: false
184
+ requirements:
185
+ - - ! '>='
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ segments:
189
+ - 0
190
+ hash: -1313956793775071095
191
+ requirements: []
192
+ rubyforge_project:
193
+ rubygems_version: 1.8.23
194
+ signing_key:
195
+ specification_version: 3
196
+ summary: ! 'Id3Tags provides two methods: * read_tags_from, which reads an MP3 file
197
+ and returns a Hash of metadata * write_tags_to, which writes a Hash of metadata
198
+ into an MP3 file'
199
+ test_files:
200
+ - spec/assets/all_id3.mp3
201
+ - spec/assets/black_pixel.png
202
+ - spec/assets/no_id3.mp3
203
+ - spec/helpers/duplicate_file_helper.rb
204
+ - spec/lib/idempotency_spec.rb
205
+ - spec/lib/read_id3_tags_spec.rb
206
+ - spec/lib/write_id3_tags_spec.rb
207
+ has_rdoc: