easytag 0.4.3 → 1.0.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 (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
@@ -0,0 +1,27 @@
1
+ require 'base64'
2
+
3
+ require_relative 'vorbis'
4
+
5
+ module EasyTag
6
+ module OggAttributeAccessors
7
+ def field_reader(attr_name, fields = nil, **opts)
8
+ fields = attr_name.to_s.upcase if fields.nil?
9
+ define_method(attr_name) do
10
+ v = self.class.read_fields(taglib.tag, fields, **opts)
11
+ self.class.post_process(v, **opts)
12
+ end
13
+ end
14
+
15
+ def album_art_reader(attr_name, **opts)
16
+ opts[:returns] = :list
17
+ define_method(attr_name) do
18
+ v = self.class.read_fields(taglib.tag, 'METADATA_BLOCK_PICTURE', **opts)
19
+ v.collect do |b64_data|
20
+ pic = TagLib::FLAC::Picture.new
21
+ pic.parse(Base64.decode64(b64_data))
22
+ EasyTag::Image.new(pic.data)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ require_relative 'base'
2
+
3
+ module EasyTag
4
+ module VorbisAttributeAccessors
5
+ include BaseAttributeAccessors
6
+
7
+
8
+ def read_fields(xiph_comment, fields, **opts)
9
+ fields = Array(fields)
10
+ fields.each do |field|
11
+ next unless xiph_comment.contains?(field)
12
+ data = xiph_comment.field_list_map[field]
13
+ return opts[:returns] == :list ? data : data.first
14
+ end
15
+ opts[:returns] == :list ? [] : nil
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module EasyTag
2
+ class BaseTagger
3
+ attr_reader :taglib
4
+
5
+ def year
6
+ date.nil? ? 0 : date.year
7
+ end
8
+
9
+ def close
10
+ taglib.close
11
+ end
12
+
13
+ def method_missing(method, *args, **kwargs)
14
+ warn "#{self.class.name}##{method} does not exist"
15
+ method.to_s[-1].eql?('?') ? false : nil
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,59 @@
1
+ module EasyTag
2
+ class TaggerFactory
3
+ def self.open(file, &block)
4
+ tagger_klass = tagger_class(file)
5
+
6
+ if block_given?
7
+ tagger = tagger_klass.new(file)
8
+ begin
9
+ block.call(tagger)
10
+ ensure
11
+ tagger.close
12
+ end
13
+ else
14
+ tagger_klass.new(file)
15
+ end
16
+ end
17
+
18
+ def self.tagger_class(file)
19
+ tagger_klass = tagger_for_filename(file)
20
+ if tagger_klass.nil?
21
+ data = nil
22
+ File.open(file, 'rb') { |fp| data = fp.read(16) }
23
+ tagger_klass = tagger_for_signature(data.bytes)
24
+ end
25
+
26
+ raise 'Could not determine file type' if tagger_klass.nil?
27
+ tagger_klass
28
+ end
29
+
30
+ def self.tagger_for_filename(fname)
31
+ case fname.split('.')[-1].downcase.to_sym
32
+ when :mp3; MP3Tagger
33
+ when :mp4, :m4a; MP4Tagger
34
+ when :flac; FLACTagger
35
+ when :ogg; OggTagger
36
+ else; nil
37
+ end
38
+ end
39
+
40
+ def self.tagger_for_signature(bytes)
41
+ case
42
+ when bytes[0...11] == [0, 0, 0, 32, 102, 116, 121, 112, 77, 52, 65] # ...ftypM4A
43
+ MP4Tagger
44
+ when bytes[0...11] == [0, 0, 0, 24, 102, 116, 121, 112, 109, 112, 52] # ...ftypmp4
45
+ MP4Tagger
46
+ when bytes[0...4] == [102, 76, 97, 67] # fLaC
47
+ FLACTagger
48
+ when bytes[0...4] == [79, 103, 103, 83] # OggS
49
+ OggTagger
50
+ when bytes[0...3] == [73, 68, 51] # ID3
51
+ MP3Tagger
52
+ when bytes[0...2] == [255, 251] # FF FB
53
+ MP3Tagger
54
+ else
55
+ nil
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,23 @@
1
+ require 'easytag/attributes/flac'
2
+
3
+ require_relative 'base'
4
+
5
+ module EasyTag
6
+ class FLACTagger < BaseTagger
7
+ extend FLACAttributeAccessors
8
+ extend VorbisAttributes
9
+
10
+ initialize_vorbis_attributes
11
+
12
+ audio_prop_reader :length
13
+ audio_prop_reader :bitrate
14
+ audio_prop_reader :sample_rate
15
+ audio_prop_reader :channels
16
+ audio_prop_reader :sample_width
17
+ audio_prop_reader :signature
18
+
19
+ def initialize(file)
20
+ @taglib = TagLib::FLAC::File.new(file)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,77 @@
1
+ require 'easytag/attributes/mp3'
2
+
3
+ require_relative 'base'
4
+
5
+ module EasyTag
6
+ class MP3Tagger < BaseTagger
7
+ extend MP3AttributeAccessors
8
+
9
+ single_tag_reader :title, 'TIT2', :title
10
+ single_tag_reader :title_sort_order, %w{TSOT XSOT}
11
+ single_tag_reader :subtitle, 'TIT3'
12
+ single_tag_reader :artist, 'TPE1', :artist
13
+ single_tag_reader :artist_sort_order, %w{TSOP XSOP}
14
+ single_tag_reader :album, 'TALB', :album
15
+ single_tag_reader :album_sort_order, %w{TSOA XSOA}
16
+ single_tag_reader :album_artist, 'TPE2'
17
+ single_tag_reader :album_artist_sort_order, 'TSO2'
18
+ single_tag_reader :compilation?, 'TCMP', nil, returns: :bool
19
+ single_tag_reader :genre, 'TCON', :genre
20
+ single_tag_reader :disc_subtitle, 'TSST'
21
+ single_tag_reader :media, 'TMED'
22
+ single_tag_reader :label, 'TPUB'
23
+ single_tag_reader :encoded_by, 'TENC'
24
+ single_tag_reader :encoder_settings, 'TSSE'
25
+ single_tag_reader :group, 'TIT1'
26
+ single_tag_reader :composer, 'TCOM'
27
+ single_tag_reader :conductor, 'TPE3'
28
+ single_tag_reader :remixer, 'TPE4'
29
+ single_tag_reader :lyrics, 'USLT'
30
+ single_tag_reader :lyricist, 'TEXT'
31
+ single_tag_reader :copyright, 'TCOP'
32
+ single_tag_reader :bpm, 'TBPM', nil, returns: :int
33
+ single_tag_reader :mood, 'TMOD'
34
+ single_tag_reader :isrc, 'TSRC'
35
+ single_tag_reader :track_number, 'TRCK', :track, cast: :int_pair, extract_list_pos: 0
36
+ single_tag_reader :total_tracks, 'TRCK', :track, cast: :int_pair, extract_list_pos: 1
37
+ single_tag_reader :disc_number, 'TPOS', nil, cast: :int_pair, extract_list_pos: 0
38
+ single_tag_reader :total_discs, 'TPOS', nil, cast: :int_pair, extract_list_pos: 1
39
+ single_tag_reader :original_date, %w{TDOR TORY}, nil, returns: :datetime
40
+
41
+ all_tags_reader :comments, 'COMM', :comment
42
+ all_tags_reader :album_art, 'APIC'
43
+
44
+ audio_prop_reader :length
45
+ audio_prop_reader :bitrate
46
+ audio_prop_reader :sample_rate
47
+ audio_prop_reader :channels
48
+ audio_prop_reader :copyrighted?
49
+ audio_prop_reader :layer
50
+ audio_prop_reader :original?
51
+ audio_prop_reader :protection_enabled?, :protection_enabled
52
+
53
+ user_info_reader :asin, 'ASIN'
54
+ user_info_reader :script, 'SCRIPT'
55
+ user_info_reader :barcode, 'BARCODE'
56
+ user_info_reader :catalog_number, 'CATALOGNUMBER'
57
+ user_info_reader :musicbrainz_track_id, 'MusicBrainz Release Track Id'
58
+ user_info_reader :musicbrainz_album_id, 'MusicBrainz Album Id'
59
+ user_info_reader :musicbrainz_artist_id, 'MusicBrainz Artist Id', returns: :list
60
+ user_info_reader :musicbrainz_album_artist_id, 'MusicBrainz Album Artist Id'
61
+ user_info_reader :musicbrainz_trm_id, 'MusicBrainz TRM Id'
62
+ user_info_reader :musicbrainz_disc_id, 'MusicBrainz Disc Id'
63
+ user_info_reader :musicbrainz_release_status, 'MusicBrainz Album Status'
64
+ user_info_reader :musicbrainz_release_type, 'MusicBrainz Album Type', returns: :list
65
+ user_info_reader :musicbrainz_release_country, 'MusicBrainz Album Release Country'
66
+ user_info_reader :musicbrainz_release_group_id, 'MusicBrainz Release Group Id'
67
+ user_info_reader :musicip_puid, 'MusicIP PUID'
68
+ user_info_reader :musicip_fingerprint, 'MusicMagic Fingerprint'
69
+
70
+ ufid_reader :musicbrainz_recording_id, 'http://musicbrainz.org'
71
+ date_reader :date
72
+
73
+ def initialize(file)
74
+ @taglib = TagLib::MPEG::File.new(file)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,73 @@
1
+ require 'easytag/attributes/mp4'
2
+
3
+ require_relative 'base'
4
+
5
+ module EasyTag
6
+ class MP4Tagger < BaseTagger
7
+ extend MP4AttributeAccessors
8
+
9
+ item_reader :title, '©nam'
10
+ item_reader :title_sort_order, 'sonm'
11
+ item_reader :artist, '©ART'
12
+ item_reader :artist_sort_order, 'soar'
13
+ item_reader :album_artist, 'aART'
14
+ item_reader :album_artist_sort_order, 'soaa'
15
+ item_reader :album, '©alb'
16
+ item_reader :album_sort_order, 'soal'
17
+ item_reader :genre, '©gen'
18
+ item_reader :comments, '©cmt', data_type: :string_list, returns: :list
19
+ item_reader :lyrics, '©lyr'
20
+ item_reader :date, '©day', returns: :datetime
21
+ item_reader :encoded_by, '©enc'
22
+ item_reader :encoder_settings, '©too'
23
+ item_reader :group, '©grp'
24
+ item_reader :compilation?, 'cpil', data_type: :bool
25
+ item_reader :bpm, 'tmpo', data_type: :int
26
+ item_reader :mood, 'mood'
27
+ item_reader :copyright, 'cprt'
28
+ item_reader :track_number, 'trkn', data_type: :int_pair, cast: :int_pair, extract_list_pos: 0
29
+ item_reader :total_tracks, 'trkn', data_type: :int_pair, cast: :int_pair, extract_list_pos: 1
30
+ item_reader :disc_number, 'disk', data_type: :int_pair, cast: :int_pair, extract_list_pos: 0
31
+ item_reader :total_discs, 'disk', data_type: :int_pair, cast: :int_pair, extract_list_pos: 1
32
+ item_reader :album_art, 'covr', data_type: :cover_art_list, returns: :list
33
+
34
+ item_reader :subtitle, '----:com.apple.iTunes:SUBTITLE'
35
+ item_reader :disc_subtitle, '----:com.apple.iTunes:DISCSUBTITLE'
36
+ item_reader :media, '----:com.apple.iTunes:MEDIA'
37
+ item_reader :label, '----:com.apple.iTunes:LABEL'
38
+ item_reader :composer, '----:com.apple.iTunes:COMPOSER'
39
+ item_reader :remixer, '----:com.apple.iTunes:REMIXER'
40
+ item_reader :conductor, '----:com.apple.iTunes:CONDUCTOR'
41
+ item_reader :lyricist, '----:com.apple.iTunes:LYRICIST'
42
+ item_reader :asin, '----:com.apple.iTunes:ASIN'
43
+ item_reader :isrc, '----:com.apple.iTunes:ISRC'
44
+ item_reader :script, '----:com.apple.iTunes:SCRIPT'
45
+ item_reader :barcode, '----:com.apple.iTunes:BARCODE'
46
+ item_reader :catalog_number, '----:com.apple.iTunes:CATALOGNUMBER'
47
+
48
+ item_reader :musicbrainz_recording_id, '----:com.apple.iTunes:MusicBrainz Track Id'
49
+ item_reader :musicbrainz_track_id, '----:com.apple.iTunes:MusicBrainz Release Track Id'
50
+ item_reader :musicbrainz_artist_id, '----:com.apple.iTunes:MusicBrainz Artist Id', data_type: :string_list, returns: :list
51
+ item_reader :musicbrainz_album_artist_id, '----:com.apple.iTunes:MusicBrainz Album Artist Id'
52
+ item_reader :musicbrainz_album_id, '----:com.apple.iTunes:MusicBrainz Album Id'
53
+ item_reader :musicbrainz_disc_id, '----:com.apple.iTunes:MusicBrainz Disc Id'
54
+ item_reader :musicbrainz_trm_id, '----:com.apple.iTunes:MusicBrainz TRM Id'
55
+ item_reader :musicbrainz_release_group_id, '----:com.apple.iTunes:MusicBrainz Release Group Id'
56
+ item_reader :musicbrainz_release_status, '----:com.apple.iTunes:MusicBrainz Album Status'
57
+ item_reader :musicbrainz_release_type, '----:com.apple.iTunes:MusicBrainz Album Type', data_type: :string_list, returns: :list
58
+ item_reader :musicbrainz_release_country, '----:com.apple.iTunes:MusicBrainz Album Release Country'
59
+ item_reader :musicip_puid, '----:com.apple.iTunes:MusicIP PUID'
60
+ item_reader :musicip_fingerprint, '----:com.apple.iTunes:fingerprint'
61
+
62
+ audio_prop_reader :length
63
+ audio_prop_reader :bitrate
64
+ audio_prop_reader :sample_rate
65
+ audio_prop_reader :channels
66
+ audio_prop_reader :bits_per_sample
67
+ audio_prop_reader :encrypted?
68
+
69
+ def initialize(file)
70
+ @taglib = TagLib::MP4::File.new(file)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,25 @@
1
+ require 'easytag/attributes/ogg'
2
+
3
+ require_relative 'base'
4
+
5
+ module EasyTag
6
+ class OggTagger < BaseTagger
7
+ extend OggAttributeAccessors
8
+ extend VorbisAttributes
9
+
10
+ initialize_vorbis_attributes
11
+
12
+ audio_prop_reader :length
13
+ audio_prop_reader :bitrate
14
+ audio_prop_reader :sample_rate
15
+ audio_prop_reader :channels
16
+ audio_prop_reader :bitrate_maximum
17
+ audio_prop_reader :bitrate_minimum
18
+ audio_prop_reader :bitrate_nominal
19
+ audio_prop_reader :vorbis_version
20
+
21
+ def initialize(file)
22
+ @taglib = TagLib::Ogg::Vorbis::File.new(file)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ require 'easytag/attributes/vorbis'
2
+
3
+ require_relative 'base'
4
+
5
+ module EasyTag
6
+ module VorbisAttributes
7
+ include VorbisAttributeAccessors
8
+
9
+ def initialize_vorbis_attributes
10
+ field_reader :title
11
+ field_reader :title_sort_order, 'TITLESORT'
12
+ field_reader :subtitle
13
+ field_reader :artist
14
+ field_reader :artist_sort_order, 'ARTISTSORT'
15
+ field_reader :album
16
+ field_reader :album_sort_order, 'ALBUMSORT'
17
+ field_reader :album_artist, 'ALBUMARTIST'
18
+ field_reader :album_artist_sort_order, 'ALBUMARTISTSORT'
19
+ field_reader :compilation?, 'COMPILATION', returns: :bool
20
+ field_reader :genre
21
+ field_reader :disc_subtitle, 'DISCSUBTITLE'
22
+ field_reader :media
23
+ field_reader :label
24
+ field_reader :encoded_by, 'ENCODEDBY'
25
+ field_reader :encoder_settings, 'ENCODERSETTINGS'
26
+ field_reader :group
27
+ field_reader :composer
28
+ field_reader :conductor
29
+ field_reader :remixer
30
+ field_reader :lyrics
31
+ field_reader :lyricist
32
+ field_reader :copyright
33
+ field_reader :bpm, returns: :int
34
+ field_reader :mood
35
+ field_reader :isrc
36
+ field_reader :track_number, 'TRACKNUMBER', returns: :int
37
+ field_reader :total_tracks, %w{TRACKTOTAL TOTALTRACKS}, returns: :int
38
+ field_reader :disc_number, 'DISCNUMBER', returns: :int
39
+ field_reader :total_discs, %W{DISCTOTAL TOTALDISCS}, returns: :int
40
+ field_reader :date, returns: :datetime
41
+ field_reader :original_date, 'ORIGINALDATE', returns: :datetime
42
+ field_reader :comments, 'COMMENT', returns: :list
43
+
44
+ album_art_reader :album_art
45
+
46
+ field_reader :asin, 'ASIN'
47
+ field_reader :script, 'SCRIPT'
48
+ field_reader :barcode, 'BARCODE'
49
+ field_reader :catalog_number, 'CATALOGNUMBER'
50
+ field_reader :musicbrainz_recording_id, 'MUSICBRAINZ_TRACKID'
51
+ field_reader :musicbrainz_track_id, 'MUSICBRAINZ_RELEASETRACKID'
52
+ field_reader :musicbrainz_album_id, 'MUSICBRAINZ_ALBUMID'
53
+ field_reader :musicbrainz_artist_id, 'MUSICBRAINZ_ARTISTID', returns: :list
54
+ field_reader :musicbrainz_album_artist_id, 'MUSICBRAINZ_ALBUMARTISTID'
55
+ field_reader :musicbrainz_trm_id, 'MUSICBRAINZ_TRMID'
56
+ field_reader :musicbrainz_disc_id, 'MUSICBRAINZ_DISCID'
57
+ field_reader :musicbrainz_release_status, 'RELEASESTATUS'
58
+ field_reader :musicbrainz_release_type, 'RELEASETYPE', returns: :list
59
+ field_reader :musicbrainz_release_country, 'RELEASECOUNTRY'
60
+ field_reader :musicbrainz_release_group_id, 'MUSICBRAINZ_RELEASEGROUPID'
61
+ field_reader :musicip_puid, 'MUSICIP_PUID'
62
+ # TODO
63
+ # field_reader :musicip_fingerprint, 'MusicMagic Fingerprint'
64
+ end
65
+ end
66
+ end
@@ -50,44 +50,22 @@ module EasyTag
50
50
 
51
51
  # get_int_pair
52
52
  #
53
- # Parses a pos/total string and returns a pair of ints
54
- def self.get_int_pair(str)
53
+ # Parses a pos/total string and returns a pair of ints (or nils)
54
+ def self.get_int_pair(input)
55
+ return input.collect { |i| i.to_i }[0..1] if input.is_a?(Array)
56
+
57
+ input = input.to_s
55
58
  pair = [0, 0]
56
59
 
57
- unless str.nil? || str.empty?
58
- if str.include?('/')
59
- pair = str.split('/').map { |it| it.to_i }
60
+ unless input.nil? || input.empty?
61
+ if input.include?('/')
62
+ pair = input.split('/').collect { |it| it.to_i }
60
63
  else
61
- pair[0] = str.to_i
64
+ pair[0] = input.to_i
62
65
  end
63
66
  end
64
67
 
65
68
  pair
66
69
  end
67
-
68
- def self.normalize_string(str)
69
- str = str.to_s
70
- # downcase string
71
- str.downcase!
72
- # we want snakecase
73
- str.gsub!(/\s/, '_')
74
- # we only want alphanumeric characters
75
- str.gsub(/\W/, '')
76
- end
77
-
78
- def self.normalize_object(object)
79
- if object.is_a?(String)
80
- normalize_string(object)
81
- elsif object.is_a?(Symbol)
82
- normalize_string(object.to_s).to_sym
83
- elsif object.is_a?(Array)
84
- new_array = []
85
- object.each { |item| new_array << normalize_object(item) }
86
- new_array
87
- else
88
- object
89
- end
90
- end
91
-
92
70
  end
93
71
  end