Commit Graph

607 Commits

Author SHA1 Message Date
Homer b07a56082a Fall back to English instead of showing raw keys
Every namespace the fork added -- main_tabs, feed, cards, watch, browse,
search, player, and now account -- exists only in en.json, because the thirteen
translated language files predate them. get_text() consults the embedded
fallback dict and then gives up and returns the key, so a non-English UI read
"main_tabs.watch" on its own tabs. This round made that worse by adding more
English-only keys.

The fallback dict now carries those namespaces verbatim from en.json. It does
not translate anything -- a German UI shows English for these strings -- but
nothing shows a dotted key, and keys added later degrade the same way instead
of breaking. Translations that do exist still win: buttons.download is still
"Herunterladen" in German.

Verified across all fourteen language files: no key in the fork's namespaces
resolves to itself in any of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v5.5.0
2026-08-09 01:22:08 +02:00
Homer 9c4749e49b Release 5.5.0
Also stops one benign libmpv message being logged as an error: `after
creating texture: OpenGL error INVALID_ENUM` comes from the GL driver, arrives
several times per playback start and per fullscreen toggle, and playback
continues regardless. Hundreds of lines a session buried the errors that
matter -- it is a debug line now.

Verified end to end on the real display with an actual video playing: ten tab
switches and four fullscreen toggles while frames were rendering, position
advancing throughout, the render context intact, no crash, and an empty error
log afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 01:18:43 +02:00
Homer fd744a1a85 Make the YouTube account reachable, and applied without a restart
The cookie login was not missing, it was buried: Downloads tab -> Custom
Options -> the "Login with Cookies" tab, where Downloads is one tab of five.
The three features that depend on it -- the Feed's account mode, Search and
Browse -- are other tabs, with no route to it. show_cookie_login_dialog(),
which opens that dialog on the right tab, had no caller anywhere in the
codebase; it does now.

An account button sits in the corner of the tab bar, visible from every tab,
saying whether cookies are in use and where they came from. The Feed offers
the same thing next to its account mode rather than greying the option out and
leaving it at that, and the disabled entry now says why it is disabled.

Applying cookies used to require a restart before playback saw them:
_build_ytdl_raw_options was read once, when the player widget was built. A
cookiesChanged signal now reaches the account button, the Feed and the live
mpv handle, and the player reloads at its current position -- ytdl_hook
consults the option when it resolves a URL, so anything already playing keeps
the streams it resolved with. YtdlpClient needed no change: it re-reads config
per call.

Two bugs in the same function: the options are joined with commas and were
never escaped, so a cookie path or a proxy URL containing one silently ended
the option and started a bogus one; and geo_proxy_url was honoured by the
downloader but never passed to the player, so a geo-restricted video would
download and then refuse to play.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 01:15:21 +02:00
Homer 79a6f26c2f Open on the Feed, and refresh it without hammering yt-dlp
The tab order was Watch, Search, Feed, Browse, Downloads, and the app opened
on Watch -- which is where the other tabs send you, not somewhere you start.
It is now Feed, Search, Browse, Downloads, Watch, with icons. Routing is
unaffected: _tab_index_of resolves by widget identity, not position.

Refresh-on-open needed care rather than a call in showEvent. A local refresh
is one yt-dlp subprocess per subscribed channel, serially, so it is braked
four ways: only channels not seen for feed.auto_refresh_on_open_minutes (30,
0 to disable), at most eight per visit, once per interval per session, and
after a delay so it does not race the tab transition or first-run setup. The
cached feed is already on screen throughout. That is a new config key rather
than the dead auto_refresh_minutes, because existing configs store that as 0
and would have read as opting out of a feature that did not exist yet.

SmoothTabWidget gained the currentChanged signal, icons and a corner slot it
never had. Its set_current_index needed a re-entrancy flag, not just an index
check: setting the tab bar's index emits its currentChanged straight back into
the same method, and at that point the stack has not moved, so every switch
fired the activation hook twice.

The grid was cleared and rebuilt after every channel finished -- flicker, lost
scroll position and every thumbnail re-read from disk each time. merge_entries
keeps existing cards. While there, _relayout was re-adding cards the layout
already owned, so layout items accumulated on every Load more, and the resize
check compared against columnCount(), which never shrinks, so it relaid out on
every resize event.

Smaller things this exposed: feed errors were written into the label the next
success overwrote, so failures were invisible; switching to account mode left
the local videos on screen; cancel() had no callers, so a refresh outlived the
tab and the window; Browse's Subscribe never said Unsubscribe though it
toggles; "Play all" queued one page while claiming otherwise; and the feed
sorted publish times against wall-clock fetch times in one COALESCE, so the
last-refreshed channel floated to the top.

Feed is the first thing seen now, so an empty one says what to do about it
instead of showing a bare grid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 01:11:40 +02:00
Homer 27933dd495 Make the player answer the keyboard and the mouse
PlayerPanel.keyPressEvent handled Space, F and Escape and was dead code:
nothing in the player called setFocusPolicy, so focus went to whichever child
button, slider or combo box was first in the tab order, and that child ate the
keys. Fixing focus alone would not have been enough -- clicking play parks
focus on the play button, which then swallows Space -- so the control bar is
explicitly NoFocus and the bindings are QShortcuts with
WidgetWithChildrenShortcut context on the panel. That fires whichever
descendant holds focus, and does not reach the Search box or the URL field on
other tabs the way an application shortcut would.

mpv's own input stays disabled. With vo=libmpv there is no mpv-owned window,
so its input layer receives nothing; enabling default bindings would mean
hand-forwarding events through a Qt-to-mpv key-name table, handing mpv the
OSD and OSC that are switched off here, and letting `q` quit the core out from
under the Qt UI. One declarative action table now drives the shortcuts, the
context menu and the buttons.

There was no mouse handling at all. Click pauses, double-click goes
fullscreen -- via a doubleClickInterval timer, so a double-click does not also
pause on the way -- the wheel changes volume and Ctrl+wheel seeks, with
sub-notch deltas accumulated so a trackpad is not inert.

The control bar gained previous, next and mute. Mute and volume are driven by
observed mpv properties rather than assumed, so the icon follows a change made
by key, menu or mpv itself. Buffering was completely invisible: a stalled
stream showed a frozen frame for 25 seconds before the retry with nothing on
screen, so paused-for-cache now surfaces a label over the video.

WatchPage owns next/previous because it owns the queue; the panel only asks.
Previous restarts the current item when it is more than five seconds in, as
every other player does, and pushes the interrupted item back onto the head of
the queue rather than dropping it.

Verified: all 38 bindings install, every action is a no-op rather than an
exception with nothing loaded, focus policies are as intended, and a
fullscreen round trip drives the player state correctly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 01:04:16 +02:00
Homer 3aca43b372 Give the app icons, tooltips and a finished theme
There was no icon system at all. Transport buttons called
QStyle.standardIcon(SP_MediaPlay), which returns the platform theme's dark
monochrome glyph -- painted onto the app's saturated red buttons at a fixed
36px with no text, that is the black square. Everything else called an icon
was an emoji baked into en.json, which is tofu wherever the emoji font is
missing. Qt stylesheets cannot recolour a QIcon, so the colour is an argument
to the new helper; that is the whole fix.

The SVGs are drawn here rather than vendored, which keeps a third-party
licence out of the tree, and they live in the module rather than as asset
files, which keeps them out of package-data and safe in a frozen build.

Tooltips were the other half: three widgets in the entire application had one
and nothing set an accessible name. Filling that in at the call sites would
have meant editing well over a hundred of them, most in upstream-owned files.
Instead one application-level event filter handles QEvent.Polish, which Qt
sends to every widget once before it is shown -- so it also reaches dialogs
built by upstream code, and survives the next merge. It maps placeholder
emoji to icons, fills empty tooltips from the button text, and logs icon-only
buttons that still have none so the gaps are findable.

StyleSheet.MAIN styles the window, inputs, buttons and tables and nothing
else, so the main tab bar, combos, sliders, lists, menus, splitters, tooltips
and the horizontal scrollbar fell through to the platform style. EXTRA_QSS
covers them, in a fork-owned module appended at the one application site.
SmoothTabWidget names its frame "tabContent" with the comment "We draw border
on content instead" -- that rule existed only inside two dialogs, and now
exists for the main window too.

Two corrections to rules that were already there: the pressed style changed
the padding, shifting every label two pixels and clipping fixed-width icon
buttons, and checkboxes were fully rounded, which reads as a radio button
rather than an on/off toggle.

Verified by screenshot on the real display: tab bar, buttons, combo carets
and checkboxes all render as intended, and all 35 icons rasterise non-empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 00:58:38 +02:00
Homer 5ae2817eea Stop the player freeing a render context libmpv still uses
The segfault: Qt destroys and recreates a widget's QOpenGLContext whenever it
moves to another top-level window, then calls initializeGL() again. libmpv
permits one render context per handle, so the second creation failed with
"There is already a mpv_render_context set" -- and the except branch assigned
self._render_ctx = None, dropping the last Python reference to the first
context, which libmpv was still holding a function pointer into. python-mpv's
MpvRenderContext has no __del__ and free() does not unregister the callback,
so the ctypes trampoline was collected while registered and the next frame
notification jumped into freed memory.

Two invariants fix it. initializeGL() now tears down any existing render
context first, so a second call is an ordinary recreation. Teardown clears
update_cb, calls free() with the GL context current, and only then drops the
reference -- and it is connected to QOpenGLContext.aboutToBeDestroyed, so it
runs before the GL context dies instead of never. The local reference during
teardown is load-bearing: it is what keeps the trampoline alive until free()
returns.

Three things were destroying that context. Fullscreen reparented the panel
into a new top-level window (twice per toggle) and put it back at the end of
the splitter, losing the pane layout; it now fullscreens the main window and
hides the chrome, reparenting nothing. The tab cross-fade and the dialog blur
both grab() the widget tree, which on an OpenGL surface forces a framebuffer
readback and returns black -- the fade is skipped for pages holding the video,
and dialogs dim rather than blur.

Verified on a real Wayland GL context: ten forced context destroy/recreate
cycles re-establish the render context every time, and the full app survives
tab switching, six fullscreen toggles and resizes with no render-context
error and a clean exit. Before this, the same startup dumped core.

Also here because they are one-line consequences of touching _create_mpv: an
explicit per-platform hwdec list ending in software decoding, and the restored
volume actually reaching mpv -- the slider set its value before connecting its
signal, so playback always started at 100.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 00:48:18 +02:00
Homer 2420091a4e Check for SageTube updates, not YTSage's
The update check asked PyPI for the `ytsage` package's version, asked GitHub
for oop7/YTSage's release notes, and pointed the download button at upstream's
releases. None of that describes this program. Being reminded to install
YTSage was the visible symptom; the cause was that the check had never been
repointed when the fork was made.

It now reads SageTube's own releases from git.houmeres.sk. Gitea's release API
is shaped like GitHub's, so the dialog and the caller are unchanged -- the
class keeps its name and signal signature, and only its body moved out to
core/ytsage_app_update.py, which is fork-owned and will not conflict on the
next merge from upstream.

Upstream's inherited tags end in `b` (v5.3.0b and earlier). packaging reads
that as a beta marker, so they sort below v5.4.0 and a stable instance cannot
be handed one. Tag parsing is defensive anyway: one unparseable tag must not
take the whole check down with it.

Also: the check is rate-limited to once a day rather than every start, the
dialog gained a "Skip this version" that survives a restart, the thread is now
joined on close, and the About dialog says SageTube. The three binary updaters
-- yt-dlp, Deno, ffmpeg -- legitimately track their own upstreams and are
deliberately untouched; ytsage_app_update's docstring says so, because "update"
is an overloaded word in this codebase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 00:36:14 +02:00
Homer 089b144626 Correct the version and make the config merge deeply
The package version had been left at the scaffolded 0.1.0 while the released
tag was v5.4.0. Nothing in the code chose 0.1.0 -- it was npm-init's equivalent,
never updated -- and it made every version comparison meaningless: the window
title, the About dialog and the update check all read it.

ConfigManager merged a stored config over the defaults with dict.update(),
which is shallow. A config written by an older build carries partial "player"
and "feed" objects, and a shallow update replaces the whole nested default with
the partial one, so keys added since came back missing. The `or 15` and
`or "auto"` fallbacks at the call sites were load-bearing because of it. The
merge is now recursive, and keys present only in the stored file are kept so a
downgrade cannot destroy settings.

Configs now carry a config_version. A file written before 5.4.0 -- including
one inherited from an upstream YTSage install -- has its stored
check_app_updates cleared once, because that setting used to point at PyPI's
`ytsage` package and oop7/YTSage's releases, neither of which is this program.

The settings tab read a missing check_app_updates as enabled and persisted that
reading on OK, so merely opening Custom Options turned the checker back on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 00:31:56 +02:00
Homer b3772ff484 The fork's documentation on the wiki, and the history left alone
Building, upstream tracking and YTSage's own README moved to the wiki; README
points there.

The history is not rewritten, unlike every other repository here. This one
still merges from oop7/YTSage and a rewrite would change every commit id, which
costs more than a clean history is worth in a fork. .github/ and
readme-translations/ are upstream's files and stay where upstream put them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
v5.4.0
2026-08-08 18:37:46 +02:00
Jaroslav Beneš b4ff618d41 Add fork documentation and rewrite README
- README: watch-first feature overview, install + libmpv requirement,
  fork attribution.
- docs/UPSTREAM.md: upstream merge procedure, expected conflict zones,
  list of SageTube-only modules.
- docs/BUILDING.md: per-OS libmpv install, managed-binary locations,
  playback reliability notes, warning that inherited GitHub release
  workflows download appimagetool/ffmpeg without checksum verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 02:46:59 +02:00
Jaroslav Beneš 8a05d5ff8f Rebrand user-visible surfaces to SageTube
The internal ytsage package name is kept deliberately - renaming it
would touch every file and destroy the ability to merge upstream
YTSage changes.

- pyproject: distribution name sagetube v0.1.0, sagetube entrypoint
  (ytsage alias retained), URLs point at the Gitea repo with an
  Upstream link to YTSage.
- Data dirs move to SageTube/ (fresh fork, fresh state) and the config
  file becomes sagetube_config.json; QApplication name and window
  title read SageTube.
- About dialog credits Houmeres and links "Based on YTSage by oop7".
- App self-update check against the upstream PyPI package is disabled
  by default; yt-dlp/deno/ffmpeg update flows are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 02:45:58 +02:00
Jaroslav Beneš a8ebe89cc1 Wire watch history, resume positions and persistent queue
WatchPage now records every played video in watch_history, saves the
playback position every 5 seconds and on stop/end (completed at >=95%),
and seeks back on replay when player.resume is "auto" - resume kicks in
between 30 seconds and 95% of the duration. The play queue persists
across restarts and reorders/removals write through immediately; the
main window's closeEvent flushes position and queue and releases libmpv.

PlayerPanel gains an automatic stall-retry: YouTube's CDN intermittently
serves stalled streams to non-browser clients (reproduced ~1/3 of
attempts headless with identical code), and a reload re-resolves onto a
healthy node - two retries after 25s of no playback, then a user-facing
error.

ytsage_constants now prepends the managed-binaries dir to PATH so
yt-dlp subprocesses (including mpv's ytdl_hook) can find the managed
Deno runtime - previously nothing exported APP_BIN_DIR, so the deno
integration silently never worked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 02:42:38 +02:00
Jaroslav Beneš 3afe96a8cd Add local subscriptions and the Feed tab
New ytsage/utils/ytsage_library_manager.py: sagetube_library.db
(separate from the upstream download-history DB) with WAL from day one,
holding subscriptions, cached feed_items, watch_history with resume
positions, and the persisted play_queue. Watch history and queue tables
are wired up by the next commit.

FeedPage:
- Local mode: FeedRefreshWorker refreshes each subscribed channel
  sequentially (feed.per_channel_items, default 15) and the grid fills
  incrementally per channel; results are cached so the feed is
  populated instantly on startup.
- Account mode: fetches youtube.com/feed/subscriptions with the user's
  browser cookies - the real logged-in feed; the option is enabled only
  while cookies are active and errors surface as a status banner.
- Sidebar lists subscriptions (double-click opens the channel in
  Browse; context menu unsubscribes). Browse's Subscribe button now
  toggles subscription state through the Feed page.

Verified live: subscribe -> refresh -> 15 videos cached in SQLite and
rendered as cards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:56:12 +02:00
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
Jaroslav Beneš 16be4b4afa Restructure main window into a watch-first tab shell
The single-page downloader layout becomes the Downloads tab of a
SmoothTabWidget with Watch / Search / Feed / Browse / Downloads pages.
The init_ui edit is deliberately small (the old central widget is now
self.download_page); all new behavior lives in new modules:

- ytsage_gui_router.py: AppRouter signal hub (playVideo, queueVideo,
  downloadVideo, openChannel, openPlaylist). A card's Download button
  deep-links into the Downloads tab with the URL prefilled and analysis
  started automatically.
- ytsage_gui_cards.py: VideoCard (thumbnail with disk cache under
  APP_THUMBNAILS_DIR, title/channel/duration, Play/Queue/Download
  actions, double-click to play) and VideoCardGrid (responsive grid,
  Load more pagination).
- ytsage_gui_watch.py: WatchPage hosting the mpv PlayerPanel and a
  drag-reorderable play queue with auto-advance on end of file.
- ytsage_gui_search.py: SearchPage running YtdlpClient.search off the
  GUI thread with Load more pagination.
- Browse and Feed pages are placeholders, implemented next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:48:39 +02:00
Jaroslav Beneš f886125857 Add embedded mpv player (render API + QOpenGLWidget)
New ytsage/gui/ytsage_gui_player.py:
- MpvRenderWidget hosts libmpv through MpvRenderContext in a
  QOpenGLWidget - works on native Wayland where wid-embedding cannot -
  with all mpv-thread callbacks marshalled to the GUI thread via queued
  signals only.
- PlayerPanel adds transport controls: play/pause, seek slider, time
  display, quality selector (caps ytdl-format height and reloads in
  place), speed, volume (persisted), subtitle toggle, fullscreen
  (reparent to top-level window), and Space/F/Escape keys.
- Playback resolves watch URLs through mpv's ytdl_hook pointed at the
  app-managed SHA256-verified yt-dlp binary (script-opts
  ytdl_hook-ytdl_path), inheriting cookies and proxy settings via
  ytdl-raw-options - stream freshness, DASH muxing and nsig handling
  stay in yt-dlp's hands.

New ytsage/core/ytsage_mpv.py probes libmpv availability; without it
the Watch UI shows a per-OS install hint and everything else works.
python-mpv added to dependencies (libmpv itself is a system package).
Config gains player.* and feed.* defaults.

Verified on Wayland: real YouTube video streams with position/duration
signals flowing and no thread-safety errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:44:25 +02:00
Jaroslav Beneš 8dd8a3c1ad Add YtdlpClient: shared JSON invocation layer for yt-dlp
Every metadata call site previously built its own command list. The new
ytsage/core/ytsage_client.py centralizes binary resolution (managed,
verified binary only), cookie/proxy args from ConfigManager (with
session-state overrides), utf-8 output handling, process-group-safe
timeouts, and a short-TTL cache for flat/search results.

Provides fetch_video_info, fetch_flat_info, fetch_flat_entries (with
-I range pagination), search (ytsearchN:), and fetch_account_feed
(youtube.com/feed/subscriptions with cookies) plus a generic YtdlpWorker
QThread. AnalysisThread now delegates its subprocess execution to the
shared runner; its signal surface is unchanged.

Groundwork for the SageTube watch/search/browse/feed features.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:39:20 +02:00
Jaroslav Beneš 1c885e3fc5 Fix low-severity robustness issues
- show_error_dialog() aborted when QApplication construction itself was
  the failure; fall back to stderr so the real error is visible.
- Auto-update QThread cleanup dropped the last Python reference while
  run() could still be unwinding ("QThread: Destroyed while thread is
  still running"); defer destruction to deleteLater on finished.
- macOS ffmpeg install no longer curl|bash-es the Homebrew bootstrap
  script unattended; it now asks the user to install Homebrew
  themselves and fails cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:36:29 +02:00
Jaroslav Beneš 4cc48ae98f Fix medium-severity defects across download, formats and tooling
- Format table: a missing acodec was treated as "has audio", skipping
  the +bestaudio merge and producing silent videos for extractors that
  omit the field.
- Progress bar: separate video/audio stream downloads each reported
  0-100%, making the bar jump backwards; per-phase scaling now maps the
  two streams onto 0-50/50-100.
- Custom commands: parse with shlex (quoted arguments with spaces were
  shredded by str.split), keep POSIX mode off on Windows so backslash
  paths survive, hide the console window like every other call site,
  close the stdout pipe, and support cancellation of a running command.
- Settings dialog: _("settings", "error_saving", ...) passed two
  positional args to the i18n helper, raising TypeError inside the
  except handler instead of showing the intended error dialog.
- ffmpeg on Windows: Path(os.getenv("LOCALAPPDATA")) crashed with
  TypeError when the variable is unset; fall back to the standard
  AppData/Local location.
- Version cache: cached path (str) was compared against a Path, so the
  cache never hit and every version query spawned a subprocess.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:35:23 +02:00
Jaroslav Beneš 288d30ad8b Make config persistence robust and history DB concurrency-safe
- Config saves are atomic (temp file + fsync + os.replace); a crash or
  power loss mid-write no longer truncates the file, which previously
  caused a silent reset to defaults on next launch.
- Stored config is merged over a deep copy of the defaults: keys added
  in newer versions resolve properly instead of returning None, and the
  nested cached_versions dict is no longer shared with (and mutated on)
  the class-level default dict.
- History SQLite connection enables WAL and a 5s busy timeout so the
  download thread can record entries while the history dialog reads
  without "database is locked" errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:31:49 +02:00
Jaroslav Beneš ebb9422591 Harden yt-dlp binary resolution and analysis subprocess handling
- get_yt_dlp_path() no longer implicitly executes a bare "yt-dlp" from
  PATH (on Windows that lookup includes the CWD, so a planted binary
  in a writable directory could be run). A system yt-dlp is used only
  behind the explicit advanced.allow_system_ytdlp config opt-in, and
  then always as a which()-resolved absolute path. Analysis and
  download refuse to exec the not-installed sentinel.
- Analysis subprocesses now run in their own session and the whole
  process group is killed on timeout, so deno grandchildren no longer
  leak; partial stderr is preserved and logged, and output decoding is
  pinned to utf-8 with replacement (Windows locale codecs crashed on
  non-UTF8 titles).
- Flat-playlist entries are filtered for None (private/deleted first
  video no longer breaks analysis).
- update_yt_dlp() normalizes the sentinel to Path, unbreaking the pip
  fallback path that crashed on str.exists().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:30:08 +02:00
Jaroslav Beneš 852804ee5c Actually suspend the download process group on pause
Pause only stopped the stdout reader loop; yt-dlp kept transferring at
full rate until the pipe buffer filled, consuming bandwidth while the
UI claimed the download was paused. Send SIGSTOP/SIGCONT to the whole
process group (yt-dlp and its ffmpeg children) on Unix. Windows has no
equivalent signal; the limitation is documented in the helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:26:44 +02:00
Jaroslav Beneš 5064b38315 Marshal channel-switch UI updates back to the GUI thread
The yt-dlp stable/nightly channel switcher mutated QLabel/QRadioButton
state directly from a raw threading.Thread, which is undefined behavior
in Qt. The worker now only runs the subprocess and emits a signal; the
connected slot applies all widget updates on the GUI thread via Qt's
queued delivery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:26:00 +02:00
Jaroslav Beneš dff14ec3e8 Enforce SHA256 verification on every binary install path
Three gaps allowed an unverified binary to reach a trusted location:

- The ffmpeg ZIP fallback logged a warning on checksum mismatch and
  installed anyway (the 7z path already aborted). Abort instead.
- The yt-dlp auto-update path downloaded and renamed the binary over
  the verified one with no checksum at all. Verify against the official
  SHA2-256SUMS like the first-install path, and use atomic os.replace.
- The yt-dlp first install streamed the download directly to the
  trusted path and only verified afterwards; a crash in between left an
  unverified executable to be run on next launch. Download to .part and
  os.replace only after verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:24:35 +02:00
Jaroslav Beneš b40deb0d5f Write ffmpeg dir to user PATH via registry instead of setx
setx truncates values at 1024 characters, and the old code fed it the
merged process PATH (system + user), permanently duplicating every
system entry into the user hive and silently dropping anything past the
limit. Read and rewrite only the HKCU Environment Path value with
winreg, preserving REG_EXPAND_SZ, and broadcast WM_SETTINGCHANGE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:23:04 +02:00
Jaroslav Beneš 202dd9eaee Fix UnboundLocalError in manual yt-dlp binary selection
Unpacking getOpenFileName() into a variable named _ made _ local to
select_ytdlp_path(), so the i18n _() calls earlier in the function
raised UnboundLocalError before the file dialog could open, breaking
manual binary selection entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:22:26 +02:00
Jaroslav Beneš a1d1f87b65 Scope cleanup routines to files created by the current download
cleanup_partial_files() deleted every *.part and *.fNNN.* file in the
whole download directory, and cleanup_subtitle_files() deleted any new
.vtt/.srt under it recursively - including files belonging to other
applications (e.g. a browser's own .part downloads in ~/Downloads).

Track every destination path yt-dlp reports for this download and
restrict both cleanup passes to those files and their .part/.ytdl
siblings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:22:05 +02:00
Jaroslav Beneš c54574224a Merge YTSage 5.2.0 history as upstream base for SageTube
Imports the full commit history of github.com/oop7/YTSage (MIT).
LICENSE keeps both copyright lines; upstream README preserved at
docs/UPSTREAM_README.md. Future syncs: git fetch upstream && git merge
upstream/main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 01:19:28 +02:00
Jaroslav Beneš 115969856c Initial commit 2026-07-24 23:00:53 +00:00
oop7 4bc43174b6 Release YTSage 5.2.0
Bump the project and package version from 5.2.0b to 5.2.0.
2026-07-01 13:42:10 +03:00
oop7 3a63e13639 Merge branch 'beta' 2026-07-01 13:41:07 +03:00
oop7 bc3e7fd4fd Persist speed limit settings to config
Speed limit settings (value and unit) are now loaded from config on app initialization and saved to config when updated. This ensures the user's speed limit preferences persist across app restarts.
2026-07-01 12:33:12 +03:00
oop7 dac298a524 Show audio codec details in format table
Highlight AC3/EAC3 surround-sound formats in the GUI format table and include channel/bitrate details where available. Also relax the audio-only filter so formats without filesize can still be shown.
2026-07-01 11:46:30 +03:00
oop7 72f9098a70 Persist and restore main window state
Restore window geometry/state on startup and save them on exit. Adds YTSageApp._load_window_state() and calls it during UI init; saves Base64-encoded geometry/state via ConfigManager when closing. Adds default config keys "window_geometry" and "window_state". Includes error handling and debug logging to avoid failures if stored values are invalid.
2026-06-14 20:19:40 +03:00
oop7 8c9966000e Improve save_path handling and use ConfigManager
Normalize path input to string, ensure parent directories are created (mkdir with parents=True), and check writability using the normalized path. Replace manual JSON file write with ConfigManager.set("download_path", ...) and add import for ConfigManager. Adjust logging to use logger.error for failures and replace logger.exception where appropriate. These changes improve robustness when saving the download path and centralize config persistence.
2026-06-14 17:59:48 +03:00
oop7 ec18262bba Bump version to 5.2.0b
Update package version from 5.0.10b to 5.2.0b in pyproject.toml and ytsage/__init__.py to reflect the new beta release and keep metadata in sync.
2026-06-14 17:54:03 +03:00
oop7 45d3cdbad4 Default generic_mode to True; preserve explicit False
Set the default generic_mode to True in the config manager and update GUI initialization to distinguish between a missing config and an explicit False. Replace usages of `ConfigManager.get(... ) or False` with a None check so that an explicit False value is respected. Changes made in ytsage/utils/ytsage_config_manager.py and GUI initializers in ytsage/gui/ytsage_gui_main.py and ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py.
2026-06-14 17:49:36 +03:00
oop7 831e66a5b9 Include readme-translations in MANIFEST
Add a recursive-include rule for the readme-translations directory to MANIFEST.in so translated README files are included in source distributions (sdist) and packaged with releases.
2026-05-25 14:04:32 +03:00
oop7 e76d47412d Correct path for localized README creation
Fix README contribution instructions to point new translations to the readme-translations/ directory. Previously the docs suggested creating README.<code>.md at the repo root; this change clarifies that localized files should be placed under readme-translations/README.<code>.md to keep translations organized.
2026-05-25 14:04:12 +03:00
oop7 d3c3094880 Fix translated README links to root
Update links in readme-translations/*.md so they point to the project root (prepend ../). Adjusted references for README.md, .github/CI_CD_README.md and LICENSE across the translation files so links resolve correctly from the readme-translations directory.
2026-05-25 14:02:55 +03:00
oop7 e1a2fe52c6 Fix README link path in Arabic translation
Update the English README link in readme-translations/README.ar.md to point to the repository root (../README.md) so the language table correctly references the top-level README.
2026-05-25 14:02:39 +03:00
oop7 2aa8c023f4 Point README links to readme-translations/
Update README links to reference translation files under the readme-translations/ directory. Adjusted the language list and the translations table to use readme-translations/ paths and corrected the English link to README.md. This organizes translation links to a dedicated folder.
2026-05-25 14:01:31 +03:00
oop7 938cc9be46 Bump version to 5.1.0
Update package version from 5.0.10b to 5.1.0 in pyproject.toml and ytsage/__init__.py to prepare for the 5.1.0 release.
2026-05-14 15:54:07 +03:00
oop7 9bf22a6b22 Merge branch 'beta' 2026-05-14 15:52:50 +03:00
oop7 d581754062 Fix image paths in README
Update README.md image src attributes to use 'branding/...' instead of '../branding/...', correcting broken links to the wordmark and various screenshots (main, Download-Settings, playlist, audio_format, Custom-Option) so images render properly in the repository's README.
2026-05-14 15:50:37 +03:00
oop7 0216f7be71 Polish Russian README translation
Refine the Russian README translation: update UI labels, headings and anchor IDs, translate screenshot alt texts and table entries, normalize installation and troubleshooting instructions, and improve wording/consistency across sections (installation, usage, features, networking, tools, localization and contribution). Small formatting and phrasing tweaks to make the Russian text clearer and more consistent.
2026-05-14 15:47:20 +03:00
oop7 39320db3a0 Polish README text, paths, and formatting
Editorial and structural updates to README.md and readme-translations/README.tr.md: fix image paths (use ../branding/ and forward slashes), standardize wording/capitalization (Application, Download, Install, etc.), reformat feature tables and installation sections for consistency, clarify install/update/run commands, improve macOS/Windows troubleshooting and antivirus guidance, adjust screenshot alt texts, and apply numerous minor localization and copy corrections in the Turkish translation.
2026-05-14 15:39:06 +03:00
oop7 3ba021dfbb Update Polish README translation and formatting
Apply copy edits and formatting fixes to readme-translations/README.pl.md: adjust wording and capitalization, standardize section headings and anchors (e.g. instalacja, funkcje, użycie), harmonize terminology (Download Settings, Generic Mode), fix tables and list formatting, update platform/executable descriptions and installation steps, revise screenshots alt text, clarify usage and troubleshooting instructions, and refresh localization & contributing sections for consistency.
2026-05-14 15:38:35 +03:00
oop7 ca176d7844 Refine README.ja.md translations and formatting
Polish Japanese README translation and formatting: update wording and punctuation for clarity, translate and normalize section headings and links, refine tables and feature lists, adjust installation and platform-specific instructions, update screenshot alt text and usage examples, and improve troubleshooting and localization entries for consistency.
2026-05-14 14:56:52 +03:00