easytag 0.4.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/Gemfile +7 -2
  4. data/README.md +33 -27
  5. data/Rakefile +7 -7
  6. data/easytag.gemspec +2 -4
  7. data/lib/easytag.rb +12 -2
  8. data/lib/easytag/attributes/base.rb +32 -128
  9. data/lib/easytag/attributes/flac.rb +25 -0
  10. data/lib/easytag/attributes/mp3.rb +91 -554
  11. data/lib/easytag/attributes/mp4.rb +22 -492
  12. data/lib/easytag/attributes/ogg.rb +27 -0
  13. data/lib/easytag/attributes/vorbis.rb +18 -0
  14. data/lib/easytag/taggers/base.rb +19 -0
  15. data/lib/easytag/taggers/factory.rb +59 -0
  16. data/lib/easytag/taggers/flac.rb +23 -0
  17. data/lib/easytag/taggers/mp3.rb +77 -0
  18. data/lib/easytag/taggers/mp4.rb +73 -0
  19. data/lib/easytag/taggers/ogg.rb +25 -0
  20. data/lib/easytag/taggers/vorbis.rb +66 -0
  21. data/lib/easytag/util.rb +9 -31
  22. data/lib/easytag/version.rb +3 -4
  23. data/scripts/build_attributes_table.rb +34 -0
  24. data/{test → spec/data}/consistency.01.m4a +0 -0
  25. data/{test → spec/data}/consistency.01.mp3 +0 -0
  26. data/spec/data/consistency.flac +0 -0
  27. data/{test/consistency.02.m4a → spec/data/consistency.m4a} +0 -0
  28. data/{test/consistency.02.mp3 → spec/data/consistency.mp3} +0 -0
  29. data/spec/data/consistency.multiple_images.flac +0 -0
  30. data/{test/no_tags.m4a → spec/data/consistency.multiple_images.m4a} +0 -0
  31. data/spec/data/consistency.multiple_images.mp3 +0 -0
  32. data/spec/data/consistency.multiple_images.ogg +0 -0
  33. data/spec/data/consistency.ogg +0 -0
  34. data/{test → spec/data}/musicbrainz.m4a +0 -0
  35. data/spec/data/no_tags.flac +0 -0
  36. data/spec/data/no_tags.m4a +0 -0
  37. data/{test → spec/data}/no_tags.mp3 +0 -0
  38. data/spec/data/no_tags.ogg +0 -0
  39. data/{test → spec/data}/only_id3v1.mp3 +0 -0
  40. data/{test → spec/data}/only_id3v2.mp3 +0 -0
  41. data/spec/flac_tagger_spec.rb +24 -0
  42. data/spec/mp3_tagger_spec.rb +64 -0
  43. data/spec/mp4_tagger_spec.rb +22 -0
  44. data/spec/ogg_tagger_spec.rb +21 -0
  45. data/spec/shared_examples.rb +115 -0
  46. data/spec/spec_helper.rb +34 -0
  47. data/spec/tagger_factory_spec.rb +27 -0
  48. data/spec/util_spec.rb +47 -0
  49. metadata +47 -48
  50. data/lib/easytag/attributes.rb +0 -0
  51. data/lib/easytag/base.rb +0 -28
  52. data/lib/easytag/file.rb +0 -44
  53. data/lib/easytag/interfaces.rb +0 -18
  54. data/lib/easytag/interfaces/base.rb +0 -24
  55. data/lib/easytag/interfaces/mp3.rb +0 -48
  56. data/lib/easytag/interfaces/mp4.rb +0 -14
  57. data/test/test_consistency.rb +0 -233
  58. data/test/test_mp3.rb +0 -98
  59. data/test/test_mp4.rb +0 -49
  60. data/test/test_util.rb +0 -72
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 53a9645ec483ba68cc28dffd34002d149b43d24d
4
- data.tar.gz: 0cf232f7f04a0f1e3bf6606c8abf363cc2d4ef9f
3
+ metadata.gz: 16662d29b534ff4cc4ec6e52e35b1f725387c01d
4
+ data.tar.gz: f7b35f5fa59150c4937a3781fc206420741e6c8f
5
5
  SHA512:
6
- metadata.gz: 026bbbf0959293a24f17103bc0fa4d53e83c3edf80bf2c1e4553ff8cfefff3c930af5dca8ba205853d20a747be0c20c114f076121c9f8145e45c6497afb45e45
7
- data.tar.gz: 78507cf793dc807eeb7ee8d32ebd57cbf0937cf453a7ced4578d4b495a2016a80b4d8165ece596e534dc511bf89a23651e27684cbe8ea62065a32ff7a94053fc
6
+ metadata.gz: 23e58b9d0726dff60df794e0595fc6fbeac6a8b8a07c3bc82c47a13094cbdc44899886393ba14f491f3c732758866a4d163154c7aa9a6ad313beac9342668ddd
7
+ data.tar.gz: b2b80e1092243a8fa6f0ff6969ea29ef38b3dfa744fff6502376cb4dac412c9b886225351e8782861a0b357cddf69db2146ecda7c3825faf3ed29c2c2267c969
@@ -1,3 +1,14 @@
1
+ ##### v1.0.0 (2013-05-26) #####
2
+ * added: detecting file type from file signature
3
+ * first major release
4
+
5
+ ##### v0.6.0 (2013-05-25) #####
6
+ * complete overhaul, this release is not compatible with any previous release
7
+ * FLAC, Ogg support added
8
+
9
+ ##### v0.4.3 (2013-08-20) #####
10
+ * fixed: uninitialized constant error with regard to file unsupported exception
11
+
1
12
  ##### v0.4.2 (2013-06-23) #####
2
13
  * added: interal support for `TagLib::ID3v2::UnknownFrame`
3
14
  * info: because a new version of taglib-ruby hasn't been officially released
data/Gemfile CHANGED
@@ -1,7 +1,12 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # for TDAT fix, temporary until maintainers push a new version
4
- gem 'taglib-ruby', github: 'robinst/taglib-ruby', ref: 'bb6453e2e3404cbc8d1e3056880922797167a218'
3
+ source 'http://rubygems.org'
4
+
5
+ group :test do
6
+ gem 'rake'
7
+ gem 'rspec'
8
+ gem 'coveralls'
9
+ end
5
10
 
6
11
  # read dependencies from gemspec file
7
12
  gemspec
data/README.md CHANGED
@@ -1,52 +1,58 @@
1
1
  EasyTag is an abstract interface to the [TagLib](http://taglib.github.io/) audio tagging library. It is designed to provide a simple and consistent API regardless of file format being read.
2
2
 
3
+ Currently support file formats are: MP3, MP4, FLAC, and Ogg.
3
4
  A quick reference to which attributes are currently supported can be found [here](https://github.com/cjlucas/ruby-easytag/wiki/Currently-Supported-Attributes).
4
5
 
5
6
  [![Build Status (master)](https://travis-ci.org/cjlucas/ruby-easytag.png?branch=master "Branch: master")](https://travis-ci.org/cjlucas/ruby-easytag)
6
7
  [![Build Status (develop)](https://travis-ci.org/cjlucas/ruby-easytag.png?branch=develop "Branch: develop")](https://travis-ci.org/cjlucas/ruby-easytag)
7
8
 
9
+ NOTE: v0.6.0+ is not compatible with any version prior to v0.6.0.
10
+
8
11
  ---
9
12
  ## Synopsis ##
13
+
10
14
  ```ruby
11
15
  require 'easytag'
12
16
 
13
- mp3 = EasyTag::File.new("01 Shout Out Loud.mp3")
14
-
15
- # easy access to attributes
16
- mp3.title
17
- # => "Shout Out Loud"
18
- mp3.artist
19
- # => "Amos Lee"
20
- mp3.year
21
- # => 2006
22
- mp3.date
23
- # => #<DateTime: 2006-10-03T00:00:00+00:00 ((2454012j,0s,0n),+0s,2299161j)>
24
- mp3.album_art
25
- # => [#<EasyTag::Image:0x007f7fa542f528>]
26
-
27
- # get access to the taglib-powered backend
28
- mp3.info
29
- # => #<TagLib::MPEG::File:0x007f7fa539e050 @__swigtype__="_p_TagLib__MPEG__File">
30
- mp3.close
31
-
32
- # A block interface is also available
33
- EasyTag::File.open('01 Shout Out Loud.m4a') do |m4a|
34
- puts m4a.title
35
- puts m4a.comments
36
- puts m4a.genre
17
+ tagger = EasyTag.open('audio.flac') # calls EasyTag::TaggerFactory::open
18
+ tagger # => <EasyTag::FLACTagger:0x007fc28aba4c90>
19
+ tagger.title # => "She Lives in My Lap"
20
+ tagger.artist # => "OutKast feat. Rosario Dawson"
21
+ tagger.album_artist # => "OutKast"
22
+ tagger.musicbrainz_artist_id # => ["73fdb566-a9b1-494c-9f32-51768ec9fd27", "9facf8dc-df23-4561-85c5-ece75d692f21"]
23
+
24
+ # The backing taglib object is also available
25
+ f.taglib # => <TagLib::FLAC::File:0x007fc28aba4c68 @__swigtype__="_p_TagLib__FLAC__File">
26
+
27
+ f.close
28
+
29
+ # A block can also be specified
30
+ EasyTag.open('audio.mp3') do |tagger|
31
+ tagger.title # => "She Lives in My Lap"
32
+ tagger.artist # => "OutKast feat. Rosario Dawson"
33
+ tagger.album_artist # => "OutKast"
34
+
35
+ # tagger.close will be called after the block is executed
36
+ end
37
+
38
+ # Taggers also return consistent defaults for missing attributes
39
+ EasyTag..open('no_tags.ogg') do |tagger|
40
+ tagger.year # => 0 (for any int attribute)
41
+ tagger.album_art # => [] (for any list attribute)
42
+ tagger.title # => nil (for any non-int, non-list attribute)
37
43
  end
38
44
  ```
45
+
46
+
39
47
  ## Requirements ##
40
48
  - Ruby versions
41
- - 1.9
42
49
  - 2.0
50
+ - 2.1
43
51
  - Dependencies
44
52
  - [TagLib](http://taglib.github.io/) (1.8+)
45
53
  - [taglib-ruby](https://github.com/robinst/taglib-ruby) (0.6.0+)
46
- - [ruby-mp3info](https://github.com/moumar/ruby-mp3info)
47
54
  - [ruby-imagespec](https://github.com/andersonbrandon/ruby-imagespec)
48
55
 
49
56
  ## TODO ##
50
57
  - API Documentation
51
- - FLAC and OGG support
52
58
  - Write support
data/Rakefile CHANGED
@@ -1,16 +1,16 @@
1
1
  $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
2
2
 
3
- require 'rake/testtask'
3
+ require 'rspec/core/rake_task'
4
4
 
5
5
  require 'easytag/version'
6
6
 
7
- task :default => [:test]
7
+ task :default => [:spec]
8
+ task :test => :spec
8
9
 
9
- task :test do
10
- Rake::TestTask.new do |t|
11
- t.libs << 'test'
12
- t.test_files = FileList['test/test_*.rb']
13
- t.verbose = false
10
+ task :spec do
11
+ RSpec::Core::RakeTask.new do |task|
12
+ task.verbose = false
13
+ task.rspec_opts = '--color'
14
14
  end
15
15
  end
16
16
 
@@ -19,9 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.test_files = s.files.select { |f| f.match(/^test\/.*\.rb$/) }
20
20
  s.platform = Gem::Platform::RUBY
21
21
 
22
- #s.add_runtime_dependency('taglib-ruby', '>= 0.6.0')
22
+ s.add_runtime_dependency('taglib-ruby', '>= 0.6.0')
23
23
  s.add_runtime_dependency('ruby-imagespec', '>= 0.3.1')
24
- s.add_runtime_dependency('ruby-mp3info', '>= 0.8')
25
-
26
- s.add_development_dependency('rake')
27
24
  end
25
+
@@ -1,5 +1,15 @@
1
1
  require 'bundler/setup'
2
2
 
3
3
  require 'easytag/util'
4
- require 'easytag/file'
5
- require 'easytag/interfaces'
4
+ require 'easytag/taggers/mp3'
5
+ require 'easytag/taggers/mp4'
6
+ require 'easytag/taggers/vorbis'
7
+ require 'easytag/taggers/flac'
8
+ require 'easytag/taggers/ogg'
9
+ require 'easytag/taggers/factory'
10
+
11
+ module EasyTag
12
+ def self.open(file, &block)
13
+ TaggerFactory.open(file, &block)
14
+ end
15
+ end
@@ -1,143 +1,47 @@
1
- module EasyTag::Attributes
2
- # for type casting
3
- module Type
4
- STRING = 0
5
- INT = 1
6
- FLOAT = 2
7
- INT_LIST = 3
8
- STRING_LIST = 4
9
- BOOLEAN = 5
10
- DATETIME = 6
11
- LIST = 7
12
- end
13
- class BaseAttribute
14
- Utilities = EasyTag::Utilities
15
-
16
- attr_reader :name, :aliases, :ivar
17
-
18
- def initialize(args)
19
- @name = args[:name]
20
- @type = args[:type]
21
- @default = args[:default] || self.class.default_for_type(@type)
22
- @options = args[:options] || {}
23
- @handler_opts = args[:handler_opts] || {}
24
- @aliases = args[:aliases] || []
25
- @ivar = BaseAttribute.name_to_ivar(@name)
26
-
27
- if args[:handler].is_a?(Symbol)
28
- @handler = method(args[:handler])
29
- elsif args[:handler].is_a?(Proc)
30
- @handler = args[:handler]
31
- end
32
-
33
- # fill default options
34
-
35
- # Remove nil objects from array (post process)
36
- @options[:compact] ||= false
37
- # Delete empty objects in array (post process)
38
- @options[:delete_empty] ||= false
39
- # normalizes key (if hash) (handler or post process)
40
- @options[:normalize] ||= false
41
- # cast key (if hash) to symbol (handler or post process)
42
- @options[:to_sym] ||= false
43
-
44
- end
45
-
46
- def self.can_clone?(obj)
47
- obj.is_a?(String) || obj.is_a?(Array) || obj.is_a?(Hash)
48
- end
49
-
50
- def self.deep_copy(obj)
51
- Marshal.load(Marshal.dump(obj))
52
- end
53
-
54
- def self.default_for_type(type)
55
- case type
56
- when Type::STRING
57
- ''
58
- when Type::DATETIME
59
- nil
60
- when Type::INT
61
- 0
62
- when Type::INT_LIST
63
- [0, 0] # TODO: don't assume an INT_LIST is always an int pair
64
- when Type::BOOLEAN
65
- false
66
- when Type::LIST
67
- []
1
+ module EasyTag
2
+ module BaseAttributeAccessors
3
+
4
+ def audio_prop_reader(attr_name, prop_name = nil, **opts)
5
+ prop_name = attr_name if prop_name.nil?
6
+ define_method(attr_name) do
7
+ v = self.class.read_audio_property(taglib, prop_name)
8
+ self.class.post_process(v, opts)
68
9
  end
69
10
  end
70
11
 
71
- def default
72
- BaseAttribute.can_clone?(@default) ?
73
- BaseAttribute.deep_copy(@default) : @default
74
- end
75
-
76
- def call(iface)
77
- data = @handler.call(iface)
78
- data = type_cast(data)
79
- post_process(data)
80
- end
81
-
82
- def type_cast(data)
83
- case @type
84
- when Type::INT
85
- data = data.to_i
86
- when Type::DATETIME
87
- data = Utilities.get_datetime(data.to_s)
88
- when Type::LIST
89
- data = Array(data)
12
+ def cast(data, key, **opts)
13
+ case opts[key]
14
+ when :int
15
+ data.to_i
16
+ when :int_pair
17
+ Utilities.get_int_pair(data)
18
+ when :datetime
19
+ Utilities.get_datetime(data.to_s)
20
+ when :list
21
+ Array(data)
22
+ when :bool
23
+ [1, '1', true].include?(data)
24
+ else
25
+ data
90
26
  end
91
-
92
- data
93
27
  end
94
28
 
95
- def post_process(data)
96
- if @options[:is_flag]
97
- data = data.to_i == 1 ? true : false
98
- end
99
-
100
- # fall back to default if data is nil
101
- data ||= default
102
-
103
- # REVIEW: the compact option may not be needed anymore
104
- # since we've done away with casting empty strings to nil
105
- data.compact! if @options[:compact] && data.respond_to?(:compact!)
106
-
107
- if @options[:delete_empty]
108
- data.select! { |item| !item.empty? if item.respond_to?(:empty?) }
109
- end
110
-
111
- data = Utilities.normalize_object(data) if @options[:normalize]
112
-
113
- # TODO: roll this out to a method that supports more than just array
114
- if @options[:to_sym] && data.is_a?(Array)
115
- data.map! { |item| item.to_sym if item.respond_to?(:to_sym) }
29
+ def extract(data, **opts)
30
+ if opts.has_key?(:extract_list_pos)
31
+ data = data[opts[:extract_list_pos]]
116
32
  end
117
-
118
33
  data
119
34
  end
120
-
121
- def self.name_to_ivar(name)
122
- name.to_s.gsub(/\?/, '').insert(0, '@').to_sym
123
- end
124
-
125
- # read handlers
126
35
 
127
- def read_default(iface)
128
- default
36
+ def post_process(data, opts)
37
+ data = cast(data, :cast, **opts)
38
+ data = extract(data, **opts)
39
+ cast(data, :returns, **opts)
129
40
  end
130
41
 
131
- def read_audio_property(iface)
132
- key = @handler_opts[:key]
133
- warn "@handler_opts[:key] doesn't exist" if key.nil?
134
- iface.info.audio_properties.send(key)
135
- end
136
-
137
- def user_info_lookup(iface)
138
- key = @handler_opts[:key]
139
- warn "@handler_opts[:key] doesn't exist" if key.nil?
140
- iface.user_info_normalized[key]
42
+ def read_audio_property(taglib, key)
43
+ taglib.audio_properties.send(key)
141
44
  end
142
45
  end
143
46
  end
47
+
@@ -0,0 +1,25 @@
1
+ require_relative 'vorbis'
2
+
3
+ module EasyTag
4
+ module FLACAttributeAccessors
5
+ def field_reader(attr_name, fields = nil, **opts)
6
+ fields = attr_name.to_s.upcase if fields.nil?
7
+ define_method(attr_name) do
8
+ v = self.class.read_fields(taglib.xiph_comment, fields, **opts)
9
+ self.class.post_process(v, **opts)
10
+ end
11
+ end
12
+
13
+ def album_art_reader(attr_name, **opts)
14
+ define_method(attr_name) do
15
+ taglib.picture_list.collect do |pic|
16
+ EasyTag::Image.new(pic.data).tap do |img|
17
+ img.mime_type = pic.mime_type
18
+ img.type = pic.type
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -4,605 +4,142 @@ require 'easytag/image'
4
4
  require 'easytag/util'
5
5
  require 'easytag/attributes/base'
6
6
 
7
- module EasyTag::Attributes
8
- class MP3Attribute < BaseAttribute
9
- attr_reader :name, :ivar
10
-
11
- def initialize(args)
12
- super(args)
13
- @id3v2_frames = args[:id3v2_frames] || []
14
- @id3v1_tag = args[:id3v1_tag] || nil
15
-
16
- # fill default options
17
-
18
- # ID3 stores boolean values as numeric strings
19
- # set to true to enable type casting (post process)
20
- @options[:is_flag] ||= false
21
- # return entire field list instead of first item in field list
22
- @options[:field_list] ||= false
7
+ module EasyTag
8
+ module MP3AttributeAccessors
9
+ include BaseAttributeAccessors
10
+
11
+ def single_tag_reader(attr_name, id3v2_frames = nil, id3v1_tag = nil, **opts)
12
+ id3v2_frames = Array(id3v2_frames)
13
+ define_method(attr_name) do
14
+ v = self.class.read_first_tag(taglib, id3v2_frames, id3v1_tag, opts)
15
+ self.class.post_process(v, opts)
16
+ end
23
17
  end
24
18
 
25
-
26
- def frames_for_id(id, iface)
27
- iface.info.id3v2_tag.frame_list(id)
19
+ def all_tags_reader(attr_name, id3v2_frames = nil, id3v1_tag = nil, **opts)
20
+ id3v2_frames = Array(id3v2_frames)
21
+ define_method(attr_name) do
22
+ v = self.class.read_all_tags(taglib, id3v2_frames, id3v1_tag, opts)
23
+ self.class.post_process(v, opts)
24
+ end
28
25
  end
29
26
 
30
- def first_frame_for_id(id, iface)
31
- frames_for_id(id, iface).first
27
+ def user_info_reader(attr_name, key = nil, **opts)
28
+ key = attr_name if key.nil?
29
+ define_method(attr_name) do
30
+ @user_info = self.class.read_user_info(taglib, **opts) if @user_info.nil?
31
+ self.class.post_process(@user_info[key], opts)
32
+ end
32
33
  end
33
34
 
34
- def data_from_frame(frame)
35
- data = nil
36
- if frame.is_a?(TagLib::ID3v2::TextIdentificationFrame)
37
- field_list = frame.field_list
38
- data = @options[:field_list] ? field_list : field_list.first
39
- elsif frame.is_a?(TagLib::ID3v2::UnsynchronizedLyricsFrame)
40
- data = frame.text
41
- elsif frame.is_a?(TagLib::ID3v2::CommentsFrame)
42
- data = frame.text
43
- elsif frame.is_a?(TagLib::ID3v2::AttachedPictureFrame)
44
- data = EasyTag::Image.new(frame.picture)
45
- data.desc = frame.description
46
- data.type = frame.type
47
- data.mime_type = frame.mime_type
48
- elsif frame.is_a?(TagLib::ID3v2::UnknownFrame)
49
- nil
50
- else
51
- warn 'no defined frames match the given frame'
35
+ def ufid_reader(attr_name, owner, **opts)
36
+ define_method(attr_name) do
37
+ v = self.class.read_ufid(taglib, owner, opts)
38
+ self.class.post_process(v, opts)
52
39
  end
40
+ end
53
41
 
54
- data
42
+ def date_reader(attr_name, **opts)
43
+ opts[:returns] = :datetime unless opts.has_key?(:returns)
44
+ define_method(attr_name) do
45
+ v = self.class.read_date(taglib, opts)
46
+ self.class.post_process(v, opts)
47
+ end
55
48
  end
56
49
 
57
- #
58
- # read handlers
59
- #
60
-
61
- # read_all_id3
62
- #
63
- # gets data from each frame id given
64
- # only falls back to the id3v1 tag if none found
65
- def read_all_id3(iface)
50
+ # gets data from each frame id given only falls back
51
+ # to the id3v1 tag if no id3v2 frames were found
52
+ def read_all_tags(taglib, id3v2_frames, id3v1_tag = nil, **opts)
66
53
  frames = []
67
- @id3v2_frames.each do |f|
68
- frames += frames_for_id(f, iface)
69
- end
54
+ id3v2_frames.each { |frame_id| frames += id3v2_frames(taglib, frame_id) }
70
55
 
71
56
  data = []
72
57
  # only check id3v1 if no id3v2 frames found
73
58
  if frames.empty?
74
- data << iface.info.id3v1_tag.send(@id3v1_tag) unless @id3v1_tag.nil?
59
+ data << id3v1_tag(taglib, id3v1_tag) unless id3v1_tag.nil?
75
60
  else
76
- frames.each { |frame| data << data_from_frame(frame) }
61
+ frames.each { |frame| data << data_from_frame(frame, **opts) }
77
62
  end
78
63
 
79
- data
64
+ data.compact
80
65
  end
81
66
 
82
- # read_first_id3
83
- #
84
- # Similar to read_all_id3, but optimized for reading only one frame at max
85
- def read_first_id3(iface)
86
- frame = nil
87
- @id3v2_frames.each do |f|
88
- frame = first_frame_for_id(f, iface) if frame.nil?
89
- end
67
+ def read_first_tag(taglib, id3v2_frames, id3v1_tag = nil, **opts)
68
+ read_all_tags(taglib, id3v2_frames, id3v1_tag, **opts).first
69
+ end
90
70
 
91
- if frame.nil?
92
- data = iface.info.id3v1_tag.send(@id3v1_tag) unless @id3v1_tag.nil?
71
+ def id3v1_tag(taglib, tag_name)
72
+ return nil if taglib.id3v1_tag.empty?
73
+ v = taglib.id3v1_tag.send(tag_name)
74
+ # TEMPFIX: nonexistent id3v1 tags return an empty string (taglib-ruby issue #49)
75
+ case
76
+ when v.is_a?(Fixnum) && v.zero?
77
+ nil
78
+ when v.is_a?(String) && v.empty?
79
+ nil
93
80
  else
94
- data = data_from_frame(frame)
81
+ v
95
82
  end
83
+ end
96
84
 
97
- data
85
+ def id3v2_frames(taglib, frame_id)
86
+ taglib.id3v2_tag.frame_list(frame_id)
98
87
  end
99
88
 
100
- def read_int_pair(iface)
101
- int_pair_str = read_first_id3(iface).to_s
102
- EasyTag::Utilities.get_int_pair(int_pair_str)
89
+ def data_from_frame(frame, **opts)
90
+ case
91
+ when frame.is_a?(TagLib::ID3v2::TextIdentificationFrame)
92
+ field_list = frame.field_list
93
+ opts[:field_list] ? field_list : field_list.first
94
+ when frame.is_a?(TagLib::ID3v2::UnsynchronizedLyricsFrame)
95
+ frame.text
96
+ when frame.is_a?(TagLib::ID3v2::CommentsFrame)
97
+ frame.text
98
+ when frame.is_a?(TagLib::ID3v2::AttachedPictureFrame)
99
+ EasyTag::Image.new(frame.picture).tap do |img|
100
+ img.desc = frame.description
101
+ img.type = frame.type
102
+ img.mime_type = frame.mime_type
103
+ end
104
+ else
105
+ nil
106
+ end
103
107
  end
104
108
 
105
- def read_field_list_as_key_value(iface)
106
- kv_hash = {}
107
- frame_data = read_all_id3(iface)
109
+ def read_user_info(taglib, **opts)
110
+ user_info = {}
111
+ frame_data = read_all_tags(taglib, ['TXXX'], nil, {field_list: true})
108
112
 
109
113
  frame_data.each do |data|
110
114
  key = data[0]
111
115
  values = data[1..-1]
112
116
 
113
- key = Utilities.normalize_string(key) if @options[:normalize]
114
- key = key.to_sym if @options[:to_sym]
115
- kv_hash[key] = values.count > 1 ? values : values.first
117
+ user_info[key] = values.count > 1 ? values : values.first
116
118
  end
117
119
 
118
- kv_hash
120
+ user_info
119
121
  end
120
122
 
121
- def read_date(iface)
122
- id3v1 = iface.info.id3v1_tag
123
-
124
- v10_year = id3v1.year.to_s if id3v1.year > 0
125
- v23_year = data_from_frame(first_frame_for_id('TYER', iface))
126
- v23_date = data_from_frame(first_frame_for_id('TDAT', iface))
127
- v24_date = data_from_frame(first_frame_for_id('TDRC', iface))
123
+ # NOTE: id3v2.3 tags (TYER+TDAT) will lose month/day information due to taglib's
124
+ # internal frame conversion. During the conversion, the TDAT frame is
125
+ # dropped and only the TYER frame is used in the conversion to TDRC.
126
+ # (see: https://github.com/taglib/taglib/issues/127)
127
+ def read_date(taglib, **opts)
128
+ v10_year = taglib.id3v1_tag.year.to_s if taglib.id3v1_tag.year > 0
129
+ v24_date = read_first_tag(taglib, ['TDRC'])
128
130
 
129
131
  # check variables in order of importance
130
- date_str = v24_date || v23_year || v10_year
131
- # only append v23_date if date_str is currently a year
132
- date_str << v23_date unless v23_date.nil? or date_str.length > 4
132
+ date_str = v24_date || v10_year
133
133
  puts "MP3#date: date_str = \"#{date_str}\"" if $DEBUG
134
134
 
135
135
  date_str
136
136
  end
137
137
 
138
- def read_ufid(iface)
139
- frames = iface.info.id3v2_tag.frame_list('UFID')
140
- ufid = nil
141
-
142
- frames.each do |frame|
143
- if @handler_opts[:owner].eql?(frame.owner)
144
- ufid = frame.identifier
145
- break
146
- end
147
- end
148
-
149
- ufid
138
+ def read_ufid(taglib, owner, opts)
139
+ frames = taglib.id3v2_tag.frame_list('UFID')
140
+ frames.each { |frame| return frame.identifier if owner.eql?(frame.owner) }
141
+ nil
150
142
  end
151
143
  end
152
144
  end
153
145
 
154
- module EasyTag::Attributes
155
- MP3_ATTRIB_ARGS = [
156
- # title
157
- {
158
- :name => :title,
159
- :id3v2_frames => ['TIT2'],
160
- :id3v1_tag => :title,
161
- :handler => :read_first_id3,
162
- :type => Type::STRING,
163
- },
164
-
165
- # title_sort_order
166
- # TSOT - (v2.4 only)
167
- # XSOT - Musicbrainz Picard custom
168
- {
169
- :name => :title_sort_order,
170
- :id3v2_frames => ['TSOT', 'XSOT'],
171
- :handler => :read_first_id3,
172
- :type => Type::STRING,
173
- },
174
-
175
- # subtitle
176
- {
177
- :name => :subtitle,
178
- :id3v2_frames => ['TIT3'],
179
- :handler => :read_first_id3,
180
- :type => Type::STRING,
181
- },
182
-
183
- # artist
184
- {
185
- :name => :artist,
186
- :id3v2_frames => ['TPE1'],
187
- :id3v1_tag => :artist,
188
- :handler => :read_first_id3,
189
- :type => Type::STRING,
190
- },
191
-
192
- # artist_sort_order
193
- # TSOP - (v2.4 only)
194
- # XSOP - Musicbrainz Picard custom
195
- {
196
- :name => :artist_sort_order,
197
- :id3v2_frames => ['TSOP', 'XSOP'],
198
- :handler => :read_first_id3,
199
- :type => Type::STRING,
200
- },
201
-
202
- # album_artist
203
- {
204
- :name => :album_artist,
205
- :id3v2_frames => ['TPE2'],
206
- :handler => :read_first_id3,
207
- :type => Type::STRING,
208
- },
209
-
210
- # album_artist_sort_order
211
- {
212
- :name => :album_artist_sort_order,
213
- :handler => :user_info_lookup,
214
- :handler_opts => {:key => :albumartistsort},
215
- :type => Type::STRING,
216
- },
217
-
218
- # album
219
- {
220
- :name => :album,
221
- :id3v2_frames => ['TALB'],
222
- :id3v1_tag => :album,
223
- :handler => :read_first_id3,
224
- :type => Type::STRING,
225
- },
226
-
227
- # compilation?
228
- {
229
- :name => :compilation?,
230
- :id3v2_frames => ['TCMP'],
231
- :handler => :read_first_id3,
232
- :type => Type::BOOLEAN,
233
- # TODO: remove is_flag option, determine boolean value implicitly
234
- :options => {:is_flag => true},
235
- },
236
-
237
- # album_sort_order
238
- # TSOA - (v2.4 only)
239
- # XSOA - Musicbrainz Picard custom
240
- {
241
- :name => :album_sort_order,
242
- :id3v2_frames => ['TSOA', 'XSOA'],
243
- :handler => :read_first_id3,
244
- :type => Type::STRING,
245
- },
246
-
247
- # genre
248
- {
249
- :name => :genre,
250
- :id3v2_frames => ['TCON'],
251
- :id3v1_tag => :genre,
252
- :handler => :read_first_id3,
253
- :type => Type::STRING,
254
- },
255
-
256
- # disc_subtitle
257
- {
258
- :name => :disc_subtitle,
259
- :id3v2_frames => ['TSST'],
260
- :handler => :read_first_id3,
261
- :type => Type::STRING,
262
- },
263
-
264
- # media
265
- {
266
- :name => :media,
267
- :id3v2_frames => ['TMED'],
268
- :handler => :read_first_id3,
269
- :type => Type::STRING,
270
- },
271
-
272
- # label
273
- {
274
- :name => :label,
275
- :id3v2_frames => ['TPUB'],
276
- :handler => :read_first_id3,
277
- :type => Type::STRING,
278
- },
279
-
280
- # encoded_by
281
- {
282
- :name => :encoded_by,
283
- :id3v2_frames => ['TENC'],
284
- :handler => :read_first_id3,
285
- :type => Type::STRING,
286
- },
287
-
288
- # encoder_settings
289
- {
290
- :name => :encoder_settings,
291
- :id3v2_frames => ['TSSE'],
292
- :handler => :read_first_id3,
293
- :type => Type::STRING,
294
- },
295
-
296
- # group
297
- {
298
- :name => :group,
299
- :id3v2_frames => ['TIT1'],
300
- :handler => :read_first_id3,
301
- :type => Type::STRING,
302
- },
303
-
304
- # composer
305
- {
306
- :name => :composer,
307
- :id3v2_frames => ['TCOM'],
308
- :handler => :read_first_id3,
309
- :type => Type::STRING,
310
- },
311
-
312
- # conductor
313
- {
314
- :name => :conductor,
315
- :id3v2_frames => ['TPE3'],
316
- :handler => :read_first_id3,
317
- :type => Type::STRING,
318
- },
319
-
320
- # remixer
321
- {
322
- :name => :remixer,
323
- :id3v2_frames => ['TPE4'],
324
- :handler => :read_first_id3,
325
- :type => Type::STRING,
326
- },
327
-
328
- # lyrics
329
- {
330
- :name => :lyrics,
331
- :id3v2_frames => ['USLT'],
332
- :handler => :read_first_id3,
333
- :type => Type::STRING,
334
- },
335
-
336
- # lyricist
337
- {
338
- :name => :lyricist,
339
- :id3v2_frames => ['TEXT'],
340
- :handler => :read_first_id3,
341
- :type => Type::STRING,
342
- },
343
-
344
- # copyright
345
- {
346
- :name => :copyright,
347
- :id3v2_frames => ['TCOP'],
348
- :handler => :read_first_id3,
349
- :type => Type::STRING,
350
- },
351
-
352
- # bpm
353
- {
354
- :name => :bpm,
355
- :id3v2_frames => ['TBPM'],
356
- :handler => :read_first_id3,
357
- :type => Type::INT,
358
- },
359
-
360
- # mood
361
- {
362
- :name => :mood,
363
- :id3v2_frames => ['TMOO'],
364
- :handler => :read_first_id3,
365
- :type => Type::STRING,
366
- },
367
-
368
- # track_num
369
- {
370
- :name => :track_num,
371
- :id3v2_frames => ['TRCK'],
372
- :id3v1_tag => :track,
373
- :default => [0, 0],
374
- :handler => :read_int_pair,
375
- :type => Type::INT_LIST, # don't know if this will ever be useful
376
- },
377
-
378
- # disc_num
379
- {
380
- :name => :disc_num,
381
- :id3v2_frames => ['TPOS'],
382
- :default => [0, 0],
383
- :handler => :read_int_pair,
384
- :type => Type::INT_LIST, # don't know if this will ever be useful
385
- },
386
-
387
- # original_date
388
- # TDOR - orig release date (v2.4 only)
389
- # TORY - orig release year (v2.3)
390
- {
391
- :name => :original_date,
392
- :id3v2_frames => ['TDOR', 'TORY'],
393
- :handler => :read_first_id3,
394
- :type => Type::DATETIME,
395
- },
396
-
397
- # comments
398
- {
399
- :name => :comments,
400
- :id3v2_frames => ['COMM'],
401
- :id3v1_tag => :comment,
402
- :handler => :read_all_id3,
403
- :default => [],
404
- :options => { :compact => true, :delete_empty => true }
405
- },
406
-
407
- # comment
408
- {
409
- :name => :comment,
410
- :handler => lambda { |iface| iface.comments.first },
411
- :type => Type::STRING,
412
- },
413
-
414
- # album_art
415
- {
416
- :name => :album_art,
417
- :id3v2_frames => ['APIC'],
418
- :handler => :read_all_id3,
419
- :default => [],
420
- },
421
-
422
- # date
423
- {
424
- :name => :date,
425
- :handler => :read_date,
426
- :type => Type::DATETIME,
427
- },
428
-
429
- # year
430
- {
431
- :name => :year,
432
- :handler => lambda { |iface| iface.date.nil? ? 0 : iface.date.year }
433
- },
434
-
435
- # apple_id
436
- {
437
- :name => :apple_id,
438
- :handler => :read_default,
439
- :type => Type::STRING,
440
- },
441
-
442
- # user_info
443
- {
444
- :name => :user_info,
445
- :id3v2_frames => ['TXXX'],
446
- :handler => :read_field_list_as_key_value,
447
- :default => {},
448
- :options => {:field_list => true},
449
- },
450
-
451
- # user_info_normalized
452
- {
453
- :name => :user_info_normalized,
454
- :id3v2_frames => ['TXXX'],
455
- :handler => :read_field_list_as_key_value,
456
- :default => {},
457
- :options => {:normalize => true,
458
- :to_sym => true,
459
- :field_list => true },
460
- },
461
-
462
- # asin
463
- {
464
- :name => :asin,
465
- :handler => :user_info_lookup,
466
- :handler_opts => {:key => :asin},
467
- :type => Type::STRING,
468
- },
469
-
470
- #
471
- # MusicBrainz Attributes
472
- #
473
-
474
- # musicbrainz_track_id
475
- {
476
- :name => :musicbrainz_track_id,
477
- :handler => :read_ufid,
478
- :handler_opts => {:owner => 'http://musicbrainz.org'},
479
- :type => Type::STRING,
480
- },
481
-
482
- # musicbrainz_album_artist_id
483
- {
484
- :name => :musicbrainz_album_artist_id,
485
- :handler => :user_info_lookup,
486
- :handler_opts => {:key => :musicbrainz_album_artist_id},
487
- :type => Type::STRING,
488
- },
489
-
490
- # musicbrainz_artist_id
491
- {
492
- :name => :musicbrainz_artist_id,
493
- :handler => :user_info_lookup,
494
- :handler_opts => {:key => :musicbrainz_artist_id},
495
- :type => Type::LIST,
496
- },
497
-
498
- # musicbrainz_album_id
499
- {
500
- :name => :musicbrainz_album_id,
501
- :handler => :user_info_lookup,
502
- :handler_opts => {:key => :musicbrainz_album_id},
503
- :type => Type::STRING,
504
- },
505
-
506
- # musicbrainz_album_status
507
- {
508
- :name => :musicbrainz_album_status,
509
- :handler => :user_info_lookup,
510
- :handler_opts => {:key => :musicbrainz_album_status},
511
- :type => Type::STRING,
512
- },
513
-
514
- # musicbrainz_album_type
515
- {
516
- :name => :musicbrainz_album_type,
517
- :handler => :user_info_lookup,
518
- :handler_opts => {:key => :musicbrainz_album_type},
519
- :type => Type::LIST,
520
- },
521
-
522
-
523
- # musicbrainz_release_group_id
524
- {
525
- :name => :musicbrainz_release_group_id,
526
- :handler => :user_info_lookup,
527
- :handler_opts => {:key => :musicbrainz_release_group_id},
528
- :type => Type::STRING,
529
- },
530
-
531
- # musicbrainz_album_release_country
532
- {
533
- :name => :musicbrainz_album_release_country,
534
- :handler => :user_info_lookup,
535
- :handler_opts => {:key => :musicbrainz_album_release_country},
536
- :type => Type::STRING,
537
- },
538
-
539
- #
540
- # Audio Properties
541
- #
542
-
543
- # length
544
- {
545
- :name => :length,
546
- :aliases => [:duration],
547
- :handler => :read_audio_property,
548
- :handler_opts => {:key => :length},
549
- :type => Type::INT,
550
- },
551
-
552
- # bitrate
553
- {
554
- :name => :bitrate,
555
- :handler => :read_audio_property,
556
- :handler_opts => {:key => :bitrate},
557
- :type => Type::INT,
558
- },
559
-
560
- # sample_rate
561
- {
562
- :name => :sample_rate,
563
- :handler => :read_audio_property,
564
- :handler_opts => {:key => :sample_rate},
565
- :type => Type::INT,
566
- },
567
-
568
- # channels
569
- {
570
- :name => :channels,
571
- :handler => :read_audio_property,
572
- :handler_opts => {:key => :channels},
573
- :type => Type::INT,
574
- },
575
-
576
- # copyrighted?
577
- {
578
- :name => :copyrighted?,
579
- :handler => :read_audio_property,
580
- :handler_opts => {:key => :copyrighted?},
581
- :type => Type::BOOLEAN,
582
- },
583
-
584
- # layer
585
- {
586
- :name => :layer,
587
- :handler => :read_audio_property,
588
- :handler_opts => {:key => :layer},
589
- :type => Type::INT,
590
- },
591
-
592
- # original?
593
- {
594
- :name => :original?,
595
- :handler => :read_audio_property,
596
- :handler_opts => {:key => :original?},
597
- :type => Type::BOOLEAN,
598
- },
599
-
600
- # protection_enabled?
601
- {
602
- :name => :protection_enabled?,
603
- :handler => :read_audio_property,
604
- :handler_opts => {:key => :protection_enabled},
605
- :type => Type::BOOLEAN,
606
- },
607
- ]
608
- end