metadata.py 6.96 KB
Newer Older
1
from django import forms
2
import arrow
3
import mutagen
4
5
6

NODEFAULT = object()

7
8
9
10
11

class TagNotFound(KeyError):
    pass


12
13
14
15
class UnsupportedTag(KeyError):
    pass


16
def get_id3_tag(f, k):
17
18
    if k == 'pictures':
        return f.tags.getall('APIC')
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    # First we try to grab the standard key
    try:
        return f.tags[k].text[0]
    except KeyError:
        pass
    # then we fallback on parsing non standard tags
    all_tags = f.tags.getall('TXXX')
    try:
        matches = [
            t
            for t in all_tags
            if t.desc.lower() == k.lower()
        ]
        return matches[0].text[0]
    except (KeyError, IndexError):
        raise TagNotFound(k)


37
38
39
40
41
42
43
44
45
46
47
48
def clean_id3_pictures(apic):
    pictures = []
    for p in list(apic):
        pictures.append({
            'mimetype': p.mime,
            'content': p.data,
            'description': p.desc,
            'type': p.type.real,
        })
    return pictures


49
def get_flac_tag(f, k):
50
51
    if k == 'pictures':
        return f.pictures
52
    try:
53
        return f.get(k, [])[0]
54
55
56
57
    except (KeyError, IndexError):
        raise TagNotFound(k)


58
59
60
61
62
63
64
65
66
67
68
69
def clean_flac_pictures(apic):
    pictures = []
    for p in list(apic):
        pictures.append({
            'mimetype': p.mime,
            'content': p.data,
            'description': p.desc,
            'type': p.type.real,
        })
    return pictures


70
71
72
73
74
75
76
77
78
79
def get_mp3_recording_id(f, k):
    try:
        return [
            t
            for t in f.tags.getall('UFID')
            if 'musicbrainz.org' in t.owner
        ][0].data.decode('utf-8')
    except IndexError:
        raise TagNotFound(k)

80
81
82
83
84
85
86
87
88
89
90
91
92

def convert_track_number(v):
    try:
        return int(v)
    except ValueError:
        # maybe the position is of the form "1/4"
        pass

    try:
        return int(v.split('/')[0])
    except (ValueError, AttributeError, IndexError):
        pass

93

94
95
96
97
98
99
100
101
102
103
104
105
106

class FirstUUIDField(forms.UUIDField):
    def to_python(self, value):
        try:
            # sometimes, Picard leaves to uuids in the field, separated
            # by a slash
            value = value.split('/')[0]
        except (AttributeError, IndexError, TypeError):
            pass

        return super().to_python(value)


107
VALIDATION = {
108
109
110
    'musicbrainz_artistid': FirstUUIDField(),
    'musicbrainz_albumid': FirstUUIDField(),
    'musicbrainz_recordingid': FirstUUIDField(),
111
112
}

113
114
CONF = {
    'OggVorbis': {
115
        'getter': lambda f, k: f[k][0],
116
117
118
        'fields': {
            'track_number': {
                'field': 'TRACKNUMBER',
119
                'to_application': convert_track_number
120
            },
121
122
123
            'title': {},
            'artist': {},
            'album': {},
124
125
126
127
            'date': {
                'field': 'date',
                'to_application': lambda v: arrow.get(v).date()
            },
128
129
            'musicbrainz_albumid': {},
            'musicbrainz_artistid': {},
EorlBruder's avatar
EorlBruder committed
130
131
132
133
134
135
136
137
138
139
140
141
            'musicbrainz_recordingid': {
                'field': 'musicbrainz_trackid'
            },
        }
    },
    'OggTheora': {
        'getter': lambda f, k: f[k][0],
        'fields': {
            'track_number': {
                'field': 'TRACKNUMBER',
                'to_application': convert_track_number
            },
142
143
144
            'title': {},
            'artist': {},
            'album': {},
EorlBruder's avatar
EorlBruder committed
145
146
147
148
149
            'date': {
                'field': 'date',
                'to_application': lambda v: arrow.get(v).date()
            },
            'musicbrainz_albumid': {
150
                'field': 'MusicBrainz Album Id'
EorlBruder's avatar
EorlBruder committed
151
152
            },
            'musicbrainz_artistid': {
153
                'field': 'MusicBrainz Artist Id'
154
155
            },
            'musicbrainz_recordingid': {
156
                'field': 'MusicBrainz Track Id'
157
158
159
160
161
            },
        }
    },
    'MP3': {
        'getter': get_id3_tag,
162
        'clean_pictures': clean_id3_pictures,
163
164
        'fields': {
            'track_number': {
165
                'field': 'TRCK',
166
                'to_application': convert_track_number
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
            },
            'title': {
                'field': 'TIT2'
            },
            'artist': {
                'field': 'TPE1'
            },
            'album': {
                'field': 'TALB'
            },
            'date': {
                'field': 'TDRC',
                'to_application': lambda v: arrow.get(str(v)).date()
            },
            'musicbrainz_albumid': {
                'field': 'MusicBrainz Album Id'
            },
            'musicbrainz_artistid': {
                'field': 'MusicBrainz Artist Id'
            },
            'musicbrainz_recordingid': {
                'field': 'UFID',
                'getter': get_mp3_recording_id,
            },
191
            'pictures': {},
192
        }
193
194
195
    },
    'FLAC': {
        'getter': get_flac_tag,
196
        'clean_pictures': clean_flac_pictures,
197
198
199
200
201
        'fields': {
            'track_number': {
                'field': 'tracknumber',
                'to_application': convert_track_number
            },
202
203
204
            'title': {},
            'artist': {},
            'album': {},
205
206
207
208
            'date': {
                'field': 'date',
                'to_application': lambda v: arrow.get(str(v)).date()
            },
209
210
            'musicbrainz_albumid': {},
            'musicbrainz_artistid': {},
211
212
213
            'musicbrainz_recordingid': {
                'field': 'musicbrainz_trackid'
            },
214
215
            'test': {},
            'pictures': {},
216
217
        }
    },
218
219
220
221
}


class Metadata(object):
222
223
224

    def __init__(self, path):
        self._file = mutagen.File(path)
225
226
227
228
229
230
231
        if self._file is None:
            raise ValueError('Cannot parse metadata from {}'.format(path))
        ft = self.get_file_type(self._file)
        try:
            self._conf = CONF[ft]
        except KeyError:
            raise ValueError('Unsupported format {}'.format(ft))
232

233
234
235
236
    def get_file_type(self, f):
        return f.__class__.__name__

    def get(self, key, default=NODEFAULT):
237
238
239
240
241
242
        try:
            field_conf = self._conf['fields'][key]
        except KeyError:
            raise UnsupportedTag(
                '{} is not supported for this file format'.format(key))
        real_key = field_conf.get('field', key)
243
        try:
244
245
            getter = field_conf.get('getter', self._conf['getter'])
            v = getter(self._file, real_key)
246
247
        except KeyError:
            if default == NODEFAULT:
248
                raise TagNotFound(real_key)
249
250
            return default

251
252
253
        converter = field_conf.get('to_application')
        if converter:
            v = converter(v)
254
255
256
        field = VALIDATION.get(key)
        if field:
            v = field.to_python(v)
257
        return v
258
259
260
261
262
263
264
265
266
267
268
269
270

    def get_picture(self, picture_type='cover_front'):
        ptype = getattr(mutagen.id3.PictureType, picture_type.upper())
        try:
            pictures = self.get('pictures')
        except (UnsupportedTag, TagNotFound):
            return

        cleaner = self._conf.get('clean_pictures', lambda v: v)
        pictures = cleaner(pictures)
        for p in pictures:
            if p['type'] == ptype:
                return p