This commit is contained in:
2026-02-11 19:06:38 -08:00
parent 1d0de1118b
commit ba623cb992
20 changed files with 848 additions and 247 deletions

View File

@@ -1222,7 +1222,7 @@ class HIFI(Provider):
from API.httpx_shared import get_shared_httpx_client
timeout_val = float(getattr(self, "api_timeout", 10.0))
client = get_shared_httpx_client(timeout=timeout_val)
client = get_shared_httpx_client()
resp = client.get(resolved_text, timeout=timeout_val)
resp.raise_for_status()
content = resp.content

View File

@@ -1404,7 +1404,7 @@ class Tidal(Provider):
from API.httpx_shared import get_shared_httpx_client
timeout_val = float(getattr(self, "api_timeout", 10.0))
client = get_shared_httpx_client(timeout=timeout_val)
client = get_shared_httpx_client()
resp = client.get(resolved_text, timeout=timeout_val)
resp.raise_for_status()
content = resp.content

View File

@@ -74,6 +74,54 @@ class MetadataProvider(ABC):
tags.append(f"source:{self.name}")
return tags
def search_tags(self, query: str, limit: int = 1) -> List[str]:
"""Return tags for the best match from `search(query)`.
Providers can override this when tags should be extracted differently from
the default search->first-item->to_tags flow.
"""
try:
items = self.search(query, limit=max(1, int(limit)))
except Exception:
return []
if not items:
return []
try:
return [str(t) for t in self.to_tags(items[0]) if t is not None]
except Exception:
return []
def identifier_query(self, identifiers: Dict[str, Any]) -> Optional[str]:
"""Return provider-specific identifier query text from parsed identifiers."""
_ = identifiers
return None
def combined_query(
self,
*,
title_hint: Optional[str],
artist_hint: Optional[str],
) -> Optional[str]:
"""Return provider-specific title+artist query text."""
_ = title_hint
_ = artist_hint
return None
def extract_url_query(self, result: Any, get_field: Any) -> Optional[str]:
"""Return provider-specific URL query derived from a piped result."""
_ = result
_ = get_field
return None
def emits_direct_tags(self) -> bool:
"""True when provider should skip selection table and emit tags directly."""
return False
class ITunesProvider(MetadataProvider):
"""Metadata provider using the iTunes Search API."""
@@ -112,6 +160,21 @@ class ITunesProvider(MetadataProvider):
debug(f"iTunes returned {len(items)} items for '{query}'")
return items
def identifier_query(self, identifiers: Dict[str, Any]) -> Optional[str]:
return identifiers.get("musicbrainz") or identifiers.get("musicbrainzalbum")
def combined_query(
self,
*,
title_hint: Optional[str],
artist_hint: Optional[str],
) -> Optional[str]:
title_text = str(title_hint or "").strip()
artist_text = str(artist_hint or "").strip()
if not title_text or not artist_text:
return None
return f"{title_text} {artist_text}"
class OpenLibraryMetadataProvider(MetadataProvider):
"""Metadata provider for OpenLibrary book metadata."""
@@ -220,6 +283,14 @@ class OpenLibraryMetadataProvider(MetadataProvider):
tags.append(f"source:{self.name}")
return tags
def identifier_query(self, identifiers: Dict[str, Any]) -> Optional[str]:
return (
identifiers.get("isbn_13")
or identifiers.get("isbn_10")
or identifiers.get("isbn")
or identifiers.get("openlibrary")
)
class GoogleBooksMetadataProvider(MetadataProvider):
"""Metadata provider for Google Books volumes API."""
@@ -329,6 +400,14 @@ class GoogleBooksMetadataProvider(MetadataProvider):
tags.append(f"source:{self.name}")
return tags
def identifier_query(self, identifiers: Dict[str, Any]) -> Optional[str]:
return (
identifiers.get("isbn_13")
or identifiers.get("isbn_10")
or identifiers.get("isbn")
or identifiers.get("openlibrary")
)
class ISBNsearchMetadataProvider(MetadataProvider):
"""Metadata provider that scrapes isbnsearch.org by ISBN.
@@ -624,6 +703,18 @@ class MusicBrainzMetadataProvider(MetadataProvider):
tags.append(f"musicbrainz:{mbid}")
return tags
def combined_query(
self,
*,
title_hint: Optional[str],
artist_hint: Optional[str],
) -> Optional[str]:
title_text = str(title_hint or "").strip()
artist_text = str(artist_hint or "").strip()
if not title_text or not artist_text:
return None
return f'recording:"{title_text}" AND artist:"{artist_text}"'
class ImdbMetadataProvider(MetadataProvider):
"""Metadata provider for IMDb titles (movies/series/episodes)."""
@@ -757,6 +848,9 @@ class ImdbMetadataProvider(MetadataProvider):
deduped.append(s)
return deduped
def identifier_query(self, identifiers: Dict[str, Any]) -> Optional[str]:
return identifiers.get("imdb")
class YtdlpMetadataProvider(MetadataProvider):
"""Metadata provider that extracts tags from a supported URL using yt-dlp.
@@ -904,6 +998,23 @@ class YtdlpMetadataProvider(MetadataProvider):
out.append(s)
return out
def extract_url_query(self, result: Any, get_field: Any) -> Optional[str]:
raw_url = (
get_field(result, "url", None)
or get_field(result, "source_url", None)
or get_field(result, "target", None)
)
if isinstance(raw_url, list) and raw_url:
raw_url = raw_url[0]
if isinstance(raw_url, str):
text = raw_url.strip()
if text.startswith(("http://", "https://")):
return text
return None
def emits_direct_tags(self) -> bool:
return True
def _coerce_archive_field_list(value: Any) -> List[str]:
"""Coerce an Archive.org metadata field to a list of strings."""