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>
This commit is contained in:
parent
8d1c274075
commit
375a24262c
8 changed files with 1428 additions and 1151 deletions
54
src/app.rs
54
src/app.rs
|
|
@ -88,6 +88,10 @@ pub struct App {
|
|||
dl_music_mode: bool,
|
||||
/// Record an ongoing live stream from the start instead of joining live.
|
||||
dl_live: bool,
|
||||
/// For Twitch channel URLs: target the `/clips` listing instead of the
|
||||
/// default profile (VODs / past broadcasts). Has no effect on other
|
||||
/// platforms or on single-video URLs.
|
||||
dl_twitch_clips: bool,
|
||||
textures: HashMap<PathBuf, Option<egui::TextureHandle>>,
|
||||
thumb_request_tx: Sender<PathBuf>,
|
||||
thumb_result_rx: Receiver<(PathBuf, Option<egui::ColorImage>)>,
|
||||
|
|
@ -225,6 +229,7 @@ impl App {
|
|||
dl_quality: DownloadQuality::Best,
|
||||
dl_music_mode: false,
|
||||
dl_live: false,
|
||||
dl_twitch_clips: false,
|
||||
textures: HashMap::new(),
|
||||
thumb_request_tx,
|
||||
thumb_result_rx,
|
||||
|
|
@ -947,11 +952,32 @@ impl App {
|
|||
so yt-dlp records from the beginning instead of joining mid-stream. \
|
||||
Also waits if the stream hasn't begun yet.",
|
||||
);
|
||||
if info.platform == Platform::Twitch
|
||||
&& matches!(info.kind, UrlKind::Channel { .. })
|
||||
{
|
||||
ui.checkbox(&mut self.dl_twitch_clips, "Clips only (Twitch)")
|
||||
.on_hover_text(
|
||||
"Pull only the channel's Clips section instead of the \
|
||||
default VOD/highlights mix. Rewrites the URL to \
|
||||
twitch.tv/<channel>/clips before submitting.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let ready = !self.dl_url.trim().is_empty();
|
||||
if ui.add_enabled(ready, egui::Button::new("⬇ Start download")).clicked() {
|
||||
let url = self.dl_url.trim().to_string();
|
||||
let mut url = self.dl_url.trim().to_string();
|
||||
// Twitch clips-only: rewrite `twitch.tv/<user>` to
|
||||
// `twitch.tv/<user>/clips`. yt-dlp's TwitchClips extractor
|
||||
// handles the rest and we still classify it as Channel so
|
||||
// the output folder is unchanged.
|
||||
if self.dl_twitch_clips
|
||||
&& info.platform == Platform::Twitch
|
||||
&& matches!(info.kind, UrlKind::Channel { .. })
|
||||
&& !url.contains("/clips")
|
||||
{
|
||||
url = format!("{}/clips", url.trim_end_matches('/'));
|
||||
}
|
||||
let dest = dest_preview.clone();
|
||||
if self.dl_music_mode {
|
||||
self.downloader.start_music(url);
|
||||
|
|
@ -1708,6 +1734,16 @@ impl App {
|
|||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(date) = video.upload_date.as_deref().map(format_upload_date) {
|
||||
if !date.is_empty() {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("📅 {date}"))
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
ui.label(egui::RichText::new("·").weak());
|
||||
}
|
||||
}
|
||||
if let Some(secs) = video.duration_secs {
|
||||
ui.label(egui::RichText::new(format_duration(secs)).small().weak());
|
||||
ui.label(egui::RichText::new("·").weak());
|
||||
|
|
@ -2076,6 +2112,12 @@ impl App {
|
|||
ui.label(egui::RichText::new("·").weak());
|
||||
}
|
||||
ui.label(egui::RichText::new(&card.id).small().monospace().weak());
|
||||
if let Some(date) = card.upload_date.as_deref().map(format_upload_date) {
|
||||
if !date.is_empty() {
|
||||
ui.label(egui::RichText::new("·").weak());
|
||||
ui.label(egui::RichText::new(date).small().weak());
|
||||
}
|
||||
}
|
||||
if let Some(secs) = card.duration_secs {
|
||||
ui.label(egui::RichText::new("·").weak());
|
||||
ui.label(egui::RichText::new(format_duration(secs)).small().weak());
|
||||
|
|
@ -2306,6 +2348,16 @@ fn format_duration(secs: f64) -> String {
|
|||
if h > 0 { format!("{h}:{m:02}:{s:02}") } else { format!("{m}:{s:02}") }
|
||||
}
|
||||
|
||||
/// Convert yt-dlp's native `YYYYMMDD` upload-date string into `YYYY-MM-DD`
|
||||
/// for display. Returns an empty string when input doesn't look like a date
|
||||
/// so callers can decide whether to skip the row entirely.
|
||||
fn format_upload_date(yyyymmdd: &str) -> String {
|
||||
if yyyymmdd.len() < 8 || !yyyymmdd.chars().all(|c| c.is_ascii_digit()) {
|
||||
return String::new();
|
||||
}
|
||||
format!("{}-{}-{}", &yyyymmdd[..4], &yyyymmdd[4..6], &yyyymmdd[6..8])
|
||||
}
|
||||
|
||||
fn format_size(bytes: u64) -> String {
|
||||
if bytes >= 1_073_741_824 {
|
||||
format!("{:.1} GB", bytes as f64 / 1_073_741_824.0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue