1 |
from UserDict import DictMixin
|
2 |
import tagpy
|
3 |
|
4 |
|
5 |
class TagExtBase(object):
|
6 |
def __init__(self, metafile, tag, map):
|
7 |
# bypass __setattr__
|
8 |
self.__dict__['metafile'] = metafile
|
9 |
self.__dict__['tag'] = tag
|
10 |
self.__dict__['map'] = map
|
11 |
|
12 |
# a shortcut for getting values from field map
|
13 |
def map_get(self, key, default=None):
|
14 |
try:
|
15 |
el = self.map[key]
|
16 |
except:
|
17 |
return default
|
18 |
type_name = type(el).__name__
|
19 |
if type_name == 'StringList':
|
20 |
return el[0]
|
21 |
elif type_name == 'id3v2_FrameList':
|
22 |
return el[0].toString()
|
23 |
return el
|
24 |
|
25 |
# from string of format %d/%d get first number
|
26 |
def afromb_a(self, value):
|
27 |
if not value:
|
28 |
return 0
|
29 |
slash_pos = value.find('/')
|
30 |
if not slash_pos:
|
31 |
return 0
|
32 |
return int(value[:slash_pos])
|
33 |
|
34 |
# from string of format %d/%d get second number
|
35 |
def afromb_b(self, value):
|
36 |
if not value:
|
37 |
return 0
|
38 |
b_pos = value.find('/')
|
39 |
if not b_pos:
|
40 |
return 0
|
41 |
b_pos += 1
|
42 |
return int(value[b_pos:])
|
43 |
|
44 |
|
45 |
class TagExtXiph(TagExtBase):
|
46 |
# used XiphComment fields are: COMPOSER, CONDUCTOR, COPYRIGHT, DISCNUMBER, DISCTOTAL, TRACKTOTAL
|
47 |
|
48 |
def __init__(self, metafile, tag):
|
49 |
super(TagExtXiph, self).__init__(metafile, tag, tag.fieldListMap())
|
50 |
|
51 |
# map internal field names to XiphComment fields
|
52 |
def field_map(self, field):
|
53 |
if field == 'composer':
|
54 |
return 'COMPOSER'
|
55 |
if field == 'conductor':
|
56 |
return 'CONDUCTOR'
|
57 |
if field == 'copyright':
|
58 |
return 'COPYRIGHT'
|
59 |
if field == 'disc':
|
60 |
return 'DISCNUMBER'
|
61 |
if field == 'disc_count':
|
62 |
return 'DISCTOTAL'
|
63 |
if field == 'track_count':
|
64 |
return 'TRACKTOTAL'
|
65 |
return None
|
66 |
|
67 |
def __getattr__(self, name):
|
68 |
field = self.field_map(name)
|
69 |
if not field:
|
70 |
return None
|
71 |
value = self.map_get(field)
|
72 |
conversion = self.metafile.map[name][1]
|
73 |
if callable(conversion) and value != None:
|
74 |
value = conversion.__call__(value)
|
75 |
return value
|
76 |
|
77 |
def __setattr__(self, name, value):
|
78 |
field = self.field_map(name)[0]
|
79 |
if not field:
|
80 |
return
|
81 |
self.tag.addField(field, unicode(value), True)
|
82 |
|
83 |
|
84 |
class TagExtID3v2(TagExtBase):
|
85 |
def __init__(self, metafile, tag):
|
86 |
super(TagExtID3v2, self).__init__(metafile, tag, tag.frameListMap())
|
87 |
|
88 |
# map internal field names to ID3v2 frame types
|
89 |
def field_map(self, field):
|
90 |
if field == 'composer':
|
91 |
return 'TCOM'
|
92 |
if field == 'conductor':
|
93 |
return 'TPE3'
|
94 |
if field == 'copyright':
|
95 |
return 'TCOP'
|
96 |
if field == 'disc' or field == 'disc_count':
|
97 |
return 'TPOS'
|
98 |
if field == 'track_count':
|
99 |
return 'TRCK'
|
100 |
return None
|
101 |
|
102 |
def __getattr__(self, name):
|
103 |
frame_type = self.field_map(name)
|
104 |
if not frame_type:
|
105 |
return None
|
106 |
value = self.map_get(frame_type)
|
107 |
# convert to single integers for disc, disc_count and track_count
|
108 |
if name == 'disc':
|
109 |
value = self.afromb_a(value)
|
110 |
elif name == 'disc_count' or name == 'track_count':
|
111 |
value = self.afromb_b(value)
|
112 |
|
113 |
return value
|
114 |
|
115 |
def __setattr__(self, name, value):
|
116 |
from tagpy.id3v2 import TextIdentificationFrame
|
117 |
|
118 |
frame_type = self.field_map(name)
|
119 |
if not frame_type:
|
120 |
return
|
121 |
|
122 |
# some special processing
|
123 |
if name == 'disc':
|
124 |
total = self.metafile.get('disc_count') or 0
|
125 |
value = '%s/%s' % (value, total)
|
126 |
elif name == 'disc_count':
|
127 |
disc = self.metafile.get('disc') or 0
|
128 |
value = '%s/%s' % (disc, value)
|
129 |
elif name == 'track_count':
|
130 |
track = self.metafile.get('track') or 0
|
131 |
value = '%s/%s' % (track, value)
|
132 |
print "name: %s, value: %s" % (name, value)
|
133 |
|
134 |
frame_list = self.map[frame_type]
|
135 |
if frame_list and frame_list.size:
|
136 |
# replace frame value if it exists
|
137 |
frame = frame_list[0]
|
138 |
frame.setText(value)
|
139 |
else:
|
140 |
# add new frame otherwise
|
141 |
frame = TextIdentificationFrame(frame_type)
|
142 |
frame.setText(value)
|
143 |
self.tag.addFrame(frame)
|
144 |
|
145 |
|
146 |
class TagExtAPE(TagExtBase):
|
147 |
def __init__(self, metafile, tag):
|
148 |
super(TagExtAPE, self).__init__(metafile, tag, tag.itemListMap())
|
149 |
|
150 |
# map internal field names to APE fields
|
151 |
def field_map(self, field):
|
152 |
if field == 'composer':
|
153 |
return 'Composer'
|
154 |
if field == 'conductor':
|
155 |
return 'Conductor'
|
156 |
if field == 'copyright':
|
157 |
return 'Copyright'
|
158 |
if field == 'disc' or field == 'disc_count':
|
159 |
return 'Disc'
|
160 |
if field == 'track_count':
|
161 |
return 'Track'
|
162 |
return None
|
163 |
|
164 |
def __getattr__(self, name):
|
165 |
field = self.field_map(name)
|
166 |
if not field:
|
167 |
return None
|
168 |
value = self.map_get(field)
|
169 |
# convert to single integers for disc, disc_count and track_count
|
170 |
if name == 'disc':
|
171 |
value = self.afromb_a(value)
|
172 |
elif name == 'disc_count' or name == 'track_count':
|
173 |
value = self.afromb_b(value)
|
174 |
|
175 |
return value
|
176 |
|
177 |
def __setattr__(self, name, value):
|
178 |
field = self.field_map(name)
|
179 |
if not field:
|
180 |
return
|
181 |
|
182 |
# some special processing
|
183 |
if name == 'disc':
|
184 |
total = self.metafile.get('disc_count') or 0
|
185 |
value = '%s/%s' % (value, total)
|
186 |
elif name == 'disc_count':
|
187 |
disc = self.metafile.get('disc') or 0
|
188 |
value = '%s/%s' % (disc, value)
|
189 |
elif name == 'track_count':
|
190 |
track = self.metafile.get('track') or 0
|
191 |
value = '%s/%s' % (track, value)
|
192 |
|
193 |
self.tag.addValue(field, value, True)
|
194 |
|
195 |
|
196 |
# a wrapper around TagPy to seamlessly handle non-standard tags
|
197 |
# specific tag handling implementation is based on the excellent taglib-sharp
|
198 |
class MetaFile(DictMixin):
|
199 |
def __init__(self, file, readAudioProperties=True, audioPropertiesStyle=tagpy.ReadStyle.Average):
|
200 |
fileref = tagpy.FileRef(file, readAudioProperties, audioPropertiesStyle)
|
201 |
# set main attrs (bypassing __setattr__)
|
202 |
self.f = fileref.file()
|
203 |
self.tag = self.f.tag()
|
204 |
self._tags = dict()
|
205 |
self._changes = dict()
|
206 |
|
207 |
def get_ext_tag(self):
|
208 |
def by_tag_class(tag):
|
209 |
tag_type = tag.__class__.__name__
|
210 |
if tag_type == 'ogg_XiphComment':
|
211 |
return TagExtXiph(self, tag)
|
212 |
if tag_type == 'id3v2_Tag':
|
213 |
return TagExtID3v2(self, tag)
|
214 |
if tag_type == 'ape_Tag':
|
215 |
return TagExtAPE(self, tag)
|
216 |
|
217 |
file = self.f
|
218 |
# choose most preferrable tag type where we have choice
|
219 |
# don't consider ID3v1 since it's not possible to add any non-standard tags to it
|
220 |
file_type = file.__class__.__name__
|
221 |
if file_type == 'flac_File':
|
222 |
tag_xiph = file.xiphComment()
|
223 |
tag_id3v2 = file.ID3v2Tag()
|
224 |
return by_tag_class(tag_xiph or tag_id3v2 or file.xiphComment(create=True))
|
225 |
if file_type == 'mpc_File':
|
226 |
tag_ape = file.APETag()
|
227 |
tag_id3v1 = file.ID3v2Tag()
|
228 |
return by_tag_class(tag_ape or tag_id3v1 or file.APETag(create=True))
|
229 |
if file_type == 'mpeg_File':
|
230 |
tag_ape = file.APETag()
|
231 |
tag_id3v2 = file.ID3v2Tag()
|
232 |
return by_tag_class(tag_ape or tag_id3v2 or file.APETag(create=True))
|
233 |
# TrueAudio and Wavpack don't seem to be available in tagpy
|
234 |
# if file_type == 'trueaudio_File':
|
235 |
# tag_id3v2 = file.ID3v2Tag(create=True)
|
236 |
# return by_tag_class(tag_id3v2)
|
237 |
# if file_type == 'wavpack_File':
|
238 |
# tag_ape = file.APETag(create=True)
|
239 |
# return by_tag_class(tag_ape)
|
240 |
# get default tag type
|
241 |
return by_tag_class(file.tag())
|
242 |
|
243 |
def __getattr__(self, name):
|
244 |
if name == 'audioprops':
|
245 |
self.audioprops = self.f.audioProperties()
|
246 |
elif name == 'tag_ext':
|
247 |
self.tag_ext = self.get_ext_tag()
|
248 |
elif name == 'map':
|
249 |
self.map = dict({
|
250 |
'album' :('tag', unicode),
|
251 |
'artist' :('tag', unicode),
|
252 |
'comment' :('tag', unicode),
|
253 |
'genre' :('tag', unicode),
|
254 |
'title' :('tag', unicode),
|
255 |
'track' :('tag', int),
|
256 |
'year' :('tag', int),
|
257 |
'composer' :('tag_ext', unicode),
|
258 |
'conductor' :('tag_ext', unicode),
|
259 |
'copyright' :('tag_ext', unicode),
|
260 |
'disc' :('tag_ext', int),
|
261 |
'disc_count' :('tag_ext', int),
|
262 |
'track_count':('tag_ext', int),
|
263 |
'bitrate' :('audioprops', None),
|
264 |
'channels' :('audioprops', None),
|
265 |
'length' :('audioprops', None),
|
266 |
'sampleRate' :('audioprops', None)})
|
267 |
|
268 |
return self.__dict__[name]
|
269 |
|
270 |
def __getitem__(self, name):
|
271 |
if self._tags.has_key(name):
|
272 |
return self._tags[name]
|
273 |
if not self.map.has_key(name):
|
274 |
return None
|
275 |
store = getattr(self, self.map[name][0])
|
276 |
self._tags[name] = getattr(store, name)
|
277 |
return self._tags[name]
|
278 |
|
279 |
def __setitem__(self, name, value):
|
280 |
if self._tags.get(name) == value:
|
281 |
return
|
282 |
if not self.map.has_key(name):
|
283 |
return
|
284 |
# get conversion from map
|
285 |
conversion = self.map[name][1]
|
286 |
if callable(conversion) and value != None:
|
287 |
value = conversion.__call__(value)
|
288 |
self._tags[name] = value
|
289 |
self._changes[name] = True
|
290 |
|
291 |
def __delitem__(self, name):
|
292 |
pass
|
293 |
|
294 |
def keys(self):
|
295 |
return self._tags.keys()
|
296 |
|
297 |
def save(self):
|
298 |
if len(self._changes):
|
299 |
for name in self._changes.iterkeys():
|
300 |
store = getattr(self, self.map[name][0])
|
301 |
value = self._tags[name]
|
302 |
setattr(store, name, value)
|
303 |
self.f.save()
|