Skip to content

feat(plugin): integrate vortex-mod-vimeo for end-to-end download#83

Merged
mpiton merged 5 commits intomainfrom
feat/vimeo-plugin-integration
Apr 21, 2026
Merged

feat(plugin): integrate vortex-mod-vimeo for end-to-end download#83
mpiton merged 5 commits intomainfrom
feat/vimeo-plugin-integration

Conversation

@mpiton
Copy link
Copy Markdown
Owner

@mpiton mpiton commented Apr 20, 2026

Summary

Closes MPI-100

Test plan

  • cargo fmt --all --checkgreen (verified locally)
  • npm run typecheckgreen (verified locally)
  • npx vitest run .../MediaGrabberDialog.test.tsx22/22 green (verified locally)
  • cargo test --workspace — delegated to CI (host lacks libwebkit2gtk-4.1-dev; requires the Linux deps from CI step)
  • Manual smoke: paste a public Vimeo URL → grabber dialog shows metadata + quality ladder → select 720p → download completes
  • QA-Dev and Sec-Reviewer review (plugin sandbox touch — BOARD OVERRIDE §2.2 applies)

Summary by CodeRabbit

  • New Features

    • Batch SoundCloud downloads now return multiple IDs and roll back partial batches on failure.
    • Plugins can be queried for link extraction and media-variant lookup; plugin manifests may include config defaults surfaced at runtime.
    • Media previews can show an optional artist subtitle; media metadata exposes artist and default quality.
  • Bug Fixes / Improvements

    • Smarter audio-only enforcement and default format/quality selection; improved plugin dispatch/error mapping.
  • Tests

    • Expanded coverage for plugin defaults, metadata parsing, audio dialog behavior, and plugin export handling.

Summary by cubic

Integrates the vortex-mod-vimeo plugin end-to-end with plugin-first metadata and variant lookup, and improves dialog defaults and audio-only UX. Adds safe-batch SoundCloud playlist downloads with rollback and seeds [config] defaults; merged downloads now honor the configured download directory. Closes MPI-100.

  • New Features

    • Vimeo: uses can_handle + extract_links/get_media_variants, de-dupes/sorts qualities, and completes downloads with an Adaptive-merge fallback.
    • Plugin config defaults: parse [config] in plugin.toml into PluginManifest.config_defaults() and seed into SharedHostResources (e.g., default_quality = 720p).
    • Dialog UX: honors metadata.defaultQuality, disables and forces audio-only when no video variants exist, shows artist as a subtitle, and saves as “Artist - Title” (defaults to mp3 when audio-only).
    • API: PluginLoader adds extract_links/get_media_variants; registry exposes function_exists; download_media_start accepts playlistItems and returns { downloadIds }; SoundCloud playlists batch-download with rollback on failure.
    • Downloads: merged (Adaptive) downloads use the configured download_dir if set.
  • Bug Fixes

    • Choose default_quality from the sorted ladder so the UI default matches the top option.
    • Warn when a plugin returns an Adaptive variant to aid HLS/DASH debugging.
    • Fix E0382 and clippy warnings in plugin metadata parsing.

Written for commit c935cce. Summary will update on new commits.

- Add extract_links/get_media_variants exports to PluginLoader trait with
  default NotFound implementations so existing loaders are unaffected
- Wire command_get_media_metadata to prefer the Vimeo plugin path when
  resolve_url returns vortex-mod-vimeo; fall back to yt-dlp for all others
- Parse [config] section in plugin.toml into config_defaults on PluginManifest
  and seed defaults into SharedHostResources so default_quality = 720p is
  honoured by the sandbox host functions at runtime
- Filter and deduplicate video variants by height, sort descending; select
  first returned variant as the dialog default quality
- MediaGrabberDialog respects metadata.defaultQuality with a fallback chain;
  AudioOnlySection forces audio-only mode when no video qualities are present
- Unit tests: manifest config-defaults extraction, host-function seeding,
  Vimeo video metadata parsing, SoundCloud track/playlist parsing, frontend
  MediaGrabberDialog regression path (22 vitest tests green)

Closes [MPI-100](/MPI/issues/MPI-100)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Plugin manifests now expose stringified config defaults which are seeded into the shared host config map during host initialization. Two plugin-facing loader methods (extract_links, get_media_variants) were added and wired through the extism loader and registry with export checks. Media metadata/download flows and frontend types/UI were extended for plugin-driven metadata, playlist batch downloads, and audio-only handling.

Changes

Cohort / File(s) Summary
Manifest & model
src-tauri/src/adapters/driven/plugin/manifest.rs, src-tauri/src/domain/model/plugin.rs
Parse optional [config] into stringified config_defaults; add PluginManifest::with_config_defaults and accessor; tests for default value encoding.
Host capabilities
src-tauri/src/adapters/driven/plugin/capabilities.rs
build_host_functions seeds shared.plugin_configs[plugin_name] with manifest.config_defaults, inserting only missing keys.
Plugin loader & registry
src-tauri/src/adapters/driven/plugin/extism_loader.rs, src-tauri/src/adapters/driven/plugin/registry.rs, src-tauri/src/domain/ports/driven/plugin_loader.rs, src-tauri/src/domain/ports/driven/tests.rs, src-tauri/src/application/command_bus.rs
Added extract_links and get_media_variants to PluginLoader (default NotFound); ExtismPluginLoader adds helper to resolve plugin by URL, validate export existence, and dispatch calls; PluginRegistry::function_exists added; mocks/tests updated.
Media metadata & download backend
src-tauri/src/adapters/driving/tauri_ipc.rs
command_get_media_metadata attempts plugin extraction before yt-dlp; download_media_start refactored for playlist-derived batch downloads, returns MediaDownloadResultDto with multiple IDs and supports playlist_items; metadata DTO adds artist and default_quality; plugin output parsing, batch rollback, and many tests added.
Frontend types & components
src/types/media.ts, src/components/MediaPreview.tsx, src/views/LinkGrabberView/...
MediaMetadata adds artist/defaultQuality; MediaDownloadResult added; MediaPreview accepts optional subtitle; LinkGrabber/MediaGrabberDialog updated for playlistItems, forced audio-only flows, artist-title naming, and disabled audio-only UI; AudioOnlySection gains disabled prop.
Tests & small updates
src-tauri/src/adapters/driven/plugin/*, src/views/LinkGrabberView/__tests__/MediaGrabberDialog.test.tsx, src-tauri/src/application/commands/resolve_links.rs
Added tests for manifest config default parsing, registry function_exists, plugin method defaults in mocks, SoundCloud URL patterns, and forced audio-only dialog behavior.

Sequence Diagram(s)

sequenceDiagram
    participant FE as Frontend (LinkGrabberView)
    participant IPC as Tauri IPC
    participant Loader as PluginLoader
    participant Reg as PluginRegistry
    participant Plugin as Extism Plugin
    participant YTD as yt-dlp Fallback

    FE->>IPC: command_get_media_metadata(url)
    IPC->>Loader: extract_links(url)
    Loader->>Reg: resolve plugin for url
    Reg->>Plugin: call_plugin("extract_links", url)
    Plugin-->>Reg: links / metadata JSON
    Reg-->>Loader: plugin result
    alt plugin returned metadata
        Loader-->>IPC: parsed metadata (artist, default_quality)
    else plugin missing/failed
        IPC->>YTD: parse_ytdlp_json(url)
        YTD-->>IPC: metadata
    end
    IPC-->>FE: MediaMetadataDto
Loading
sequenceDiagram
    participant FE as Frontend (MediaGrabberDialog)
    participant IPC as Tauri IPC
    participant Loader as PluginLoader
    participant Reg as PluginRegistry
    participant Plugin as Extism Plugin

    FE->>FE: detect mediaType=audio && missing qualities
    FE->>FE: set requiresAudioOnly=true, disable quality selector
    FE->>IPC: download_media_start(url, playlistItems?)
    IPC->>Loader: extract_links(url) [if plugin handles playlists]
    Loader->>Reg: resolve plugin for url
    Reg->>Plugin: invoke extract_links
    Plugin-->>Reg: playlist items
    Reg-->>Loader: items
    Loader-->>IPC: resolved targets
    IPC->>IPC: start_media_download_for_url each target
    IPC-->>FE: MediaDownloadResultDto { download_ids: [...] }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

configuration

Poem

I nibble defaults into the ground,
Plugins wake and echo sound,
Playlists hop and links unfurl,
Audio-only finds its pearl,
Downloads leap — a joyous whirl! 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.43% 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
Title check ✅ Passed The title clearly summarizes the main change: integrating the vortex-mod-vimeo plugin for end-to-end downloads, which is the primary objective of this pull request.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vimeo-plugin-integration

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR wires vortex-mod-vimeo end-to-end following the existing hexagonal plugin contract: can_handleextract_links / get_media_variants → quality ladder → download with AdaptiveStreamOnly fallback. It also adds plugin [config] default seeding into SharedHostResources, audio-only UX enforcement, SoundCloud batch-download with rollback, and artist subtitle rendering.

  • P1 — SoundCloud track title wrong for set-fetched tracks (tauri_ipc.rs:1414): parse_soundcloud_metadata returns extract_links.title (the collection/album title) when present, overriding track.title (the individual track title), producing wrong filenames.

Confidence Score: 4/5

Safe to merge after resolving the SoundCloud track title P1; remaining findings are P2 style/polish items.

One P1 logic issue: SoundCloud single-track metadata parser can return collection title instead of per-track title. All other findings are P2. Core Vimeo wiring, quality sorting, config-defaults seeding, and batch-download rollback are well-implemented and tested.

src-tauri/src/adapters/driving/tauri_ipc.rs — specifically parse_soundcloud_metadata around line 1414.

Important Files Changed

Filename Overview
src-tauri/src/adapters/driving/tauri_ipc.rs Core IPC layer wiring Vimeo/SoundCloud plugin paths; contains a P1 logic issue where extract_links.title shadows the per-track title for SoundCloud single-track responses, and two P2 concerns.
src/views/LinkGrabberView/MediaGrabberDialog/AudioOnlySection.tsx New audio-only component; works correctly but uses three hardcoded English strings instead of i18n t() calls.
src-tauri/src/adapters/driven/plugin/manifest.rs Parses [config] defaults from plugin.toml; logic correct, well-tested. Minor: RawConfigEntry silently ignores unknown keys.
src-tauri/src/adapters/driven/plugin/capabilities.rs Seeds config_defaults into SharedHostResources using or_insert_with; well-tested.
src-tauri/src/adapters/driven/plugin/extism_loader.rs Adds extract_links/get_media_variants via call_url_plugin_function helper; clean with proper function_exists guard.
src/views/LinkGrabberView/MediaGrabberDialog/MediaGrabberDialog.tsx Respects metadata.defaultQuality with correct fallback chain; requiresAudioOnly and effectiveAudioOnly composition are correct.
src/types/media.ts Adds artist, defaultQuality, downloadIds fields; clean.

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/views/LinkGrabberView/MediaGrabberDialog/AudioOnlySection.tsx
Line: 34-40

Comment:
**Hardcoded English strings — missing i18n**

`"Audio Only"` (line 34) and `"Audio Format"` (line 40) are literal English strings. The rest of the dialog layer uses `useTranslation` + `t(...)` for every user-visible string. This component should follow the same pattern to keep the UI translatable.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/views/LinkGrabberView/MediaGrabberDialog/AudioOnlySection.tsx
Line: 56-58

Comment:
**Hardcoded format hint string also needs i18n**

The helper text "Popular formats: MP3 (wide compatibility), M4A (iTunes), OPUS (quality)" is also a hard-coded English string not going through `t(...)`, inconsistent with the rest of the view layer.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src-tauri/src/adapters/driving/tauri_ipc.rs
Line: 1350-1373

Comment:
**`Adaptive` variants with no `height` are silently discarded, yielding an empty quality list**

The `Adaptive` match arm pushes a quality entry only when `variant.height` is `Some` and non-zero. HLS/DASH manifests typically omit a fixed height, so an `Adaptive`-only plugin response produces empty `available_qualities` while `available_formats` is populated. A brief comment clarifying the contract would resolve the earlier thread concern.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src-tauri/src/adapters/driving/tauri_ipc.rs
Line: 1413-1415

Comment:
**SoundCloud track: `extract_links.title` shadows `track.title` for per-track title**

For a `"track"` response, `extract_links.title` is the top-level (collection/album) title, while `track.title` is the individual track title. Returning `extract_links.title` when present will use the collection name for a track fetched as part of a set, producing the wrong download filename.

```suggestion
                title: track.title.clone(),
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src-tauri/src/adapters/driven/plugin/manifest.rs
Line: 37-40

Comment:
**`RawConfigEntry` accepts unknown fields silently**

A typo like `defaut = "720p"` would be silently swallowed, leaving the default unset. Adding a test that asserts `default` is captured even when other sibling keys exist would make the contract explicit.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src-tauri/src/adapters/driving/tauri_ipc.rs
Line: 1492-1494

Comment:
**Plugin-name string literal duplicated in batch-download path**

`"vortex-mod-soundcloud"` appears again here. Extracting it to a named constant shared with any other occurrence would reduce future maintenance risk.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (5): Last reviewed commit: "fix: address PR review comments" | Re-trigger Greptile

Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs Outdated
Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs Outdated
Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/views/LinkGrabberView/MediaGrabberDialog/AudioOnlySection.tsx (1)

30-33: Consider matching label cursor style to disabled state.

When the switch is disabled, cursor-pointer on the label is slightly misleading. A conditional class (e.g., cursor-not-allowed) would better reflect interactivity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/LinkGrabberView/MediaGrabberDialog/AudioOnlySection.tsx` around
lines 30 - 33, The label element for htmlFor="audio-only" in the
AudioOnlySection component always uses "cursor-pointer"; update its className to
be conditional based on the switch's disabled state (the prop/state that
controls interactivity in AudioOnlySection) so it uses "cursor-not-allowed" when
disabled and "cursor-pointer" when enabled, ensuring the label's cursor reflects
interactivity.
src-tauri/src/adapters/driven/plugin/manifest.rs (1)

143-153: Add direct tests for remaining encode_config_default branches.

Integer, Float, Datetime, and Table branches are implemented but not asserted yet. Adding focused unit tests would harden this serializer contract and prevent subtle regressions.

Also applies to: 234-279

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/adapters/driven/plugin/manifest.rs` around lines 143 - 153, Add
focused unit tests for encode_config_default that exercise the remaining
branches: construct toml::Value::Integer, toml::Value::Float,
toml::Value::Datetime, and toml::Value::Table (and optionally Array) inputs,
call encode_config_default, and assert the Result is Ok with the expected string
(use i.to_string(), f.to_string(), dt.to_string(), and serde_json::to_string for
Table/Array) and also assert the error mapping for invalid serialization where
applicable; locate the function by name encode_config_default and add tests in
the same module (or a tests module) to ensure those branches are covered.
src/views/LinkGrabberView/MediaGrabberDialog/MediaGrabberDialog.tsx (1)

66-80: Consider extracting the duplicated condition.

The shouldForceAudioOnly condition (lines 66-69) duplicates the requiresAudioOnly computation (lines 51-54). Since requiresAudioOnly is already derived state, the effect could reference it directly.

💡 Optional simplification
   useEffect(() => {
     if (!metadata) return;
-    const shouldForceAudioOnly =
-      link.mediaType === "audio" &&
-      metadata.availableQualities.length === 0 &&
-      metadata.availableAudioFormats.length > 0;
     const firstQuality =
       metadata.defaultQuality ?? metadata.availableQualities[0]?.quality ?? "1080p";
     const firstFormat = metadata.availableFormats[0] ?? "mp4";
     const firstAudioFormat =
       metadata.availableAudioFormats[0] ??
-      (shouldForceAudioOnly ? "mp3" : "m4a");
-    setAudioOnly(shouldForceAudioOnly);
+      (requiresAudioOnly ? "mp3" : "m4a");
+    setAudioOnly(requiresAudioOnly);
     setQualitySelection(firstQuality);
     setFormatSelection(firstFormat);
     setAudioFormat(firstAudioFormat);
-  }, [link.mediaType, metadata]);
+  }, [link.mediaType, metadata, requiresAudioOnly]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/LinkGrabberView/MediaGrabberDialog/MediaGrabberDialog.tsx` around
lines 66 - 80, The effect is recomputing the same audio-only condition; replace
the local shouldForceAudioOnly with the existing requiresAudioOnly variable (use
requiresAudioOnly when setting audioOnly and when computing firstAudioFormat
fallback), remove the duplicated boolean expression, and update the effect
dependency array to include requiresAudioOnly (and keep metadata/link.mediaType
as needed) so the effect runs when that derived state changes.
src/views/LinkGrabberView/LinkGrabberView.tsx (1)

49-64: Consider typing the mutation return value for future extensibility.

The backend now returns MediaDownloadResultDto { download_ids: Vec<u64> }, but the mutation is typed as useTauriMutation<unknown, ...>. While the return value is currently unused (only toast + navigate on success), typing it would enable future features like tracking batch downloads.

💡 Optional improvement
+interface MediaDownloadResult {
+  downloadIds: number[];
+}
+
-const { mutate: startMediaDownload } = useTauriMutation<
-  unknown,
+const { mutate: startMediaDownload } = useTauriMutation<
+  MediaDownloadResult,
   {
     url: string;
     quality: string;
     format: string;
     audioOnly: boolean;
     title?: string;
     playlistItems: string[];
   }
 >("download_media_start", {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/LinkGrabberView/LinkGrabberView.tsx` around lines 49 - 64, The
mutation return type is currently unknown; update the useTauriMutation call in
LinkGrabberView (the const { mutate: startMediaDownload } =
useTauriMutation<...>("download_media_start", ...)) to use the backend DTO type
(e.g., MediaDownloadResultDto with download_ids: number[]) instead of unknown,
and either import that DTO type from your shared/backend types or declare a
local interface matching the backend shape so future code can access the
download_ids from startMediaDownload's result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/adapters/driving/tauri_ipc.rs`:
- Around line 759-775: The batch loop over batch_targets calls
start_media_download_for_url and aborts on the first Err, leaving earlier
downloads started; change the loop in the block that constructs download_ids to
instead handle each start_media_download_for_url(await) with match so you
collect successful download IDs and failed item errors (e.g., collect Vec<Uuid>
download_ids and Vec<String> errors), then return a DTO that conveys partial
success (or return Ok when there are some download_ids and include errors via a
new field or log them) or, alternatively, on error cancel already-started
downloads by calling whatever cancellation API exists for the returned IDs;
update MediaDownloadResultDto (or create a new result type) and the loop around
start_media_download_for_url/state.command_bus/state.plugin_loader to reflect
this behavior.

---

Nitpick comments:
In `@src-tauri/src/adapters/driven/plugin/manifest.rs`:
- Around line 143-153: Add focused unit tests for encode_config_default that
exercise the remaining branches: construct toml::Value::Integer,
toml::Value::Float, toml::Value::Datetime, and toml::Value::Table (and
optionally Array) inputs, call encode_config_default, and assert the Result is
Ok with the expected string (use i.to_string(), f.to_string(), dt.to_string(),
and serde_json::to_string for Table/Array) and also assert the error mapping for
invalid serialization where applicable; locate the function by name
encode_config_default and add tests in the same module (or a tests module) to
ensure those branches are covered.

In `@src/views/LinkGrabberView/LinkGrabberView.tsx`:
- Around line 49-64: The mutation return type is currently unknown; update the
useTauriMutation call in LinkGrabberView (the const { mutate: startMediaDownload
} = useTauriMutation<...>("download_media_start", ...)) to use the backend DTO
type (e.g., MediaDownloadResultDto with download_ids: number[]) instead of
unknown, and either import that DTO type from your shared/backend types or
declare a local interface matching the backend shape so future code can access
the download_ids from startMediaDownload's result.

In `@src/views/LinkGrabberView/MediaGrabberDialog/AudioOnlySection.tsx`:
- Around line 30-33: The label element for htmlFor="audio-only" in the
AudioOnlySection component always uses "cursor-pointer"; update its className to
be conditional based on the switch's disabled state (the prop/state that
controls interactivity in AudioOnlySection) so it uses "cursor-not-allowed" when
disabled and "cursor-pointer" when enabled, ensuring the label's cursor reflects
interactivity.

In `@src/views/LinkGrabberView/MediaGrabberDialog/MediaGrabberDialog.tsx`:
- Around line 66-80: The effect is recomputing the same audio-only condition;
replace the local shouldForceAudioOnly with the existing requiresAudioOnly
variable (use requiresAudioOnly when setting audioOnly and when computing
firstAudioFormat fallback), remove the duplicated boolean expression, and update
the effect dependency array to include requiresAudioOnly (and keep
metadata/link.mediaType as needed) so the effect runs when that derived state
changes.
🪄 Autofix (Beta)

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

Run ID: 05aa8da9-7863-4720-ae10-ddcd07b737b1

📥 Commits

Reviewing files that changed from the base of the PR and between 57e16ad and 2c7fea8.

📒 Files selected for processing (16)
  • src-tauri/src/adapters/driven/plugin/capabilities.rs
  • src-tauri/src/adapters/driven/plugin/extism_loader.rs
  • src-tauri/src/adapters/driven/plugin/manifest.rs
  • src-tauri/src/adapters/driven/plugin/registry.rs
  • src-tauri/src/adapters/driving/tauri_ipc.rs
  • src-tauri/src/application/command_bus.rs
  • src-tauri/src/application/commands/resolve_links.rs
  • src-tauri/src/domain/model/plugin.rs
  • src-tauri/src/domain/ports/driven/plugin_loader.rs
  • src-tauri/src/domain/ports/driven/tests.rs
  • src/components/MediaPreview.tsx
  • src/types/media.ts
  • src/views/LinkGrabberView/LinkGrabberView.tsx
  • src/views/LinkGrabberView/MediaGrabberDialog/AudioOnlySection.tsx
  • src/views/LinkGrabberView/MediaGrabberDialog/MediaGrabberDialog.tsx
  • src/views/LinkGrabberView/__tests__/MediaGrabberDialog.test.tsx

Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 16 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src-tauri/src/adapters/driving/tauri_ipc.rs">

<violation number="1" location="src-tauri/src/adapters/driving/tauri_ipc.rs:1314">
P2: `Adaptive` variants are silently discarded. If a Vimeo video only provides HLS/DASH adaptive streams (no progressive `video` variants), both `available_qualities` and `available_formats` will be empty. Because `media_type` is still `"video"`, the `requiresAudioOnly` guard in `MediaGrabberDialog` evaluates to `false`, rendering a blank quality selector with no actionable options. Either handle adaptive variants here (e.g., surface them as selectable qualities) or propagate a signal so the frontend can show an appropriate fallback.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs Outdated
- Fix E0382 compile error in start_media_downloads: compute soundcloud
  track title before moving target.url into start_media_download_for_url
- Pick default_quality from sorted qualities list so UI top entry matches
  the default (Greptile P2 — previously captured first variant pre-sort)
- Log a warning when a plugin returns an Adaptive variant instead of
  silently dropping it (Greptile P2 — aids future debugging of HLS/DASH)
- Update parse_plugin_video_metadata test to assert the highest quality
  (1080p) as default, matching the new ordering
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src-tauri/src/adapters/driving/tauri_ipc.rs (1)

759-774: ⚠️ Potential issue | 🟠 Major

Batch download still fails non-atomically on first error.

Line 763–773 can start earlier downloads, then Line 772 (await?) aborts on a later failure. This leaves partially-started background downloads while returning an error for the whole request.

Suggested direction
 if let Some(targets) = batch_targets {
     let mut download_ids = Vec::with_capacity(targets.len());
+    let mut failures = Vec::new();
     for target in targets {
         let title = Some(soundcloud_track_download_title(&target));
-        let id = start_media_download_for_url(
+        match start_media_download_for_url(
             state.command_bus.clone(),
             state.plugin_loader.clone(),
             target.url,
             quality.clone(),
             format.clone(),
             true,
             title,
         )
-        .await?;
-        download_ids.push(id);
+        .await {
+            Ok(id) => download_ids.push(id),
+            Err(err) => failures.push(err),
+        }
     }
+    if download_ids.is_empty() && !failures.is_empty() {
+        return Err(failures.join("; "));
+    }
+    // optionally log `failures` for partial success visibility
     return Ok(MediaDownloadResultDto { download_ids });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/adapters/driving/tauri_ipc.rs` around lines 759 - 774, The loop
in handling batch_targets starts downloads one-by-one and immediately awaits
start_media_download_for_url, so a later await? failure leaves earlier downloads
running; fix by initiating all start requests without awaiting (e.g., collect
futures for start_media_download_for_url for each target using
soundcloud_track_download_title), then await them together with a fallible join
(tokio::try_join! / futures::future::try_join_all) so the whole batch fails
atomically; if you must spawn background downloads, record started download_ids
and on any subsequent failure call the cancellation/stop path for each started
id to roll back partial starts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/adapters/driving/tauri_ipc.rs`:
- Around line 1135-1141: The code currently bubbles up errors from the plugin
metadata extraction (the tokio::task::spawn_blocking call that invokes
load_plugin_media_metadata with url_clone), which prevents the yt-dlp fallback;
change the error handling so plugin extraction failures are logged (or recorded)
but do not cause an early Err return—treat plugin errors as a None metadata
result and proceed to the fallback path instead; specifically, inside the block
around tokio::task::spawn_blocking(...).await, replace the two .map_err(...)?
chains that propagate errors with handling that logs the error (including
context: plugin loader and url_clone) and returns Ok(None) so the surrounding if
let Some(metadata) = ... can continue without aborting the whole flow.

---

Duplicate comments:
In `@src-tauri/src/adapters/driving/tauri_ipc.rs`:
- Around line 759-774: The loop in handling batch_targets starts downloads
one-by-one and immediately awaits start_media_download_for_url, so a later
await? failure leaves earlier downloads running; fix by initiating all start
requests without awaiting (e.g., collect futures for
start_media_download_for_url for each target using
soundcloud_track_download_title), then await them together with a fallible join
(tokio::try_join! / futures::future::try_join_all) so the whole batch fails
atomically; if you must spawn background downloads, record started download_ids
and on any subsequent failure call the cancellation/stop path for each started
id to roll back partial starts.
🪄 Autofix (Beta)

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

Run ID: 099564f7-f10e-45dd-a854-fed89dca5b10

📥 Commits

Reviewing files that changed from the base of the PR and between 2c7fea8 and f3e624f.

📒 Files selected for processing (1)
  • src-tauri/src/adapters/driving/tauri_ipc.rs

Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/views/LinkGrabberView/MediaGrabberDialog/MediaGrabberDialog.tsx (1)

64-76: Consider adding link.mediaType to the effect dependencies.

The effect depends on requiresAudioOnly, which is derived from link.mediaType. If link changes but metadata doesn't immediately update (e.g., cached), the audio-only state might not sync correctly. Adding link.mediaType explicitly would make the dependency clearer.

However, this is likely a minor edge case since link changes typically trigger new metadata fetches.

💡 Suggested change
-  }, [metadata, requiresAudioOnly]);
+  }, [metadata, requiresAudioOnly, link.mediaType]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/views/LinkGrabberView/MediaGrabberDialog/MediaGrabberDialog.tsx` around
lines 64 - 76, The useEffect that initializes selection state (the effect using
metadata, requiresAudioOnly, and calling setAudioOnly, setQualitySelection,
setFormatSelection, setAudioFormat) should also include link.mediaType in its
dependency array so changes to the link's media type update audioOnly
immediately; update the dependency list for the useEffect to include
link?.mediaType (or link.mediaType) alongside metadata and requiresAudioOnly to
ensure the effect re-runs when the link's media type changes.
src-tauri/src/adapters/driving/tauri_ipc.rs (2)

1016-1031: Consider cleaning up the temp file on subsequent errors.

If download_to_file succeeds but a later step fails (e.g., unique_destination or the file move/copy), the temp file at file_info.path is left behind. While not critical, orphaned files in the temp directory could accumulate over time.

💡 Suggested approach

Consider wrapping the post-download operations in a scope that removes the temp file on early return:

// After download_to_file succeeds, ensure cleanup on failure
let cleanup_on_error = |path: &std::path::Path| {
    if let Err(e) = std::fs::remove_file(path) {
        tracing::debug!(path = %path.display(), error = %e, "temp file cleanup failed");
    }
};

// Then in error paths after line 1031:
// cleanup_on_error(&file_info.path);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/adapters/driving/tauri_ipc.rs` around lines 1016 - 1031, After
download_to_file succeeds, ensure the temporary file at file_info.path is
removed on any subsequent error (e.g., failures from unique_destination or file
move/copy); implement a small cleanup closure or RAII guard right after
plugin_loader.download_to_file returns that calls
std::fs::remove_file(&file_info.path) on error paths (logging failures with
tracing::debug), and invoke that cleanup before returning Err in all
post-download failure branches so the temp_dir/vortex-downloads files don't
accumulate.

1502-1514: Edge case: non-matching selection IDs produce a user-unfriendly error.

When selected_item_ids is non-empty but none of the IDs match any track in the playlist, the error "no SoundCloud tracks matched the selected playlist items" is returned. This could confuse users if track IDs have changed since metadata was fetched (e.g., playlist was modified).

Consider logging the mismatched IDs for debugging or providing a more informative error message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/adapters/driving/tauri_ipc.rs` around lines 1502 - 1514, The
error returned when no tracks match the selection should include which selected
IDs were not found to aid debugging; compute the set of available track IDs from
extract_links.tracks (e.g., collect track.id into available_ids), compute the
difference missing_ids = selected_ids - available_ids, and if selected_item_ids
was non-empty return or log an error that includes the missing_ids (or a
friendlier message saying "selected items not found: [...]") instead of the
generic message; update the block around selected_item_ids/selected_ids/tracks
to include this missing-id-aware error and preferably log the missing IDs via
the existing logger before returning the Err.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src-tauri/src/adapters/driving/tauri_ipc.rs`:
- Around line 1016-1031: After download_to_file succeeds, ensure the temporary
file at file_info.path is removed on any subsequent error (e.g., failures from
unique_destination or file move/copy); implement a small cleanup closure or RAII
guard right after plugin_loader.download_to_file returns that calls
std::fs::remove_file(&file_info.path) on error paths (logging failures with
tracing::debug), and invoke that cleanup before returning Err in all
post-download failure branches so the temp_dir/vortex-downloads files don't
accumulate.
- Around line 1502-1514: The error returned when no tracks match the selection
should include which selected IDs were not found to aid debugging; compute the
set of available track IDs from extract_links.tracks (e.g., collect track.id
into available_ids), compute the difference missing_ids = selected_ids -
available_ids, and if selected_item_ids was non-empty return or log an error
that includes the missing_ids (or a friendlier message saying "selected items
not found: [...]") instead of the generic message; update the block around
selected_item_ids/selected_ids/tracks to include this missing-id-aware error and
preferably log the missing IDs via the existing logger before returning the Err.

In `@src/views/LinkGrabberView/MediaGrabberDialog/MediaGrabberDialog.tsx`:
- Around line 64-76: The useEffect that initializes selection state (the effect
using metadata, requiresAudioOnly, and calling setAudioOnly,
setQualitySelection, setFormatSelection, setAudioFormat) should also include
link.mediaType in its dependency array so changes to the link's media type
update audioOnly immediately; update the dependency list for the useEffect to
include link?.mediaType (or link.mediaType) alongside metadata and
requiresAudioOnly to ensure the effect re-runs when the link's media type
changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0c13102c-2ef8-4186-af10-f8e8ce6c1074

📥 Commits

Reviewing files that changed from the base of the PR and between f3e624f and 3cc8f7a.

📒 Files selected for processing (6)
  • src-tauri/src/adapters/driven/plugin/manifest.rs
  • src-tauri/src/adapters/driving/tauri_ipc.rs
  • src/types/media.ts
  • src/views/LinkGrabberView/LinkGrabberView.tsx
  • src/views/LinkGrabberView/MediaGrabberDialog/AudioOnlySection.tsx
  • src/views/LinkGrabberView/MediaGrabberDialog/MediaGrabberDialog.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/types/media.ts
  • src/views/LinkGrabberView/LinkGrabberView.tsx
  • src-tauri/src/adapters/driven/plugin/manifest.rs

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src-tauri/src/adapters/driving/tauri_ipc.rs`:
- Around line 1060-1074: AdaptiveStreamOnly handling currently calls
dirs::download_dir() (via unique_destination) instead of using the
app-configured download directory, causing adaptive streams to land in the
system default; update resolve_media_stream (or the callsite) to accept the
configured download directory from AppState and use that path when creating the
destination (instead of dirs::download_dir()), or alternatively change
resolve_media_stream to return StreamResolution::LocalFile with the temp path
and let the caller (which has AppState and invokes StartDownloadCommand) perform
the final placement using the configured download directory and
unique_destination; refer to functions/types resolve_media_stream,
AdaptiveStreamOnly, StreamResolution::LocalFile, StartDownloadCommand, and
unique_destination to locate the relevant code to change.
🪄 Autofix (Beta)

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

Run ID: fc5dbc38-e513-4108-8c0a-ff36ec0e523b

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc8f7a and 01a2a59.

📒 Files selected for processing (1)
  • src-tauri/src/adapters/driving/tauri_ipc.rs

Comment thread src-tauri/src/adapters/driving/tauri_ipc.rs Outdated
@mpiton mpiton merged commit da819a3 into main Apr 21, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant