175 lines
6.7 KiB
Python
175 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
import mutagen
|
|
import math
|
|
import os
|
|
import copy
|
|
|
|
import media.update.replaygain as gain
|
|
from media.monitor.exceptions import BadSongFile
|
|
from media.monitor.log import Loggable
|
|
import media.monitor.pure as mmp
|
|
|
|
"""
|
|
list of supported easy tags in mutagen version 1.20
|
|
['albumartistsort', 'musicbrainz_albumstatus', 'lyricist', 'releasecountry',
|
|
'date', 'performer', 'musicbrainz_albumartistid', 'composer', 'encodedby',
|
|
'tracknumber', 'musicbrainz_albumid', 'album', 'asin', 'musicbrainz_artistid',
|
|
'mood', 'copyright', 'author', 'media', 'length', 'version', 'artistsort',
|
|
'titlesort', 'discsubtitle', 'website', 'musicip_fingerprint', 'conductor',
|
|
'compilation', 'barcode', 'performer:*', 'composersort', 'musicbrainz_discid',
|
|
'musicbrainz_albumtype', 'genre', 'isrc', 'discnumber', 'musicbrainz_trmid',
|
|
'replaygain_*_gain', 'musicip_puid', 'artist', 'title', 'bpm',
|
|
'musicbrainz_trackid', 'arranger', 'albumsort', 'replaygain_*_peak',
|
|
'organization']
|
|
"""
|
|
|
|
airtime2mutagen = {
|
|
"MDATA_KEY_TITLE": "title",
|
|
"MDATA_KEY_CREATOR": "artist",
|
|
"MDATA_KEY_SOURCE": "album",
|
|
"MDATA_KEY_GENRE": "genre",
|
|
"MDATA_KEY_MOOD": "mood",
|
|
"MDATA_KEY_TRACKNUMBER": "tracknumber",
|
|
"MDATA_KEY_BPM": "bpm",
|
|
"MDATA_KEY_LABEL": "organization",
|
|
"MDATA_KEY_COMPOSER": "composer",
|
|
"MDATA_KEY_ENCODER": "encodedby",
|
|
"MDATA_KEY_CONDUCTOR": "conductor",
|
|
"MDATA_KEY_YEAR": "date",
|
|
"MDATA_KEY_URL": "website",
|
|
"MDATA_KEY_ISRC": "isrc",
|
|
"MDATA_KEY_COPYRIGHT": "copyright",
|
|
}
|
|
|
|
# Some airtime attributes are special because they must use the mutagen object
|
|
# itself to calculate the value that they need. The lambda associated with each
|
|
# key should attempt to extract the corresponding value from the mutagen object
|
|
# itself pass as 'm'. In the case when nothing can be extracted the lambda
|
|
# should return some default value to be assigned anyway or None so that the
|
|
# airtime metadata object will skip the attribute outright.
|
|
|
|
airtime_special = {
|
|
"MDATA_KEY_DURATION" :
|
|
lambda m: format_length(getattr(m.info, u'length', 0.0)),
|
|
"MDATA_KEY_BITRATE" :
|
|
lambda m: getattr(m.info, "bitrate", 0),
|
|
"MDATA_KEY_SAMPLERATE" :
|
|
lambda m: getattr(m.info, u'sample_rate', 0),
|
|
"MDATA_KEY_MIME" :
|
|
lambda m: m.mime[0] if len(m.mime) > 0 else u'',
|
|
}
|
|
mutagen2airtime = dict( (v,k) for k,v in airtime2mutagen.iteritems()
|
|
if isinstance(v, str) )
|
|
|
|
truncate_table = {
|
|
'MDATA_KEY_GENRE' : 64,
|
|
'MDATA_KEY_TITLE' : 512,
|
|
'MDATA_KEY_CREATOR' : 512,
|
|
'MDATA_KEY_SOURCE' : 512,
|
|
'MDATA_KEY_MOOD' : 64,
|
|
'MDATA_KEY_LABEL' : 512,
|
|
'MDATA_KEY_COMPOSER' : 512,
|
|
'MDATA_KEY_ENCODER' : 255,
|
|
'MDATA_KEY_CONDUCTOR' : 512,
|
|
'MDATA_KEY_YEAR' : 16,
|
|
'MDATA_KEY_URL' : 512,
|
|
'MDATA_KEY_ISRC' : 512,
|
|
'MDATA_KEY_COPYRIGHT' : 512,
|
|
}
|
|
|
|
def format_length(mutagen_length):
|
|
"""Convert mutagen length to airtime length"""
|
|
t = float(mutagen_length)
|
|
h = int(math.floor(t / 3600))
|
|
t = t % 3600
|
|
m = int(math.floor(t / 60))
|
|
s = t % 60
|
|
# will be ss.uuu
|
|
s = str(s)
|
|
seconds = s.split(".")
|
|
s = seconds[0]
|
|
# have a maximum of 6 subseconds.
|
|
if len(seconds[1]) >= 6: ss = seconds[1][0:6]
|
|
else: ss = seconds[1][0:]
|
|
return "%s:%s:%s.%s" % (h, m, s, ss)
|
|
|
|
def truncate_to_length(item, length):
|
|
if isinstance(item, int): item = str(item)
|
|
if isinstance(item, basestring):
|
|
if len(item) > length: return item[0:length]
|
|
else: return item
|
|
|
|
class Metadata(Loggable):
|
|
# TODO : refactor the way metadata is being handled. Right now things are a
|
|
# little bit messy. Some of the handling is in m.m.pure while the rest is
|
|
# here. Also interface is not very consistent
|
|
|
|
@staticmethod
|
|
def write_unsafe(path,md):
|
|
if not os.path.exists(path):
|
|
raise BadSongFile(path)
|
|
song_file = mutagen.File(path, easy=True)
|
|
for airtime_k, airtime_v in md.iteritems():
|
|
if airtime_k in airtime2mutagen:
|
|
# The unicode cast here is mostly for integers that need to be
|
|
# strings
|
|
song_file[ airtime2mutagen[airtime_k] ] = unicode(airtime_v)
|
|
song_file.save()
|
|
|
|
|
|
def __init__(self, fpath):
|
|
# Forcing the unicode through
|
|
try: fpath = fpath.decode("utf-8")
|
|
except: pass
|
|
try: full_mutagen = mutagen.File(fpath, easy=True)
|
|
except Exception: raise BadSongFile(fpath)
|
|
self.path = fpath
|
|
# TODO : Simplify the way all of these rules are handled right not it's
|
|
# extremely unclear and needs to be refactored.
|
|
metadata = {}
|
|
# Load only the metadata avilable in mutagen into metdata
|
|
for k,v in full_mutagen.iteritems():
|
|
# Special handling of attributes here
|
|
if isinstance(v, list):
|
|
# TODO : some files have multiple fields for the same metadata.
|
|
# genre is one example. In that case mutagen will return a list
|
|
# of values
|
|
metadata[k] = v[0]
|
|
#if len(v) == 1: metadata[k] = v[0]
|
|
#else: raise Exception("Unknown mutagen %s:%s" % (k,str(v)))
|
|
else: metadata[k] = v
|
|
self.__metadata = {}
|
|
# Start populating a dictionary of airtime metadata in __metadata
|
|
for muta_k, muta_v in metadata.iteritems():
|
|
# We must check if we can actually translate the mutagen key into
|
|
# an airtime key before doing the conversion
|
|
if muta_k in mutagen2airtime:
|
|
airtime_key = mutagen2airtime[muta_k]
|
|
# Apply truncation in the case where airtime_key is in our
|
|
# truncation table
|
|
muta_v = \
|
|
truncate_to_length(muta_v, truncate_table[airtime_key])\
|
|
if airtime_key in truncate_table else muta_v
|
|
self.__metadata[ airtime_key ] = muta_v
|
|
# Now we extra the special values that are calculated from the mutagen
|
|
# object itself:
|
|
for special_key,f in airtime_special.iteritems():
|
|
new_val = f(full_mutagen)
|
|
if new_val is not None:
|
|
self.__metadata[special_key] = f(full_mutagen)
|
|
# Finally, we "normalize" all the metadata here:
|
|
self.__metadata = mmp.normalized_metadata(self.__metadata, fpath)
|
|
# Now we must load the md5:
|
|
self.__metadata['MDATA_KEY_MD5'] = mmp.file_md5(fpath,max_length=-1)
|
|
self.__metadata['MDATA_KEY_REPLAYGAIN'] = \
|
|
gain.calculate_replay_gain(fpath)
|
|
|
|
def is_recorded(self):
|
|
return mmp.is_airtime_recorded( self.__metadata )
|
|
|
|
def extract(self):
|
|
return copy.deepcopy(self.__metadata)
|
|
|
|
def utf8(self):
|
|
return mmp.convert_dict_value_to_utf8(self.extract())
|