easytag 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -1
- data/easytag.gemspec +5 -1
- data/lib/easytag/attributes.rb +0 -0
- data/lib/easytag/attributes/base.rb +103 -0
- data/lib/easytag/attributes/mp3.rb +382 -0
- data/lib/easytag/attributes/mp4.rb +309 -0
- data/lib/easytag/interfaces/mp3.rb +24 -157
- data/lib/easytag/interfaces/mp4.rb +7 -175
- data/lib/easytag/version.rb +1 -1
- data/test/test_consistency.rb +8 -2
- data/test/test_mp3.rb +13 -13
- data/test/test_mp4.rb +1 -1
- data/test/test_util.rb +5 -0
- metadata +10 -5
@@ -0,0 +1,309 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'easytag/attributes/base'
|
3
|
+
|
4
|
+
module EasyTag::Attributes
|
5
|
+
# type of TagLib::MP4::Item
|
6
|
+
module ItemType
|
7
|
+
STRING = 0 # not part of TagLib::MP4::Item, just for convenience
|
8
|
+
STRING_LIST = 1
|
9
|
+
BOOL = 2
|
10
|
+
INT = 3
|
11
|
+
INT_PAIR = 4
|
12
|
+
COVER_ART_LIST = 5
|
13
|
+
end
|
14
|
+
|
15
|
+
class MP4Attribute < BaseAttribute
|
16
|
+
attr_reader :name, :ivar
|
17
|
+
|
18
|
+
def initialize(args)
|
19
|
+
super(args)
|
20
|
+
|
21
|
+
@item_ids = args[:item_ids]
|
22
|
+
@item_type = args[:item_type] || ItemType::STRING
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def item_for_id(id, iface)
|
28
|
+
iface.info.tag.item_list_map.fetch(id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def data_from_item(item)
|
32
|
+
case @item_type
|
33
|
+
when ItemType::STRING
|
34
|
+
item.to_string_list[0]
|
35
|
+
when ItemType::STRING_LIST
|
36
|
+
item.to_string_list
|
37
|
+
when ItemType::BOOL
|
38
|
+
item.to_bool
|
39
|
+
when ItemType::INT
|
40
|
+
item.to_int
|
41
|
+
when ItemType::INT_PAIR
|
42
|
+
item.to_int_pair
|
43
|
+
when ItemType::COVER_ART_LIST
|
44
|
+
artwork = []
|
45
|
+
item.to_cover_art_list.each do |img|
|
46
|
+
artwork << EasyTag::Image.new(img.data)
|
47
|
+
end
|
48
|
+
artwork
|
49
|
+
else
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# read handlers
|
55
|
+
|
56
|
+
def read_first_item(iface)
|
57
|
+
item = nil
|
58
|
+
@item_ids.each do |id|
|
59
|
+
item = item_for_id(id, iface) if item.nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
data_from_item(item) unless item.nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
def read_user_info(iface)
|
66
|
+
kv_hash = {}
|
67
|
+
iface.info.tag.item_list_map.to_a.each do |key, value|
|
68
|
+
match_data = key.match(/\:com.apple.iTunes\:(.*)/)
|
69
|
+
if match_data
|
70
|
+
key = match_data[1]
|
71
|
+
key = Utilities.normalize_string(key) if @options[:normalize]
|
72
|
+
key = key.to_sym if @options[:to_sym]
|
73
|
+
kv_hash[key] = value.to_string_list[0]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
kv_hash
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module EasyTag::Attributes
|
84
|
+
MP4_ATTRIB_ARGS = [
|
85
|
+
# title
|
86
|
+
{
|
87
|
+
:name => :title,
|
88
|
+
:item_ids => ['©nam'],
|
89
|
+
:handler => :read_first_item,
|
90
|
+
},
|
91
|
+
|
92
|
+
# title_sort_order
|
93
|
+
{
|
94
|
+
:name => :title_sort_order,
|
95
|
+
:item_ids => ['sonm'],
|
96
|
+
:handler => :read_first_item,
|
97
|
+
},
|
98
|
+
|
99
|
+
# artist
|
100
|
+
{
|
101
|
+
:name => :artist,
|
102
|
+
:item_ids => ['©ART'],
|
103
|
+
:handler => :read_first_item,
|
104
|
+
},
|
105
|
+
|
106
|
+
# artist_sort_order
|
107
|
+
{
|
108
|
+
:name => :artist_sort_order,
|
109
|
+
:item_ids => ['soar'],
|
110
|
+
:handler => :read_first_item,
|
111
|
+
},
|
112
|
+
|
113
|
+
# album_artist
|
114
|
+
{
|
115
|
+
:name => :album_artist,
|
116
|
+
:item_ids => ['aART'],
|
117
|
+
:handler => :read_first_item,
|
118
|
+
},
|
119
|
+
|
120
|
+
# album_artist_sort_order
|
121
|
+
{
|
122
|
+
:name => :album_artist_sort_order,
|
123
|
+
:item_ids => ['soaa'],
|
124
|
+
:handler => :read_first_item,
|
125
|
+
},
|
126
|
+
|
127
|
+
# album
|
128
|
+
{
|
129
|
+
:name => :album,
|
130
|
+
:item_ids => ['©alb'],
|
131
|
+
:handler => :read_first_item,
|
132
|
+
},
|
133
|
+
|
134
|
+
# album_sort_order
|
135
|
+
{
|
136
|
+
:name => :album_sort_order,
|
137
|
+
:item_ids => ['soal'],
|
138
|
+
:handler => :read_first_item,
|
139
|
+
},
|
140
|
+
|
141
|
+
# genre
|
142
|
+
{
|
143
|
+
:name => :genre,
|
144
|
+
:item_ids => ['©gen'],
|
145
|
+
:handler => :read_first_item,
|
146
|
+
},
|
147
|
+
|
148
|
+
# comments
|
149
|
+
{
|
150
|
+
:name => :comments,
|
151
|
+
:item_ids => ['©cmt'],
|
152
|
+
:handler => :read_first_item,
|
153
|
+
:item_type => ItemType::STRING_LIST,
|
154
|
+
:default => [],
|
155
|
+
},
|
156
|
+
|
157
|
+
# comment
|
158
|
+
{
|
159
|
+
:name => :comment,
|
160
|
+
:handler => lambda { |iface| iface.comments.first }
|
161
|
+
},
|
162
|
+
|
163
|
+
# lyrics
|
164
|
+
{
|
165
|
+
:name => :lyrics,
|
166
|
+
:item_ids => ['©lyr'],
|
167
|
+
:handler => :read_first_item,
|
168
|
+
},
|
169
|
+
|
170
|
+
# date
|
171
|
+
{
|
172
|
+
:name => :date,
|
173
|
+
:item_ids => ['©day'],
|
174
|
+
:handler => :read_first_item,
|
175
|
+
:type => Type::DATETIME,
|
176
|
+
},
|
177
|
+
|
178
|
+
# year
|
179
|
+
{
|
180
|
+
:name => :year,
|
181
|
+
:handler => lambda { |iface| iface.date.nil? ? 0 : iface.date.year }
|
182
|
+
},
|
183
|
+
|
184
|
+
# apple_id
|
185
|
+
{
|
186
|
+
:name => :apple_id,
|
187
|
+
:item_ids => ['apid'],
|
188
|
+
:handler => :read_first_item,
|
189
|
+
},
|
190
|
+
|
191
|
+
# encoded_by
|
192
|
+
{
|
193
|
+
:name => :encoded_by,
|
194
|
+
:item_ids => ['©enc'],
|
195
|
+
:handler => :read_first_item,
|
196
|
+
},
|
197
|
+
|
198
|
+
# encoder_settings
|
199
|
+
{
|
200
|
+
:name => :encoder_settings,
|
201
|
+
:item_ids => ['©too'],
|
202
|
+
:handler => :read_first_item,
|
203
|
+
},
|
204
|
+
|
205
|
+
# group
|
206
|
+
{
|
207
|
+
:name => :group,
|
208
|
+
:item_ids => ['©grp'],
|
209
|
+
:handler => :read_first_item,
|
210
|
+
},
|
211
|
+
|
212
|
+
# compilation?
|
213
|
+
{
|
214
|
+
:name => :compilation?,
|
215
|
+
:item_ids => ['cpil'],
|
216
|
+
:handler => :read_first_item,
|
217
|
+
:item_type => ItemType::BOOL,
|
218
|
+
:default => false,
|
219
|
+
},
|
220
|
+
|
221
|
+
# bpm
|
222
|
+
{
|
223
|
+
:name => :bpm,
|
224
|
+
:item_ids => ['tmpo'],
|
225
|
+
:handler => :read_first_item,
|
226
|
+
:item_type => ItemType::INT,
|
227
|
+
:default => 0,
|
228
|
+
},
|
229
|
+
|
230
|
+
# copyright
|
231
|
+
{
|
232
|
+
:name => :copyright,
|
233
|
+
:item_ids => ['cprt'],
|
234
|
+
:handler => :read_first_item,
|
235
|
+
},
|
236
|
+
|
237
|
+
# track_num
|
238
|
+
{
|
239
|
+
:name => :track_num,
|
240
|
+
:item_ids => ['trkn'],
|
241
|
+
:handler => :read_first_item,
|
242
|
+
:item_type => ItemType::INT_PAIR,
|
243
|
+
:default => [0, 0],
|
244
|
+
},
|
245
|
+
|
246
|
+
# disc_num
|
247
|
+
{
|
248
|
+
:name => :disc_num,
|
249
|
+
:item_ids => ['disk'],
|
250
|
+
:handler => :read_first_item,
|
251
|
+
:item_type => ItemType::INT_PAIR,
|
252
|
+
:default => [0, 0],
|
253
|
+
},
|
254
|
+
|
255
|
+
# album_art
|
256
|
+
{
|
257
|
+
:name => :album_art,
|
258
|
+
:item_ids => ['covr'],
|
259
|
+
:handler => :read_first_item,
|
260
|
+
:item_type => ItemType::COVER_ART_LIST,
|
261
|
+
:default => [],
|
262
|
+
},
|
263
|
+
|
264
|
+
# user_info
|
265
|
+
{
|
266
|
+
:name => :user_info,
|
267
|
+
:handler => :read_user_info,
|
268
|
+
:default => {},
|
269
|
+
:options => {:normalize => true, :to_sym => true},
|
270
|
+
},
|
271
|
+
|
272
|
+
# subtitle
|
273
|
+
{
|
274
|
+
:name => :subtitle,
|
275
|
+
:handler => lambda { |iface| iface.user_info[:subtitle] }
|
276
|
+
},
|
277
|
+
|
278
|
+
# disc_subtitle
|
279
|
+
{
|
280
|
+
:name => :disc_subtitle,
|
281
|
+
:handler => lambda { |iface| iface.user_info[:discsubtitle] }
|
282
|
+
},
|
283
|
+
|
284
|
+
# media
|
285
|
+
{
|
286
|
+
:name => :media,
|
287
|
+
:handler => lambda { |iface| iface.user_info[:media] }
|
288
|
+
},
|
289
|
+
|
290
|
+
# label
|
291
|
+
{
|
292
|
+
:name => :label,
|
293
|
+
:handler => lambda { |iface| iface.user_info[:label] }
|
294
|
+
},
|
295
|
+
|
296
|
+
# composer
|
297
|
+
{
|
298
|
+
:name => :composer,
|
299
|
+
:handler => lambda { |iface| iface.user_info[:composer] }
|
300
|
+
},
|
301
|
+
|
302
|
+
# lyricist
|
303
|
+
{
|
304
|
+
:name => :lyricist,
|
305
|
+
:handler => lambda { |iface| iface.user_info[:lyricist] }
|
306
|
+
},
|
307
|
+
|
308
|
+
]
|
309
|
+
end
|
@@ -1,186 +1,53 @@
|
|
1
1
|
require 'mp3info'
|
2
|
+
require 'yaml'
|
2
3
|
|
3
|
-
require 'easytag'
|
4
|
+
require 'easytag/attributes/mp3'
|
4
5
|
|
5
6
|
module EasyTag::Interfaces
|
6
7
|
|
7
8
|
class MP3 < Base
|
8
9
|
def initialize(file)
|
9
10
|
@info = TagLib::MPEG::File.new(file)
|
10
|
-
@id3v1 = @info.id3v1_tag
|
11
|
-
@id3v2 = @info.id3v2_tag
|
12
11
|
|
13
|
-
|
12
|
+
add_tdat_to_taglib(file)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def add_tdat_to_taglib(file)
|
18
|
+
# this is required because taglib hash issues with the TDAT+TYER
|
14
19
|
# frame (https://github.com/taglib/taglib/issues/127)
|
15
|
-
|
20
|
+
id3v2_hash = ID3v2.new
|
16
21
|
|
17
22
|
File.open(file) do |fp|
|
18
23
|
fp.read(3) # read past ID3 identifier
|
19
24
|
begin
|
20
|
-
|
25
|
+
id3v2_hash.from_io(fp)
|
21
26
|
rescue ID3v2Error => e
|
22
27
|
warn 'no id3v2 tags found'
|
23
28
|
end
|
24
29
|
end
|
25
30
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
obj_for_frame_id('TIT2') or Base.obj_or_nil(@id3v1.title)
|
30
|
-
end
|
31
|
-
|
32
|
-
def title_sort_order
|
33
|
-
# TSOT - (v2.4 only)
|
34
|
-
# XSOT - Musicbrainz Picard custom
|
35
|
-
obj_for_frame_id('TSOT') || obj_for_frame_id('XSOT')
|
36
|
-
end
|
37
|
-
|
38
|
-
def artist
|
39
|
-
obj_for_frame_id('TPE1') or Base.obj_or_nil(@id3v1.artist)
|
40
|
-
end
|
41
|
-
|
42
|
-
def artist_sort_order
|
43
|
-
# TSOP - (v2.4 only)
|
44
|
-
# XSOP - Musicbrainz Picard custom
|
45
|
-
obj_for_frame_id('TSOP') || obj_for_frame_id('XSOP')
|
46
|
-
end
|
47
|
-
|
48
|
-
def album_artist
|
49
|
-
obj_for_frame_id('TPE2')
|
50
|
-
end
|
51
|
-
|
52
|
-
def album_artist_sort_order
|
53
|
-
user_info[:albumartistsort]
|
54
|
-
end
|
55
|
-
|
56
|
-
def album
|
57
|
-
obj_for_frame_id('TALB') or Base.obj_or_nil(@id3v1.album)
|
58
|
-
end
|
59
|
-
|
60
|
-
def album_sort_order
|
61
|
-
# TSOA - (v2.4 only)
|
62
|
-
# XSOA - Musicbrainz Picard custom
|
63
|
-
obj_for_frame_id('TSOA') || obj_for_frame_id('XSOA')
|
64
|
-
end
|
65
|
-
|
66
|
-
# REVIEW: TCON supports genre refining, which we currently don't utilize
|
67
|
-
def genre
|
68
|
-
obj_for_frame_id('TCON') or Base.obj_or_nil(@id3v1.genre)
|
69
|
-
end
|
70
|
-
|
71
|
-
def comments
|
72
|
-
return @comments unless @comments.nil?
|
73
|
-
|
74
|
-
comm_frame = lookup_frames('COMM').first
|
75
|
-
comm_str = comm_frame ? comm_frame.text : @id3v1.comment
|
76
|
-
|
77
|
-
@comments = Base.obj_or_nil(comm_str)
|
78
|
-
end
|
79
|
-
|
80
|
-
def year
|
81
|
-
date.nil? ? 0 : date.year
|
82
|
-
end
|
83
|
-
|
84
|
-
def date
|
85
|
-
return @date unless @date.nil?
|
86
|
-
|
87
|
-
v10_year = @id3v1.year.to_s if @id3v1.year > 0
|
88
|
-
v23_year = obj_for_frame_id('TYER')
|
89
|
-
v23_date = Base.obj_or_nil(@id3v2_hash['TDAT'])
|
90
|
-
v24_date = obj_for_frame_id('TDRC')
|
31
|
+
# delete all TDAT frames (taglib-ruby segfaults when trying to read)
|
32
|
+
frames = @info.id3v2_tag.frame_list('TDAT')
|
33
|
+
frames.each { |frame| @info.id3v2_tag.remove_frame(frame) }
|
91
34
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
date_str << v23_date unless v23_date.nil? or date_str.length > 4
|
96
|
-
puts "MP3#date: date_str = \"#{date_str}\"" if $DEBUG
|
35
|
+
if id3v2_hash['TDAT']
|
36
|
+
frame = TagLib::ID3v2::TextIdentificationFrame
|
37
|
+
.new('TDAT', TagLib::String::UTF8)
|
97
38
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
def original_date
|
102
|
-
return @original_date unless @original_date.nil?
|
103
|
-
|
104
|
-
# TDOR - orig release date (v2.4 only)
|
105
|
-
# TORY - orig release year (v2.3)
|
106
|
-
date_str = obj_for_frame_id('TDOR') || obj_for_frame_id('TORY')
|
107
|
-
@original_date ||= EasyTag::Utilities.get_datetime(date_str)
|
108
|
-
end
|
109
|
-
|
110
|
-
def album_art
|
111
|
-
return @album_art unless @album_art.nil?
|
112
|
-
|
113
|
-
@album_art = []
|
114
|
-
@id3v2.frame_list('APIC').each do |apic|
|
115
|
-
img = EasyTag::Image.new(apic.picture)
|
116
|
-
img.desc = apic.description
|
117
|
-
img.type = apic.type
|
118
|
-
img.mime_type = apic.mime_type
|
119
|
-
|
120
|
-
@album_art << img
|
39
|
+
frame.text = id3v2_hash['TDAT']
|
40
|
+
@info.id3v2_tag.add_frame(frame)
|
121
41
|
end
|
122
|
-
|
123
|
-
@album_art
|
124
42
|
end
|
125
43
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
end
|
132
|
-
|
133
|
-
def disc_num
|
134
|
-
int_pair_for_frame_id('TPOS')
|
135
|
-
end
|
136
|
-
|
137
|
-
def user_info
|
138
|
-
return @user_info unless @user_info.nil?
|
139
|
-
|
140
|
-
@user_info = {}
|
141
|
-
lookup_frames('TXXX').each do |frame|
|
142
|
-
key, value = frame.field_list
|
143
|
-
key = EasyTag::Utilities.normalize_string(key)
|
144
|
-
@user_info[key.to_sym] = value
|
44
|
+
EasyTag::Attributes::MP3_ATTRIB_ARGS.each do |attrib_args|
|
45
|
+
attrib = EasyTag::Attributes::MP3Attribute.new(attrib_args)
|
46
|
+
define_method(attrib.name) do
|
47
|
+
instance_variable_get(attrib.ivar) ||
|
48
|
+
instance_variable_set(attrib.ivar, attrib.call(self))
|
145
49
|
end
|
146
|
-
|
147
|
-
@user_info
|
148
50
|
end
|
149
51
|
|
150
|
-
def disc_subtitle
|
151
|
-
obj_for_frame_id('TSST')
|
152
|
-
end
|
153
|
-
|
154
|
-
def media
|
155
|
-
obj_for_frame_id('TMED')
|
156
|
-
end
|
157
|
-
|
158
|
-
def label
|
159
|
-
obj_for_frame_id('TPUB')
|
160
|
-
end
|
161
|
-
|
162
|
-
private
|
163
|
-
|
164
|
-
# for TPOS and TRCK
|
165
|
-
def int_pair_for_frame_id(frame_id)
|
166
|
-
str = obj_for_frame_id(frame_id)
|
167
|
-
EasyTag::Utilities.get_int_pair(str)
|
168
|
-
end
|
169
|
-
|
170
|
-
def obj_for_frame_id(frame_id)
|
171
|
-
Base.obj_or_nil(lookup_first_field(frame_id))
|
172
|
-
end
|
173
|
-
|
174
|
-
def lookup_frames(frame_id)
|
175
|
-
frames = @id3v2.frame_list(frame_id)
|
176
|
-
end
|
177
|
-
|
178
|
-
# get the first field in the first frame
|
179
|
-
def lookup_first_field(frame_id)
|
180
|
-
frame = lookup_frames(frame_id).first
|
181
|
-
warn "frame '#{frame_id}' is not present" if frame.nil?
|
182
|
-
frame.field_list.first unless frame.nil?
|
183
|
-
end
|
184
52
|
end
|
185
53
|
end
|
186
|
-
|