Files
SageTube/ytsage/gui/ytsage_gui_search.py
T
Jaroslav Beneš 8e935c6116 Implement channel and playlist browsing
BrowsePage accepts pasted or routed channel/playlist URLs:
- Channels get Videos / Shorts / Live sub-tabs mapped to the channel's
  /videos, /shorts and /streams listings, lazily fetched 24 at a time
  with -I range pagination; channel title and id resolve from a cheap
  -I 1:1 metadata fetch. Subscribe emits subscribeRequested for the
  Feed page to wire up.
- Playlists get a single grid with Play all (bulk-enqueues into the
  Watch queue) and a Download button that deep-links the playlist into
  the Downloads tab.

Workers are parented to their pages and fetch errors surface as status
text instead of crashing (verified against a channel with no videos
tab). fetch_flat_info gains an items="1:1" limiter so metadata probes
no longer enumerate whole channels.

Verified live: kurzgesagt channel (24 cards, title+id resolved) and a
17-video playlist with Play-all queueing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:53:44 +02:00

92 lines
3.3 KiB
Python

"""
Search tab - in-app YouTube search via yt-dlp (ytsearchN:)
"""
from typing import Any, Dict, List, Optional
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget
from .ytsage_gui_cards import VideoCardGrid
from ..core.ytsage_client import YtdlpWorker
from ..utils.ytsage_localization import _
from ..utils.ytsage_logger import logger
PAGE_SIZE = 24
class SearchPage(QWidget):
def __init__(self, router, parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
self._router = router
self._worker: Optional[YtdlpWorker] = None
self._query = ""
self._offset = 0
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)
bar = QHBoxLayout()
self.query_input = QLineEdit()
self.query_input.setPlaceholderText(_("search.placeholder"))
self.query_input.returnPressed.connect(self.start_search)
self.query_input.setMinimumHeight(38)
bar.addWidget(self.query_input, stretch=1)
self.search_btn = QPushButton(_("search.button"))
self.search_btn.clicked.connect(self.start_search)
self.search_btn.setMinimumHeight(38)
bar.addWidget(self.search_btn)
layout.addLayout(bar)
self.status_label = QLabel("")
self.status_label.setStyleSheet("color: #9aa0a6; padding: 2px;")
layout.addWidget(self.status_label)
self.grid = VideoCardGrid(router, self)
self.grid.loadMoreRequested.connect(self.load_more)
layout.addWidget(self.grid, stretch=1)
# ---------------------------------------------------------------- search
def start_search(self) -> None:
query = self.query_input.text().strip()
if not query or self._worker is not None:
return
self._query = query
self._offset = 0
self.grid.clear()
self._fetch(append=False)
def load_more(self) -> None:
if self._worker is None and self._query:
self._offset += PAGE_SIZE
self._fetch(append=True)
def _fetch(self, append: bool) -> None:
self.status_label.setText(_("search.searching"))
self.search_btn.setEnabled(False)
query, offset = self._query, self._offset
self._worker = YtdlpWorker(lambda c: c.search(query, n=PAGE_SIZE, offset=offset), parent=self)
self._worker.result.connect(lambda entries: self._on_results(entries, append))
self._worker.error.connect(self._on_error)
self._worker.finished.connect(self._on_finished)
self._worker.start()
def _on_results(self, entries: List[Dict[str, Any]], append: bool) -> None:
show_more = len(entries) >= PAGE_SIZE
if append:
self.grid.append_entries(entries, show_load_more=show_more)
else:
self.grid.set_entries(entries, show_load_more=show_more)
self.status_label.setText(_("search.results_count", count=self.grid.card_count()))
def _on_error(self, message: str) -> None:
logger.error(f"Search failed: {message}")
self.status_label.setText(_("search.failed", error=message[:200]))
def _on_finished(self) -> None:
self.search_btn.setEnabled(True)
if self._worker is not None:
self._worker.deleteLater()
self._worker = None