f
This commit is contained in:
166
Provider/HIFI.py
166
Provider/HIFI.py
@@ -12,19 +12,18 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from API.hifi import HifiApiClient
|
||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from Provider.tidal_shared import (
|
||||
from API.Tidal import (
|
||||
HifiApiClient,
|
||||
build_track_tags,
|
||||
coerce_duration_seconds,
|
||||
extract_artists,
|
||||
stringify,
|
||||
)
|
||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from SYS import pipeline as pipeline_context
|
||||
from SYS.logger import debug, log
|
||||
|
||||
URL_API = (
|
||||
"https://tidal-api.binimum.org",
|
||||
"https://triton.squid.wtf",
|
||||
"https://wolf.qqdl.site",
|
||||
"https://maus.qqdl.site",
|
||||
@@ -33,6 +32,7 @@ URL_API = (
|
||||
"https://hund.qqdl.site",
|
||||
"https://tidal.kinoplus.online",
|
||||
"https://tidal-api.binimum.org",
|
||||
"https://tidal-api.binimum.org",
|
||||
)
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ def _format_total_seconds(seconds: Any) -> str:
|
||||
return f"{mins}:{secs:02d}"
|
||||
|
||||
|
||||
class HIFI(Provider):
|
||||
class Tidal(Provider):
|
||||
|
||||
TABLE_AUTO_STAGES = {
|
||||
"hifi.track": ["download-file"],
|
||||
@@ -237,6 +237,16 @@ class HIFI(Provider):
|
||||
except Exception:
|
||||
return "", None
|
||||
|
||||
scheme = str(parsed.scheme or "").lower().strip()
|
||||
if scheme == "hifi":
|
||||
# Handle hifi://view/id
|
||||
view = str(parsed.netloc or "").lower().strip()
|
||||
path_parts = [p for p in (parsed.path or "").split("/") if p]
|
||||
identifier = None
|
||||
if path_parts:
|
||||
identifier = self._parse_int(path_parts[0])
|
||||
return view, identifier
|
||||
|
||||
parts = [segment for segment in (parsed.path or "").split("/") if segment]
|
||||
if not parts:
|
||||
return "", None
|
||||
@@ -248,7 +258,7 @@ class HIFI(Provider):
|
||||
return "", None
|
||||
|
||||
view = parts[idx].lower()
|
||||
if view not in {"album", "track"}:
|
||||
if view not in {"album", "track", "artist"}:
|
||||
return "", None
|
||||
|
||||
for segment in parts[idx + 1:]:
|
||||
@@ -279,6 +289,7 @@ class HIFI(Provider):
|
||||
annotations=["tidal", "track"],
|
||||
media_kind="audio",
|
||||
full_metadata=dict(detail) if isinstance(detail, dict) else {},
|
||||
selection_args=["-url", f"hifi://track/{track_id}"],
|
||||
)
|
||||
|
||||
def _extract_artist_selection_context(self, selected_items: List[Any]) -> List[Tuple[int, str]]:
|
||||
@@ -802,6 +813,11 @@ class HIFI(Provider):
|
||||
full_metadata=md,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def url_patterns() -> List[str]:
|
||||
"""Return URL prefixes handled by this provider."""
|
||||
return ["hifi://", "tidal.com"]
|
||||
|
||||
@staticmethod
|
||||
def _find_ffmpeg() -> Optional[str]:
|
||||
exe = shutil.which("ffmpeg")
|
||||
@@ -1113,34 +1129,28 @@ class HIFI(Provider):
|
||||
if isinstance(getattr(result, "full_metadata", None), dict):
|
||||
md = dict(getattr(result, "full_metadata") or {})
|
||||
|
||||
if not md.get("manifest"):
|
||||
track_id = self._extract_track_id_from_result(result)
|
||||
if track_id:
|
||||
detail = self._fetch_track_details(track_id)
|
||||
if isinstance(detail, dict) and detail:
|
||||
try:
|
||||
md.update(detail)
|
||||
except Exception:
|
||||
md = detail
|
||||
track_id = self._extract_track_id_from_result(result)
|
||||
if track_id:
|
||||
# Multi-part enrichment from API: metadata, tags, and lyrics.
|
||||
full_data = self._fetch_all_track_data(track_id)
|
||||
if isinstance(full_data, dict):
|
||||
# 1. Update metadata
|
||||
api_md = full_data.get("metadata")
|
||||
if isinstance(api_md, dict):
|
||||
md.update(api_md)
|
||||
|
||||
# 2. Update tags (re-sync result.tag so cmdlet sees them)
|
||||
api_tags = full_data.get("tags")
|
||||
if isinstance(api_tags, list) and api_tags:
|
||||
result.tag = set(api_tags)
|
||||
|
||||
# Best-effort: fetch synced lyric subtitles for MPV (LRC).
|
||||
try:
|
||||
track_id_for_lyrics = self._extract_track_id_from_result(result)
|
||||
except Exception:
|
||||
track_id_for_lyrics = None
|
||||
if track_id_for_lyrics and not md.get("_tidal_lyrics_subtitles"):
|
||||
lyr = self._fetch_track_lyrics(track_id_for_lyrics)
|
||||
if isinstance(lyr, dict) and lyr:
|
||||
try:
|
||||
md.setdefault("lyrics", lyr)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
subtitles = lyr.get("subtitles")
|
||||
# 3. Handle lyrics
|
||||
lyrics = full_data.get("lyrics")
|
||||
if isinstance(lyrics, dict) and lyrics:
|
||||
md.setdefault("lyrics", lyrics)
|
||||
subtitles = lyrics.get("subtitles")
|
||||
if isinstance(subtitles, str) and subtitles.strip():
|
||||
md["_tidal_lyrics_subtitles"] = subtitles.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Ensure downstream cmdlets see our enriched metadata.
|
||||
try:
|
||||
@@ -1665,6 +1675,7 @@ class HIFI(Provider):
|
||||
tag=tags,
|
||||
columns=columns,
|
||||
full_metadata=full_md,
|
||||
selection_args=["-url", path],
|
||||
)
|
||||
if url_value:
|
||||
try:
|
||||
@@ -1739,66 +1750,34 @@ class HIFI(Provider):
|
||||
return contexts
|
||||
|
||||
def _fetch_track_details(self, track_id: int) -> Optional[Dict[str, Any]]:
|
||||
if track_id <= 0:
|
||||
return None
|
||||
|
||||
info_data = self._fetch_track_info(track_id)
|
||||
|
||||
for base in self.api_urls:
|
||||
endpoint = f"{base.rstrip('/')}/track/"
|
||||
try:
|
||||
client = self._get_api_client_for_base(base)
|
||||
payload = client.track(track_id) if client else None
|
||||
data = payload.get("data") if isinstance(payload, dict) else None
|
||||
if isinstance(data, dict):
|
||||
merged: Dict[str, Any] = {}
|
||||
if isinstance(info_data, dict):
|
||||
merged.update(info_data)
|
||||
merged.update(data)
|
||||
return merged
|
||||
except Exception as exc:
|
||||
log(f"[hifi] Track lookup failed for {endpoint}: {exc}", file=sys.stderr)
|
||||
continue
|
||||
return None
|
||||
"""Legacy wrapper returning just metadata from the consolidated API call."""
|
||||
res = self._fetch_all_track_data(track_id)
|
||||
return res.get("metadata") if res else None
|
||||
|
||||
def _fetch_track_info(self, track_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Legacy wrapper; now part of _fetch_all_track_data."""
|
||||
return self._fetch_track_details(track_id)
|
||||
|
||||
def _fetch_all_track_data(self, track_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch full track details including metadata, tags, and lyrics from the API."""
|
||||
if track_id <= 0:
|
||||
return None
|
||||
for base in self.api_urls:
|
||||
endpoint = f"{base.rstrip('/')}/info/"
|
||||
try:
|
||||
client = self._get_api_client_for_base(base)
|
||||
payload = client.info(track_id) if client else None
|
||||
data = payload.get("data") if isinstance(payload, dict) else None
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
if not client:
|
||||
continue
|
||||
# This method in the API client handles merging info+track and building tags.
|
||||
return client.get_full_track_metadata(track_id)
|
||||
except Exception as exc:
|
||||
debug(f"[hifi] Info lookup failed for {endpoint}: {exc}")
|
||||
debug(f"[hifi] Full track fetch failed for {base}: {exc}")
|
||||
continue
|
||||
return None
|
||||
|
||||
def _fetch_track_lyrics(self, track_id: int) -> Optional[Dict[str, Any]]:
|
||||
if track_id <= 0:
|
||||
return None
|
||||
for base in self.api_urls:
|
||||
endpoint = f"{base.rstrip('/')}/lyrics/"
|
||||
try:
|
||||
client = self._get_api_client_for_base(base)
|
||||
payload = client.lyrics(track_id) if client else None
|
||||
if not isinstance(payload, dict):
|
||||
continue
|
||||
|
||||
lyrics_obj = payload.get("lyrics")
|
||||
if isinstance(lyrics_obj, dict) and lyrics_obj:
|
||||
return lyrics_obj
|
||||
|
||||
data_obj = payload.get("data")
|
||||
if isinstance(data_obj, dict) and data_obj:
|
||||
return data_obj
|
||||
except Exception as exc:
|
||||
debug(f"[hifi] Lyrics lookup failed for {endpoint}: {exc}")
|
||||
continue
|
||||
return None
|
||||
"""Legacy wrapper returning just lyrics from the consolidated API call."""
|
||||
res = self._fetch_all_track_data(track_id)
|
||||
return res.get("lyrics") if res else None
|
||||
|
||||
def _build_track_columns(self, detail: Dict[str, Any], track_id: int) -> List[Tuple[str, str]]:
|
||||
values: List[Tuple[str, str]] = [
|
||||
@@ -1816,8 +1795,8 @@ class HIFI(Provider):
|
||||
def _build_track_tags(self, metadata: Dict[str, Any]) -> set[str]:
|
||||
return build_track_tags(metadata)
|
||||
|
||||
@staticmethod
|
||||
def selection_auto_stage(
|
||||
self,
|
||||
table_type: str,
|
||||
stage_args: Optional[Sequence[str]] = None,
|
||||
) -> Optional[List[str]]:
|
||||
@@ -2004,17 +1983,11 @@ class HIFI(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)):
|
||||
try:
|
||||
meta = (
|
||||
current_table.get_table_metadata()
|
||||
if current_table is not None and hasattr(current_table, "get_table_metadata")
|
||||
else {}
|
||||
)
|
||||
except Exception:
|
||||
meta = {}
|
||||
if isinstance(meta, dict) and meta.get("resolved_manifest"):
|
||||
return False
|
||||
return False
|
||||
|
||||
contexts = self._extract_track_selection_context(selected_items)
|
||||
try:
|
||||
@@ -2047,17 +2020,7 @@ class HIFI(Provider):
|
||||
pass
|
||||
results_payload: List[Dict[str, Any]] = []
|
||||
for track_id, title, path, detail in track_details:
|
||||
# Decode the DASH MPD manifest to a local file and use it as the selectable/playable path.
|
||||
try:
|
||||
from cmdlet._shared import resolve_tidal_manifest_path
|
||||
|
||||
manifest_path = resolve_tidal_manifest_path(
|
||||
{"full_metadata": detail, "path": f"hifi://track/{track_id}"}
|
||||
)
|
||||
except Exception:
|
||||
manifest_path = None
|
||||
|
||||
resolved_path = str(manifest_path) if manifest_path else f"hifi://track/{track_id}"
|
||||
resolved_path = f"hifi://track/{track_id}"
|
||||
|
||||
artists = self._extract_artists(detail)
|
||||
artist_display = ", ".join(artists) if artists else ""
|
||||
@@ -2086,6 +2049,7 @@ class HIFI(Provider):
|
||||
columns=columns,
|
||||
full_metadata=detail,
|
||||
tag=tags,
|
||||
selection_args=["-url", resolved_path],
|
||||
)
|
||||
if url_value:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user