This commit is contained in:
2026-01-11 18:56:26 -08:00
parent e70db8d8a6
commit 6076ea307b
7 changed files with 773 additions and 1476 deletions

View File

@@ -13,13 +13,15 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple
from urllib.parse import urlparse
from API.Tidal import (
HifiApiClient,
Tidal as TidalApiClient,
build_track_tags,
coerce_duration_seconds,
extract_artists,
stringify,
)
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
from ProviderCore.inline_utils import collect_choice
from cmdlet._shared import get_field
from SYS import pipeline as pipeline_context
from SYS.logger import debug, log
@@ -64,7 +66,9 @@ def _format_total_seconds(seconds: Any) -> str:
return f"{mins}:{secs:02d}"
class Tidal(Provider):
class HIFI(Provider):
PROVIDER_NAME = "hifi"
TABLE_AUTO_STAGES = {
"hifi.track": ["download-file"],
@@ -97,7 +101,7 @@ class Tidal(Provider):
self.api_timeout = float(self.config.get("timeout", 10.0))
except Exception:
self.api_timeout = 10.0
self.api_clients = [HifiApiClient(base_url=url, timeout=self.api_timeout) for url in self.api_urls]
self.api_clients = [TidalApiClient(base_url=url, timeout=self.api_timeout) for url in self.api_urls]
def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]:
normalized, parsed = parse_inline_query_arguments(query)
@@ -281,7 +285,7 @@ class Tidal(Provider):
if isinstance(detail, dict):
title = self._stringify(detail.get("title")) or title
return SearchResult(
res = SearchResult(
table="hifi.track",
title=title,
path=f"hifi://track/{track_id}",
@@ -291,6 +295,12 @@ class Tidal(Provider):
full_metadata=dict(detail) if isinstance(detail, dict) else {},
selection_args=["-url", f"hifi://track/{track_id}"],
)
if isinstance(detail, dict):
try:
res.tag = self._build_track_tags(detail)
except Exception:
pass
return res
def _extract_artist_selection_context(self, selected_items: List[Any]) -> List[Tuple[int, str]]:
contexts: List[Tuple[int, str]] = []
@@ -1130,25 +1140,36 @@ class Tidal(Provider):
md = dict(getattr(result, "full_metadata") or {})
track_id = self._extract_track_id_from_result(result)
if track_id:
debug(f"[hifi] download: track_id={track_id}, manifest_present={bool(md.get('manifest'))}, tag_count={len(result.tag) if result.tag else 0}")
# Enrichment: fetch full metadata if manifest or detailed info (like tags/lyrics) is missing.
# We check for 'manifest' because it's required for DASH playback.
# We also check for lyrics/subtitles to ensure they are available for add-file.
has_lyrics = bool(md.get("_tidal_lyrics_subtitles")) or bool(md.get("lyrics"))
if track_id and (not md.get("manifest") or not md.get("artist") or len(result.tag or []) <= 1 or not has_lyrics):
debug(f"[hifi] Enriching track data (reason: manifest={not md.get('manifest')}, lyrics={not has_lyrics}, tags={len(result.tag or [])})")
# Multi-part enrichment from API: metadata, tags, and lyrics.
full_data = self._fetch_all_track_data(track_id)
debug(f"[hifi] download: enrichment full_data present={bool(full_data)}")
if isinstance(full_data, dict):
# 1. Update metadata
api_md = full_data.get("metadata")
if isinstance(api_md, dict):
debug(f"[hifi] download: updating metadata with {len(api_md)} keys")
md.update(api_md)
# 2. Update tags (re-sync result.tag so cmdlet sees them)
api_tags = full_data.get("tags")
debug(f"[hifi] download: enrichment tags={api_tags}")
if isinstance(api_tags, list) and api_tags:
result.tag = set(api_tags)
# 3. Handle lyrics
lyrics = full_data.get("lyrics")
if isinstance(lyrics, dict) and lyrics:
md.setdefault("lyrics", lyrics)
subtitles = lyrics.get("subtitles")
lyrics_dict = full_data.get("lyrics")
if isinstance(lyrics_dict, dict) and lyrics_dict:
md.setdefault("lyrics", lyrics_dict)
subtitles = lyrics_dict.get("subtitles")
if isinstance(subtitles, str) and subtitles.strip():
md["_tidal_lyrics_subtitles"] = subtitles.strip()
@@ -1328,7 +1349,7 @@ class Tidal(Provider):
return False, None
def _get_api_client_for_base(self, base_url: str) -> Optional[HifiApiClient]:
def _get_api_client_for_base(self, base_url: str) -> Optional[TidalApiClient]:
base = base_url.rstrip("/")
for client in self.api_clients:
if getattr(client, "base_url", "").rstrip("/") == base:
@@ -1739,6 +1760,10 @@ class Tidal(Provider):
or payload.get("path")
or payload.get("url")
)
# Guard against method binding (e.g. str.title) being returned by getattr(str, "title")
if callable(title):
title = None
if not title:
title = f"Track {track_id}"
path = (
@@ -1983,12 +2008,6 @@ class Tidal(Provider):
return True
# Optimization: If we are selecting tracks, do NOT force a "Detail View" (resolving manifest) here.
# This allows batch selection to flow immediately to `download-file` (via TABLE_AUTO_STAGES)
# or other downstream cmdlets. The download logic (HIFI.download) handles manifest resolution locally.
if table_type == "hifi.track" or (is_generic_hifi and any(str(get_field(i, "path")).startswith("hifi://track/") for i in selected_items)):
return False
contexts = self._extract_track_selection_context(selected_items)
try:
debug(f"[hifi.selector] track contexts={len(contexts)}")

View File

@@ -501,6 +501,26 @@ class InternetArchive(Provider):
"internetarchive.formats": ["download-file"],
}
def maybe_show_picker(
self,
*,
url: str,
item: Optional[Any] = None,
parsed: Dict[str, Any],
config: Dict[str, Any],
quiet_mode: bool,
) -> Optional[int]:
"""Generic hook for download-file to show a selection table for IA items."""
from cmdlet._shared import get_field as sh_get_field
return maybe_show_formats_table(
raw_urls=[url] if url else [],
piped_items=[item] if item else [],
parsed=parsed,
config=config,
quiet_mode=quiet_mode,
get_field=sh_get_field,
)
def __init__(self, config: Optional[Dict[str, Any]] = None):
super().__init__(config)
conf = _pick_provider_config(self.config)