feat(analyzer): parse comment fields from mp3 files (#3082)

### Description

Upload comments from mp3 files into libretime `comments` and
`description` fields.

**This is a new feature**:

Yes

**I have updated the documentation to reflect these changes**:

No none required

### Testing Notes

**What I did:**

I uploaded tracks that contained comments into LibreTime and checked the
database to ensure that the `comments` and `description` fields were
correctly populated. I then went to the UI and confirmed that the
description field had the MP3 comment in it inside of the metadata
editor. I then uploaded some files that did not have comments to make
sure I did not break any existing functionality.

**How you can replicate my testing:**

Follow the steps in what I did

### **Links**

Fixes #526

---------

Co-authored-by: Kyle Robbertze <paddatrapper@users.noreply.github.com>
This commit is contained in:
dakriy 2024-11-22 10:28:06 -08:00 committed by GitHub
parent ce257a1f35
commit 02a779b413
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 39 deletions

View File

@ -5,10 +5,24 @@ from typing import Any, Dict
import mutagen import mutagen
from libretime_shared.files import compute_md5 from libretime_shared.files import compute_md5
from mutagen.easyid3 import EasyID3
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def flatten(xss):
return [x for xs in xss for x in xs]
def comment_get(id3, _):
comments = [v.text for k, v in id3.items() if "COMM" in k or "comment" in k]
return flatten(comments)
EasyID3.RegisterKey("comment", comment_get)
def analyze_metadata(filepath_: str, metadata: Dict[str, Any]): def analyze_metadata(filepath_: str, metadata: Dict[str, Any]):
""" """
Extract audio metadata from tags embedded in the file using mutagen. Extract audio metadata from tags embedded in the file using mutagen.
@ -71,34 +85,36 @@ def analyze_metadata(filepath_: str, metadata: Dict[str, Any]):
except (AttributeError, KeyError, IndexError): except (AttributeError, KeyError, IndexError):
pass pass
extracted_tags_mapping = { extracted_tags_mapping = [
"title": "track_title", ("title", "track_title"),
"artist": "artist_name", ("artist", "artist_name"),
"album": "album_title", ("album", "album_title"),
"bpm": "bpm", ("bpm", "bpm"),
"composer": "composer", ("composer", "composer"),
"conductor": "conductor", ("conductor", "conductor"),
"copyright": "copyright", ("copyright", "copyright"),
"comment": "comment", ("comment", "comment"),
"encoded_by": "encoder", ("comment", "comments"),
"genre": "genre", ("comment", "description"),
"isrc": "isrc", ("encoded_by", "encoder"),
"label": "label", ("genre", "genre"),
"organization": "label", ("isrc", "isrc"),
# "length": "length", ("label", "label"),
"language": "language", ("organization", "label"),
"last_modified": "last_modified", # ("length", "length"),
"mood": "mood", ("language", "language"),
"bit_rate": "bit_rate", ("last_modified", "last_modified"),
"replay_gain": "replaygain", ("mood", "mood"),
# "tracknumber": "track_number", ("bit_rate", "bit_rate"),
# "track_total": "track_total", ("replay_gain", "replaygain"),
"website": "website", # ("tracknumber", "track_number"),
"date": "year", # ("track_total", "track_total"),
# "mime_type": "mime", ("website", "website"),
} ("date", "year"),
# ("mime_type", "mime"),
]
for extracted_key, metadata_key in extracted_tags_mapping.items(): for extracted_key, metadata_key in extracted_tags_mapping:
try: try:
metadata[metadata_key] = extracted[extracted_key] metadata[metadata_key] = extracted[extracted_key]
if isinstance(metadata[metadata_key], list): if isinstance(metadata[metadata_key], list):

View File

@ -96,12 +96,18 @@ tags = {
"comment": "Test Comment", "comment": "Test Comment",
} }
mp3Tags = {
**tags,
"comments": tags["comment"],
"description": tags["comment"],
}
FILES_TAGGED = [ FILES_TAGGED = [
FixtureMeta( FixtureMeta(
here / "s1-jointstereo-tagged.mp3", here / "s1-jointstereo-tagged.mp3",
{ {
**meta, **meta,
**tags, **mp3Tags,
"bit_rate": approx(128000, abs=1e2), "bit_rate": approx(128000, abs=1e2),
"channels": 2, "channels": 2,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -111,7 +117,7 @@ FILES_TAGGED = [
here / "s1-mono-tagged.mp3", here / "s1-mono-tagged.mp3",
{ {
**meta, **meta,
**tags, **mp3Tags,
"bit_rate": approx(64000, abs=1e2), "bit_rate": approx(64000, abs=1e2),
"channels": 1, "channels": 1,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -121,7 +127,7 @@ FILES_TAGGED = [
here / "s1-stereo-tagged.mp3", here / "s1-stereo-tagged.mp3",
{ {
**meta, **meta,
**tags, **mp3Tags,
"bit_rate": approx(128000, abs=1e2), "bit_rate": approx(128000, abs=1e2),
"channels": 2, "channels": 2,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -151,7 +157,7 @@ FILES_TAGGED = [
here / "s1-mono-tagged.m4a", here / "s1-mono-tagged.m4a",
{ {
**meta, **meta,
**tags, **mp3Tags,
"bit_rate": approx(65000, abs=5e4), "bit_rate": approx(65000, abs=5e4),
"channels": 2, # Weird "channels": 2, # Weird
"mime": "audio/mp4", "mime": "audio/mp4",
@ -161,7 +167,7 @@ FILES_TAGGED = [
here / "s1-stereo-tagged.m4a", here / "s1-stereo-tagged.m4a",
{ {
**meta, **meta,
**tags, **mp3Tags,
"bit_rate": approx(128000, abs=1e5), "bit_rate": approx(128000, abs=1e5),
"channels": 2, "channels": 2,
"mime": "audio/mp4", "mime": "audio/mp4",
@ -228,12 +234,18 @@ tags = {
"comment": "Ł Ą Ż Ę Ć Ń Ś Ź", "comment": "Ł Ą Ż Ę Ć Ń Ś Ź",
} }
mp3Tags = {
**tags,
"comments": tags["comment"],
"description": tags["comment"],
}
FILES_TAGGED += [ FILES_TAGGED += [
FixtureMeta( FixtureMeta(
here / "s1-jointstereo-tagged-utf8.mp3", here / "s1-jointstereo-tagged-utf8.mp3",
{ {
**meta, **meta,
**tags, **mp3Tags,
"bit_rate": approx(128000, abs=1e2), "bit_rate": approx(128000, abs=1e2),
"channels": 2, "channels": 2,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -243,7 +255,7 @@ FILES_TAGGED += [
here / "s1-mono-tagged-utf8.mp3", here / "s1-mono-tagged-utf8.mp3",
{ {
**meta, **meta,
**tags, **mp3Tags,
"bit_rate": approx(64000, abs=1e2), "bit_rate": approx(64000, abs=1e2),
"channels": 1, "channels": 1,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -253,7 +265,7 @@ FILES_TAGGED += [
here / "s1-stereo-tagged-utf8.mp3", here / "s1-stereo-tagged-utf8.mp3",
{ {
**meta, **meta,
**tags, **mp3Tags,
"bit_rate": approx(128000, abs=1e2), "bit_rate": approx(128000, abs=1e2),
"channels": 2, "channels": 2,
"mime": "audio/mp3", "mime": "audio/mp3",
@ -283,7 +295,7 @@ FILES_TAGGED += [
here / "s1-mono-tagged-utf8.m4a", here / "s1-mono-tagged-utf8.m4a",
{ {
**meta, **meta,
**tags, **mp3Tags,
"bit_rate": approx(65000, abs=5e4), "bit_rate": approx(65000, abs=5e4),
"channels": 2, # Weird "channels": 2, # Weird
"mime": "audio/mp4", "mime": "audio/mp4",
@ -293,7 +305,7 @@ FILES_TAGGED += [
here / "s1-stereo-tagged-utf8.m4a", here / "s1-stereo-tagged-utf8.m4a",
{ {
**meta, **meta,
**tags, **mp3Tags,
"bit_rate": approx(128000, abs=1e5), "bit_rate": approx(128000, abs=1e5),
"channels": 2, "channels": 2,
"mime": "audio/mp4", "mime": "audio/mp4",

View File

@ -27,8 +27,8 @@ def test_analyze_metadata(filepath: Path, metadata: dict):
del metadata["length"] del metadata["length"]
del found["length"] del found["length"]
# mp3,ogg,flac files does not support comments yet # ogg,flac files does not support comments yet
if not filepath.suffix == ".m4a": if not filepath.suffix == ".m4a" and not filepath.suffix == ".mp3":
if "comment" in metadata: if "comment" in metadata:
del metadata["comment"] del metadata["comment"]