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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Refine and standardize wording in readme-translations/README.de.md and README.it.md. Improvements include clearer UI labels (Analyze/Download), consistent terminology for installation and features, corrected language names, updated tables and formatting, improved screenshot alt texts, and various grammar/translation fixes to align both locales with the main README for clarity and consistency.
Copy edits and consistency fixes for translated README files in readme-translations (Spanish, Hindi and Indonesian). Changes include corrected wording and grammar, standardized headings and labels (installation, usage, settings), localized UI/alt text updates, clarified install/update instructions, unified terminology (e.g. Generic Mode, FFmpeg/yt-dlp detection), and various formatting improvements across language tables and troubleshooting notes. Pure documentation updates only.
Refine Arabic localization and formatting in readme-translations/README.ar.md. Updates include clearer word choices (e.g., تحميل vs تنزيل), consistent UI label translations (Analyze/Download), corrected language names and links, improved table layouts, localized installation and troubleshooting instructions, updated screenshot alt texts, and additional notes about playlist export and Generic Mode. These changes tidy wording and improve consistency for Arabic readers.
For playlist downloads, treat an item as completed if a file was created even when the downloader returned a non-zero code. Emit 100% progress for such cases and update the status to "download.completed (with some errors)" when return_code != 0, otherwise keep the normal completed status. This avoids reporting false failures for playlist items that produced output despite errors.
When downloading playlists, add --ignore-errors and --no-abort-on-error so a single failure won't stop the entire playlist download. Also normalize values stored in the version cache: coerce version_info to a string (or empty string) and path to a string or None to ensure consistent types when reading/writing cached tool metadata.
Downloader: add detection for "Auto-generated" subtitle selection and only append --write-auto-subs when an auto-generated subtitle is explicitly chosen (prevents always enabling the flag when any subtitles are requested). GUI: update subtitle label to display the number of selected subtitles when >0, falling back to the zero-selected message otherwise. Small UX and behavior fixes in DownloadThread and AnalysisMixin.
When building the output template for single-file downloads, remove any playlist-specific preamble (e.g. '%(playlist_index)s - ') from filename_part. Adds a regex-based substitution to clean up leftover playlist_index placeholders so single downloads don't get playlist-style prefixes in their filenames.
Enhance playlist export output for Text and CSV formats. Text files now include a numeric index, a title fallback ("Video N"), and a human-readable duration ([H:MM:SS] or [MM:SS]) when available before the URL. CSV export adds a "Playlist Index" column and writes a formatted title with index and duration. Duration parsing is protected against non-integer values. M3U and JSON export behavior unchanged.
Add two new localization keys across multiple language files: "remember_settings" (cookie dialog) and "private_video" (download/error message) to inform users about remembering cookie settings and private videos. Also fix minor formatting in en.json (split combined line for open_folder/save_playlist/reset). Updated locale files: ar, de, en, es, fr, hi, id, it, ja, pl, pt, ru, tr, zh.