your commit message
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user