1 |
import tagpy
|
2 |
|
3 |
|
4 |
def cached_get(func):
|
5 |
def decor(self):
|
6 |
name = func.__name__[:-4]
|
7 |
if name in self.cache:
|
8 |
return self.cache[name]
|
9 |
self.cache[name] = func(self)
|
10 |
return self.cache[name]
|
11 |
return decor
|
12 |
|
13 |
def cached_set(func):
|
14 |
def decor(self, value):
|
15 |
name = func.__name__[:-4]
|
16 |
if name in self.cache and value == self.cache[name]:
|
17 |
return
|
18 |
func(self, value)
|
19 |
self.cache[name] = value
|
20 |
return decor
|
21 |
|
22 |
def cached_property(func_get, func_set=None):
|
23 |
if func_set:
|
24 |
func_set = cached_set(func_set)
|
25 |
return property(cached_get(func_get), func_set)
|
26 |
|
27 |
|
28 |
class TagExtBase(object):
|
29 |
class Def:
|
30 |
@staticmethod
|
31 |
def default_property(name):
|
32 |
def default_get(self):
|
33 |
return getattr(self.tag, name)
|
34 |
def default_set(self, value):
|
35 |
return setattr(self.tag, name, value)
|
36 |
default_get.__name__ = name+'_get'
|
37 |
default_set.__name__ = name+'_set'
|
38 |
return cached_property(default_get, default_set)
|
39 |
|
40 |
def __init__(self, tag, map):
|
41 |
self.tag = tag
|
42 |
self.map = map
|
43 |
self.cache = dict()
|
44 |
|
45 |
album = Def.default_property('album') # FIXME: problema del utf !!!
|
46 |
|
47 |
comment = Def.default_property('comment') # FIXME: problema del utf !!!
|
48 |
|
49 |
title = Def.default_property('title') # FIXME: problema del utf !!!
|
50 |
|
51 |
track = Def.default_property('track')
|
52 |
|
53 |
year = Def.default_property('year')
|
54 |
|
55 |
# a shortcut for getting values from field map
|
56 |
def map_get(self, key, default=None):
|
57 |
if not key:
|
58 |
return default
|
59 |
try:
|
60 |
return self.map[key]
|
61 |
except:
|
62 |
return default
|
63 |
|
64 |
def try_float(self, str):
|
65 |
if not str:
|
66 |
return 0.0
|
67 |
try:
|
68 |
return float(str)
|
69 |
except:
|
70 |
return 0.0
|
71 |
|
72 |
def try_int(self, str):
|
73 |
if not str:
|
74 |
return 0
|
75 |
try:
|
76 |
return int(str)
|
77 |
except:
|
78 |
return 0
|
79 |
|
80 |
def get_avail_properties(self):
|
81 |
def get_properties(class_dict):
|
82 |
for name, attr in class_dict.items():
|
83 |
if type(attr).__name__ == 'property':
|
84 |
yield name
|
85 |
|
86 |
prop_list = list()
|
87 |
prop_list.extend(get_properties(self.__class__.__dict__))
|
88 |
for base in self.__class__.__bases__:
|
89 |
prop_list.extend(get_properties(base.__dict__))
|
90 |
return prop_list
|
91 |
|
92 |
def copy_to(self, other_tag):
|
93 |
prop_list = self.get_avail_properties()
|
94 |
for prop in prop_list:
|
95 |
if hasattr(other_tag, prop):
|
96 |
setattr(other_tag, prop, getattr(self, prop))
|
97 |
|
98 |
|
99 |
class TagExtXiph(TagExtBase):
|
100 |
class Def:
|
101 |
@staticmethod
|
102 |
def default_property(prop, field_name=None, first=False):
|
103 |
field = field_name or prop.upper()
|
104 |
|
105 |
def default_get(self):
|
106 |
if first:
|
107 |
return self.get_first_field(field)
|
108 |
return self.get_field(field)
|
109 |
def default_set(self, value):
|
110 |
self.set_field(field, value)
|
111 |
|
112 |
default_get.__name__ = prop+'_get'
|
113 |
default_set.__name__ = prop+'_set'
|
114 |
return cached_property(default_get, default_set)
|
115 |
|
116 |
def __init__(self, tag):
|
117 |
super(TagExtXiph, self).__init__(tag, tag.fieldListMap())
|
118 |
|
119 |
def get_field(self, key):
|
120 |
value_list = self.map_get(key)
|
121 |
res = list()
|
122 |
if value_list:
|
123 |
for value in value_list:
|
124 |
res.append(value)
|
125 |
return res
|
126 |
|
127 |
def get_first_field(self, key):
|
128 |
values = self.get_field(key)
|
129 |
if len(values):
|
130 |
return values[0]
|
131 |
return None
|
132 |
|
133 |
def set_field(self, key, value_list):
|
134 |
#from _tagpy import StringList # very hacky stuff, but no other way to do it :D
|
135 |
|
136 |
if not key:
|
137 |
raise TypeError('key not given')
|
138 |
if isinstance(value_list, str) or isinstance(value_list, unicode):
|
139 |
value_list = [value_list]
|
140 |
if not getattr(value_list, '__iter__', False):
|
141 |
raise TypeError('value_list must be iterable')
|
142 |
if not len(value_list):
|
143 |
self.tag.removeField(key)
|
144 |
return
|
145 |
to_add = list()
|
146 |
for value in value_list:
|
147 |
if isinstance(value, str) or isinstance(value, unicode) and len(value.strip()):
|
148 |
to_add.append(value)
|
149 |
#if not len(to_add):
|
150 |
self.tag.removeField(key)
|
151 |
for value in to_add:
|
152 |
self.tag.addField(key, value, False)
|
153 |
#else:
|
154 |
#str_list = StringList()
|
155 |
#for value in to_add:
|
156 |
# str_list.append(value)
|
157 |
#self.map[key] = str_list
|
158 |
|
159 |
def set_field_int(self, key, number):
|
160 |
if not key:
|
161 |
raise TypeError('key not given')
|
162 |
if not isinstance(number, int):
|
163 |
raise TypeError("number must be of type 'int'")
|
164 |
if number == 0:
|
165 |
self.tag.removeField(key)
|
166 |
else:
|
167 |
self.set_field(key, str(number))
|
168 |
|
169 |
def album_artists_get(self):
|
170 |
value = self.get_field('ALBUMARTIST')
|
171 |
if not value:
|
172 |
value = self.get_field('ALBUM ARTIST')
|
173 |
if value:
|
174 |
return value
|
175 |
return self.get_field('ENSEMBLE')
|
176 |
def album_artists_set(self, value):
|
177 |
if 'ALBUM ARTIST' in self.map:
|
178 |
self.tag.removeField('ALBUM ARTIST')
|
179 |
if 'ENSEMBLE' in self.map:
|
180 |
self.tag.removeField('ENSEMBLE')
|
181 |
self.set_field('ALBUMARTIST', value)
|
182 |
album_artists = cached_property(album_artists_get, album_artists_set)
|
183 |
|
184 |
album_artists_sort = Def.default_property('album_artists_sort', 'ALBUMARTISTSORT')
|
185 |
|
186 |
album_sort = Def.default_property('album_sort', 'ALBUMSORT', True)
|
187 |
|
188 |
amazon_id = Def.default_property('amazon_id', 'ASIN', True)
|
189 |
|
190 |
def beats_per_minute_get(self):
|
191 |
value = self.get_first_field('TEMPO')
|
192 |
value = self.try_float(value)
|
193 |
if value:
|
194 |
return int(round(value))
|
195 |
return 0
|
196 |
def beats_per_minute_set(self, value):
|
197 |
self.set_field_int('TEMPO', value)
|
198 |
beats_per_minute = cached_property(beats_per_minute_get, beats_per_minute_set)
|
199 |
|
200 |
composers = Def.default_property('composers', 'COMPOSER')
|
201 |
|
202 |
composers_sort = Def.default_property('composers_sort', 'COMPOSERSORT')
|
203 |
|
204 |
conductor = Def.default_property('conductor', first=True)
|
205 |
|
206 |
copyright = Def.default_property('copyright', first=True)
|
207 |
|
208 |
def disc_get(self):
|
209 |
str = self.get_first_field('DISCNUMBER')
|
210 |
if not str:
|
211 |
return 0
|
212 |
num = self.try_int(str)
|
213 |
if num:
|
214 |
return num
|
215 |
split = str.partition('/')
|
216 |
if not split[2]:
|
217 |
return 0
|
218 |
return self.try_int(split[0])
|
219 |
def disc_set(self, value):
|
220 |
self.set_field_int('DISCTOTAL', self.disc_count)
|
221 |
self.set_field_int('DISCNUMBER', value)
|
222 |
disc = cached_property(disc_get, disc_set)
|
223 |
|
224 |
def disc_count_get(self):
|
225 |
str = self.get_first_field('DISCTOTAL')
|
226 |
num = self.try_int(str)
|
227 |
if num:
|
228 |
return num
|
229 |
str = self.get_first_field('DISCNUMBER')
|
230 |
if not str:
|
231 |
return 0
|
232 |
split = str.partition('/')
|
233 |
if not split[2]:
|
234 |
return 0
|
235 |
return self.try_int(split[2])
|
236 |
def disc_count_set(self, value):
|
237 |
self.set_field_int('DISCTOTAL', value)
|
238 |
disc_count = cached_property(disc_count_get, disc_count_set)
|
239 |
|
240 |
genres = Def.default_property('genres', 'GENRE')
|
241 |
|
242 |
def is_compilation_get(self):
|
243 |
str = self.get_first_field('COMPILATION')
|
244 |
num = self.try_int(str)
|
245 |
if num:
|
246 |
return True
|
247 |
return False
|
248 |
def is_compilation_set(self, value):
|
249 |
if value:
|
250 |
self.set_field_int('COMPILATION', 1)
|
251 |
else:
|
252 |
self.tag.removeField('COMPILATION')
|
253 |
is_compilation = cached_property(is_compilation_get, is_compilation_set)
|
254 |
|
255 |
lyrics = Def.default_property('lyrics', first=True)
|
256 |
|
257 |
grouping = Def.default_property('grouping', first=True)
|
258 |
|
259 |
musicbrainz_artist_id = Def.default_property('musicbrainz_artist_id', 'MUSICBRAINZ_ARTISTID', True)
|
260 |
|
261 |
musicbrainz_disc_id = Def.default_property('musicbrainz_disc_id', 'MUSICBRAINZ_DISCID', True)
|
262 |
|
263 |
musicbrainz_release_artist_id = Def.default_property('musicbrainz_release_artist_id', 'MUSICBRAINZ_ALBUMARTISTID', True)
|
264 |
|
265 |
musicbrainz_release_id = Def.default_property('musicbrainz_release_id', 'MUSICBRAINZ_ALBUMID', True)
|
266 |
|
267 |
musicbrainz_release_status = Def.default_property('musicbrainz_release_status', 'MUSICBRAINZ_ALBUMSTATUS', True)
|
268 |
|
269 |
musicbrainz_release_country = Def.default_property('musicbrainz_release_country', 'RELEASECOUNTRY', True)
|
270 |
|
271 |
musicbrainz_release_type = Def.default_property('musicbrainz_release_type', 'MUSICBRAINZ_ALBUMTYPE', True)
|
272 |
|
273 |
musicbrainz_track_id = Def.default_property('musicbrainz_track_id', 'MUSICBRAINZ_TRACKID', True)
|
274 |
|
275 |
musicip_id = Def.default_property('musicip_id', 'MUSICIP_PUID', True)
|
276 |
|
277 |
performers = Def.default_property('performers', 'ARTIST')
|
278 |
|
279 |
performers_sort = Def.default_property('performers_sort', 'ARTISTSORT')
|
280 |
|
281 |
title_sort = Def.default_property('title_sort', 'TITLESORT', True)
|
282 |
|
283 |
def track_count_get(self):
|
284 |
value = self.try_int(self.get_first_field('TRACKTOTAL'))
|
285 |
if value:
|
286 |
return value
|
287 |
value = self.get_first_field('TRACKNUMBER')
|
288 |
if not isinstance(value, unicode):
|
289 |
return 0
|
290 |
return self.try_int(value.partition('/')[2])
|
291 |
def track_count_set(self, value):
|
292 |
self.set_field_int('TRACKTOTAL', value)
|
293 |
track_count = cached_property(track_count_get, track_count_set)
|
294 |
|
295 |
|
296 |
class TagExtID3v2(TagExtBase):
|
297 |
class Def:
|
298 |
# default property handler for TextIdentification frames
|
299 |
@staticmethod
|
300 |
def default_property(prop, frame_type, as_string=False):
|
301 |
def default_get(self):
|
302 |
if as_string:
|
303 |
return self.get_text_as_string(frame_type)
|
304 |
return self.get_text_as_array(frame_type)
|
305 |
def default_set(self, value):
|
306 |
self.set_text(frame_type, value)
|
307 |
|
308 |
default_get.__name__ = prop+'_get'
|
309 |
default_set.__name__ = prop+'_set'
|
310 |
return cached_property(default_get, default_set)
|
311 |
|
312 |
# default property handler for UserTextIdentification frames
|
313 |
@staticmethod
|
314 |
def default_user_property(prop, description):
|
315 |
def default_get(self):
|
316 |
return self.get_text_user(description)
|
317 |
def default_set(self, value):
|
318 |
self.set_text_user(description, value)
|
319 |
|
320 |
default_get.__name__ = prop+'_get'
|
321 |
default_set.__name__ = prop+'_set'
|
322 |
return cached_property(default_get, default_set)
|
323 |
|
324 |
|
325 |
def __init__(self, tag):
|
326 |
super(TagExtID3v2, self).__init__(tag, tag.frameListMap())
|
327 |
|
328 |
# returns array of values from TextIdentification frame
|
329 |
# works with frame types which have string data
|
330 |
def get_text_as_array(self, frame_type):
|
331 |
frame_list = self.map_get(frame_type)
|
332 |
res = list()
|
333 |
if frame_list and len(frame_list):
|
334 |
frame = frame_list[0]
|
335 |
# we always use the first frame in the list
|
336 |
for field in frame.fieldList():
|
337 |
res.append(field)
|
338 |
return res
|
339 |
|
340 |
# returns value of TextIdentification frame as string
|
341 |
# works with frame types which have string data
|
342 |
def get_text_as_string(self, frame_type):
|
343 |
frame_list = self.map_get(frame_type)
|
344 |
if frame_list and len(frame_list):
|
345 |
return frame_list[0].toString()
|
346 |
return None
|
347 |
|
348 |
# returns integer at specified index from '/' separated list of numbers in TextIdentification frame
|
349 |
# works with frame types which have string data
|
350 |
def get_text_as_int(self, frame_type, index):
|
351 |
str = self.get_text_as_string(frame_type)
|
352 |
if not str:
|
353 |
return 0
|
354 |
values = str.split('/', index + 2)
|
355 |
if len(values) < index + 1:
|
356 |
return 0
|
357 |
try:
|
358 |
return int(values[index])
|
359 |
except:
|
360 |
return 0
|
361 |
|
362 |
# returns value of UserTextIdentification frame with specified description
|
363 |
def get_text_user(self, description):
|
364 |
if not description:
|
365 |
raise TypeError('description not given')
|
366 |
frame_list = self.map_get('TXXX')
|
367 |
if not frame_list:
|
368 |
return None
|
369 |
for frame in frame_list:
|
370 |
if frame.description() == description:
|
371 |
return frame.fieldList()[1]
|
372 |
return None
|
373 |
|
374 |
# return identifier from UniqueFileIdentifierFrame with specified owner
|
375 |
def get_text_ufid(self, owner):
|
376 |
if not owner:
|
377 |
raise TypeError('owner not given')
|
378 |
frame_list = self.map_get('UFID')
|
379 |
if not frame_list:
|
380 |
return None
|
381 |
for frame in frame_list:
|
382 |
if frame.owner() == owner:
|
383 |
return frame.identifier()
|
384 |
|
385 |
def _set_text(self, frame_type, value_list, frame_class='TextIdentificationFrame'):
|
386 |
from tagpy.id3v2 import TextIdentificationFrame, UserTextIdentificationFrame, UniqueFileIdentifierFrame
|
387 |
from _tagpy import StringList
|
388 |
|
389 |
if not frame_type:
|
390 |
raise TypeError('frame type not given')
|
391 |
if isinstance(value_list, str) or isinstance(value_list, unicode):
|
392 |
value_list = [value_list]
|
393 |
if not getattr(value_list, '__iter__', False):
|
394 |
raise TypeError('value_list must be iterable')
|
395 |
to_add = list()
|
396 |
for value in value_list:
|
397 |
if isinstance(value, str) or isinstance(value, unicode) and len(value.strip()):
|
398 |
to_add.append(value)
|
399 |
if not len(to_add):
|
400 |
return
|
401 |
|
402 |
if frame_class == 'TextIdentificationFrame':
|
403 |
self.tag.removeFrames(frame_type) # trash old values
|
404 |
frame = TextIdentificationFrame(frame_type)
|
405 |
str_list = StringList()
|
406 |
for value in to_add:
|
407 |
str_list.append(value)
|
408 |
frame.setTextEncoding(tagpy.StringType.UTF8)
|
409 |
frame.setText(str_list)
|
410 |
elif frame_class == 'UserTextIdentificationFrame':
|
411 |
frame = UserTextIdentificationFrame()
|
412 |
frame_type = frame_type.encode('ascii', 'ignore')
|
413 |
frame.setDescription(frame_type)
|
414 |
frame.setTextEncoding(tagpy.StringType.UTF8)
|
415 |
frame.setText(to_add[0])
|
416 |
elif frame_class == 'UniqueFileIdentifierFrame':
|
417 |
# UFID frame doesn't like UTF, so make sure we give only ascii as it's value
|
418 |
ufid_value = to_add[0].encode('ascii', 'ignore')
|
419 |
frame = UniqueFileIdentifierFrame(frame_type, ufid_value)
|
420 |
else:
|
421 |
return
|
422 |
|
423 |
self.tag.addFrame(frame)
|
424 |
|
425 |
# set TextIdentification frame (value can be either string or list of strings)
|
426 |
def set_text(self, frame_type, value_list):
|
427 |
return self._set_text(frame_type, value_list, 'TextIdentificationFrame')
|
428 |
|
429 |
# set UserTextIdentification frame
|
430 |
def set_text_user(self, description, value):
|
431 |
if not description:
|
432 |
raise TypeError('description not given')
|
433 |
return self._set_text(description, value, 'UserTextIdentificationFrame')
|
434 |
|
435 |
# set UniqueFileIdentifierFrame
|
436 |
def set_text_ufid(self, owner, identifier):
|
437 |
if not owner:
|
438 |
raise TypeError('owner not given')
|
439 |
if not identifier:
|
440 |
raise TypeError('identifier not given')
|
441 |
return self._set_text(owner, identifier, 'UniqueFileIdentifierFrame')
|
442 |
|
443 |
# set TextIdentification frame as string of format %d/%d (number of count) or %d (number) when count=0
|
444 |
def set_text_as_ints(self, frame_type, number, count=0):
|
445 |
number = int(number)
|
446 |
count = int(count)
|
447 |
if not number and not count:
|
448 |
self.tag.removeFrames(frame_type)
|
449 |
elif count != 0:
|
450 |
self.set_text(frame_type, '%d/%d' % (number, count))
|
451 |
else:
|
452 |
self.set_text(frame_type, str(number))
|
453 |
|
454 |
album_artists = Def.default_property('album_artists', 'TPE2')
|
455 |
|
456 |
album_artists_sort = Def.default_property('album_artists_sort', 'TSO2')
|
457 |
|
458 |
album_sort = Def.default_property('album_sort', 'TSOA', True)
|
459 |
|
460 |
amazon_id = Def.default_user_property('amazon_id', 'ASIN')
|
461 |
|
462 |
def beats_per_minute_get(self):
|
463 |
value = self.get_text_as_string('TBPM')
|
464 |
value = self.try_float(value)
|
465 |
if value:
|
466 |
return int(round(value))
|
467 |
return 0
|
468 |
def beats_per_minute_set(self, value):
|
469 |
self.set_text_as_ints('TBPM', value)
|
470 |
beats_per_minute = cached_property(beats_per_minute_get, beats_per_minute_set)
|
471 |
|
472 |
composers = Def.default_property('composers', 'TCOM')
|
473 |
|
474 |
composers_sort = Def.default_property('composers_sort', 'TSOC')
|
475 |
|
476 |
conductor = Def.default_property('conductor', 'TPE3', True)
|
477 |
|
478 |
copyright = Def.default_property('copyright', 'TCOP', True)
|
479 |
|
480 |
def disc_get(self):
|
481 |
return self.get_text_as_int('TPOS', 0)
|
482 |
def disc_set(self, value):
|
483 |
self.set_text_as_ints('TPOS', value, self.disc_count)
|
484 |
disc = cached_property(disc_get, disc_set)
|
485 |
|
486 |
def disc_count_get(self):
|
487 |
return self.get_text_as_int('TPOS', 1)
|
488 |
def disc_count_set(self, value):
|
489 |
self.set_text_as_ints('TPOS', self.disc, value)
|
490 |
disc_count = cached_property(disc_count_get, disc_count_set)
|
491 |
|
492 |
def genres_get(self):
|
493 |
from tagpy import id3v1
|
494 |
|
495 |
genre_list = self.get_text_as_array('TCON')
|
496 |
if not len(genre_list):
|
497 |
return genre_list
|
498 |
|
499 |
res = list()
|
500 |
for genre in genre_list:
|
501 |
if not genre:
|
502 |
continue
|
503 |
# check if we have id3v1 genre index instead of genre as string
|
504 |
try:
|
505 |
genre_index = int(genre)
|
506 |
genre = id3v1.genreList()[genre_index]
|
507 |
except:
|
508 |
pass
|
509 |
res.append(genre)
|
510 |
return res
|
511 |
def genres_set(self, value):
|
512 |
self.set_text('TCON', value)
|
513 |
genres = cached_property(genres_get, genres_set)
|
514 |
|
515 |
grouping = Def.default_property('grouping', 'TIT1', True)
|
516 |
|
517 |
# not implemented yet :)
|
518 |
#lyrics = cached_property(lyrics_get, lyrics_set)
|
519 |
|
520 |
musicbrainz_artist_id = Def.default_user_property('musicbrainz_artist_id', 'MusicBrainz Artist Id')
|
521 |
|
522 |
musicbrainz_disc_id = Def.default_user_property('musicbrainz_disc_id', 'MusicBrainz Disc Id')
|
523 |
|
524 |
musicbrainz_release_artist_id = Def.default_user_property('musicbrainz_release_artist_id', 'MusicBrainz Album Artist IdD')
|
525 |
|
526 |
musicbrainz_release_id = Def.default_user_property('musicbrainz_release_id', '"MusicBrainz Album Id')
|
527 |
|
528 |
musicbrainz_release_status = Def.default_user_property('musicbrainz_release_status', 'MusicBrainz Album Status')
|
529 |
|
530 |
musicbrainz_release_country = Def.default_user_property('musicbrainz_release_country', 'MusicBrainz Album Release Country')
|
531 |
|
532 |
musicbrainz_release_type = Def.default_user_property('musicbrainz_release_type', 'MusicBrainz Album Type')
|
533 |
|
534 |
def musicbrainz_track_id_get(self):
|
535 |
return self.get_text_ufid('http://musicbrainz.org')
|
536 |
def musicbrainz_track_id_set(self, value):
|
537 |
self.set_text_ufid('http://musicbrainz.org', value)
|
538 |
musicbrainz_track_id = cached_property(musicbrainz_track_id_get, musicbrainz_track_id_set)
|
539 |
|
540 |
musicip_id = Def.default_user_property('musicip_id', 'MusicIP PUID')
|
541 |
|
542 |
performers = Def.default_property('performers', 'TPE1')
|
543 |
|
544 |
performers_sort = Def.default_property('performers_sort', 'TSOP')
|
545 |
|
546 |
title_sort = Def.default_property('title_sort', 'TSOT', True)
|
547 |
|
548 |
def track_count_get(self):
|
549 |
return self.get_text_as_int('TRCK', 1)
|
550 |
def track_count_set(self, value):
|
551 |
self.set_text_as_ints('TRCK', self.track, value)
|
552 |
track_count = cached_property(track_count_get, track_count_set)
|
553 |
|
554 |
|
555 |
class TagExtAPE(TagExtBase):
|
556 |
class Def:
|
557 |
@staticmethod
|
558 |
def default_property(prop, key, first=False):
|
559 |
def default_get(self):
|
560 |
if first:
|
561 |
return self.get_first_item(key)
|
562 |
return self.get_item(key)
|
563 |
def default_set(self, value):
|
564 |
self.set_item(key, value)
|
565 |
|
566 |
default_get.__name__ = prop+'_get'
|
567 |
default_set.__name__ = prop+'_set'
|
568 |
return cached_property(default_get, default_set)
|
569 |
|
570 |
def __init__(self, tag):
|
571 |
super(TagExtAPE, self).__init__(tag, tag.itemListMap())
|
572 |
|
573 |
def get_item(self, key):
|
574 |
item_list = self.map_get(key)
|
575 |
res = list()
|
576 |
if item_list:
|
577 |
value_list = item_list.toStringList()
|
578 |
for value in value_list:
|
579 |
res.append(value)
|
580 |
return res
|
581 |
|
582 |
def get_first_item(self, key):
|
583 |
values = self.get_item(key)
|
584 |
if len(values):
|
585 |
return values[0]
|
586 |
return None
|
587 |
|
588 |
# returns integer at specified index from '/' separated list of numbers
|
589 |
def get_item_as_int(self, key, index):
|
590 |
str = self.get_first_item(key)
|
591 |
if not str:
|
592 |
return 0
|
593 |
values = str.split('/', index + 2)
|
594 |
if len(values) < index + 1:
|
595 |
return 0
|
596 |
try:
|
597 |
return int(values[index])
|
598 |
except:
|
599 |
return 0
|
600 |
|
601 |
def set_item(self, key, value_list):
|
602 |
from _tagpy import ape_Item, StringList # very hacky stuff, but no other way to do it :D
|
603 |
|
604 |
if not key:
|
605 |
raise TypeError('key not given')
|
606 |
if isinstance(value_list, str) or isinstance(value_list, unicode):
|
607 |
value_list = [value_list]
|
608 |
if not getattr(value_list, '__iter__', False):
|
609 |
raise TypeError('value_list must be iterable')
|
610 |
if not len(value_list):
|
611 |
self.tag.removeItem(key)
|
612 |
return
|
613 |
to_add = list()
|
614 |
for value in value_list:
|
615 |
if isinstance(value, str) or isinstance(value, unicode) and len(value.strip()):
|
616 |
to_add.append(value)
|
617 |
self.tag.removeItem(key) # trash old values
|
618 |
if len(to_add):
|
619 |
str_list = StringList()
|
620 |
for value in to_add:
|
621 |
str_list.append(value)
|
622 |
item = ape_Item(key, str_list)
|
623 |
self.tag.setItem(key, item)
|
624 |
|
625 |
# set item as string of format %d/%d (number of count) or %d (number) when count=0
|
626 |
def set_item_as_ints(self, key, number, count=0):
|
627 |
number = int(number)
|
628 |
count = int(count)
|
629 |
if not number and not count:
|
630 |
self.tag.removeItem(key)
|
631 |
elif count != 0:
|
632 |
self.set_item(key, '%d/%d' % (number, count))
|
633 |
else:
|
634 |
self.set_item(key, str(number))
|
635 |
|
636 |
def album_artists_get(self):
|
637 |
list = self.get_item('Album Artist')
|
638 |
if not len(list):
|
639 |
list = self.get_item('AlbumArtist')
|
640 |
return list
|
641 |
def album_artists_set(self, value):
|
642 |
self.set_item('Album Artist', value)
|
643 |
# compatibility
|
644 |
# has to be in upper case, cause all APE item keys are uppercased in TagLib!!
|
645 |
if 'ALBUMARTIST' in self.map:
|
646 |
self.set_item('AlbumArtist', value)
|
647 |
album_artists = cached_property(album_artists_get, album_artists_set)
|
648 |
|
649 |
album_artists_sort = Def.default_property('album_artists_sort', 'AlbumArtistSort')
|
650 |
|
651 |
album_sort = Def.default_property('album_sort', 'AlbumSort', True)
|
652 |
|
653 |
amazon_id = Def.default_property('amazon_id', 'ASIN', True)
|
654 |
|
655 |
def beats_per_minute_get(self):
|
656 |
value = self.get_item('BPM')
|
657 |
value = self.try_float(value)
|
658 |
if value:
|
659 |
return int(round(value))
|
660 |
return 0
|
661 |
def beats_per_minute_set(self, value):
|
662 |
self.set_item_as_ints('BPM', value)
|
663 |
beats_per_minute = cached_property(beats_per_minute_get, beats_per_minute_set)
|
664 |
|
665 |
composers = Def.default_property('composers', 'Composer')
|
666 |
|
667 |
composers_sort = Def.default_property('composers_sort', 'ComposerSort')
|
668 |
|
669 |
conductor = Def.default_property('conductor', 'Conductor', True)
|
670 |
|
671 |
copyright = Def.default_property('copyright', 'Copyright', True)
|
672 |
|
673 |
def disc_get(self):
|
674 |
return self.get_item_as_int('Disc', 0)
|
675 |
def disc_set(self, value):
|
676 |
self.set_item_as_ints('Disc', value, self.disc_count)
|
677 |
disc = cached_property(disc_get, disc_set)
|
678 |
|
679 |
def disc_count_get(self):
|
680 |
return self.get_item_as_int('Disc', 1)
|
681 |
def disc_count_set(self, value):
|
682 |
self.set_item_as_ints('Disc', self.disc, value)
|
683 |
disc_count = cached_property(disc_count_get, disc_count_set)
|
684 |
|
685 |
genres = Def.default_property('genres', 'Genre')
|
686 |
|
687 |
grouping = Def.default_property('grouping', 'Grouping', True)
|
688 |
|
689 |
lyrics = Def.default_property('lyrics', 'Lyrics', True)
|
690 |
|
691 |
musicbrainz_artist_id = Def.default_property('musicbrainz_artist_id', 'MUSICBRAINZ_ARTISTID', True)
|
692 |
|
693 |
musicbrainz_disc_id = Def.default_property('musicbrainz_disc_id', 'MUSICBRAINZ_DISCID', True)
|
694 |
|
695 |
musicbrainz_release_artist_id = Def.default_property('musicbrainz_release_artist_id', 'MUSICBRAINZ_ALBUMARTISTID', True)
|
696 |
|
697 |
musicbrainz_release_id = Def.default_property('musicbrainz_release_id', 'MUSICBRAINZ_ALBUMID', True)
|
698 |
|
699 |
musicbrainz_release_status = Def.default_property('musicbrainz_release_status', 'MUSICBRAINZ_ALBUMSTATUS', True)
|
700 |
|
701 |
musicbrainz_release_country = Def.default_property('musicbrainz_release_country', 'RELEASECOUNTRY', True)
|
702 |
|
703 |
musicbrainz_release_type = Def.default_property('musicbrainz_release_type', 'MUSICBRAINZ_ALBUMTYPE', True)
|
704 |
|
705 |
musicbrainz_track_id = Def.default_property('musicbrainz_track_id', 'MUSICBRAINZ_TRACKID', True)
|
706 |
|
707 |
musicip_id = Def.default_property('musicip_id', 'MUSICIP_PUID', True)
|
708 |
|
709 |
performers = Def.default_property('performers', 'Artist')
|
710 |
|
711 |
performers_sort = Def.default_property('performers_sort', 'ArtistSort')
|
712 |
|
713 |
title_sort = Def.default_property('title_sort', 'TitleSort', True)
|
714 |
|
715 |
def track_count_get(self):
|
716 |
return self.get_item_as_int('Track', 1)
|
717 |
def track_count_set(self, value):
|
718 |
self.set_item_as_ints('Track', self.track, value)
|
719 |
track_count = cached_property(track_count_get, track_count_set)
|
720 |
|
721 |
|
722 |
# a wrapper around TagPy to seamlessly handle non-standard tags
|
723 |
# specific tag handling implementation is based on the excellent taglib-sharp
|
724 |
class MetaFile(object):
|
725 |
def __init__(self, file, readAudioProperties=True, audioPropertiesStyle=tagpy.ReadStyle.Average):
|
726 |
fileref = tagpy.FileRef(file, readAudioProperties, audioPropertiesStyle)
|
727 |
self.f = fileref.file()
|
728 |
self.audioprops = self.f.audioProperties()
|
729 |
self._tag = self.f.tag()
|
730 |
self.tag = self.get_ext_tag()
|
731 |
|
732 |
def get_ext_tag(self):
|
733 |
def by_tag_class(tag):
|
734 |
tag_type = tag.__class__.__name__
|
735 |
if tag_type == 'ogg_XiphComment':
|
736 |
return TagExtXiph(tag)
|
737 |
if tag_type == 'id3v2_Tag':
|
738 |
return TagExtID3v2(tag)
|
739 |
if tag_type == 'ape_Tag':
|
740 |
return TagExtAPE(tag)
|
741 |
return None
|
742 |
|
743 |
file = self.f
|
744 |
# choose most preferrable tag type where we have choice
|
745 |
# don't consider ID3v1 since it's not possible to add any non-standard tags to it
|
746 |
file_type = file.__class__.__name__
|
747 |
if file_type == 'flac_File':
|
748 |
tag_xiph = file.xiphComment()
|
749 |
tag_id3v2 = file.ID3v2Tag()
|
750 |
return by_tag_class(tag_xiph or tag_id3v2 or file.xiphComment(True))
|
751 |
if file_type == 'mpc_File':
|
752 |
return by_tag_class(file.APETag(True))
|
753 |
if file_type == 'mpeg_File':
|
754 |
tag_ape = file.APETag()
|
755 |
tag_id3v2 = file.ID3v2Tag()
|
756 |
return by_tag_class(tag_ape or tag_id3v2 or file.APETag(True))
|
757 |
# TrueAudio and Wavpack don't seem to be available in tagpy
|
758 |
# if file_type == 'trueaudio_File':
|
759 |
# tag_id3v2 = file.ID3v2Tag(create=True)
|
760 |
# return by_tag_class(tag_id3v2)
|
761 |
# if file_type == 'wavpack_File':
|
762 |
# tag_ape = file.APETag(create=True)
|
763 |
# return by_tag_class(tag_ape)
|
764 |
# get default tag type
|
765 |
return by_tag_class(file.tag()) or file.tag()
|
766 |
|
767 |
def save(self):
|
768 |
self.f.save()
|