2025-12-11 19:04:02 -08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class SearchResult:
|
|
|
|
|
"""Unified search result format across all search providers."""
|
|
|
|
|
|
|
|
|
|
table: str # Provider name: "libgen", "soulseek", "bandcamp", "youtube", etc.
|
|
|
|
|
title: str # Display title/filename
|
|
|
|
|
path: str # Download target (URL, path, magnet, identifier)
|
|
|
|
|
|
|
|
|
|
detail: str = "" # Additional description
|
|
|
|
|
annotations: List[str] = field(default_factory=list) # Tags: ["120MB", "flac", "ready"]
|
|
|
|
|
media_kind: str = "other" # Type: "book", "audio", "video", "game", "magnet"
|
|
|
|
|
size_bytes: Optional[int] = None
|
2025-12-11 23:21:45 -08:00
|
|
|
tag: set[str] = field(default_factory=set) # Searchable tag values
|
2025-12-11 19:04:02 -08:00
|
|
|
columns: List[Tuple[str, str]] = field(default_factory=list) # Display columns
|
|
|
|
|
full_metadata: Dict[str, Any] = field(default_factory=dict) # Extra metadata
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
|
|
|
"""Convert to dictionary for pipeline processing."""
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"table": self.table,
|
|
|
|
|
"title": self.title,
|
|
|
|
|
"path": self.path,
|
|
|
|
|
"detail": self.detail,
|
|
|
|
|
"annotations": self.annotations,
|
|
|
|
|
"media_kind": self.media_kind,
|
|
|
|
|
"size_bytes": self.size_bytes,
|
2025-12-11 23:21:45 -08:00
|
|
|
"tag": list(self.tag),
|
2025-12-11 19:04:02 -08:00
|
|
|
"columns": list(self.columns),
|
|
|
|
|
"full_metadata": self.full_metadata,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-12-19 02:29:42 -08:00
|
|
|
class Provider(ABC):
|
|
|
|
|
"""Unified provider base class.
|
|
|
|
|
|
|
|
|
|
This replaces the older split between "search providers" and "file providers".
|
|
|
|
|
Concrete providers may implement any subset of:
|
|
|
|
|
- search(query, ...)
|
|
|
|
|
- download(result, output_dir)
|
|
|
|
|
- upload(file_path, ...)
|
|
|
|
|
- login(...)
|
|
|
|
|
- validate()
|
|
|
|
|
"""
|
2025-12-11 19:04:02 -08:00
|
|
|
|
|
|
|
|
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
|
|
|
self.config = config or {}
|
|
|
|
|
self.name = self.__class__.__name__.lower()
|
|
|
|
|
|
2025-12-19 02:29:42 -08:00
|
|
|
# Standard lifecycle/auth hook.
|
|
|
|
|
def login(self, **_kwargs: Any) -> bool:
|
|
|
|
|
return True
|
|
|
|
|
|
2025-12-11 19:04:02 -08:00
|
|
|
def search(
|
|
|
|
|
self,
|
|
|
|
|
query: str,
|
|
|
|
|
limit: int = 50,
|
|
|
|
|
filters: Optional[Dict[str, Any]] = None,
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> List[SearchResult]:
|
|
|
|
|
"""Search for items matching the query."""
|
2025-12-19 02:29:42 -08:00
|
|
|
raise NotImplementedError(f"Provider '{self.name}' does not support search")
|
2025-12-11 19:04:02 -08:00
|
|
|
|
|
|
|
|
def download(self, result: SearchResult, output_dir: Path) -> Optional[Path]:
|
|
|
|
|
"""Download an item from a search result."""
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
2025-12-19 02:29:42 -08:00
|
|
|
def upload(self, file_path: str, **kwargs: Any) -> str:
|
|
|
|
|
"""Upload a file and return a URL or identifier."""
|
|
|
|
|
raise NotImplementedError(f"Provider '{self.name}' does not support upload")
|
|
|
|
|
|
2025-12-11 19:04:02 -08:00
|
|
|
def validate(self) -> bool:
|
|
|
|
|
"""Check if provider is available and properly configured."""
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
2025-12-19 02:29:42 -08:00
|
|
|
def selector(self, selected_items: List[Any], *, ctx: Any, stage_is_last: bool = True, **_kwargs: Any) -> bool:
|
|
|
|
|
"""Optional hook for handling `@N` selection semantics.
|
2025-12-11 19:04:02 -08:00
|
|
|
|
2025-12-19 02:29:42 -08:00
|
|
|
The CLI can delegate selection behavior to a provider/store instead of
|
|
|
|
|
applying the default selection filtering.
|
2025-12-11 19:04:02 -08:00
|
|
|
|
2025-12-19 02:29:42 -08:00
|
|
|
Return True if the selection was handled and default behavior should be skipped.
|
|
|
|
|
"""
|
2025-12-11 19:04:02 -08:00
|
|
|
|
2025-12-19 02:29:42 -08:00
|
|
|
_ = selected_items
|
|
|
|
|
_ = ctx
|
|
|
|
|
_ = stage_is_last
|
|
|
|
|
return False
|
2025-12-11 19:04:02 -08:00
|
|
|
|
|
|
|
|
|
2025-12-19 02:29:42 -08:00
|
|
|
class SearchProvider(Provider):
|
|
|
|
|
"""Compatibility alias for older code.
|
|
|
|
|
|
|
|
|
|
Prefer inheriting from Provider directly.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FileProvider(Provider):
|
|
|
|
|
"""Compatibility alias for older code.
|
|
|
|
|
|
|
|
|
|
Prefer inheriting from Provider directly.
|
|
|
|
|
"""
|