feat: subtitles, delete-all, auto refresh, and menu cleanup
Subtitles: a language preference in Settings drives what is shown and what is downloaded. yt-dlp saves WebVTT sidecars next to each download, including YouTube's auto-generated track, and the player attaches them as <track> elements so they work offline. Sidecars are removed with their video, and by Delete all. WebVTT rather than muxed subtitle streams because WebKit reads a <track> reliably and largely ignores subtitle tracks inside an MP4. Downloads in progress now appear under the downloaded-only filter, so a download you just started does not vanish from the list you are watching it in. That view also gains a Delete all, behind a confirmation naming what goes. The feed refreshes on launch and whenever the player closes, so the Refresh button is only for staleness. Player: Delete moved to the footer and shortened, Open on YouTube is now an external-link icon. Removes the Edit and Help menus. Cut/Copy/Paste move to the app menu, without which their shortcuts would stop working in the search field. The webview context menu is suppressed outside text fields — its Reload and Back items act on a page the app does not present as one. Settings now warns that 4K AV1 plays back with artefacts: the files decode cleanly in ffmpeg, so it is the built-in decoder, not the download.
This commit is contained in:
@@ -690,6 +690,7 @@ async fn cache_thumbnails(state: &State<'_, AppState>) {
|
||||
pub async fn download_video(
|
||||
video_id: String,
|
||||
quality: String,
|
||||
sub_langs: String,
|
||||
app: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
@@ -723,7 +724,7 @@ pub async fn download_video(
|
||||
.join(downloader::OUTPUT_TEMPLATE)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let mut args = downloader::build_args(&video_id, &out_template, &quality);
|
||||
let mut args = downloader::build_args(&video_id, &out_template, &quality, &sub_langs);
|
||||
// Without this yt-dlp looks for ffmpeg on PATH, which a bundled app has no
|
||||
// reason to have. Merging video and audio would fail on a clean machine.
|
||||
args.push("--ffmpeg-location".into());
|
||||
@@ -922,6 +923,55 @@ async fn cleanup_partials(library: PathBuf, video_id: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// WebVTT files yt-dlp wrote beside a download, as (language, path) pairs.
|
||||
#[tauri::command]
|
||||
pub async fn list_subtitles(
|
||||
video_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<(String, String)>, String> {
|
||||
let library = state.library.lock().await.clone();
|
||||
let Ok(mut entries) = tokio::fs::read_dir(&library).await else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if !name.contains(&video_id) || !name.ends_with(".vtt") {
|
||||
continue;
|
||||
}
|
||||
// yt-dlp names them "<base>.<lang>.vtt".
|
||||
let lang = name
|
||||
.trim_end_matches(".vtt")
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
out.push((lang, entry.path().to_string_lossy().to_string()));
|
||||
}
|
||||
out.sort();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Removes every download and the files behind them, including subtitles.
|
||||
#[tauri::command]
|
||||
pub async fn delete_all_downloads(state: State<'_, AppState>) -> Result<usize, String> {
|
||||
let paths = state.db.lock().await.all_download_paths()?;
|
||||
for p in &paths {
|
||||
let _ = tokio::fs::remove_file(p).await;
|
||||
}
|
||||
// Subtitle sidecars are not tracked in the database, so sweep them here.
|
||||
let library = state.library.lock().await.clone();
|
||||
if let Ok(mut entries) = tokio::fs::read_dir(&library).await {
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.ends_with(".vtt") || name.contains(".part") || name.ends_with(".ytdl") {
|
||||
let _ = tokio::fs::remove_file(entry.path()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
state.db.lock().await.clear_all_downloads()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_download(
|
||||
video_id: String,
|
||||
@@ -931,7 +981,17 @@ pub async fn delete_download(
|
||||
if let Some(p) = path {
|
||||
let _ = tokio::fs::remove_file(&p).await;
|
||||
}
|
||||
cleanup_partials(state.library.lock().await.clone(), &video_id).await;
|
||||
let library = state.library.lock().await.clone();
|
||||
cleanup_partials(library.clone(), &video_id).await;
|
||||
// The .vtt sidecars belong to the video, so they go with it.
|
||||
if let Ok(mut entries) = tokio::fs::read_dir(&library).await {
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.contains(&video_id) && name.ends_with(".vtt") {
|
||||
let _ = tokio::fs::remove_file(entry.path()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
state.db.lock().await.clear_download(&video_id)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user