[music] Reorganize importer and fix lookups
This commit is contained in:
@ -1,16 +1,17 @@
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Iterable
|
||||
|
||||
import musicbrainzngs
|
||||
from dateutil.parser import parse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
musicbrainzngs.set_useragent("Vrobbler", "1.0", "help@unbl.ink")
|
||||
|
||||
|
||||
def lookup_album_from_mb(musicbrainz_id: str) -> dict:
|
||||
release_dict = {}
|
||||
|
||||
musicbrainzngs.set_useragent("vrobbler", "0.3.0")
|
||||
release_data = musicbrainzngs.get_release_by_id(
|
||||
musicbrainz_id,
|
||||
includes=["artists", "release-groups", "recordings"],
|
||||
@ -51,7 +52,6 @@ def lookup_album_from_mb(musicbrainz_id: str) -> dict:
|
||||
|
||||
|
||||
def lookup_album_dict_from_mb(release_name: str, artist_name: str) -> dict:
|
||||
musicbrainzngs.set_useragent("vrobbler", "0.3.0")
|
||||
|
||||
top_result = {}
|
||||
|
||||
@ -84,7 +84,6 @@ def lookup_album_dict_from_mb(release_name: str, artist_name: str) -> dict:
|
||||
|
||||
|
||||
def lookup_artist_from_mb(artist_name: str) -> dict:
|
||||
musicbrainzngs.set_useragent("vrobbler", "0.3.0")
|
||||
|
||||
try:
|
||||
top_result = musicbrainzngs.search_artists(artist=artist_name)[
|
||||
@ -104,7 +103,7 @@ def lookup_artist_from_mb(artist_name: str) -> dict:
|
||||
|
||||
|
||||
def lookup_track_from_mb(
|
||||
track_name: str, artist_mb_id: str, album_mb_id: str
|
||||
track_name: str, artist_mb_id: str, album_mb_id: str = ""
|
||||
) -> dict:
|
||||
logger.info(
|
||||
"[lookup_track_from_mb] called",
|
||||
@ -114,7 +113,6 @@ def lookup_track_from_mb(
|
||||
"album_mb_id": album_mb_id,
|
||||
},
|
||||
)
|
||||
musicbrainzngs.set_useragent("vrobbler", "0.3.0")
|
||||
|
||||
try:
|
||||
results = musicbrainzngs.search_recordings(
|
||||
@ -138,3 +136,352 @@ def lookup_track_from_mb(
|
||||
return {}
|
||||
|
||||
return top_result
|
||||
|
||||
|
||||
def get_album_metadata(album_name, artist_name, strict=True) -> dict:
|
||||
"""
|
||||
Get detailed metadata for an album from MusicBrainz.
|
||||
|
||||
:param album_name: Name of the album
|
||||
:param artist_name: Name of the artist
|
||||
:param strict: If True, only exact matches on album and artist (case-insensitive)
|
||||
:return: dict with album metadata, or None if not found
|
||||
"""
|
||||
try:
|
||||
result = musicbrainzngs.search_releases(
|
||||
release=album_name, artist=artist_name, limit=5
|
||||
)
|
||||
|
||||
for release in result.get("release-list", []):
|
||||
title = release["title"]
|
||||
primary_artist = release["artist-credit"][0]["artist"]["name"]
|
||||
|
||||
title_match = title.lower() == album_name.lower()
|
||||
artist_match = primary_artist.lower() == artist_name.lower()
|
||||
|
||||
if not strict or (title_match and artist_match):
|
||||
all_artists = [
|
||||
ac["artist"]["name"]
|
||||
for ac in release["artist-credit"]
|
||||
if isinstance(ac, dict) and "artist" in ac
|
||||
]
|
||||
|
||||
return {
|
||||
"album_title": title,
|
||||
"primary_artist": primary_artist,
|
||||
"all_artists": all_artists,
|
||||
"mbid": release["id"],
|
||||
"release_date": release.get(
|
||||
"date"
|
||||
), # May be partial (e.g., just year)
|
||||
"release_group_mbid": release["release-group"]["id"],
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
print("MusicBrainz error:", e)
|
||||
return {}
|
||||
|
||||
|
||||
def get_recording_mbid_exact(
|
||||
track_title: str, artist_name: str, album_name: str
|
||||
) -> tuple[str, int]:
|
||||
try:
|
||||
result = musicbrainzngs.search_releases(
|
||||
artist=artist_name, release=album_name, limit=1
|
||||
)
|
||||
releases = result.get("release-list", [])
|
||||
if not releases:
|
||||
raise Exception("No releases found")
|
||||
|
||||
release_id = releases[0]["id"]
|
||||
|
||||
release_data = musicbrainzngs.get_release_by_id(
|
||||
release_id, includes=["recordings"]
|
||||
)
|
||||
tracks = release_data["release"]["medium-list"][0]["track-list"]
|
||||
|
||||
for track in tracks:
|
||||
if track["recording"]["title"].lower() == track_title.lower():
|
||||
return track["recording"]["id"], int(
|
||||
track["recording"]["length"]
|
||||
)
|
||||
|
||||
raise Exception("No recording found")
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
print(f"MusicBrainz error: {e}")
|
||||
raise Exception(e)
|
||||
|
||||
|
||||
def get_artist_metadata_extended(artist_name, strict=True):
|
||||
"""
|
||||
Fetch artist metadata including MBID, name, origin, tags, and description.
|
||||
|
||||
:param artist_name: The artist's name
|
||||
:param strict: If True, only return exact name match
|
||||
:return: dict with metadata, or None if not found
|
||||
"""
|
||||
try:
|
||||
# Step 1: Search for artist
|
||||
search_results = musicbrainzngs.search_artists(
|
||||
artist=artist_name, limit=5
|
||||
)
|
||||
for artist in search_results.get("artist-list", []):
|
||||
if not strict or artist["name"].lower() == artist_name.lower():
|
||||
mbid = artist["id"]
|
||||
|
||||
# Step 2: Get detailed info about the artist
|
||||
details = musicbrainzngs.get_artist_by_id(
|
||||
mbid, includes=["tags", "url-rels"]
|
||||
)["artist"]
|
||||
|
||||
begin_date = details.get("life-span", {}).get("begin")
|
||||
area = details.get("area", {}).get("name")
|
||||
disambiguation = details.get("disambiguation")
|
||||
tags = [t["name"] for t in details.get("tag-list", [])]
|
||||
|
||||
# Step 3: Try to find a Wikipedia or Wikidata link
|
||||
description_url = None
|
||||
for rel in details.get("url-relation-list", []):
|
||||
if rel["type"] == "wikipedia":
|
||||
description_url = rel["target"]
|
||||
break
|
||||
elif rel["type"] == "wikidata":
|
||||
description_url = rel["target"]
|
||||
|
||||
return {
|
||||
"mbid": mbid,
|
||||
"name": details["name"],
|
||||
"disambiguation": disambiguation,
|
||||
"begin_date": begin_date,
|
||||
"area": area,
|
||||
"tags": tags,
|
||||
"description_url": description_url, # user can fetch summary if needed
|
||||
}
|
||||
|
||||
return None
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
print("MusicBrainz error:", e)
|
||||
return None
|
||||
|
||||
|
||||
def get_artist_metadata_brief(artist_id):
|
||||
"""Fetch basic artist metadata by MBID."""
|
||||
try:
|
||||
details = musicbrainzngs.get_artist_by_id(
|
||||
artist_id, includes=["tags", "aliases", "url-rels"]
|
||||
)["artist"]
|
||||
|
||||
begin_date = details.get("life-span", {}).get("begin")
|
||||
area = details.get("area", {}).get("name")
|
||||
disambiguation = details.get("disambiguation")
|
||||
tags = [t["name"] for t in details.get("tag-list", [])]
|
||||
|
||||
description_url = None
|
||||
for rel in details.get("url-relation-list", []):
|
||||
if rel["type"] == "wikipedia":
|
||||
description_url = rel["target"]
|
||||
break
|
||||
elif rel["type"] == "wikidata" and not description_url:
|
||||
description_url = rel["target"]
|
||||
|
||||
return {
|
||||
"mbid": artist_id,
|
||||
"name": details["name"],
|
||||
"disambiguation": disambiguation,
|
||||
"begin_date": begin_date,
|
||||
"area": area,
|
||||
"tags": tags,
|
||||
"description_url": description_url,
|
||||
}
|
||||
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
print("MusicBrainz error (artist lookup):", e)
|
||||
return None
|
||||
|
||||
|
||||
def parse_date(date_str):
|
||||
"""Parse MusicBrainz date format into sortable datetime object."""
|
||||
if not date_str:
|
||||
return None
|
||||
for fmt in ("%Y-%m-%d", "%Y-%m", "%Y"):
|
||||
try:
|
||||
return datetime.strptime(date_str, fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def get_album_metadata_with_artist(album_name, artist_name, strict=True):
|
||||
"""
|
||||
Get metadata for the earliest release of an album and its primary artist.
|
||||
|
||||
:param album_name: Album title
|
||||
:param artist_name: Name of the artist
|
||||
:param strict: If True, enforce exact match for album and artist
|
||||
:return: dict with album and primary artist metadata
|
||||
"""
|
||||
try:
|
||||
result = musicbrainzngs.search_releases(
|
||||
release=album_name, artist=artist_name, limit=100
|
||||
)
|
||||
|
||||
query_album = album_name.strip().casefold()
|
||||
query_artist = artist_name.strip().casefold()
|
||||
|
||||
valid_releases = []
|
||||
for release in result.get("release-list", []):
|
||||
release_title = release["title"].strip()
|
||||
primary_artist = release["artist-credit"][0]["artist"]
|
||||
artist_name_actual = primary_artist["name"].strip()
|
||||
|
||||
if strict:
|
||||
if release_title.casefold() != query_album:
|
||||
continue
|
||||
if artist_name_actual.casefold() != query_artist:
|
||||
continue
|
||||
|
||||
release_date = parse_date(release.get("date"))
|
||||
valid_releases.append((release, release_date))
|
||||
|
||||
if not valid_releases:
|
||||
return None
|
||||
|
||||
# Sort releases by earliest release date
|
||||
valid_releases.sort(key=lambda x: x[1] or datetime.max)
|
||||
release, _ = valid_releases[0]
|
||||
|
||||
primary_artist = release["artist-credit"][0]["artist"]
|
||||
all_artists = [
|
||||
ac["artist"]["name"]
|
||||
for ac in release["artist-credit"]
|
||||
if "artist" in ac
|
||||
]
|
||||
|
||||
artist_metadata = get_artist_metadata_brief(primary_artist["id"])
|
||||
|
||||
return {
|
||||
"album_title": release["title"],
|
||||
"primary_artist_name": primary_artist["name"],
|
||||
"all_artists": all_artists,
|
||||
"mbid": release["id"],
|
||||
"release_group_mbid": release["release-group"]["id"],
|
||||
"release_date": release.get("date"),
|
||||
"primary_artist": artist_metadata,
|
||||
}
|
||||
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
print("MusicBrainz error (album lookup):", e)
|
||||
return None
|
||||
|
||||
|
||||
def get_artist_metadata_brief(artist_id):
|
||||
try:
|
||||
details = musicbrainzngs.get_artist_by_id(
|
||||
artist_id, includes=["tags", "aliases", "url-rels"]
|
||||
)["artist"]
|
||||
|
||||
begin_date = details.get("life-span", {}).get("begin")
|
||||
area = details.get("area", {}).get("name")
|
||||
disambiguation = details.get("disambiguation")
|
||||
tags = [t["name"] for t in details.get("tag-list", [])]
|
||||
|
||||
description_url = None
|
||||
for rel in details.get("url-relation-list", []):
|
||||
if rel["type"] == "wikipedia":
|
||||
description_url = rel["target"]
|
||||
break
|
||||
elif rel["type"] == "wikidata" and not description_url:
|
||||
description_url = rel["target"]
|
||||
|
||||
return {
|
||||
"mbid": artist_id,
|
||||
"name": details["name"],
|
||||
"disambiguation": disambiguation,
|
||||
"begin_date": begin_date,
|
||||
"area": area,
|
||||
"tags": tags,
|
||||
"description_url": description_url,
|
||||
}
|
||||
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
print("MusicBrainz error (artist lookup):", e)
|
||||
return None
|
||||
|
||||
|
||||
def get_track_metadata_with_artist(track_title, artist_name, strict=True):
|
||||
"""
|
||||
Get metadata for the earliest-known recording of a track, including artist info.
|
||||
|
||||
:param track_title: Track title
|
||||
:param artist_name: Artist name
|
||||
:param strict: If True, match exactly (case-insensitive)
|
||||
:return: dict with track + release + artist metadata
|
||||
"""
|
||||
try:
|
||||
result = musicbrainzngs.search_recordings(
|
||||
recording=track_title, artist=artist_name, limit=100
|
||||
)
|
||||
|
||||
query_track = track_title.strip().casefold()
|
||||
query_artist = artist_name.strip().casefold()
|
||||
|
||||
valid_candidates = []
|
||||
|
||||
for recording in result.get("recording-list", []):
|
||||
rec_title = recording["title"].strip()
|
||||
artist_credit = recording["artist-credit"][0]["artist"]
|
||||
artist_name_actual = artist_credit["name"].strip()
|
||||
|
||||
if strict:
|
||||
if rec_title.casefold() != query_track:
|
||||
continue
|
||||
if artist_name_actual.casefold() != query_artist:
|
||||
continue
|
||||
|
||||
if "release-list" not in recording:
|
||||
continue
|
||||
|
||||
for release in recording["release-list"]:
|
||||
release_date = parse_date(release.get("date"))
|
||||
if release_date:
|
||||
valid_candidates.append(
|
||||
(recording["id"], release, release_date)
|
||||
)
|
||||
|
||||
if not valid_candidates:
|
||||
return None
|
||||
|
||||
# Pick the earliest release
|
||||
valid_candidates.sort(key=lambda x: x[2])
|
||||
recording_id, release, _ = valid_candidates[0]
|
||||
|
||||
# Fetch full recording info
|
||||
full_recording = musicbrainzngs.get_recording_by_id(
|
||||
recording_id, includes=["artists", "releases"]
|
||||
)["recording"]
|
||||
|
||||
primary_artist = full_recording["artist-credit"][0]["artist"]
|
||||
all_artists = [
|
||||
ac["artist"]["name"]
|
||||
for ac in full_recording["artist-credit"]
|
||||
if "artist" in ac
|
||||
]
|
||||
artist_metadata = get_artist_metadata_brief(primary_artist["id"])
|
||||
|
||||
return {
|
||||
"track_title": full_recording["title"],
|
||||
"length_ms": full_recording.get("length"),
|
||||
"recording_mbid": recording_id,
|
||||
"release_title": release["title"],
|
||||
"release_date": release.get("date"),
|
||||
"release_group_mbid": release["release-group"]["id"],
|
||||
"primary_artist_name": primary_artist["name"],
|
||||
"all_artists": all_artists,
|
||||
"primary_artist": artist_metadata,
|
||||
}
|
||||
|
||||
except musicbrainzngs.WebServiceError as e:
|
||||
print("MusicBrainz error (track lookup):", e)
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user