mirror of
https://code.eliotberriot.com/funkwhale/funkwhale.git
synced 2025-10-06 04:39:57 +02:00
This commit is contained in:
parent
7bb0fa2e64
commit
05f0129025
9 changed files with 916 additions and 397 deletions
|
@ -8,7 +8,8 @@ import mutagen.oggtheora
|
|||
import mutagen.oggvorbis
|
||||
import mutagen.flac
|
||||
|
||||
from django import forms
|
||||
from rest_framework import serializers
|
||||
from rest_framework.compat import Mapping
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
NODEFAULT = object()
|
||||
|
@ -122,85 +123,23 @@ def get_mp3_recording_id(f, k):
|
|||
raise TagNotFound(k)
|
||||
|
||||
|
||||
def convert_position(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
|
||||
|
||||
|
||||
class FirstUUIDField(forms.UUIDField):
|
||||
def to_python(self, value):
|
||||
try:
|
||||
# sometimes, Picard leaves two uuids in the field, separated
|
||||
# by a slash or a ;
|
||||
value = value.split(";")[0].split("/")[0].strip()
|
||||
except (AttributeError, IndexError, TypeError):
|
||||
pass
|
||||
|
||||
return super().to_python(value)
|
||||
|
||||
|
||||
def get_date(value):
|
||||
ADDITIONAL_FORMATS = ["%Y-%d-%m %H:%M"] # deezer date format
|
||||
try:
|
||||
parsed = pendulum.parse(str(value))
|
||||
return datetime.date(parsed.year, parsed.month, parsed.day)
|
||||
except pendulum.exceptions.ParserError:
|
||||
pass
|
||||
|
||||
for date_format in ADDITIONAL_FORMATS:
|
||||
try:
|
||||
parsed = datetime.datetime.strptime(value, date_format)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
return datetime.date(parsed.year, parsed.month, parsed.day)
|
||||
|
||||
raise ParseError("{} cannot be parsed as a date".format(value))
|
||||
|
||||
|
||||
def split_and_return_first(separator):
|
||||
def inner(v):
|
||||
return v.split(separator)[0].strip()
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
VALIDATION = {
|
||||
"musicbrainz_artistid": FirstUUIDField(),
|
||||
"musicbrainz_albumid": FirstUUIDField(),
|
||||
"musicbrainz_recordingid": FirstUUIDField(),
|
||||
"musicbrainz_albumartistid": FirstUUIDField(),
|
||||
}
|
||||
VALIDATION = {}
|
||||
|
||||
CONF = {
|
||||
"OggOpus": {
|
||||
"getter": lambda f, k: f[k][0],
|
||||
"fields": {
|
||||
"track_number": {
|
||||
"field": "TRACKNUMBER",
|
||||
"to_application": convert_position,
|
||||
},
|
||||
"disc_number": {"field": "DISCNUMBER", "to_application": convert_position},
|
||||
"position": {"field": "TRACKNUMBER"},
|
||||
"disc_number": {"field": "DISCNUMBER"},
|
||||
"title": {},
|
||||
"artist": {},
|
||||
"album_artist": {
|
||||
"field": "albumartist",
|
||||
"to_application": split_and_return_first(";"),
|
||||
},
|
||||
"album_artist": {"field": "albumartist"},
|
||||
"album": {},
|
||||
"date": {"field": "date", "to_application": get_date},
|
||||
"date": {"field": "date"},
|
||||
"musicbrainz_albumid": {},
|
||||
"musicbrainz_artistid": {},
|
||||
"musicbrainz_albumartistid": {},
|
||||
"musicbrainz_recordingid": {"field": "musicbrainz_trackid"},
|
||||
"mbid": {"field": "musicbrainz_trackid"},
|
||||
"license": {},
|
||||
"copyright": {},
|
||||
},
|
||||
|
@ -208,23 +147,17 @@ CONF = {
|
|||
"OggVorbis": {
|
||||
"getter": lambda f, k: f[k][0],
|
||||
"fields": {
|
||||
"track_number": {
|
||||
"field": "TRACKNUMBER",
|
||||
"to_application": convert_position,
|
||||
},
|
||||
"disc_number": {"field": "DISCNUMBER", "to_application": convert_position},
|
||||
"position": {"field": "TRACKNUMBER"},
|
||||
"disc_number": {"field": "DISCNUMBER"},
|
||||
"title": {},
|
||||
"artist": {},
|
||||
"album_artist": {
|
||||
"field": "albumartist",
|
||||
"to_application": split_and_return_first(";"),
|
||||
},
|
||||
"album_artist": {"field": "albumartist"},
|
||||
"album": {},
|
||||
"date": {"field": "date", "to_application": get_date},
|
||||
"date": {"field": "date"},
|
||||
"musicbrainz_albumid": {},
|
||||
"musicbrainz_artistid": {},
|
||||
"musicbrainz_albumartistid": {},
|
||||
"musicbrainz_recordingid": {"field": "musicbrainz_trackid"},
|
||||
"mbid": {"field": "musicbrainz_trackid"},
|
||||
"license": {},
|
||||
"copyright": {},
|
||||
"pictures": {
|
||||
|
@ -236,20 +169,17 @@ CONF = {
|
|||
"OggTheora": {
|
||||
"getter": lambda f, k: f[k][0],
|
||||
"fields": {
|
||||
"track_number": {
|
||||
"field": "TRACKNUMBER",
|
||||
"to_application": convert_position,
|
||||
},
|
||||
"disc_number": {"field": "DISCNUMBER", "to_application": convert_position},
|
||||
"position": {"field": "TRACKNUMBER"},
|
||||
"disc_number": {"field": "DISCNUMBER"},
|
||||
"title": {},
|
||||
"artist": {},
|
||||
"album_artist": {"field": "albumartist"},
|
||||
"album": {},
|
||||
"date": {"field": "date", "to_application": get_date},
|
||||
"date": {"field": "date"},
|
||||
"musicbrainz_albumid": {"field": "MusicBrainz Album Id"},
|
||||
"musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
|
||||
"musicbrainz_albumartistid": {"field": "MusicBrainz Album Artist Id"},
|
||||
"musicbrainz_recordingid": {"field": "MusicBrainz Track Id"},
|
||||
"mbid": {"field": "MusicBrainz Track Id"},
|
||||
"license": {},
|
||||
"copyright": {},
|
||||
},
|
||||
|
@ -258,20 +188,17 @@ CONF = {
|
|||
"getter": get_id3_tag,
|
||||
"clean_pictures": clean_id3_pictures,
|
||||
"fields": {
|
||||
"track_number": {"field": "TRCK", "to_application": convert_position},
|
||||
"disc_number": {"field": "TPOS", "to_application": convert_position},
|
||||
"position": {"field": "TRCK"},
|
||||
"disc_number": {"field": "TPOS"},
|
||||
"title": {"field": "TIT2"},
|
||||
"artist": {"field": "TPE1"},
|
||||
"album_artist": {"field": "TPE2"},
|
||||
"album": {"field": "TALB"},
|
||||
"date": {"field": "TDRC", "to_application": get_date},
|
||||
"date": {"field": "TDRC"},
|
||||
"musicbrainz_albumid": {"field": "MusicBrainz Album Id"},
|
||||
"musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
|
||||
"musicbrainz_albumartistid": {"field": "MusicBrainz Album Artist Id"},
|
||||
"musicbrainz_recordingid": {
|
||||
"field": "UFID",
|
||||
"getter": get_mp3_recording_id,
|
||||
},
|
||||
"mbid": {"field": "UFID", "getter": get_mp3_recording_id},
|
||||
"pictures": {},
|
||||
"license": {"field": "WCOP"},
|
||||
"copyright": {"field": "TCOP"},
|
||||
|
@ -281,20 +208,17 @@ CONF = {
|
|||
"getter": get_flac_tag,
|
||||
"clean_pictures": clean_flac_pictures,
|
||||
"fields": {
|
||||
"track_number": {
|
||||
"field": "tracknumber",
|
||||
"to_application": convert_position,
|
||||
},
|
||||
"disc_number": {"field": "discnumber", "to_application": convert_position},
|
||||
"position": {"field": "tracknumber"},
|
||||
"disc_number": {"field": "discnumber"},
|
||||
"title": {},
|
||||
"artist": {},
|
||||
"album_artist": {"field": "albumartist"},
|
||||
"album": {},
|
||||
"date": {"field": "date", "to_application": get_date},
|
||||
"date": {"field": "date"},
|
||||
"musicbrainz_albumid": {},
|
||||
"musicbrainz_artistid": {},
|
||||
"musicbrainz_albumartistid": {},
|
||||
"musicbrainz_recordingid": {"field": "musicbrainz_trackid"},
|
||||
"mbid": {"field": "musicbrainz_trackid"},
|
||||
"test": {},
|
||||
"pictures": {},
|
||||
"license": {},
|
||||
|
@ -304,7 +228,7 @@ CONF = {
|
|||
}
|
||||
|
||||
ALL_FIELDS = [
|
||||
"track_number",
|
||||
"position",
|
||||
"disc_number",
|
||||
"title",
|
||||
"artist",
|
||||
|
@ -314,13 +238,13 @@ ALL_FIELDS = [
|
|||
"musicbrainz_albumid",
|
||||
"musicbrainz_artistid",
|
||||
"musicbrainz_albumartistid",
|
||||
"musicbrainz_recordingid",
|
||||
"mbid",
|
||||
"license",
|
||||
"copyright",
|
||||
]
|
||||
|
||||
|
||||
class Metadata(object):
|
||||
class Metadata(Mapping):
|
||||
def __init__(self, filething, kind=mutagen.File):
|
||||
self._file = kind(filething)
|
||||
if self._file is None:
|
||||
|
@ -368,6 +292,21 @@ class Metadata(object):
|
|||
else:
|
||||
return self.fallback.get(key, default=default)
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
Return a dict with all support metadata fields, if they are available
|
||||
"""
|
||||
final = {}
|
||||
for field in self._conf["fields"]:
|
||||
if field in ["pictures"]:
|
||||
continue
|
||||
value = self.get(field, None)
|
||||
if value is None:
|
||||
continue
|
||||
final[field] = str(value)
|
||||
|
||||
return final
|
||||
|
||||
def _get_from_self(self, key, default=NODEFAULT):
|
||||
try:
|
||||
field_conf = self._conf["fields"][key]
|
||||
|
@ -390,25 +329,6 @@ class Metadata(object):
|
|||
v = field.to_python(v)
|
||||
return v
|
||||
|
||||
def all(self, ignore_parse_errors=True):
|
||||
"""
|
||||
Return a dict containing all metadata of the file
|
||||
"""
|
||||
|
||||
data = {}
|
||||
for field in ALL_FIELDS:
|
||||
try:
|
||||
data[field] = self.get(field, None)
|
||||
except (TagNotFound, forms.ValidationError):
|
||||
data[field] = None
|
||||
except ParseError as e:
|
||||
if not ignore_parse_errors:
|
||||
raise
|
||||
logger.warning("Unparsable field {}: {}".format(field, str(e)))
|
||||
data[field] = None
|
||||
|
||||
return data
|
||||
|
||||
def get_picture(self, *picture_types):
|
||||
if not picture_types:
|
||||
raise ValueError("You need to request at least one picture type")
|
||||
|
@ -430,3 +350,166 @@ class Metadata(object):
|
|||
for p in pictures:
|
||||
if p["type"] == ptype:
|
||||
return p
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.get(key)
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
def __iter__(self):
|
||||
for field in self._conf["fields"]:
|
||||
yield field
|
||||
|
||||
|
||||
class ArtistField(serializers.Field):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.for_album = kwargs.pop("for_album", False)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_value(self, data):
|
||||
if self.for_album:
|
||||
keys = [("names", "album_artist"), ("mbids", "musicbrainz_albumartistid")]
|
||||
else:
|
||||
keys = [("names", "artist"), ("mbids", "musicbrainz_artistid")]
|
||||
|
||||
final = {}
|
||||
for field, key in keys:
|
||||
final[field] = data.get(key, None)
|
||||
|
||||
return final
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# we have multiple values that can be separated by various separators
|
||||
separators = [";", "/"]
|
||||
# we get a list like that if tagged via musicbrainz
|
||||
# ae29aae4-abfb-4609-8f54-417b1f4d64cc; 3237b5a8-ae44-400c-aa6d-cea51f0b9074;
|
||||
raw_mbids = data["mbids"]
|
||||
used_separator = None
|
||||
mbids = [raw_mbids]
|
||||
if raw_mbids:
|
||||
for separator in separators:
|
||||
if separator in raw_mbids:
|
||||
used_separator = separator
|
||||
mbids = [m.strip() for m in raw_mbids.split(separator)]
|
||||
break
|
||||
|
||||
# now, we split on artist names, using the same separator as the one used
|
||||
# by mbids, if any
|
||||
if used_separator and mbids:
|
||||
names = [n.strip() for n in data["names"].split(used_separator)]
|
||||
else:
|
||||
names = [data["names"]]
|
||||
|
||||
final = []
|
||||
for i, name in enumerate(names):
|
||||
try:
|
||||
mbid = mbids[i]
|
||||
except IndexError:
|
||||
mbid = None
|
||||
artist = {"name": name, "mbid": mbid}
|
||||
final.append(artist)
|
||||
|
||||
field = serializers.ListField(child=ArtistSerializer(), min_length=1)
|
||||
|
||||
return field.to_internal_value(final)
|
||||
|
||||
|
||||
class AlbumField(serializers.Field):
|
||||
def get_value(self, data):
|
||||
return data
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
title = data.get("album")
|
||||
except TagNotFound:
|
||||
raise serializers.ValidationError("Missing album tag")
|
||||
final = {
|
||||
"title": title,
|
||||
"release_date": data.get("date", None),
|
||||
"mbid": data.get("musicbrainz_albumid", None),
|
||||
}
|
||||
artists_field = ArtistField(for_album=True)
|
||||
payload = artists_field.get_value(data)
|
||||
artists = artists_field.to_internal_value(payload)
|
||||
album_serializer = AlbumSerializer(data=final)
|
||||
album_serializer.is_valid(raise_exception=True)
|
||||
album_serializer.validated_data["artists"] = artists
|
||||
return album_serializer.validated_data
|
||||
|
||||
|
||||
class CoverDataField(serializers.Field):
|
||||
def get_value(self, data):
|
||||
return data
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return data.get_picture("cover_front", "other")
|
||||
|
||||
|
||||
class PermissiveDateField(serializers.CharField):
|
||||
def to_internal_value(self, value):
|
||||
if not value:
|
||||
return None
|
||||
value = super().to_internal_value(str(value))
|
||||
ADDITIONAL_FORMATS = [
|
||||
"%Y-%d-%m %H:%M", # deezer date format
|
||||
"%Y-%W", # weird date format based on week number, see #718
|
||||
]
|
||||
|
||||
for date_format in ADDITIONAL_FORMATS:
|
||||
try:
|
||||
parsed = datetime.datetime.strptime(value, date_format)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
return datetime.date(parsed.year, parsed.month, parsed.day)
|
||||
|
||||
try:
|
||||
parsed = pendulum.parse(str(value))
|
||||
return datetime.date(parsed.year, parsed.month, parsed.day)
|
||||
except pendulum.exceptions.ParserError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ArtistSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
mbid = serializers.UUIDField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class AlbumSerializer(serializers.Serializer):
|
||||
title = serializers.CharField()
|
||||
mbid = serializers.UUIDField(required=False, allow_null=True)
|
||||
release_date = PermissiveDateField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class PositionField(serializers.CharField):
|
||||
def to_internal_value(self, v):
|
||||
v = super().to_internal_value(v)
|
||||
if not v:
|
||||
return 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
|
||||
|
||||
|
||||
class TrackMetadataSerializer(serializers.Serializer):
|
||||
title = serializers.CharField()
|
||||
position = PositionField(allow_null=True, required=False)
|
||||
disc_number = PositionField(allow_null=True, required=False)
|
||||
copyright = serializers.CharField(allow_null=True, required=False)
|
||||
license = serializers.CharField(allow_null=True, required=False)
|
||||
mbid = serializers.UUIDField(allow_null=True, required=False)
|
||||
|
||||
album = AlbumField()
|
||||
artists = ArtistField()
|
||||
cover_data = CoverDataField()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue