Fixed #469: parsing of embedded file cover for ogg files tagged with MusicBrainz Picard
This commit is contained in:
parent
2703c61c48
commit
08ce00e3cd
|
@ -1,8 +1,13 @@
|
||||||
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import mutagen
|
|
||||||
import pendulum
|
import pendulum
|
||||||
|
|
||||||
|
import mutagen._util
|
||||||
|
import mutagen.oggtheora
|
||||||
|
import mutagen.oggvorbis
|
||||||
|
import mutagen.flac
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -83,6 +88,31 @@ def clean_flac_pictures(apic):
|
||||||
return pictures
|
return pictures
|
||||||
|
|
||||||
|
|
||||||
|
def clean_ogg_pictures(metadata_block_picture):
|
||||||
|
pictures = []
|
||||||
|
for b64_data in [metadata_block_picture]:
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = base64.b64decode(b64_data)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
picture = mutagen.flac.Picture(data)
|
||||||
|
except mutagen.flac.FLACError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pictures.append(
|
||||||
|
{
|
||||||
|
"mimetype": picture.mime,
|
||||||
|
"content": picture.data,
|
||||||
|
"description": "",
|
||||||
|
"type": picture.type.real,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return pictures
|
||||||
|
|
||||||
|
|
||||||
def get_mp3_recording_id(f, k):
|
def get_mp3_recording_id(f, k):
|
||||||
try:
|
try:
|
||||||
return [t for t in f.tags.getall("UFID") if "musicbrainz.org" in t.owner][
|
return [t for t in f.tags.getall("UFID") if "musicbrainz.org" in t.owner][
|
||||||
|
@ -197,6 +227,10 @@ CONF = {
|
||||||
"musicbrainz_recordingid": {"field": "musicbrainz_trackid"},
|
"musicbrainz_recordingid": {"field": "musicbrainz_trackid"},
|
||||||
"license": {},
|
"license": {},
|
||||||
"copyright": {},
|
"copyright": {},
|
||||||
|
"pictures": {
|
||||||
|
"field": "metadata_block_picture",
|
||||||
|
"to_application": clean_ogg_pictures,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"OggTheora": {
|
"OggTheora": {
|
||||||
|
@ -216,9 +250,8 @@ CONF = {
|
||||||
"musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
|
"musicbrainz_artistid": {"field": "MusicBrainz Artist Id"},
|
||||||
"musicbrainz_albumartistid": {"field": "MusicBrainz Album Artist Id"},
|
"musicbrainz_albumartistid": {"field": "MusicBrainz Album Artist Id"},
|
||||||
"musicbrainz_recordingid": {"field": "MusicBrainz Track Id"},
|
"musicbrainz_recordingid": {"field": "MusicBrainz Track Id"},
|
||||||
# somehow, I cannot successfully create an ogg theora file
|
"license": {},
|
||||||
# with the proper license field
|
"copyright": {},
|
||||||
# "license": {"field": "license"},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"MP3": {
|
"MP3": {
|
||||||
|
@ -288,10 +321,11 @@ ALL_FIELDS = [
|
||||||
|
|
||||||
|
|
||||||
class Metadata(object):
|
class Metadata(object):
|
||||||
def __init__(self, path):
|
def __init__(self, filething, kind=mutagen.File):
|
||||||
self._file = mutagen.File(path)
|
self._file = kind(filething)
|
||||||
if self._file is None:
|
if self._file is None:
|
||||||
raise ValueError("Cannot parse metadata from {}".format(path))
|
raise ValueError("Cannot parse metadata from {}".format(filething))
|
||||||
|
self.fallback = self.load_fallback(filething, self._file)
|
||||||
ft = self.get_file_type(self._file)
|
ft = self.get_file_type(self._file)
|
||||||
try:
|
try:
|
||||||
self._conf = CONF[ft]
|
self._conf = CONF[ft]
|
||||||
|
@ -301,7 +335,40 @@ class Metadata(object):
|
||||||
def get_file_type(self, f):
|
def get_file_type(self, f):
|
||||||
return f.__class__.__name__
|
return f.__class__.__name__
|
||||||
|
|
||||||
|
def load_fallback(self, filething, parent):
|
||||||
|
"""
|
||||||
|
In some situations, such as Ogg Theora files tagged with MusicBrainz Picard,
|
||||||
|
part of the tags are only available in the ogg vorbis comments
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
filething.seek(0)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if isinstance(parent, mutagen.oggtheora.OggTheora):
|
||||||
|
try:
|
||||||
|
return Metadata(filething, kind=mutagen.oggvorbis.OggVorbis)
|
||||||
|
except (ValueError, mutagen._util.MutagenError):
|
||||||
|
raise
|
||||||
|
pass
|
||||||
|
|
||||||
def get(self, key, default=NODEFAULT):
|
def get(self, key, default=NODEFAULT):
|
||||||
|
try:
|
||||||
|
return self._get_from_self(key)
|
||||||
|
except TagNotFound:
|
||||||
|
if not self.fallback:
|
||||||
|
if default != NODEFAULT:
|
||||||
|
return default
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
return self.fallback.get(key, default=default)
|
||||||
|
except UnsupportedTag:
|
||||||
|
if not self.fallback:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
return self.fallback.get(key, default=default)
|
||||||
|
|
||||||
|
def _get_from_self(self, key, default=NODEFAULT):
|
||||||
try:
|
try:
|
||||||
field_conf = self._conf["fields"][key]
|
field_conf = self._conf["fields"][key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import mutagen.oggtheora
|
||||||
|
import mutagen.oggvorbis
|
||||||
|
|
||||||
from funkwhale_api.music import metadata
|
from funkwhale_api.music import metadata
|
||||||
|
|
||||||
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
@ -145,7 +147,7 @@ def test_can_get_metadata_from_id3_mp3_file(field, value):
|
||||||
assert data.get(field) == value
|
assert data.get(field) == value
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name", ["test.mp3", "sample.flac"])
|
@pytest.mark.parametrize("name", ["test.mp3", "sample.flac", "with_cover.ogg"])
|
||||||
def test_can_get_pictures(name):
|
def test_can_get_pictures(name):
|
||||||
path = os.path.join(DATA_DIR, name)
|
path = os.path.join(DATA_DIR, name)
|
||||||
data = metadata.Metadata(path)
|
data = metadata.Metadata(path)
|
||||||
|
@ -243,3 +245,20 @@ def test_metadata_all_ignore_parse_errors_false(mocker):
|
||||||
mocker.patch.object(data, "get", side_effect=metadata.ParseError("Failure"))
|
mocker.patch.object(data, "get", side_effect=metadata.ParseError("Failure"))
|
||||||
with pytest.raises(metadata.ParseError):
|
with pytest.raises(metadata.ParseError):
|
||||||
data.all(ignore_parse_errors=False)
|
data.all(ignore_parse_errors=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_metadata_fallback_ogg_theora(mocker):
|
||||||
|
path = os.path.join(DATA_DIR, "with_cover.ogg")
|
||||||
|
data = metadata.Metadata(path)
|
||||||
|
|
||||||
|
assert isinstance(data._file, mutagen.oggtheora.OggTheora)
|
||||||
|
assert isinstance(data.fallback, metadata.Metadata)
|
||||||
|
assert isinstance(data.fallback._file, mutagen.oggvorbis.OggVorbis)
|
||||||
|
|
||||||
|
expected_result = data.fallback.get("pictures")
|
||||||
|
fallback_get = mocker.spy(data.fallback, "get")
|
||||||
|
|
||||||
|
assert expected_result is not None
|
||||||
|
assert data.get("pictures", "default") == expected_result
|
||||||
|
|
||||||
|
fallback_get.assert_called_once_with("pictures", "default")
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
Fixed parsing of embedded file cover for ogg files tagged with MusicBrainz (#469)
|
Loading…
Reference in New Issue