diff --git a/api/funkwhale_api/music/metadata.py b/api/funkwhale_api/music/metadata.py index feec90cc7..801ea6a9b 100644 --- a/api/funkwhale_api/music/metadata.py +++ b/api/funkwhale_api/music/metadata.py @@ -70,6 +70,45 @@ def clean_id3_pictures(apic): return pictures +def get_mp4_tag(f, k): + if k == "pictures": + return f.get("covr") + raw_value = f.get(k, None) + + if not raw_value: + raise TagNotFound(k) + + value = raw_value[0] + try: + return value.decode() + except AttributeError: + return value + + +def get_mp4_position(raw_value): + return raw_value[0] + + +def clean_mp4_pictures(raw_pictures): + pictures = [] + for p in list(raw_pictures): + if p.imageformat == p.FORMAT_JPEG: + mimetype = "image/jpeg" + elif p.imageformat == p.FORMAT_PNG: + mimetype = "image/png" + else: + continue + pictures.append( + { + "mimetype": mimetype, + "content": bytes(p), + "description": "", + "type": mutagen.id3.PictureType.COVER_FRONT, + } + ) + return pictures + + def get_flac_tag(f, k): if k == "pictures": return f.pictures @@ -216,6 +255,33 @@ CONF = { "copyright": {"field": "TCOP"}, }, }, + "MP4": { + "getter": get_mp4_tag, + "clean_pictures": clean_mp4_pictures, + "fields": { + "position": {"field": "trkn", "to_application": get_mp4_position}, + "disc_number": {"field": "disk", "to_application": get_mp4_position}, + "title": {"field": "©nam"}, + "artist": {"field": "©ART"}, + "album_artist": {"field": "aART"}, + "album": {"field": "©alb"}, + "date": {"field": "©day"}, + "musicbrainz_albumid": { + "field": "----:com.apple.iTunes:MusicBrainz Album Id" + }, + "musicbrainz_artistid": { + "field": "----:com.apple.iTunes:MusicBrainz Artist Id" + }, + "genre": {"field": "©gen"}, + "musicbrainz_albumartistid": { + "field": "----:com.apple.iTunes:MusicBrainz Album Artist Id" + }, + "mbid": {"field": "----:com.apple.iTunes:MusicBrainz Track Id"}, + "pictures": {}, + "license": {"field": "----:com.apple.iTunes:LICENSE"}, + "copyright": {"field": "cprt"}, + }, + }, "FLAC": { "getter": get_flac_tag, "clean_pictures": clean_flac_pictures, diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index bf3a0daf6..09c8cbd12 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -34,6 +34,8 @@ AUDIO_EXTENSIONS_AND_MIMETYPE = [ ("ogg", "audio/ogg"), ("opus", "audio/opus"), ("mp3", "audio/mpeg"), + ("aac", "audio/x-m4a"), + ("m4a", "audio/x-m4a"), ("flac", "audio/x-flac"), ("flac", "audio/flac"), ] diff --git a/api/tests/music/test.m4a b/api/tests/music/test.m4a new file mode 100644 index 000000000..57b65b7b5 Binary files /dev/null and b/api/tests/music/test.m4a differ diff --git a/api/tests/music/test_metadata.py b/api/tests/music/test_metadata.py index 716d9c979..9a3826ce3 100644 --- a/api/tests/music/test_metadata.py +++ b/api/tests/music/test_metadata.py @@ -149,6 +149,7 @@ def test_can_get_metadata_from_id3_mp3_file(field, value): "sample.flac", "with_cover.ogg", "with_cover.opus", + "test.m4a", ], ) def test_can_get_pictures(name): @@ -189,6 +190,35 @@ def test_can_get_metadata_from_flac_file(field, value): assert data.get(field) == value +@pytest.mark.parametrize( + "field,value", + [ + ("title", "Peer Gynt Suite no. 1, op. 46: I. Morning"), + ("artist", "Edvard Grieg"), + ("album_artist", "Edvard Grieg; Musopen Symphony Orchestra"), + ("album", "Peer Gynt Suite no. 1, op. 46"), + ("date", "2012-08-15"), + ("position", 1), + ("disc_number", 2), + ("musicbrainz_albumid", "a766da8b-8336-47aa-a3ee-371cc41ccc75"), + ("mbid", "bd21ac48-46d8-4e78-925f-d9cc2a294656"), + ("musicbrainz_artistid", "013c8e5b-d72a-4cd3-8dee-6c64d6125823"), + ( + "musicbrainz_albumartistid", + "013c8e5b-d72a-4cd3-8dee-6c64d6125823;5b4d7d2d-36df-4b38-95e3-a964234f520f", + ), + ("license", "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/"), + ("copyright", "Someone"), + ("genre", "Dubstep"), + ], +) +def test_can_get_metadata_from_m4a_file(field, value): + path = os.path.join(DATA_DIR, "test.m4a") + data = metadata.Metadata(path) + + assert data.get(field) == value + + def test_can_get_metadata_from_flac_file_not_crash_if_empty(): path = os.path.join(DATA_DIR, "sample.flac") data = metadata.Metadata(path) diff --git a/changes/changelog.d/661.enhancement b/changes/changelog.d/661.enhancement new file mode 100644 index 000000000..38553fbfa --- /dev/null +++ b/changes/changelog.d/661.enhancement @@ -0,0 +1 @@ +Support for M4A/AAC files (#661) diff --git a/front/src/components/library/FileUpload.vue b/front/src/components/library/FileUpload.vue index 025eab930..d88efd5ab 100644 --- a/front/src/components/library/FileUpload.vue +++ b/front/src/components/library/FileUpload.vue @@ -158,7 +158,7 @@ export default { currentTab: "summary", uploadUrl: this.$store.getters['instance/absoluteUrl']("/api/v1/uploads/"), importReference, - supportedExtensions: ["flac", "ogg", "mp3", "opus"], + supportedExtensions: ["flac", "ogg", "mp3", "opus", "aac", "m4a"], isLoadingQuota: false, quotaStatus: null, uploads: {