your commit message

This commit is contained in:
2026-02-25 17:35:38 -08:00
parent 39a84b3274
commit 834be06ab9
12 changed files with 517 additions and 543 deletions

View File

@@ -6,7 +6,7 @@ import subprocess
import time
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Callable, Dict, List, Optional, Tuple
from urllib.parse import urlparse
from API.Tidal import (
@@ -457,15 +457,32 @@ class Tidal(Provider):
if idx >= len(parts):
return "", None
view = parts[idx].lower()
if view not in {"album", "track", "artist"}:
# Scan ALL (view, id) pairs in the path, e.g.
# /album/634516/track/634519 → [("album", 634516), ("track", 634519)]
# When multiple views are present, prefer the more specific one:
# track > album > artist
_VIEW_PRIORITY = {"track": 2, "album": 1, "artist": 0}
_VALID_VIEWS = set(_VIEW_PRIORITY)
found: dict[str, int] = {}
i = idx
while i < len(parts):
v = parts[i].lower()
if v in _VALID_VIEWS:
# Look ahead for the first integer following this view keyword
for j in range(i + 1, len(parts)):
cand = self._parse_int(parts[j])
if cand is not None:
found[v] = cand
i = j # advance past the id
break
i += 1
if not found:
return "", None
for segment in parts[idx + 1:]:
identifier = self._parse_int(segment)
if identifier is not None:
return view, identifier
return view, None
# Return the highest-priority view that was found
best_view = max(found, key=lambda v: _VIEW_PRIORITY.get(v, -1))
return best_view, found[best_view]
def _track_detail_to_result(self, detail: Optional[Dict[str, Any]], track_id: int) -> SearchResult:
if isinstance(detail, dict):
@@ -700,7 +717,9 @@ class Tidal(Provider):
def _tracks_for_album(self, *, album_id: Optional[int], album_title: str, artist_name: str = "", limit: int = 200) -> List[SearchResult]:
title = str(album_title or "").strip()
if not title:
# When album_id is provided the /album/ endpoint can resolve tracks directly —
# no title is required. Only bail out early when we have neither.
if not title and not album_id:
return []
def _norm_album(text: str) -> str:
@@ -1351,6 +1370,9 @@ class Tidal(Provider):
subtitles = lyrics.get("subtitles")
if isinstance(subtitles, str) and subtitles.strip():
md["_tidal_lyrics_subtitles"] = subtitles.strip()
# Generic key consumed by download-file._emit_local_file to
# persist lyrics as a store note without provider-specific logic.
md["_notes"] = {"lyric": subtitles.strip()}
# Ensure downstream cmdlets see our enriched metadata.
try:
@@ -1523,6 +1545,19 @@ class Tidal(Provider):
if not identifier:
return False, None
# In download-file flows, return a provider action so the cmdlet can
# invoke this provider's bulk download hook and emit each track.
if output_dir is not None:
return True, {
"action": "download_items",
"path": f"tidal://album/{identifier}",
"title": f"Album {identifier}",
"metadata": {
"album_id": identifier,
},
"media_kind": "audio",
}
try:
track_results = self._tracks_for_album(
album_id=identifier,
@@ -1562,6 +1597,76 @@ class Tidal(Provider):
return False, None
def download_items(
self,
result: SearchResult,
output_dir: Path,
*,
emit: Callable[[Path, str, str, Dict[str, Any]], None],
progress: Any,
quiet_mode: bool,
path_from_result: Callable[[Any], Path],
config: Optional[Dict[str, Any]] = None,
) -> int:
_ = progress
_ = quiet_mode
_ = path_from_result
_ = config
metadata = getattr(result, "full_metadata", None)
md: Dict[str, Any] = dict(metadata) if isinstance(metadata, dict) else {}
album_id = self._parse_int(md.get("album_id") or md.get("albumId") or md.get("id"))
album_title = stringify(md.get("album_title") or md.get("title") or md.get("album"))
artist_name = stringify(md.get("artist_name") or md.get("_artist_name") or md.get("artist"))
if not artist_name:
artist_obj = md.get("artist")
if isinstance(artist_obj, dict):
artist_name = stringify(artist_obj.get("name"))
path_text = stringify(getattr(result, "path", ""))
if path_text:
view, identifier = self._parse_tidal_url(path_text)
if view == "album" and not album_id:
album_id = identifier
if not album_id:
return 0
try:
track_results = self._tracks_for_album(
album_id=album_id,
album_title=album_title,
artist_name=artist_name,
limit=500,
)
except Exception:
return 0
if not track_results:
return 0
downloaded_count = 0
for track_result in track_results:
try:
downloaded = self.download(track_result, output_dir)
except Exception:
downloaded = None
if not downloaded:
continue
tr_md_raw = getattr(track_result, "full_metadata", None)
tr_md = dict(tr_md_raw) if isinstance(tr_md_raw, dict) else {}
source = stringify(tr_md.get("url") or getattr(track_result, "path", ""))
relpath = str(downloaded.name)
emit(downloaded, source, relpath, tr_md)
downloaded_count += 1
return downloaded_count
def _get_api_client_for_base(self, base_url: str) -> Optional[TidalApiClient]:
base = base_url.rstrip("/")
for client in self.api_clients: