Commit graph

18 commits

Author SHA1 Message Date
Luna
d797f2a698 Cross-compile groundwork: make the tree build for Windows (3.1)
`cargo check --target x86_64-pc-windows-gnu` is now green (only the
upstream egui f32:From<f64> warnings). The Linux-only desktop deps are
target-gated so the rest of the stack — which already cross-compiles —
can build off Linux:

- Cargo.toml: ksni and rfd's xdg-portal backend move to
  [target.'cfg(target_os = "linux")'.dependencies]; non-Linux gets rfd
  with its native Win32/AppKit backend (default features).
- tray.rs: cfg-split. The ksni/SNI implementation stays Linux-only; other
  OSes get a no-op `start() -> None`, i.e. windowed-only (identical to the
  Linux no-SNI-host path). TrayEvent/TrayHandle stay cross-platform.
- plex.rs: add a Windows `symlink_file` arm to make_symlink (also fixes an
  unused-variable warning on non-unix).

The rest was already portable (disk_space/statvfs, mpv IPC UnixStream,
chmod/PermissionsExt guards all cfg(unix)). macOS is Unix so those apply
there unchanged. Still not a *shipped* binary: needs a real per-OS tray
backend and a linking CI matrix. ROADMAP 3.1 + CLAUDE.md updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 04:22:18 -07:00
Luna
9ba375f7b1 Add a renderer escape hatch (wgpu default, glow fallback)
The desktop forces the wgpu/Vulkan renderer because glow/OpenGL crashes
on NVIDIA + Wayland maximize. But on some systems wgpu creates the window
and never presents a frame, so nothing appears. Keep wgpu the default but
compile glow back in and let it be selected via `--renderer glow` or
`YT_OFFLINE_RENDERER=glow` for those machines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 00:04:24 -07:00
Luna
5cb011a13d Fix crash on maximize: switch eframe to wgpu (Vulkan) renderer
Maximizing the window crashed the GUI immediately with:

  Error: Glutin(Error { raw_code: Some(12291), kind: OutOfMemory })

12291 is EGL_BAD_ALLOC. The default eframe `glow` (OpenGL) renderer
fails to reallocate its surface when the window resizes to maximized
dimensions on NVIDIA + Wayland — a well-known fragility of NVIDIA's
GL-on-Wayland path. It surfaces as a clean Err from run_native (not a
panic), which is why nothing landed in yt-offline.crash.log.

Switch to the wgpu (Vulkan) renderer, which reconfigures the swapchain
cleanly on resize:
- Cargo.toml: drop eframe's default glow feature, enable wgpu, keep
  accesskit / default_fonts / wayland / x11.
- main.rs: NativeOptions.renderer = Renderer::Wgpu.

Verified on the NVIDIA GTX 1650 SUPER + Wayland session that triggered
the crash: app launches and maximizes cleanly, no EGL error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 05:14:06 -07:00
Luna
c1bd88d800 Stability hardening: crash.log + disk-full preflight (2.5)
Two independent improvements from Phase 2.5.

## Panic hook → crash.log

GUI users launched from a .desktop file have no stderr — today panics
vanish without trace. Install a panic::set_hook early in main() that
appends a structured entry to <library_root>/yt-offline.crash.log for
every panic from every thread (UI, axum workers, downloader spawns,
tray bg thread, thumbnail decode workers).

Each entry: ISO-8601 timestamp, thread name, file:line, panic message,
and a backtrace when RUST_BACKTRACE is set. The default panic hook
still runs after ours, so dev workflows keep getting the stderr trace.

Bounded: when the log passes 256 KB the next write rotates it to .1
(one previous generation kept). No external date library — small
civil-time formula in-tree.

## Disk-full preflight

Before yt-dlp ever launches, statvfs the target filesystem. If free
space is under 500 MB, push a synthetic Failed job classified as
DiskFull instead of starting yt-dlp. That way:

- Users see the actual problem ("only 230 MB free on /mnt/…") in the
  Downloads modal with the same "free up space and retry" hint the
  error-classifier emits for mid-download ENOSPC.
- No half-written file lands on disk.
- No 30-second yt-dlp warmup wasted on a doomed run.

statvfs returning None (non-Unix, missing path) skips the check —
better to let yt-dlp run than to refuse on no data.

5 new tests: 3 in crash (civil-time, log shape, real-panic capture),
2 in disk_space (statvfs plumbing + missing-path None). 74 pass total.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:53:56 -07:00
Luna
abe7d63794 Add system tray with minimize-to-tray (Tartube parity 1.6)
Uses ksni (StatusNotifierItem via zbus) so we keep our pure-Rust /
no-GTK stack — zbus is already pulled in by notify-rust and rfd.

Behavior:
- Tray icon shows the bundled icon.png; left-click brings the window
  back to focus
- Right-click menu: Show / Hide / Quit
- Clicking the window's X button minimizes to tray instead of exiting
- Ctrl+Q (or the tray's Quit item) actually exits

Failure modes are silent and non-blocking: no DBus → app behaves as if
tray flag was off. No StatusNotifier host (e.g. GNOME without the
AppIndicator extension) → icon invisible but app still runs normally,
and the X-button close cancellation only fires when the tray spawn
returned a handle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:17:10 -07:00
Luna
0d7ae83873 Web UI: move downloads from bottom bar to ⬇ modal
The fixed bottom #jobs bar ate up to 40vh of screen real estate even
when only one job was running, and on mobile it competed with the URL
input. Replace it with a ⬇ button in the header that:

- shows a badge with active+queued count (turns accent-colored when >0)
- opens a modal listing all jobs when clicked (or with the 'd' shortcut)
- repaints live from the same WS/polling snapshot that drove the old bar

The footer URL input + quality picker stay where they are — that's the
entry point, not the status display.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:03:58 -07:00
Luna
9e64297da6 WebSocket job progress + mobile-responsive web UI
WebSocket progress
- New `/ws/progress` route using axum 0.7's `ws` feature. Upgrades to
  a WebSocket that streams the same `ProgressResponse` payload as the
  HTTP polling endpoint, pushed instantly whenever a job moves.
- `WebState.progress_tx` is a `tokio::sync::broadcast::Sender<String>`
  (capacity 16, lossy — Lagged subscribers resync on the next tick).
- A background tokio task ticks every 500 ms while any download is
  running or queued, every 5 s when idle. Skips the JSON encode when
  no subscribers are listening, so a single-user install pays zero
  CPU for the broadcast when the browser tab is closed.
- JS replaces the per-interval HTTP poll with a WebSocket connection.
  The polling path stays as fallback for mobile carriers / reverse
  proxies that strip WS — when the socket closes or fails to open,
  schedulePoll() resumes immediately and a reconnect attempt fires
  after 5 s.
- Initial snapshot is sent right after subscribe so the UI is live
  before the next tick.

Mobile-responsive web UI
- Existing `@media(max-width:640px)` block tightened:
  - Hide the H1, AGPL footer notice, sort dropdown, and library-size
    counter — they don't fit and aren't actionable.
  - Header buttons get min 34×34 hit targets (Apple HIG / Material).
  - `.card-foot` wraps and grows to 34×34 buttons so the five action
    icons (play / watched / favourite / bookmark / waiting) stay
    tappable instead of cramming into thumb-unfriendly 22 px chips.
  - Modals go full-screen (no 10 px page-margin) so the iPhone safe
    area doesn't crop content.
  - Footer (URL bar + download button) wraps onto two rows so neither
    squeezes the other off-screen.
  - Bulk-actions toolbar wraps and gets bigger buttons.
  - Per-job stdout preview hides on small screens.
- New `@media(max-width:380px)` collapses the grid to single column
  for very narrow phones.

55 unit tests still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:59:48 -07:00
Luna
375a24262c Upload date in UI; extract HTML_UI; Twitch clips mode; DB connection pool
Four nice-to-have items, plus surfacing the upload date in both UIs
since the user explicitly asked.

Upload date display
- Desktop: card meta row now includes a `YYYY-MM-DD` cell between the
  ID and the duration; detail panel header shows it as `📅 YYYY-MM-DD`.
- Web: video player modal title gains a date+duration subline so the
  viewer has context without opening the metadata pane (card meta
  already shows the date).
- New `format_upload_date()` helper on the desktop side; the web side
  reuses the existing `fmtDate()` JS.

Extract HTML_UI to include_str!
- `LOGIN_HTML` and `HTML_UI` moved out of `src/web.rs` into
  `src/web_ui/login.html` and `src/web_ui/index.html`. The Rust file
  drops from 2915 lines to 1806; editors now syntax-highlight the
  HTML/CSS/JS, and UI diffs no longer overwhelm the Rust diff view.
- `include_str!` bakes them into the binary at compile time, so there
  is no runtime fs lookup and no extra deploy artifacts.

Twitch clips-only mode
- `classify_twitch` now recognises `twitch.tv/<user>/clips` as a
  Channel URL (was Unknown), so yt-dlp's TwitchClips extractor is
  reachable from our normal download pipeline.
- Both UIs gain a "Clips only" toggle visible only when the URL is
  a Twitch channel (not a specific VOD/clip). On submit, the URL is
  rewritten to `twitch.tv/<user>/clips` before dispatch.
- 1 new platform test covers the clips-listing classification.

DB connection pool (r2d2_sqlite)
- `Database` now wraps an `r2d2`-managed SQLite pool. File-backed
  databases get up to 4 connections; the in-memory fallback is capped
  at 1 (since each in-memory connection is a fresh empty DB by default
  in SQLite).
- `WebState.db` drops its outer `Mutex` — concurrent handlers each
  check out their own connection from the pool. Static-file fetches
  through `/files/` no longer serialize on the watched/positions/
  password lookups that the auth middleware performs.
- Pool init failures are converted to `rusqlite::Error` so the
  existing `Result<T>` return types still work.

47 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 01:17:08 -07:00
Luna
1d72069913 Add full-scan mode, persist password in DB, fix channel re-check URLs, SRT subs, cookie clear, UI fixes
- Full-scan download toggle: omits --break-on-existing so all unarchived
  videos are fetched regardless of order, filling gaps in partial archives
- Filter "Aborting remaining downloads" from job logs (break-on-existing noise)
- Password hash moved from config.toml to SQLite settings table; survives
  git checkouts and rebuilds. config.toml and yt-offline.db added to .gitignore;
  config auto-generated on first run if absent
- Channel re-check URLs derived from folder name (/@handle or /channel/UCxxx)
  instead of info.json's canonical channel_url, preventing duplicate UCxxx folders
- SRT subtitles detected by library scanner and served via /api/sub-vtt/* endpoint
  that converts to WebVTT on the fly; browser <track> element can now play them
- Cookie clear button in both desktop and web UI (DELETE /api/cookies)
- Per-job X close button on finished download jobs in desktop GUI
- Fix widget ID clash when multiple download jobs are shown (push_id per job)
- Watched videos excluded from Continue Watching list and badge count
- settings table added to SQLite schema (key/value store)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 05:49:55 -07:00
Luna
1ef3fe56c6 Add web themes, network binding, UI auth, library maintenance; optimize + fix rescan
Features:
- Port all 7 desktop themes (Dracula, Trans, Emo variants, etc.) to the web UI
- Configurable bind interface (localhost/Tailscale/LAN/all) with detected IPs,
  settable from both the web UI and desktop GUI
- Optional password that gates the whole UI and all API access via session
  cookies (Argon2), with a login page, logout, and middleware; settable from
  both UIs
- Library maintenance: scan for duplicate video IDs and missing assets
  (thumbnail/info.json/description), remove duplicates (scan-and-confirm, with
  out-of-library path guard), and re-fetch missing sidecars via yt-dlp.
  Surfaced in both the web UI and a desktop GUI window.

Optimizations / cleanup:
- Cache has_chapters at scan time (library.rs) instead of re-reading and
  parsing every info.json on each /api/library request
- Use the cached channel size in get_library
- Pool a single SQLite connection in WebState instead of opening one per request
- Remove a 1.2 GB stray nested src/yt-offline duplicate tree

Fixes:
- Rescan no longer clears all thumbnail textures (caused thumbnails to unload);
  prune only removed entries and bump library_generation so the card grid
  refreshes correctly

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 02:58:09 -07:00
Luna
74b3efd990 Major update with lots of new features and fixes 2026-05-17 05:36:04 -07:00
Luna
8b4796b518 Fix web UI, sort labels, add channel right-click menu
Web UI:
- Replace SSE with simple 600ms polling (GET /api/progress) — SSE added
  complexity with no real benefit for a local tool and was the source of
  connection failures
- Poll task moved into request handler (dl.poll() on each /api/progress call)
  so no background goroutine needed; simplifies state greatly
- Remove tokio-stream dependency (no longer needed)
- Fix: create channels dir before opening DB in web serve()
- Rescan also refreshes watched set from DB
- Bulk watched, sort, channel download button all working in web UI

Sort labels:
- Rename "Dur ↑" / "Dur ↓" → "Shortest" / "Longest"
- Rename "Size ↑" / "Size ↓" → "Smallest" / "Largest"

Channel right-click context menu:
- Right-click any channel in sidebar → "Check for new videos" starts a
  yt-dlp job on the stored channel_url from its info.json
- Also shows "Open folder" shortcut
- Grayed out with tooltip if no URL is stored yet (channel has no info.json)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-14 07:25:54 -07:00
Luna
56f1f1bd80 Fix PKGBUILD and notify-rust backend to eliminate libdbus build dep
- notify-rust: switch feature d → z (zbus pure-Rust backend); removes the
  system libdbus/pkgconf dependency that caused makepkg build failure
- PKGBUILD: fix license GPL3 → AGPL-3.0-only, makedepends rustup → rust

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-14 07:10:17 -07:00
Luna
80cce00b99 Add web UI, AGPL3, notifications, continue watching, bulk watched, storage stats, scheduler
License:
- GPL3 → AGPL3

New features:
- Web interface (--web [port]): axum/tokio server with SSE progress, library browser,
  download trigger, watched toggle; embedded single-page HTML/JS UI
- Desktop notifications via notify-rust when downloads complete or fail
- Continue Watching sidebar entry showing videos with saved resume positions
- Bulk select mode: select multiple videos, mark all watched/unwatched at once
- Storage stats: channel disk usage shown in sidebar next to video count
- Scheduled channel checks: configurable interval (hours), auto re-downloads all
  tracked channels using channel_url from info.json; enabled in Settings

Theme fixes:
- Scene Queen: remove override_text_color so active button text uses fg_stroke
  (dark navy on neon green) instead of near-white — fixes unreadable contrast
- Trans: override_text_color → hot pink #cc0066 for pink-tinted text throughout

Settings additions:
- Auto-check channels toggle + interval_hours DragValue
- Web UI port setting

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-11 04:27:04 -07:00
Luna
5b3b8fc901 Add watched tracking, resume positions, sort, density slider, channel metadata, mpv IPC, browser selector, auto-rescan
- SQLite DB for watched/position persistence (database.rs)
- Mark watched toggle on cards and detail panel
- mpv IPC socket integration for resume position tracking (unix)
- Sort by title, duration, file size
- Thumbnail density slider in top bar
- Channel metadata banner (subscriber count, uploader, channel URL)
- Cookie browser selector dropdown in settings
- Auto-rescan library when a download job finishes
- Fix yt-dlp --no-progress-bar flag (does not exist; removed)
- Browser field wired through config → downloader

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-11 03:47:29 -07:00
Luna
3a69628b42 changed it lol 2026-05-11 03:19:53 -07:00
Luna
abf3af5768 First version of youtube-backup, a tool to backup your YouTube channel. 2026-05-11 02:33:52 -07:00
luna
acf188738a Add the Rust/egui app sources
- library.rs: scans channels/<name>/ into channels + videos by filename stem
- downloader.rs: runs yt-dlp in a background thread, streams progress to the UI
- app.rs / main.rs: channel sidebar, searchable thumbnail list, detail/description
  panel, downloads panel; plays videos via mpv (falls back to xdg-open)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 01:35:22 -07:00