feat: auth: add auth module

This commit is contained in:
2026-04-05 02:36:10 +02:00
parent 1ed51fdbdb
commit 449952c6c1
19 changed files with 711 additions and 375 deletions
+16 -7
View File
@@ -10,24 +10,33 @@ from .base import BaseEnricher
from .audio_tag import AudioTagEnricher
from .file_name import FileNameEnricher
from .musixmatch import MusixmatchSpotifyEnricher
from ..authenticators import BaseAuthenticator, MusixmatchAuthenticator
from ..models import TrackMeta
# Enrichers run in order; earlier ones have higher priority.
# There are only a few of them, so we can just call them sequentially without worrying about async concurrency or batching.
_ENRICHERS: list[BaseEnricher] = [
AudioTagEnricher(),
FileNameEnricher(),
MusixmatchSpotifyEnricher(),
]
async def enrich_track(track: TrackMeta) -> TrackMeta:
def create_enrichers(
authenticators: dict[str, BaseAuthenticator],
) -> list[BaseEnricher]:
"""Instantiate all enrichers."""
mxm_auth = authenticators["musixmatch"]
assert isinstance(mxm_auth, MusixmatchAuthenticator)
return [
AudioTagEnricher(),
FileNameEnricher(),
MusixmatchSpotifyEnricher(mxm_auth),
]
async def enrich_track(track: TrackMeta, enrichers: list[BaseEnricher]) -> TrackMeta:
"""Run all enrichers and return a track with missing fields filled in.
Each enricher sees the cumulative state (earlier enrichers' results
are already applied). A field is only set if it is currently None.
"""
for enricher in _ENRICHERS:
for enricher in enrichers:
try:
# Skip if all provided fields are already filled
if all(
+13 -24
View File
@@ -5,30 +5,21 @@ Description: Musixmatch metadata enricher (matcher.track.get by Spotify track ID
"""
from typing import Optional
from urllib.parse import urlencode
import httpx
from loguru import logger
from .base import BaseEnricher
from ..authenticators.musixmatch import MusixmatchAuthenticator
from ..models import TrackMeta
from ..config import (
HTTP_TIMEOUT,
MUSIXMATCH_TRACK_MATCH_URL,
MUSIXMATCH_USERTOKEN,
)
_MXM_HEADERS = {"Cookie": "x-mxm-token-guid="}
_MXM_TRACK_MATCH_BASE_PARAMS = {
"format": "json",
"app_id": "web-desktop-app-v1.0",
"usertoken": MUSIXMATCH_USERTOKEN,
}
from ..config import MUSIXMATCH_TRACK_MATCH_URL
class MusixmatchSpotifyEnricher(BaseEnricher):
"""Fill title, artist, album, and length from Musixmatch using Spotify track ID."""
def __init__(self, auth: MusixmatchAuthenticator) -> None:
self.auth = auth
@property
def name(self) -> str:
return "musixmatch"
@@ -38,25 +29,23 @@ class MusixmatchSpotifyEnricher(BaseEnricher):
return {"title", "artist", "album", "length"}
async def enrich(self, track: TrackMeta) -> Optional[dict]:
if not track.trackid or not MUSIXMATCH_USERTOKEN:
if not track.trackid:
return None
params = {
**_MXM_TRACK_MATCH_BASE_PARAMS,
"track_spotify_id": track.trackid,
}
url = f"{MUSIXMATCH_TRACK_MATCH_URL}?{urlencode(params)}"
logger.debug(f"Musixmatch enricher: looking up trackid={track.trackid}")
try:
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
resp = await client.get(url, headers=_MXM_HEADERS)
resp.raise_for_status()
data = resp.json()
data = await self.auth.get_json(
MUSIXMATCH_TRACK_MATCH_URL,
{"track_spotify_id": track.trackid},
)
except Exception as e:
logger.warning(f"Musixmatch enricher: request failed: {e}")
return None
if data is None:
return None
body = data.get("message", {}).get("body")
t = body.get("track") if isinstance(body, dict) else None
if not isinstance(t, dict):