removed TUI and others
This commit is contained in:
@@ -4608,6 +4608,25 @@ function M._raw_format_selection_id(fmt)
|
||||
return display_id
|
||||
end
|
||||
|
||||
function M._raw_info_has_audio_variant_selectors(raw)
|
||||
if type(raw) ~= 'table' or type(raw.formats) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
|
||||
for _, fmt in ipairs(raw.formats) do
|
||||
if type(fmt) == 'table' then
|
||||
local format_id = trim(tostring(fmt.format_id or ''))
|
||||
local vcodec = tostring(fmt.vcodec or 'none')
|
||||
local acodec = tostring(fmt.acodec or 'none')
|
||||
if format_id:match('^%d+%-%w+$') and vcodec == 'none' and acodec ~= 'none' then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function M._raw_format_picker_score(fmt)
|
||||
local note = trim(tostring(fmt and (fmt.format_note or fmt.format) or '')):lower()
|
||||
local format_id = trim(tostring(fmt and fmt.format_id or '')):lower()
|
||||
@@ -4715,6 +4734,14 @@ local function _cache_formats_from_raw_info(url, raw, source_label)
|
||||
return nil, 'missing url'
|
||||
end
|
||||
|
||||
if raw == nil then
|
||||
raw = mp.get_property_native('ytdl-raw-info')
|
||||
end
|
||||
|
||||
if M._raw_info_has_audio_variant_selectors(raw) then
|
||||
return nil, 'raw info requires validated probe'
|
||||
end
|
||||
|
||||
local tbl, err = _build_formats_table_from_raw_info(url, raw)
|
||||
if type(tbl) ~= 'table' or type(tbl.rows) ~= 'table' then
|
||||
return nil, err or 'raw format conversion failed'
|
||||
|
||||
@@ -477,7 +477,7 @@ class MPV:
|
||||
pipeline += f" | file -add -plugin local -instance {_q(path or '')}"
|
||||
|
||||
try:
|
||||
from TUI.pipeline_runner import PipelineRunner # noqa: WPS433
|
||||
from SYS.pipeline_runner import PipelineRunner # noqa: WPS433
|
||||
|
||||
runner = PipelineRunner()
|
||||
result = runner.run_pipeline(pipeline)
|
||||
|
||||
@@ -305,7 +305,7 @@ def _run_pipeline(
|
||||
json_output: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
# Import after sys.path fix.
|
||||
from TUI.pipeline_runner import PipelineRunner # noqa: WPS433
|
||||
from SYS.pipeline_runner import PipelineRunner # noqa: WPS433
|
||||
|
||||
def _json_safe(value: Any) -> Any:
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from SYS.provider_helpers import TableProviderMixin
|
||||
from SYS.logger import log
|
||||
|
||||
|
||||
class YouTube(TableProviderMixin, Provider):
|
||||
"""YouTube video search provider using yt_dlp.
|
||||
|
||||
This provider uses the new table system (strict ResultTable adapter pattern) for
|
||||
consistent selection and auto-stage integration. It exposes videos as SearchResult
|
||||
rows with metadata enrichment for:
|
||||
- video_id: Unique YouTube video identifier
|
||||
- uploader: Channel/creator name
|
||||
- duration: Video length in seconds
|
||||
- view_count: Number of views
|
||||
- _selection_args: For @N expansion control and download-file routing
|
||||
|
||||
SELECTION FLOW:
|
||||
1. User runs: search-file -plugin youtube "linux tutorial"
|
||||
2. Results show video rows with uploader, duration, views
|
||||
3. User selects a video: @1
|
||||
4. Selection metadata routes to download-file with the YouTube URL
|
||||
5. download-file uses yt_dlp to download the video
|
||||
"""
|
||||
|
||||
TABLE_AUTO_STAGES = {
|
||||
"youtube": ["download-file"],
|
||||
}
|
||||
# If the user provides extra args on the selection stage, forward them to download-file.
|
||||
AUTO_STAGE_USE_SELECTION_ARGS = True
|
||||
|
||||
@property
|
||||
def preserve_order(self) -> bool:
|
||||
return True
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
limit: int = 10,
|
||||
filters: Optional[Dict[str,
|
||||
Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> List[SearchResult]:
|
||||
# Use the yt_dlp Python module (installed via requirements.txt).
|
||||
try:
|
||||
import yt_dlp # type: ignore
|
||||
|
||||
ydl_opts: Dict[str,
|
||||
Any] = {
|
||||
"quiet": True,
|
||||
"skip_download": True,
|
||||
"extract_flat": True
|
||||
}
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[arg-type]
|
||||
search_query = f"ytsearch{limit}:{query}"
|
||||
info = ydl.extract_info(search_query, download=False)
|
||||
entries = info.get("entries") or []
|
||||
results: List[SearchResult] = []
|
||||
for video_data in entries[:limit]:
|
||||
title = video_data.get("title", "Unknown")
|
||||
video_id = video_data.get("id", "")
|
||||
url = video_data.get(
|
||||
"url"
|
||||
) or f"https://youtube.com/watch?v={video_id}"
|
||||
uploader = video_data.get("uploader", "Unknown")
|
||||
duration = video_data.get("duration", 0)
|
||||
view_count = video_data.get("view_count", 0)
|
||||
|
||||
duration_str = (
|
||||
f"{int(duration // 60)}:{int(duration % 60):02d}"
|
||||
if duration else ""
|
||||
)
|
||||
views_str = f"{view_count:,}" if view_count else ""
|
||||
|
||||
results.append(
|
||||
SearchResult(
|
||||
table="youtube",
|
||||
title=title,
|
||||
path=url,
|
||||
detail=f"By: {uploader}",
|
||||
annotations=[duration_str,
|
||||
f"{views_str} views"],
|
||||
media_kind="video",
|
||||
columns=[
|
||||
("Title",
|
||||
title),
|
||||
("Uploader",
|
||||
uploader),
|
||||
("Duration",
|
||||
duration_str),
|
||||
("Views",
|
||||
views_str),
|
||||
],
|
||||
full_metadata={
|
||||
"video_id": video_id,
|
||||
"uploader": uploader,
|
||||
"duration": duration,
|
||||
"view_count": view_count,
|
||||
# Selection metadata for table system and @N expansion
|
||||
"_selection_args": ["-url", url],
|
||||
},
|
||||
)
|
||||
)
|
||||
return results
|
||||
except Exception:
|
||||
log("[youtube] yt_dlp import failed", file=sys.stderr)
|
||||
return []
|
||||
|
||||
def validate(self) -> bool:
|
||||
try:
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# Minimal provider registration for the new table system
|
||||
try:
|
||||
from SYS.result_table_adapters import register_plugin
|
||||
from SYS.result_table_api import ResultModel, ColumnSpec, metadata_column, title_column
|
||||
|
||||
def _convert_search_result_to_model(sr: Any) -> ResultModel:
|
||||
"""Convert YouTube SearchResult to ResultModel for strict table display."""
|
||||
d = sr.to_dict() if hasattr(sr, "to_dict") else (sr if isinstance(sr, dict) else {"title": getattr(sr, "title", str(sr))})
|
||||
title = d.get("title") or ""
|
||||
path = d.get("path") or None
|
||||
columns = d.get("columns") or getattr(sr, "columns", None) or []
|
||||
|
||||
# Extract metadata from columns and full_metadata
|
||||
metadata: Dict[str, Any] = {}
|
||||
for name, value in columns:
|
||||
key = str(name or "").strip().lower()
|
||||
if key in ("uploader", "duration", "views", "video_id"):
|
||||
metadata[key] = value
|
||||
|
||||
try:
|
||||
fm = d.get("full_metadata") or {}
|
||||
if isinstance(fm, dict):
|
||||
for k, v in fm.items():
|
||||
metadata[str(k).strip().lower()] = v
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ResultModel(
|
||||
title=str(title),
|
||||
path=str(path) if path else None,
|
||||
ext=None,
|
||||
size_bytes=None,
|
||||
metadata=metadata,
|
||||
source="youtube"
|
||||
)
|
||||
|
||||
def _adapter(items: Iterable[Any]) -> Iterable[ResultModel]:
|
||||
"""Adapter to convert SearchResults to ResultModels."""
|
||||
for it in items:
|
||||
try:
|
||||
yield _convert_search_result_to_model(it)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def _has_metadata(rows: List[ResultModel], key: str) -> bool:
|
||||
"""Check if any row has a given metadata key with a non-empty value."""
|
||||
for row in rows:
|
||||
md = row.metadata or {}
|
||||
if key in md:
|
||||
val = md[key]
|
||||
if val is None:
|
||||
continue
|
||||
if isinstance(val, str) and not val.strip():
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
def _columns_factory(rows: List[ResultModel]) -> List[ColumnSpec]:
|
||||
"""Build column specifications from available metadata in rows."""
|
||||
cols = [title_column()]
|
||||
if _has_metadata(rows, "uploader"):
|
||||
cols.append(metadata_column("uploader", "Uploader"))
|
||||
if _has_metadata(rows, "duration"):
|
||||
cols.append(metadata_column("duration", "Duration"))
|
||||
if _has_metadata(rows, "views"):
|
||||
cols.append(metadata_column("views", "Views"))
|
||||
return cols
|
||||
|
||||
def _selection_fn(row: ResultModel) -> List[str]:
|
||||
"""Return selection args for @N expansion and auto-download integration.
|
||||
|
||||
Uses explicit -url flag to ensure the YouTube URL is properly routed
|
||||
to download-file for yt_dlp download processing.
|
||||
"""
|
||||
metadata = row.metadata or {}
|
||||
|
||||
# Check for explicit selection args first
|
||||
args = metadata.get("_selection_args") or metadata.get("selection_args")
|
||||
if isinstance(args, (list, tuple)) and args:
|
||||
return [str(x) for x in args if x is not None]
|
||||
|
||||
# Fallback to direct URL
|
||||
if row.path:
|
||||
return ["-url", row.path]
|
||||
|
||||
return ["-title", row.title or ""]
|
||||
|
||||
register_plugin(
|
||||
"youtube",
|
||||
_adapter,
|
||||
columns=_columns_factory,
|
||||
selection_fn=_selection_fn,
|
||||
metadata={"description": "YouTube video search using yt_dlp"},
|
||||
)
|
||||
except Exception:
|
||||
# best-effort registration
|
||||
pass
|
||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
|
||||
from SYS.provider_helpers import TableProviderMixin
|
||||
from SYS.logger import debug, log
|
||||
from SYS.models import DownloadError, DownloadMediaResult, DownloadOptions
|
||||
@@ -32,6 +32,7 @@ from tool.ytdlp import (
|
||||
_read_text_file,
|
||||
collapse_picker_formats,
|
||||
format_for_table_selection,
|
||||
get_display_format_id,
|
||||
get_selection_format_id,
|
||||
is_browseable_format,
|
||||
is_url_supported_by_ytdlp,
|
||||
@@ -357,6 +358,12 @@ def _format_id_for_query_index(
|
||||
return normalized or s_val
|
||||
|
||||
candidate_formats = collapse_picker_formats(fmts, video_audio_suffix="bestaudio")
|
||||
if s_val and not s_val.startswith("#"):
|
||||
for item in candidate_formats:
|
||||
if get_display_format_id(item) == s_val:
|
||||
normalized = get_selection_format_id(item, video_audio_suffix="bestaudio")
|
||||
return normalized or s_val
|
||||
|
||||
filtered_formats = candidate_formats if candidate_formats else list(fmts)
|
||||
if idx <= 0 or idx > len(filtered_formats):
|
||||
raise ValueError(f"Format index {idx} out of range")
|
||||
@@ -497,6 +504,10 @@ def _build_pipe_objects(
|
||||
class ytdlp(TableProviderMixin, Provider):
|
||||
"""yt-dlp-backed search and direct download plugin."""
|
||||
|
||||
PLUGIN_NAME = "ytdlp"
|
||||
PLUGIN_ALIASES = ("youtube",)
|
||||
SEARCH_QUERY_KEYS = ("search", "q")
|
||||
|
||||
@classmethod
|
||||
def url_patterns(cls) -> Tuple[str, ...]:
|
||||
try:
|
||||
@@ -529,10 +540,47 @@ class ytdlp(TableProviderMixin, Provider):
|
||||
|
||||
TABLE_AUTO_STAGES = {
|
||||
"ytdlp.formatlist": ["download-file"],
|
||||
"ytdlp.search": ["download-file"],
|
||||
"youtube": ["download-file"],
|
||||
}
|
||||
AUTO_STAGE_USE_SELECTION_ARGS = True
|
||||
|
||||
def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]:
|
||||
normalized_query, inline_args = parse_inline_query_arguments(query)
|
||||
search_parts: List[str] = []
|
||||
|
||||
for key in self.SEARCH_QUERY_KEYS:
|
||||
value = str(inline_args.pop(key, "") or "").strip()
|
||||
if value:
|
||||
search_parts.append(value)
|
||||
|
||||
if normalized_query:
|
||||
search_parts.append(normalized_query)
|
||||
|
||||
resolved_query = " ".join(part for part in search_parts if part).strip()
|
||||
if not resolved_query:
|
||||
resolved_query = str(query or "").strip()
|
||||
|
||||
filters: Dict[str, Any] = dict(inline_args or {})
|
||||
filters.setdefault("search_provider", "youtube")
|
||||
return resolved_query, filters
|
||||
|
||||
def get_table_type(
|
||||
self,
|
||||
query: str,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
_ = query, filters
|
||||
return "youtube"
|
||||
|
||||
def get_table_title(
|
||||
self,
|
||||
query: str,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
_ = filters
|
||||
q = str(query or "").strip() or "*"
|
||||
return f"YouTube: {q}"
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
@@ -567,7 +615,7 @@ class ytdlp(TableProviderMixin, Provider):
|
||||
|
||||
results.append(
|
||||
SearchResult(
|
||||
table="ytdlp.search",
|
||||
table="youtube",
|
||||
title=title,
|
||||
path=url,
|
||||
detail=f"By: {uploader}",
|
||||
@@ -1443,11 +1491,11 @@ try:
|
||||
return ["-title", row.title or ""]
|
||||
|
||||
_register_table_plugin_once(
|
||||
"ytdlp.search",
|
||||
"youtube",
|
||||
_search_adapter,
|
||||
columns=_search_columns_factory,
|
||||
selection_fn=_search_selection_fn,
|
||||
metadata={"description": "ytdlp video search using yt-dlp"},
|
||||
metadata={"description": "YouTube search using yt-dlp"},
|
||||
)
|
||||
except Exception as exc:
|
||||
debug(f"[ytdlp] Provider registration note: {exc}")
|
||||
|
||||
Reference in New Issue
Block a user