1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

feat: add video quality selection for yt-dlp captures (#17)

- ytdlp::download() accepts quality: Option<&str>; quality_format()
  maps best/1080p/720p/480p/360p to yt-dlp -f format strings
- perform_capture() threads quality through to the downloader
- CaptureBody gains optional quality field; capture_handler validates
  it against the allowlist (400 on unknown values) before spawning
- CLI passes None (preserves existing best-quality behaviour)
- Frontend: isVideoSource() mirrors determine_source() exactly —
  shows quality picker only for yt-dlp-backed sources, excludes
  playlist/channel shorthands and tweet/thread paths
- submitCapture(archiveId, locator, quality) sends quality in POST body
- CSS: .capture-quality styles the inline select to fit the capture row
- Tests: quality_format unit tests in ytdlp.rs; two new route tests
  (valid quality accepted, invalid quality rejected with 400)
- Docs: video quality section added under Supported Platforms
This commit is contained in:
TheGeneralist 2026-07-05 20:42:41 +02:00 committed by GitHub
parent 84c5730b6a
commit b8e496457f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 689 additions and 89 deletions

View file

@ -331,6 +331,26 @@ fn determine_source(path: &str) -> Source {
Source::Other
}
/// Resolves `locator` to the canonical URL that yt-dlp should receive, or
/// returns `None` if the locator does not map to a yt-dlp-downloadable source
/// (e.g. tweet/thread shorthands, web pages, local files, playlists/channels).
///
/// Use this to gate the probe endpoint: only call `fetch_metadata` when this
/// returns `Some`.
pub fn locator_to_ytdlp_url(locator: &str) -> Option<String> {
let source = determine_source(locator);
match source {
Source::YouTubeVideo
| Source::X
| Source::Instagram
| Source::Facebook
| Source::TikTok
| Source::Reddit
| Source::Snapchat => Some(expand_shorthand_to_url(locator, &source)),
_ => None,
}
}
fn hash_exists(hash: &str, file_extension: &str, store_path: &Path) -> Result<bool> {
let path = store_path.join(raw_relative_path_from_hash(hash, file_extension)?);
Ok(path.exists())
@ -427,20 +447,6 @@ fn local_file_extension(path: &str) -> String {
.map_or(String::new(), |ext| format!(".{}", ext.to_string_lossy()))
}
fn media_file_extension(source: Source, path: &str) -> String {
match source {
Source::YouTubeVideo
| Source::X
| Source::Instagram
| Source::Facebook
| Source::TikTok
| Source::Reddit
| Source::Snapchat => ".mp4".to_string(),
Source::Local => local_file_extension(path),
_ => String::new(),
}
}
fn tweet_id_from_archive_path(path: &str) -> Option<String> {
path.split(':').next_back().and_then(parse_tweet_id)
}
@ -677,6 +683,7 @@ pub fn perform_capture(
archive_paths: &ArchivePaths,
locator: &str,
archive_id: Option<&str>,
quality: Option<&str>,
) -> Result<CaptureResult> {
// Append a UUID so parallel captures starting in the same millisecond
// never collide on the staging directory or file names.
@ -1015,7 +1022,7 @@ pub fn perform_capture(
_ => None,
};
let hash = match source {
let (hash, file_extension) = match source {
Source::YouTubeVideo
| Source::X
| Source::Instagram
@ -1023,8 +1030,8 @@ pub fn perform_capture(
| Source::TikTok
| Source::Reddit
| Source::Snapchat => {
match downloader::ytdlp::download(path.clone(), store_path, &timestamp) {
Ok(h) => h,
match downloader::ytdlp::download(path.clone(), store_path, &timestamp, quality) {
Ok(result) => result,
Err(e) => {
return Err(fail_run(
&conn,
@ -1037,7 +1044,7 @@ pub fn perform_capture(
}
Source::Local => {
match downloader::local::save(path.clone(), store_path, &timestamp) {
Ok(h) => h,
Ok(h) => (h, local_file_extension(&path)),
Err(e) => {
return Err(fail_run(
&conn,
@ -1058,8 +1065,6 @@ pub fn perform_capture(
}
_ => unreachable!(),
};
let file_extension = media_file_extension(source, &path);
let temp_file = store_path
.join("temp")
.join(&timestamp)