[music] Add mgmt command to see mismatched metadata
Some checks failed
build / test (push) Has been cancelled
Some checks failed
build / test (push) Has been cancelled
This commit is contained in:
38
PROJECT.org
38
PROJECT.org
@ -88,7 +88,7 @@ fetching and simple saving.
|
||||
*** Metadata sources
|
||||
**** Scraper
|
||||
|
||||
* Backlog [1/16] :vrobbler:project:personal:
|
||||
* Backlog [2/16] :vrobbler:project:personal:
|
||||
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles:
|
||||
:PROPERTIES:
|
||||
:ID: 702462cf-d54b-48c6-8a7c-78b8de751deb
|
||||
@ -500,15 +500,6 @@ needed import celery task. This is how the WebDAV celery task currently works.
|
||||
This would also be an opporunity to clean up the code around WebDAV imports
|
||||
and make them more re-usable for other import services.
|
||||
|
||||
** TODO [#A] Generate a report of tracks with mistmatched metadata :music:tracks:metadata:
|
||||
:PROPERTIES:
|
||||
:ID: 684b8cd2-a3c1-4995-ba9e-7abdb02c37f2
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
|
||||
We should have a management command that outputs a CSV file of track IDs where the log["raw_data"] artist
|
||||
does not match the actual track artist. Specific cases are
|
||||
|
||||
** TODO [#A] Add an exception list of artists as a constant that are exempted from splitting :music:artists:metadata:
|
||||
:PROPERTIES:
|
||||
@ -521,6 +512,7 @@ Certain artists like "Simon & Garfunkel" are actually one artist. While we don't
|
||||
tracks into featured artists, we should have a "LITERAL_ARTIST_TITLES" constant that can have exceptions like
|
||||
this put into it and then we stop trying to pull the artist apart when we run into it.
|
||||
|
||||
|
||||
** TODO [#A] Before enriching anything, trust the POST data :feature:scrobbles:metadata:
|
||||
:PROPERTIES:
|
||||
:ID: db6b05f8-09f4-49f5-9838-fbacc9fe9cd0
|
||||
@ -542,6 +534,31 @@ async with the POST data stored in the log["raw_data"] and used by the celery en
|
||||
to go try to enrich the media instance. Should this enrichment fail, tag the scrobble as "enrichment-failed"
|
||||
log a warning and move on.
|
||||
|
||||
|
||||
** DONE [#A] Generate a report of tracks with mistmatched metadata :music:tracks:metadata:
|
||||
:PROPERTIES:
|
||||
:ID: 684b8cd2-a3c1-4995-ba9e-7abdb02c37f2
|
||||
:END:
|
||||
|
||||
*** Description
|
||||
|
||||
We should have a management command that outputs a CSV file of track IDs where
|
||||
the log["raw_data"]["Artist"] (for Jellyfin) or log["raw_data"]["artist"]
|
||||
(mopidy) value does not match the Track.artists names.
|
||||
|
||||
And we should see the same thing for albums (log["raw_data"]["Album"] or
|
||||
log["raw_data]["album"]).
|
||||
|
||||
It should output the fields "track_id", "track_artist_name", "track_album_name",
|
||||
"raw_artist", "raw_album", "source"
|
||||
|
||||
Where source is either Jellyfin or Mopidy based on the keys.
|
||||
|
||||
Put the file /tmp/metadata-report.csv by default and overwrite exsiting reports.
|
||||
|
||||
The command should also accept a file-path to overide this default.
|
||||
|
||||
|
||||
** DONE [#A] Date parsing failing in eBird imports :birds:ebird:importers:
|
||||
:PROPERTIES:
|
||||
:ID: 72e0254f-39d8-4843-9857-623e0362d77e
|
||||
@ -565,6 +582,7 @@ We should also add a "error_log" to the importers so that errors tha occur are
|
||||
surfaced, even when 0 successful files were processed. And we should make sure
|
||||
all importers do this as well.
|
||||
|
||||
|
||||
* Version 48.0 [2/2]
|
||||
** DONE [#B] Show team or player images on sport detail and scrobble detail :sports:templates:
|
||||
:PROPERTIES:
|
||||
|
||||
@ -0,0 +1,127 @@
|
||||
import csv
|
||||
import logging
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_source(raw_data):
|
||||
if "Artist" in raw_data:
|
||||
return "Jellyfin"
|
||||
if "artist" in raw_data:
|
||||
return "Mopidy"
|
||||
return None
|
||||
|
||||
|
||||
def _get_raw_values(raw_data, source):
|
||||
if source == "Jellyfin":
|
||||
return raw_data.get("Artist", ""), raw_data.get("Album", "")
|
||||
return raw_data.get("artist", ""), raw_data.get("album", "")
|
||||
|
||||
|
||||
def _normalize(name):
|
||||
return name.strip().casefold()
|
||||
|
||||
|
||||
def _artist_mismatch(raw_artist, track_artist_names):
|
||||
if not raw_artist or not track_artist_names:
|
||||
return False
|
||||
track_names = [_normalize(n) for n in track_artist_names.split(" / ")]
|
||||
raw = _normalize(raw_artist)
|
||||
if raw in track_names:
|
||||
return False
|
||||
if raw == _normalize(track_artist_names):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _album_mismatch(raw_album, track_album_name):
|
||||
if not raw_album or not track_album_name:
|
||||
return False
|
||||
return _normalize(raw_album) != _normalize(track_album_name)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Outputs a CSV of track IDs where raw metadata from scrobble logs "
|
||||
"does not match the track's stored artists or album"
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--file-path",
|
||||
type=str,
|
||||
default="/tmp/metadata-report.csv",
|
||||
help="Output CSV file path (default: /tmp/metadata-report.csv)",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
file_path = options["file_path"]
|
||||
|
||||
qs = (
|
||||
Scrobble.objects.filter(media_type=Scrobble.MediaType.TRACK)
|
||||
.exclude(log__isnull=True)
|
||||
.exclude(log={})
|
||||
.select_related("track__album")
|
||||
.prefetch_related("track__artists")
|
||||
.iterator()
|
||||
)
|
||||
|
||||
rows = []
|
||||
for scrobble in qs:
|
||||
track = scrobble.track
|
||||
if not track:
|
||||
continue
|
||||
|
||||
raw_data = scrobble.log.get("raw_data")
|
||||
if not raw_data:
|
||||
continue
|
||||
|
||||
source = _get_source(raw_data)
|
||||
if not source:
|
||||
continue
|
||||
|
||||
raw_artist, raw_album = _get_raw_values(raw_data, source)
|
||||
if not raw_artist and not raw_album:
|
||||
continue
|
||||
|
||||
track_artist_names = " / ".join(
|
||||
track.artists.all().values_list("name", flat=True)
|
||||
)
|
||||
track_album_name = track.album.name if track.album else ""
|
||||
|
||||
if _artist_mismatch(raw_artist, track_artist_names) or _album_mismatch(
|
||||
raw_album, track_album_name
|
||||
):
|
||||
rows.append(
|
||||
{
|
||||
"track_id": track.id,
|
||||
"track_artist_name": track_artist_names,
|
||||
"track_album_name": track_album_name,
|
||||
"raw_artist": raw_artist,
|
||||
"raw_album": raw_album,
|
||||
"source": source,
|
||||
}
|
||||
)
|
||||
|
||||
fieldnames = [
|
||||
"track_id",
|
||||
"track_artist_name",
|
||||
"track_album_name",
|
||||
"raw_artist",
|
||||
"raw_album",
|
||||
"source",
|
||||
]
|
||||
with open(file_path, "w", newline="") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Wrote {len(rows)} mismatched track(s) to {file_path}"
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user