Skip to content

various: replace yt-dlp with custom implementation - #616

Open
temidaradev wants to merge 1 commit into
masterfrom
custom-installer
Open

various: replace yt-dlp with custom implementation#616
temidaradev wants to merge 1 commit into
masterfrom
custom-installer

Conversation

@temidaradev

@temidaradev temidaradev commented Aug 8, 2026

Copy link
Copy Markdown
Member

uhh yeah so yt-dlp was stupid

Sanity Checking

  • I have read and followed the contribution guidelines.
  • My commits follow Kopuz's scoped commit convention and history hygiene
    rules.
  • I have disclosed any AI assistance as required by the AI policy in the
    contribution guidelines, or this pull request did not use AI assistance.
  • I have tested and self-reviewed my changes.

Style and Consistency

  • My changes are consistent with the existing crate boundaries and Dioxus
    style.
  • I ran cargo fmt --all --check or cargo fmt --all as appropriate.
  • I ran cargo clippy --workspace --all-targets -- -D warnings, or
    explained why it could not be run.
  • I kept generated assets, translations, and packaging files in sync when
    this change depends on them.

Testing

  • I ran the smallest relevant verifier for this change.
  • I documented any platform or verifier that I could not run.

Tested on platform(s):

  • x86_64-linux
  • aarch64-linux
  • x86_64-darwin
  • aarch64-darwin
  • Windows
  • Android
  • iOS

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Native YouTube Music downloads replace the yt-dlp workflow. The change adds native search and stream resolution, download jobs, format conversion, metadata and artwork handling, updated configuration and routing, localized UI strings, documentation, and packaging dependencies.

Changes

Native YouTube download flow

Layer / File(s) Summary
Configuration and application wiring
crates/config/src/lib.rs, crates/server/..., crates/kopuz_route/..., crates/kopuz/..., crates/components/...
Configuration types, routes, sidebar entries, migrations, and application wiring now use YouTube download names. Legacy configuration keys remain readable.
Download jobs and media processing
crates/pages/src/youtube_download_jobs.rs
The new job subsystem resolves streams, downloads with retries and fallback streaming, validates output paths, processes formats with ffmpeg, handles artwork and metadata, and records completion history.
YouTube downloads page
crates/pages/src/youtube_downloads.rs, crates/pages/src/lib.rs, crates/pages/Cargo.toml
The new page supports YouTube Music search, format and output selection, download options, progress states, result rendering, and history management.
Download localization
crates/i18n/locales/*.ftl
The old ytdlp_* messages are replaced with youtube_download_* messages for search, options, formats, statuses, and errors.
Documentation and distribution updates
README.md, docs/README-*.md, .github/ISSUE_TEMPLATE/*, CONTRIBUTING.md, packaging/...
Documentation and issue templates describe native YouTube downloads. Packaging removes yt-dlp and python-mutagen, adds ffmpeg, and grants required Music and Downloads write access.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant YoutubeDownloadsPage
  participant YoutubeDownloadClient
  participant YoutubeDownloadJobs
  participant ffmpeg
  User->>YoutubeDownloadsPage: Enter search query
  YoutubeDownloadsPage->>YoutubeDownloadClient: search(query)
  YoutubeDownloadClient-->>YoutubeDownloadsPage: Track results
  User->>YoutubeDownloadsPage: Start download
  YoutubeDownloadsPage->>YoutubeDownloadJobs: start_download(track, options)
  YoutubeDownloadJobs->>YoutubeDownloadClient: resolve_stream(video_id)
  YoutubeDownloadClient-->>YoutubeDownloadJobs: Stream information
  YoutubeDownloadJobs->>ffmpeg: Convert and embed metadata when required
  ffmpeg-->>YoutubeDownloadJobs: Output file
  YoutubeDownloadJobs-->>YoutubeDownloadsPage: Progress and completion status
Loading

Possibly related PRs

Suggested reviewers: umceko, notashelf

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly states that the pull request replaces yt-dlp with a custom implementation.
Description check ✅ Passed The description identifies the yt-dlp replacement and includes relevant contribution, style, and testing information.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packaging/flatpak/com.temidaradev.kopuz.json (1)

40-40: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep ffmpeg available to the Flatpak sandbox.

start_download() requires ffmpeg when conversion, metadata, or embedded artwork is requested, but the Flatpak manifest has no ffmpeg dependency. Add an ffmpeg module or the matching Flatpak extension/SDK extension so these options do not fail inside the app sandbox.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packaging/flatpak/com.temidaradev.kopuz.json` at line 40, Add an ffmpeg
module or compatible Flatpak/SDK extension to the manifest module list in
com.temidaradev.kopuz.json, ensuring the ffmpeg executable is available inside
the sandbox for start_download() conversion, metadata, and embedded-artwork
options.
🧹 Nitpick comments (3)
crates/i18n/locales/fil.ftl (1)

104-147: 🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

Complete the YouTube download translations in all supplied locales.

The new block is only partially localized. Most user-facing values remain English in each supplied non-English locale. Translate the messages while preserving Fluent keys and variable references.

  • crates/i18n/locales/fil.ftl#L104-L147: Translate the Filipino YouTube download messages.
  • crates/i18n/locales/fr.ftl#L87-L130: Translate the French YouTube download messages.
  • crates/i18n/locales/gr.ftl#L87-L130: Translate the Greek YouTube download messages.
  • crates/i18n/locales/he.ftl#L87-L130: Translate the Hebrew YouTube download messages.
  • crates/i18n/locales/hu.ftl#L87-L130: Translate the Hungarian YouTube download messages.
  • crates/i18n/locales/tr.ftl#L87-L130: Translate the Turkish YouTube download messages.
  • crates/i18n/locales/uk.ftl#L87-L130: Translate the Ukrainian YouTube download messages.
  • crates/i18n/locales/vi-VN.ftl#L104-L147: Translate the Vietnamese YouTube download messages.
  • crates/i18n/locales/zh-CN.ftl#L87-L130: Translate the Simplified Chinese YouTube download messages.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/i18n/locales/fil.ftl` around lines 104 - 147, Translate every
user-facing value in the YouTube download message block, preserving all Fluent
keys and variable references. Apply the appropriate Filipino translations in
crates/i18n/locales/fil.ftl lines 104-147, French in crates/i18n/locales/fr.ftl
lines 87-130, Greek in crates/i18n/locales/gr.ftl lines 87-130, Hebrew in
crates/i18n/locales/he.ftl lines 87-130, Hungarian in crates/i18n/locales/hu.ftl
lines 87-130, Turkish in crates/i18n/locales/tr.ftl lines 87-130, Ukrainian in
crates/i18n/locales/uk.ftl lines 87-130, Vietnamese in
crates/i18n/locales/vi-VN.ftl lines 104-147, and Simplified Chinese in
crates/i18n/locales/zh-CN.ftl lines 87-130; ensure no English source text
remains in these localized blocks.
crates/pages/src/youtube_downloads.rs (1)

457-464: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

A truncated failure message hides the reason for the failure.

Line 459 applies truncate to the status paragraph. For JobStatus::Failed, line 437 renders the raw error message, which can be long, for example the ffmpeg stderr text produced at crates/pages/src/youtube_download_jobs.rs line 705. The user sees a clipped line and has no way to read the rest.

Add a title attribute so the full text is available on hover.

♻️ Proposed fix
                     p {
                         class: if matches!(&job.status, JobStatus::Failed(_)) {
                             "text-red-400 text-xs mt-0.5 truncate"
                         } else {
                             "text-slate-500 text-xs mt-0.5"
                         },
+                        title: "{status}",
                         "{status}"
                     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pages/src/youtube_downloads.rs` around lines 457 - 464, Add a title
attribute to the status paragraph in the JobStatus rendering block so it
contains the full status text, including untruncated failure messages, while
preserving the existing truncate styling and status display.
crates/config/src/lib.rs (1)

1155-1185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the organize_by_album default in the legacy migration test.

The legacy JSON omits organize_by_album. That field is the only option with a non-false serde default (default_true). The test does not cover it, so a future change to the #[serde(default = "default_true")] attribute would silently flip every migrated config to false without failing this test.

♻️ Proposed additional assertion
         assert!(config.youtube_download_options.write_thumbnail);
+        assert!(config.youtube_download_options.organize_by_album);
+        assert!(!config.youtube_download_options.overwrite_existing);
         assert_eq!(config.youtube_download_history.len(), 1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/config/src/lib.rs` around lines 1155 - 1185, Extend
legacy_downloader_config_migrates_to_native_youtube_fields to assert that
config.youtube_download_options.organize_by_album remains true when the legacy
JSON omits the field, covering its default_true serde behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/i18n/locales/ar.ftl`:
- Around line 87-130: Translate the complete youtube_download_* message block,
preserving every Fluent key and variable, in crates/i18n/locales/ar.ftl lines
87-130, de.ftl lines 87-130, es.ftl lines 87-130, ro.ftl lines 87-130, ru.ftl
lines 87-130, sv.ftl lines 104-147, ta.ftl lines 104-147, tok-SP.ftl lines
87-130, and tok.ftl lines 87-130 using each locale’s native language or writing
system.

In `@crates/i18n/locales/id.ftl`:
- Around line 104-147: Translate every new YouTube download message in
crates/i18n/locales/id.ftl:104-147 into Indonesian;
crates/i18n/locales/it.ftl:104-147 into Italian;
crates/i18n/locales/ja.ftl:87-130 into Japanese;
crates/i18n/locales/ko.ftl:87-130 into Korean;
crates/i18n/locales/ml.ftl:104-147 into Malayalam;
crates/i18n/locales/nl.ftl:104-147 into Dutch; crates/i18n/locales/pl.ftl:87-130
into Polish; crates/i18n/locales/pt-BR.ftl:87-130 into Brazilian Portuguese; and
crates/i18n/locales/pt-PT.ftl:87-130 into European Portuguese. Preserve every
Fluent key, variable such as $percent, $speed, $eta, $error, and $path, format
identifiers, and the intended UI meaning.

In `@crates/pages/src/youtube_download_jobs.rs`:
- Around line 156-180: Make run_preflight_checks asynchronous from the download
button flow by moving its blocking duplicate-job, find_binary, and
validate_output_directory work into tokio::task::spawn_blocking. Await the
preflight result inside the existing spawn in the download click handler,
preserving the current validation errors and success behavior without blocking
the UI thread.
- Around line 563-584: Update the cover download flow around reqwest::get in
download_cover to use the existing bounded HTTP-client pattern, including
connect and total request timeouts. Enforce a maximum response body size before
collecting bytes, preserving the current success handling and warning/None
behavior for request and body failures.
- Around line 332-339: Track destination ownership in the download flow around
destination_path and the result error handling: record whether this job
successfully created or replaced the destination, and only remove destination on
failure when that flag is true. Preserve existing cleanup for source_path and
cover, while ensuring overwrite_existing failures never delete a pre-existing
user file.

In `@crates/pages/src/youtube_downloads.rs`:
- Around line 301-315: Update the button rendered by OptionToggle to include an
aria-pressed attribute bound to props.enabled, so assistive technology exposes
the toggle’s current state while preserving the existing click behavior and
styling.
- Around line 134-139: Remove the per-keystroke config mutation from the oninput
handler in the YouTube downloads form, keeping only page-error clearing, value
reading, and out_dir updates. Persist youtube_download_output_dir in an onblur
handler when the field loses focus, while leaving the folder picker’s existing
discrete config write unchanged.

---

Outside diff comments:
In `@packaging/flatpak/com.temidaradev.kopuz.json`:
- Line 40: Add an ffmpeg module or compatible Flatpak/SDK extension to the
manifest module list in com.temidaradev.kopuz.json, ensuring the ffmpeg
executable is available inside the sandbox for start_download() conversion,
metadata, and embedded-artwork options.

---

Nitpick comments:
In `@crates/config/src/lib.rs`:
- Around line 1155-1185: Extend
legacy_downloader_config_migrates_to_native_youtube_fields to assert that
config.youtube_download_options.organize_by_album remains true when the legacy
JSON omits the field, covering its default_true serde behavior.

In `@crates/i18n/locales/fil.ftl`:
- Around line 104-147: Translate every user-facing value in the YouTube download
message block, preserving all Fluent keys and variable references. Apply the
appropriate Filipino translations in crates/i18n/locales/fil.ftl lines 104-147,
French in crates/i18n/locales/fr.ftl lines 87-130, Greek in
crates/i18n/locales/gr.ftl lines 87-130, Hebrew in crates/i18n/locales/he.ftl
lines 87-130, Hungarian in crates/i18n/locales/hu.ftl lines 87-130, Turkish in
crates/i18n/locales/tr.ftl lines 87-130, Ukrainian in crates/i18n/locales/uk.ftl
lines 87-130, Vietnamese in crates/i18n/locales/vi-VN.ftl lines 104-147, and
Simplified Chinese in crates/i18n/locales/zh-CN.ftl lines 87-130; ensure no
English source text remains in these localized blocks.

In `@crates/pages/src/youtube_downloads.rs`:
- Around line 457-464: Add a title attribute to the status paragraph in the
JobStatus rendering block so it contains the full status text, including
untruncated failure messages, while preserving the existing truncate styling and
status display.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e1696255-261f-4f14-ba53-88d78106267b

📥 Commits

Reviewing files that changed from the base of the PR and between 0f8b355 and 6e0ffb8.

📒 Files selected for processing (53)
  • .github/ISSUE_TEMPLATE/feature_request.yml
  • .github/ISSUE_TEMPLATE/performance_report.yml
  • CONTRIBUTING.md
  • README.md
  • crates/components/src/normal/sidebar.rs
  • crates/components/src/vaxry/sidebar.rs
  • crates/config/src/lib.rs
  • crates/i18n/locales/ar.ftl
  • crates/i18n/locales/de.ftl
  • crates/i18n/locales/en.ftl
  • crates/i18n/locales/es.ftl
  • crates/i18n/locales/fil.ftl
  • crates/i18n/locales/fr.ftl
  • crates/i18n/locales/gr.ftl
  • crates/i18n/locales/he.ftl
  • crates/i18n/locales/hu.ftl
  • crates/i18n/locales/id.ftl
  • crates/i18n/locales/it.ftl
  • crates/i18n/locales/ja.ftl
  • crates/i18n/locales/ko.ftl
  • crates/i18n/locales/ml.ftl
  • crates/i18n/locales/nl.ftl
  • crates/i18n/locales/pl.ftl
  • crates/i18n/locales/pt-BR.ftl
  • crates/i18n/locales/pt-PT.ftl
  • crates/i18n/locales/ro.ftl
  • crates/i18n/locales/ru.ftl
  • crates/i18n/locales/sv.ftl
  • crates/i18n/locales/ta.ftl
  • crates/i18n/locales/tok-SP.ftl
  • crates/i18n/locales/tok.ftl
  • crates/i18n/locales/tr.ftl
  • crates/i18n/locales/uk.ftl
  • crates/i18n/locales/vi-VN.ftl
  • crates/i18n/locales/zh-CN.ftl
  • crates/kopuz/src/main.rs
  • crates/kopuz_route/src/lib.rs
  • crates/pages/Cargo.toml
  • crates/pages/src/lib.rs
  • crates/pages/src/youtube_download_jobs.rs
  • crates/pages/src/youtube_downloads.rs
  • crates/pages/src/ytdlp.rs
  • crates/pages/src/ytdlp_jobs.rs
  • crates/server/src/lib.rs
  • crates/server/src/youtube_download.rs
  • docs/README-ML.md
  • docs/README-PT-PT.md
  • docs/README-TR.md
  • packaging/aur/PKGBUILD
  • packaging/aur/PKGBUILD-bin
  • packaging/flatpak/com.temidaradev.kopuz.json
  • packaging/nix/crane.nix
  • packaging/nix/shell.nix
💤 Files with no reviewable changes (2)
  • crates/pages/src/ytdlp.rs
  • crates/pages/src/ytdlp_jobs.rs

Comment on lines +87 to +130
youtube_downloads = التنزيلات
youtube_download_title = YouTube Downloads
youtube_download_subtitle = Search YouTube Music and download with Kopuz
youtube_download_options = Options
youtube_download_search_placeholder = Song title, artist, or album…
youtube_download_search = Search
youtube_download_searching = Searching…
youtube_download_results = Search results
youtube_download_download = Download
youtube_download_output_dir_placeholder = Output directory (defaults to music folder)
youtube_download_pick_folder = Pick folder
youtube_download_clear_history = Clear history
youtube_download_empty_state = Search for a song to start
youtube_download_no_results = No songs found
youtube_download_jobs = Downloads
youtube_download_file_options = File options
youtube_download_embed_metadata = Embed metadata
youtube_download_embed_metadata_desc = Save title, artist, album, and source URL in the audio file
youtube_download_embed_thumbnail = Embed cover artwork
youtube_download_embed_thumbnail_desc = Embed when the format supports it; otherwise save a sidecar JPG
youtube_download_write_thumbnail = Save cover separately
youtube_download_write_thumbnail_desc = Write a JPG beside the downloaded audio
youtube_download_organize_album = Organize by album
youtube_download_organize_album_desc = Put downloads in an album-named folder
youtube_download_overwrite = Overwrite existing files
youtube_download_overwrite_desc = Replace a file with the same output name
youtube_download_ffmpeg_hint = Original downloads need no external downloader. ffmpeg is only required for conversion, metadata, or embedded artwork.
youtube_download_format_original = Original quality
youtube_download_format_mp3 = MP3
youtube_download_format_flac = FLAC
youtube_download_format_opus = OPUS
youtube_download_format_wav = WAV
youtube_download_status_resolving = Resolving native YouTube stream…
youtube_download_status_downloading_eta = { $percent }% { $speed } ETA { $eta }
youtube_download_status_downloading = { $percent }%
youtube_download_status_processing = Processing…
youtube_download_status_completed = Completed
youtube_download_status_waiting = Waiting…
youtube_download_error_search = Search failed: { $error }
youtube_download_error_ffmpeg_required = ffmpeg is required for this format or the selected file options. Install ffmpeg or choose Original quality and disable embedding.
youtube_download_error_duplicate_active = This song is already downloading.
youtube_download_error_output_not_directory = Output path is not a directory: { $path }
youtube_download_error_output_prepare = Failed to prepare output directory: { $error }
youtube_download_error_output_not_writable = Output directory is not writable: { $path }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Translate the native download block in all affected locales.

The new youtube_download_* messages are copied in English across these non-English locale files. This makes the native download workflow appear untranslated.

  • crates/i18n/locales/ar.ftl#L87-L130: add Arabic translations.
  • crates/i18n/locales/de.ftl#L87-L130: add German translations.
  • crates/i18n/locales/es.ftl#L87-L130: add Spanish translations.
  • crates/i18n/locales/ro.ftl#L87-L130: add Romanian translations.
  • crates/i18n/locales/ru.ftl#L87-L130: add Russian translations.
  • crates/i18n/locales/sv.ftl#L104-L147: add Swedish translations.
  • crates/i18n/locales/ta.ftl#L104-L147: add Tamil translations.
  • crates/i18n/locales/tok-SP.ftl#L87-L130: add sitelen pona translations.
  • crates/i18n/locales/tok.ftl#L87-L130: add toki pona translations.

Preserve all Fluent keys and variables.

📍 Affects 9 files
  • crates/i18n/locales/ar.ftl#L87-L130 (this comment)
  • crates/i18n/locales/de.ftl#L87-L130
  • crates/i18n/locales/es.ftl#L87-L130
  • crates/i18n/locales/ro.ftl#L87-L130
  • crates/i18n/locales/ru.ftl#L87-L130
  • crates/i18n/locales/sv.ftl#L104-L147
  • crates/i18n/locales/ta.ftl#L104-L147
  • crates/i18n/locales/tok-SP.ftl#L87-L130
  • crates/i18n/locales/tok.ftl#L87-L130
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/i18n/locales/ar.ftl` around lines 87 - 130, Translate the complete
youtube_download_* message block, preserving every Fluent key and variable, in
crates/i18n/locales/ar.ftl lines 87-130, de.ftl lines 87-130, es.ftl lines
87-130, ro.ftl lines 87-130, ru.ftl lines 87-130, sv.ftl lines 104-147, ta.ftl
lines 104-147, tok-SP.ftl lines 87-130, and tok.ftl lines 87-130 using each
locale’s native language or writing system.

Comment on lines +104 to +147
youtube_downloads = Unduhan
youtube_download_title = YouTube Downloads
youtube_download_subtitle = Search YouTube Music and download with Kopuz
youtube_download_options = Options
youtube_download_search_placeholder = Song title, artist, or album…
youtube_download_search = Search
youtube_download_searching = Searching…
youtube_download_results = Search results
youtube_download_download = Download
youtube_download_output_dir_placeholder = Output directory (defaults to music folder)
youtube_download_pick_folder = Pick folder
youtube_download_clear_history = Clear history
youtube_download_empty_state = Search for a song to start
youtube_download_no_results = No songs found
youtube_download_jobs = Downloads
youtube_download_file_options = File options
youtube_download_embed_metadata = Embed metadata
youtube_download_embed_metadata_desc = Save title, artist, album, and source URL in the audio file
youtube_download_embed_thumbnail = Embed cover artwork
youtube_download_embed_thumbnail_desc = Embed when the format supports it; otherwise save a sidecar JPG
youtube_download_write_thumbnail = Save cover separately
youtube_download_write_thumbnail_desc = Write a JPG beside the downloaded audio
youtube_download_organize_album = Organize by album
youtube_download_organize_album_desc = Put downloads in an album-named folder
youtube_download_overwrite = Overwrite existing files
youtube_download_overwrite_desc = Replace a file with the same output name
youtube_download_ffmpeg_hint = Original downloads need no external downloader. ffmpeg is only required for conversion, metadata, or embedded artwork.
youtube_download_format_original = Original quality
youtube_download_format_mp3 = MP3
youtube_download_format_flac = FLAC
youtube_download_format_opus = OPUS
youtube_download_format_wav = WAV
youtube_download_status_resolving = Resolving native YouTube stream…
youtube_download_status_downloading_eta = { $percent }% { $speed } ETA { $eta }
youtube_download_status_downloading = { $percent }%
youtube_download_status_processing = Processing…
youtube_download_status_completed = Completed
youtube_download_status_waiting = Waiting…
youtube_download_error_search = Search failed: { $error }
youtube_download_error_ffmpeg_required = ffmpeg is required for this format or the selected file options. Install ffmpeg or choose Original quality and disable embedding.
youtube_download_error_duplicate_active = This song is already downloading.
youtube_download_error_output_not_directory = Output path is not a directory: { $path }
youtube_download_error_output_prepare = Failed to prepare output directory: { $error }
youtube_download_error_output_not_writable = Output directory is not writable: { $path }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Translate the new YouTube download strings in every locale.

The replacement block uses English values in all nine supplied non-English locale files. This leaves the new download UI untranslated. Translate each value while preserving the Fluent keys and Fluent variables.

  • crates/i18n/locales/id.ftl#L104-L147: Translate the new YouTube download values into Indonesian.
  • crates/i18n/locales/it.ftl#L104-L147: Translate the new YouTube download values into Italian.
  • crates/i18n/locales/ja.ftl#L87-L130: Translate the new YouTube download values into Japanese.
  • crates/i18n/locales/ko.ftl#L87-L130: Translate the new YouTube download values into Korean.
  • crates/i18n/locales/ml.ftl#L104-L147: Translate the new YouTube download values into Malayalam.
  • crates/i18n/locales/nl.ftl#L104-L147: Translate the new YouTube download values into Dutch.
  • crates/i18n/locales/pl.ftl#L87-L130: Translate the new YouTube download values into Polish.
  • crates/i18n/locales/pt-BR.ftl#L87-L130: Translate the new YouTube download values into Brazilian Portuguese.
  • crates/i18n/locales/pt-PT.ftl#L87-L130: Translate the new YouTube download values into European Portuguese.
📍 Affects 9 files
  • crates/i18n/locales/id.ftl#L104-L147 (this comment)
  • crates/i18n/locales/it.ftl#L104-L147
  • crates/i18n/locales/ja.ftl#L87-L130
  • crates/i18n/locales/ko.ftl#L87-L130
  • crates/i18n/locales/ml.ftl#L104-L147
  • crates/i18n/locales/nl.ftl#L104-L147
  • crates/i18n/locales/pl.ftl#L87-L130
  • crates/i18n/locales/pt-BR.ftl#L87-L130
  • crates/i18n/locales/pt-PT.ftl#L87-L130
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/i18n/locales/id.ftl` around lines 104 - 147, Translate every new
YouTube download message in crates/i18n/locales/id.ftl:104-147 into Indonesian;
crates/i18n/locales/it.ftl:104-147 into Italian;
crates/i18n/locales/ja.ftl:87-130 into Japanese;
crates/i18n/locales/ko.ftl:87-130 into Korean;
crates/i18n/locales/ml.ftl:104-147 into Malayalam;
crates/i18n/locales/nl.ftl:104-147 into Dutch; crates/i18n/locales/pl.ftl:87-130
into Polish; crates/i18n/locales/pt-BR.ftl:87-130 into Brazilian Portuguese; and
crates/i18n/locales/pt-PT.ftl:87-130 into European Portuguese. Preserve every
Fluent key, variable such as $percent, $speed, $eta, $error, and $path, format
identifiers, and the intended UI meaning.

Comment on lines +156 to +180
pub fn run_preflight_checks(
video_id: &str,
out_dir: &str,
format: AudioFormat,
options: &YoutubeDownloadOptions,
) -> Result<(), String> {
if JOBS.read().iter().any(|job| {
job.video_id == video_id
&& matches!(
job.status,
JobStatus::Pending
| JobStatus::Resolving
| JobStatus::Downloading
| JobStatus::Processing
)
}) {
return Err(i18n::t("youtube_download_error_duplicate_active"));
}

if requires_ffmpeg(format, options) && find_binary("ffmpeg").is_none() {
return Err(i18n::t("youtube_download_error_ffmpeg_required"));
}

validate_output_directory(&output_root(out_dir))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

run_preflight_checks performs blocking work on the UI thread.

run_preflight_checks runs synchronously from the download button handler in crates/pages/src/youtube_downloads.rs line 181. It performs two blocking operations:

  • Line 175 calls find_binary, which calls search_dirs. On the first call of the process, search_dirs (lines 879-884) spawns a login shell ($SHELL -lc) and waits for its output. A user's shell profile can take hundreds of milliseconds or longer to initialize.
  • Line 179 calls validate_output_directory, which performs blocking fs::create_dir_all, a file create probe, and a file delete. On a network mount or a slow external volume, these block for seconds.

Both stall the render thread, so the window freezes on the first download click.

Consider running the preflight checks on a blocking worker and making the caller async, for example by moving the body into tokio::task::spawn_blocking and awaiting it inside the existing spawn in the click handler.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pages/src/youtube_download_jobs.rs` around lines 156 - 180, Make
run_preflight_checks asynchronous from the download button flow by moving its
blocking duplicate-job, find_binary, and validate_output_directory work into
tokio::task::spawn_blocking. Await the preflight result inside the existing
spawn in the download click handler, preserving the current validation errors
and success behavior without blocking the UI thread.

Comment on lines +332 to +339
let _ = tokio::fs::remove_file(&source_path).await;
if let Err(error) = result {
let _ = tokio::fs::remove_file(&destination).await;
if let Some(path) = cover {
let _ = tokio::fs::remove_file(path).await;
}
return Err(error);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

The error path can delete a pre-existing user file.

When options.overwrite_existing is true, destination_path (line 774) returns the wanted path unchanged, so destination can point at a file that already existed before this job started.

If the processing step then fails, line 334 deletes destination. That removes a user file which this download never created and never successfully replaced. A failed download must not destroy the previous copy.

Track whether this job actually created the destination, and delete it only in that case.

🛡️ Proposed fix
     let stem = output_stem(track);
     let process = requires_ffmpeg(format, options);
     let extension = output_extension(format, stream.format, process);
     let wanted = destination_dir.join(format!("{stem}.{extension}"));
     let destination = destination_path(&wanted, options.overwrite_existing);
+    // Only a file this job created may be removed on failure; an
+    // overwrite target that predates the job must survive a failed download.
+    let destination_preexisted = destination.exists();
     let _ = tokio::fs::remove_file(&source_path).await;
     if let Err(error) = result {
-        let _ = tokio::fs::remove_file(&destination).await;
+        if !destination_preexisted {
+            let _ = tokio::fs::remove_file(&destination).await;
+        }
         if let Some(path) = cover {
             let _ = tokio::fs::remove_file(path).await;
         }
         return Err(error);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let _ = tokio::fs::remove_file(&source_path).await;
if let Err(error) = result {
let _ = tokio::fs::remove_file(&destination).await;
if let Some(path) = cover {
let _ = tokio::fs::remove_file(path).await;
}
return Err(error);
}
let _ = tokio::fs::remove_file(&source_path).await;
if let Err(error) = result {
if !destination_preexisted {
let _ = tokio::fs::remove_file(&destination).await;
}
if let Some(path) = cover {
let _ = tokio::fs::remove_file(path).await;
}
return Err(error);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pages/src/youtube_download_jobs.rs` around lines 332 - 339, Track
destination ownership in the download flow around destination_path and the
result error handling: record whether this job successfully created or replaced
the destination, and only remove destination on failure when that flag is true.
Preserve existing cleanup for source_path and cover, while ensuring
overwrite_existing failures never delete a pre-existing user file.

Comment on lines +563 to +584
let response = match reqwest::get(&url).await {
Ok(response) if response.status().is_success() => response,
Ok(response) => {
tracing::warn!(
target: "youtube_download",
status = %response.status(),
"cover download returned an error"
);
return None;
}
Err(error) => {
tracing::warn!(target: "youtube_download", %error, "cover download failed");
return None;
}
};
let bytes = match response.bytes().await {
Ok(bytes) => bytes,
Err(error) => {
tracing::warn!(target: "youtube_download", %error, "cover body failed");
return None;
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to the cover download.

Line 563 uses reqwest::get, which builds a default client with no connect timeout and no total request timeout. download_track awaits download_cover at line 304, between the audio download and the processing step. If the cover host stops responding, the job hangs in Downloading indefinitely, and the user cannot cancel it.

The audio paths already bound every request (lines 365, 388, and 501). Apply the same treatment here. Bound the body size as well, so an oversized response cannot exhaust memory.

🛡️ Proposed fix
-    let response = match reqwest::get(&url).await {
+    let client = match reqwest::Client::builder()
+        .connect_timeout(std::time::Duration::from_secs(10))
+        .timeout(std::time::Duration::from_secs(30))
+        .build()
+    {
+        Ok(client) => client,
+        Err(error) => {
+            tracing::warn!(target: "youtube_download", %error, "cover client build failed");
+            return None;
+        }
+    };
+    let response = match client.get(&url).send().await {
         Ok(response) if response.status().is_success() => response,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pages/src/youtube_download_jobs.rs` around lines 563 - 584, Update the
cover download flow around reqwest::get in download_cover to use the existing
bounded HTTP-client pattern, including connect and total request timeouts.
Enforce a maximum response body size before collecting bytes, preserving the
current success handling and warning/None behavior for request and body
failures.

Comment on lines +134 to +139
oninput: move |event| {
page_error.set(None);
let value = event.value();
out_dir.set(value.clone());
config.write().youtube_download_output_dir = value;
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Do not write the whole app config on every keystroke.

Line 138 writes config.write().youtube_download_output_dir on each oninput event. config is provided as app-wide context in crates/kopuz/src/main.rs line 1565, so every character typed marks the config dirty (main.rs lines 876-882) and re-renders every subscriber of that signal. The comment at main.rs lines 867-874 records that whole-config writes were expensive enough to starve the audio stream.

out_dir is already a local signal, so the input stays responsive without the config write. Persist the value when the field loses focus instead. The folder picker at line 150 already writes the config at a single discrete moment and needs no change.

♻️ Proposed fix
                     oninput: move |event| {
                         page_error.set(None);
-                        let value = event.value();
-                        out_dir.set(value.clone());
-                        config.write().youtube_download_output_dir = value;
+                        out_dir.set(event.value());
                     },
+                    onblur: move |_| {
+                        config.write().youtube_download_output_dir = out_dir();
+                    },
                     onkeydown: move |event| event.stop_propagation(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
oninput: move |event| {
page_error.set(None);
let value = event.value();
out_dir.set(value.clone());
config.write().youtube_download_output_dir = value;
},
oninput: move |event| {
page_error.set(None);
out_dir.set(event.value());
},
onblur: move |_| {
config.write().youtube_download_output_dir = out_dir();
},
onkeydown: move |event| event.stop_propagation(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pages/src/youtube_downloads.rs` around lines 134 - 139, Remove the
per-keystroke config mutation from the oninput handler in the YouTube downloads
form, keeping only page-error clearing, value reading, and out_dir updates.
Persist youtube_download_output_dir in an onblur handler when the field loses
focus, while leaving the folder picker’s existing discrete config write
unchanged.

Comment on lines +301 to +315
fn OptionToggle(props: OptionToggleProps) -> Element {
rsx! {
button {
class: "flex items-start gap-2 py-1.5 text-left group",
onclick: move |_| props.on_change.call(!props.enabled),
div {
class: if props.enabled {
"w-4 h-4 mt-0.5 rounded border border-white/40 bg-white/20 flex items-center justify-center shrink-0"
} else {
"w-4 h-4 mt-0.5 rounded border border-white/15 bg-transparent flex items-center justify-center shrink-0"
},
if props.enabled {
i { class: "fa-solid fa-check text-white text-[9px]" }
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Expose the toggle state to assistive technology.

OptionToggle renders a button whose on/off state is conveyed only by CSS classes and a check glyph. A screen reader announces the label but not whether the option is enabled, so a non-sighted user cannot determine or verify the current setting.

Add aria-pressed bound to props.enabled.

♿ Proposed fix
         button {
             class: "flex items-start gap-2 py-1.5 text-left group",
+            "aria-pressed": "{props.enabled}",
             onclick: move |_| props.on_change.call(!props.enabled),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn OptionToggle(props: OptionToggleProps) -> Element {
rsx! {
button {
class: "flex items-start gap-2 py-1.5 text-left group",
onclick: move |_| props.on_change.call(!props.enabled),
div {
class: if props.enabled {
"w-4 h-4 mt-0.5 rounded border border-white/40 bg-white/20 flex items-center justify-center shrink-0"
} else {
"w-4 h-4 mt-0.5 rounded border border-white/15 bg-transparent flex items-center justify-center shrink-0"
},
if props.enabled {
i { class: "fa-solid fa-check text-white text-[9px]" }
}
}
fn OptionToggle(props: OptionToggleProps) -> Element {
rsx! {
button {
class: "flex items-start gap-2 py-1.5 text-left group",
"aria-pressed": "{props.enabled}",
onclick: move |_| props.on_change.call(!props.enabled),
div {
class: if props.enabled {
"w-4 h-4 mt-0.5 rounded border border-white/40 bg-white/20 flex items-center justify-center shrink-0"
} else {
"w-4 h-4 mt-0.5 rounded border border-white/15 bg-transparent flex items-center justify-center shrink-0"
},
if props.enabled {
i { class: "fa-solid fa-check text-white text-[9px]" }
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/pages/src/youtube_downloads.rs` around lines 301 - 315, Update the
button rendered by OptionToggle to include an aria-pressed attribute bound to
props.enabled, so assistive technology exposes the toggle’s current state while
preserving the existing click behavior and styling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant