id3_tags 0.0.1

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