Skip to content

feat(pwa): add protocol and file handlers for magnet links and torrent files - #783

Merged
s0up4200 merged 20 commits into
developfrom
feat/pwa-protocol-file-handlers
Feb 5, 2026
Merged

feat(pwa): add protocol and file handlers for magnet links and torrent files#783
s0up4200 merged 20 commits into
developfrom
feat/pwa-protocol-file-handlers

Conversation

@s0up4200

@s0up4200 s0up4200 commented Dec 14, 2025

Copy link
Copy Markdown
Collaborator

Summary

  • Add PWA protocol handler for magnet: links - clicking magnet links opens qui when installed as PWA
  • Add PWA file handler for .torrent files - opening torrent files from Finder/file manager opens qui
  • Add registerProtocolHandler() fallback for Firefox magnet link support
  • Use launch_handler.client_mode: "navigate-existing" to reuse existing window instead of opening new ones
  • Create /add route with instance selection and AddTorrentDialog integration
  • Store add intent across login redirects so auth flow doesn't lose the torrent data
  • Add error handling: timeout feedback when file handling fails after auth, invalid file feedback, magnet URL validation

Closes #329

Browser Support

Magnet links:

  • Chrome/Edge 102+: Automatic via PWA manifest protocol handler
  • Firefox: Via registerProtocolHandler() - user registers from dashboard banner or Settings

Torrent files:

  • Chrome/Edge 102+: Via File Handling API (LaunchQueue)
  • Firefox/Safari: Not supported, use drag-and-drop or manual file selection

Test plan

  • Install qui as PWA in Chrome/Edge
  • Click a magnet link on any website - should open qui and show AddTorrentDialog
  • Open a .torrent file from Finder - should open qui and show AddTorrentDialog
  • Test with multiple instances configured - should show instance selector
  • Test while logged out - should redirect to login, then back to /add after auth
  • Test opening invalid file type - should show error toast and redirect home
  • Test Firefox registerProtocolHandler from dashboard banner
  • Test Firefox registerProtocolHandler from Settings > Security

Summary by CodeRabbit

  • New Features

    • Full /add flow to import torrents via magnet links, URLs, or .torrent files with auth gating, instance selection, and post-login return handling.
    • Launch-queue consumer initialized at app startup to receive external payloads.
  • UI

    • Magnet-handler banner on Dashboard and "Register as Handler" control in Settings.
    • Login now returns users to the intended add flow after auth.
  • Utilities

    • Magnet link normalization/extraction, protocol-handler utilities, and persisted add-intent for post-login routing.
  • PWA

    • Register as magnet: handler, accept .torrent files, and support protocol/file launches.
  • Bug Fixes

    • Improved error handling and user-facing toast for add-torrent failures.
  • Documentation

    • Added Unraid deployment guidance to README.

…t files

Add PWA protocol handler for magnet: links and file handler for .torrent
files, enabling qui to open torrents directly from the OS when installed
as a PWA.

- Add protocol_handlers, file_handlers, launch_handler to PWA manifest
- Create /add route to handle PWA launches with instance selection
- Add LaunchQueue API TypeScript declarations
- Store add intent across login redirects with error handling
- Add timeout/error feedback when file handling fails after auth
- Validate magnet URLs in route schema
@coderabbitai

coderabbitai Bot commented Dec 14, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a new /add route with comprehensive magnet/.torrent ingestion and UI, session-backed AddIntent for post-auth navigation, PWA protocol/file handlers and launch behavior, browser protocol-registration utilities and banner/Settings UI, Launch Queue consumer/types, magnet normalization helpers, and related router wiring and docs.

Changes

Cohort / File(s) Summary
Add route & handler
web/src/routes/add.tsx
New /add file route and AddTorrentHandler: validates query, enforces auth (stores AddIntent), consumes magnet URLs, Launch Queue payloads and .torrent files, handles instance selection, and renders AddTorrentDialog with lifecycle handlers.
Intent persistence & post-auth navigation
web/src/lib/add-intent.ts, web/src/hooks/useAuth.ts, web/src/pages/Login.tsx
New AddIntent sessionStorage API plus navigateAfterAuth; replaced direct post-auth navigate calls to resume /add flows after login.
Launch Queue integration & startup
web/src/lib/launch-queue.ts, web/src/types/launch-queue.d.ts, web/src/main.tsx
New Launch Queue types and in-memory event API (emit/consume/subscribe), consumer that bridges window.launchQueue to app events, and startup invocation via setupLaunchQueueConsumer() in main.tsx.
Magnet helpers
web/src/lib/magnet.ts
New functions normalizeMagnetLink and extractMagnetFromTargetURL to validate, decode, and extract magnet links (including query-param extraction).
PWA manifest / protocol & file handlers
web/vite.config.ts
PWA manifest updated: scope/start_url -> ./; added protocol_handlers for magnet: (./add?url=%s), file_handlers for application/x-bittorrent (./add), and launch_handler with client_mode: "navigate-existing".
Protocol registration utilities
web/src/lib/protocol-handler.ts
New utilities: feature detection, browser guidance, registerMagnetHandler, and localStorage-based banner-dismiss helpers.
UI: banner & Settings/Dashboard
web/src/components/MagnetHandlerBanner.tsx, web/src/pages/Settings.tsx, web/src/pages/Dashboard.tsx
Added dismissible MagnetHandlerBanner component; Dashboard now renders it. Settings shows a Browser Integration card to register magnet handler with toast feedback.
Router wiring / generated types
web/src/routeTree.gen.ts, web/src/routes/add
Generated router updates to register AddRoute (/add) into route tree and TanStack React Router type maps/imports.
Add-torrent dialog error handling
web/src/components/torrents/AddTorrentDialog.tsx
Added onError handling for the add-torrent mutation to surface user-facing toast messages on failure.
Docs
README.md
Large documentation addition: Unraid deployment guidance and instructions.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant Browser
    participant App
    participant Session as SessionStorage
    participant Auth
    participant Instances
    participant LaunchQueue

    User->>Browser: Click magnet link / open .torrent
    Browser->>App: Navigate to /add or deliver launchQueue event
    App->>LaunchQueue: setup consumer / receive event
    App->>Session: getAndClearAddIntent()
    alt payload found
        App->>App: initialize add payload (magnet/files)
    end
    App->>Auth: Check authentication
    alt Not authenticated
        App->>Session: storeAddIntent({magnet,hasFiles,openAdd})
        App->>Browser: Redirect to /login
        Auth->>Browser: User authenticates
        Browser->>App: Login callback -> navigateAfterAuth(navigate)
        App->>Session: getAndClearAddIntent()
    end
    App->>Instances: Load active instances
    alt no instances
        App->>Browser: Redirect to /settings
    else multiple instances
        App->>User: Render InstanceSelector
        User->>App: Select instance
    end
    App->>App: Render AddTorrentDialog with payload
    User->>App: Confirm -> consume payload / navigate
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

web, area/frontend

Poem

🐇 I sniffed a magnet on the breeze,
Saved my hop through login with ease.
Files and links tumbled into place,
A banner waved — register the chase!
Carrots, code, and a speedy embrace.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main changes: adding PWA protocol handlers for magnet links and file handlers for torrent files.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #329: enables registering magnet links with qui, provides user-facing registration mechanisms (Dashboard banner and Settings entry), and integrates magnet handling into the add-torrent flow.
Out of Scope Changes check ✅ Passed All changes are directly related to PWA protocol/file handling and magnet link integration. The README addition and error handling improvements are supporting changes that enable the feature.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pwa-protocol-file-handlers

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

@s0up4200 s0up4200 added this to the v1.11.0 milestone Dec 14, 2025
@s0up4200 s0up4200 added enhancement New feature or request torrent pwa labels Dec 14, 2025

@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: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e36312f and 5b31453.

📒 Files selected for processing (7)
  • web/src/hooks/useAuth.ts (3 hunks)
  • web/src/lib/add-intent.ts (1 hunks)
  • web/src/pages/Login.tsx (2 hunks)
  • web/src/routeTree.gen.ts (11 hunks)
  • web/src/routes/add.tsx (1 hunks)
  • web/src/types/launch-queue.d.ts (1 hunks)
  • web/vite.config.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.

Applied to files:

  • web/src/routes/add.tsx
🧬 Code graph analysis (3)
web/src/pages/Login.tsx (1)
web/src/lib/add-intent.ts (1)
  • navigateAfterAuth (39-49)
web/src/routes/add.tsx (4)
web/src/hooks/useAuth.ts (1)
  • useAuth (12-73)
web/src/hooks/useInstances.ts (1)
  • useInstances (10-188)
web/src/components/torrents/AddTorrentDialog.tsx (2)
  • AddTorrentDropPayload (104-106)
  • AddTorrentDialog (164-1671)
web/src/lib/add-intent.ts (1)
  • storeAddIntent (15-21)
web/src/hooks/useAuth.ts (1)
web/src/lib/add-intent.ts (1)
  • navigateAfterAuth (39-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (6)
web/src/types/launch-queue.d.ts (1)

11-30: Good minimal typing; consider exporting types if you’ll reference them directly elsewhere.
As-is, it’s sufficient for window.launchQueue?.setConsumer(...) type inference. If other modules need to name LaunchParams/LaunchQueue, export them (or put them inside declare global) to avoid ad-hoc re-declarations later.

web/src/routes/add.tsx (1)

95-113: Dialog flow + instance selection logic looks solid.
The selectedInstanceId/resolvedInstanceId resolution and navigation on close/cancel is clear, and the “wait for payload” UI is a good fallback.

Also applies to: 146-174

web/src/hooks/useAuth.ts (1)

6-38: Good centralization of post-auth navigation (once storage access is hardened).
Using navigateAfterAuth(navigate) in both login/setup aligns auth flows with the new /add intent behavior.

web/vite.config.ts (1)

90-107: The current manifest configuration is correct and requires no changes.

All three handler fields (protocol_handlers, file_handlers, launch_handler) are valid per the Chrome Web App Manifest specification and properly emitted by vite-plugin-pwa v1.2.0. Browsers match file_handlers primarily by file extension (.torrent), not by MIME type, so the current configuration will correctly associate .torrent files with your app regardless of whether the server sends application/x-bittorrent or application/octet-stream. Adding the latter MIME type provides no additional coverage.

web/src/pages/Login.tsx (1)

17-17: LGTM! Post-auth navigation now respects add intents.

The integration with navigateAfterAuth correctly enables the OIDC callback flow to redirect users to /add when they have a pending torrent operation (magnet link or file), preserving the user's intent across the authentication boundary. The fallback to "/" ensures existing behavior is maintained when no intent is stored.

Also applies to: 64-64

web/src/routeTree.gen.ts (1)

1-354: Auto-generated routing configuration.

This file is automatically generated by TanStack Router and correctly reflects the new /add route integration. The additions are consistent with the existing route structure and require no manual changes.

Comment thread web/src/lib/add-intent.ts
Comment thread web/src/routes/add.tsx
Comment thread web/src/routes/add.tsx
@NekoShinobi

Copy link
Copy Markdown

@s0up4200 is it possible to add firefox support by calling

navigator.registerProtocolHandler("magnet", "{base_url}/add?magnet=%s", "qui") 

as part of a button? (only works with https / maybe a flag for non-secure contexts, but not sure).
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler

Add Firefox support for magnet link handling via navigator.registerProtocolHandler().
This complements the PWA manifest protocol_handlers which only work in Chromium.

- Add protocol-handler.ts utility with secure context detection
- Add dismissible banner on dashboard prompting registration
- Add Browser Integration card in Settings > Security tab
- Use withBasePath() for correct URL construction
- Show toast feedback on registration attempts
@s0up4200

Copy link
Copy Markdown
Collaborator Author

@s0up4200 is it possible to add firefox support by calling

navigator.registerProtocolHandler("magnet", "{base_url}/add?magnet=%s", "qui") 

as part of a button? (only works with https / maybe a flag for non-secure contexts, but not sure). https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler

Done. Added a dismissable banner on Dashboard, and a small section in the Security tab in Settings. Should work for both https and localhost.

@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: 0

🧹 Nitpick comments (1)
web/src/components/MagnetHandlerBanner.tsx (1)

30-39: Consider auto-dismissing banner after successful registration.

The registration handler works correctly with appropriate toast feedback. One optional enhancement: automatically dismiss the banner after successful registration (in addition to showing the toast) to reduce UI clutter once the user has taken action.

Current behavior is acceptable since the browser confirmation prompt itself provides feedback, but auto-dismissing would improve the UX.

Apply this diff if you'd like to auto-dismiss:

  const handleRegister = () => {
    const success = registerMagnetHandler()
    if (success) {
      toast.success("Magnet handler registration requested", {
        description: "If prompted by your browser, please accept to complete registration.",
      })
+     dismissProtocolHandlerBanner()
+     setDismissed(true)
    } else {
      toast.error("Failed to register magnet handler")
    }
  }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b31453 and c73e497.

📒 Files selected for processing (4)
  • web/src/components/MagnetHandlerBanner.tsx (1 hunks)
  • web/src/lib/protocol-handler.ts (1 hunks)
  • web/src/pages/Dashboard.tsx (2 hunks)
  • web/src/pages/Settings.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-25T11:39:54.748Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 637
File: web/src/pages/Dashboard.tsx:805-831
Timestamp: 2025-11-25T11:39:54.748Z
Learning: In web/src/pages/Dashboard.tsx, the TrackerIconImage component intentionally receives displayDomain (incognito-mapped name) instead of the real domain in incognito mode. This causes icon lookups to fail and show only fallback letters, which is desired behavior for privacy - hiding both tracker names and icons when incognito mode is enabled.

Applied to files:

  • web/src/pages/Dashboard.tsx
🧬 Code graph analysis (4)
web/src/lib/protocol-handler.ts (1)
web/src/lib/base-url.ts (1)
  • withBasePath (24-29)
web/src/components/MagnetHandlerBanner.tsx (1)
web/src/lib/protocol-handler.ts (4)
  • isProtocolHandlerBannerDismissed (39-45)
  • canRegisterProtocolHandler (15-18)
  • registerMagnetHandler (25-34)
  • dismissProtocolHandlerBanner (50-56)
web/src/pages/Settings.tsx (1)
web/src/lib/protocol-handler.ts (2)
  • canRegisterProtocolHandler (15-18)
  • registerMagnetHandler (25-34)
web/src/pages/Dashboard.tsx (1)
web/src/components/MagnetHandlerBanner.tsx (1)
  • MagnetHandlerBanner (17-62)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (11)
web/src/pages/Dashboard.tsx (2)

8-8: LGTM! Clean import addition.

The import of MagnetHandlerBanner is correctly placed with other component imports.


2279-2280: LGTM! Well-placed banner integration.

The MagnetHandlerBanner is appropriately positioned after PasswordIssuesBanner, keeping related notification UI elements grouped together. The component handles its own conditional rendering, so no wrapper logic is needed here.

web/src/pages/Settings.tsx (2)

49-49: LGTM! Protocol handler utilities imported.

The imports of canRegisterProtocolHandler and registerMagnetHandler are clean and follow the project's import conventions.


999-1032: LGTM! Well-integrated browser integration feature.

The Browser Integration card provides a clear, user-friendly interface for registering qui as a magnet link handler. Key strengths:

  • Conditional rendering ensures it only appears when supported
  • Clear description explains the functionality
  • Success toast includes helpful prompt about browser confirmation
  • Error handling provides feedback on failures

The implementation follows existing Settings page patterns and integrates nicely with the Security tab.

web/src/components/MagnetHandlerBanner.tsx (3)

17-28: LGTM! Clean conditional rendering logic.

The component correctly handles visibility conditions:

  • Checks browser support via canRegisterProtocolHandler()
  • Respects user dismissal preference
  • State initialization properly calls the function once on mount

41-44: LGTM! Dismissal logic is correct.

The dismiss handler properly persists the dismissal state and updates local state to hide the banner.


46-60: LGTM! Clear and accessible banner UI.

The banner design is user-friendly with:

  • Clear messaging about the functionality
  • Visible icon and action buttons
  • Appropriate color scheme for an informational banner
  • Responsive layout
web/src/lib/protocol-handler.ts (4)

10-18: LGTM! Robust browser capability detection.

The canRegisterProtocolHandler function correctly checks both:

  • API availability via optional chaining
  • Secure context requirement (HTTPS or localhost)

The JSDoc comment helpfully explains that this complements PWA manifest protocol handlers for broader browser support.


20-34: LGTM! Correct protocol handler registration.

The registerMagnetHandler function properly:

  • Constructs the handler URL with the correct %s placeholder
  • Uses withBasePath for base URL handling
  • Wraps the registration call in try-catch
  • Returns a boolean indicating if registration was attempted

Note: The function returns true when the browser shows the registration prompt, not when the user confirms it. This is expected behavior since the browser API doesn't provide a callback for user confirmation.


36-45: LGTM! Safe localStorage access with sensible default.

The isProtocolHandlerBannerDismissed function safely handles localStorage access and returns false on error, ensuring the banner shows by default if storage is unavailable (fail-open behavior).


47-56: LGTM! Safe persistence with appropriate error handling.

The dismissProtocolHandlerBanner function safely persists the dismissal state and logs a warning if localStorage is unavailable. This graceful degradation is appropriate for a non-critical preference.

@NekoShinobi

NekoShinobi commented Dec 17, 2025

Copy link
Copy Markdown

@s0up4200 is it possible to add firefox support by calling

navigator.registerProtocolHandler("magnet", "{base_url}/add?magnet=%s", "qui") 

as part of a button? (only works with https / maybe a flag for non-secure contexts, but not sure). https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler

Done. Added a dismissable banner on Dashboard, and a small section in the Security tab in Settings. Should work for both https and localhost.

Nice, exactly what I was looking for.

How do you get access to this button again if you were to dismiss it?
nevermind, I read over your message about the security tab. Looks great!

@s0up4200

Copy link
Copy Markdown
Collaborator Author

@s0up4200 is it possible to add firefox support by calling

navigator.registerProtocolHandler("magnet", "{base_url}/add?magnet=%s", "qui") 

as part of a button? (only works with https / maybe a flag for non-secure contexts, but not sure). https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler

Done. Added a dismissable banner on Dashboard, and a small section in the Security tab in Settings. Should work for both https and localhost.

Nice, exactly what I was looking for.

How do you get access to this button again if you were to dismiss it?

Either by deleting the key from localStorage, or by going into Settings -> Security (couldnt find a better section for it)

@s0up4200 s0up4200 modified the milestones: v1.11.0, v1.12.0 Dec 18, 2025

@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: 0

♻️ Duplicate comments (2)
web/src/routes/add.tsx (2)

19-23: Coerce expectingFiles from the URL.

URL query parameters are always strings. The current schema uses z.boolean().optional() on line 22, which cannot parse the string "true" from /add?expectingFiles=true without coercion.

🔎 Proposed fix
 const addSearchSchema = z.object({
   magnet: z.string().regex(/^magnet:\?/i).optional(),
   instance: z.coerce.number().optional(),
-  expectingFiles: z.boolean().optional(),
+  expectingFiles: z.coerce.boolean().optional(),
 })

52-79: Add cleanup to prevent state updates after unmount.

The setConsumer callback (lines 55–78) captures setDropPayload and navigate. If the component unmounts during an auth redirect before the launch is delivered, the callback will still fire when files arrive, causing a state update on an unmounted component.

🔎 Proposed fix with mounted guard
 // Handle files from file handler (launchQueue API)
 useEffect(() => {
   if (!window.launchQueue) return
+
+  let isMounted = true

   window.launchQueue.setConsumer(async (launchParams) => {
+    if (!isMounted) return
     if (!launchParams.files?.length) return

     const files: File[] = []
     for (const handle of launchParams.files) {
       try {
         const file = await handle.getFile()
         if (file.name.toLowerCase().endsWith(".torrent")) {
           files.push(file)
         }
       } catch (err) {
         console.error("Failed to get file from handle:", err)
       }
     }

     if (files.length > 0) {
+      if (!isMounted) return
       hasReceivedPayload.current = true
       setDropPayload({ type: "file", files })
     } else if (launchParams.files.length > 0) {
       // Files were provided but none were valid .torrent files
+      if (!isMounted) return
       toast.error("No valid .torrent files found")
       navigate({ to: "/" })
     }
   })
+
+  return () => {
+    isMounted = false
+  }
 }, [navigate])
🧹 Nitpick comments (1)
web/src/routes/add.tsx (1)

81-93: Consider increasing the timeout duration.

The 2-second timeout (line 85) may fire prematurely if the auth redirect takes time or if the browser is slow to deliver the launchQueue event. A longer timeout (3-5 seconds) would reduce false-positive timeout errors while still providing timely feedback.

🔎 Optional adjustment
   const timeout = setTimeout(() => {
     if (!hasReceivedPayload.current) {
       toast.error("Unable to open torrent file. Please try again.")
       navigate({ to: "/" })
     }
-  }, 2000)
+  }, 5000)
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 433907a and f238140.

📒 Files selected for processing (3)
  • web/src/lib/add-intent.ts (1 hunks)
  • web/src/routes/add.tsx (1 hunks)
  • web/vite.config.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/src/lib/add-intent.ts
  • web/vite.config.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.

Applied to files:

  • web/src/routes/add.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (5)
web/src/routes/add.tsx (5)

25-28: LGTM!

The route definition correctly uses createFileRoute with search validation and component assignment.


30-49: LGTM!

The component initialization and magnet payload setup are well-structured. The hasReceivedPayload ref correctly tracks payload receipt to prevent state confusion after consumption.


95-112: LGTM!

The event handlers are well-structured with appropriate useCallback dependencies and clear navigation logic.


114-186: LGTM!

The rendering logic comprehensively handles all states: auth loading, unauthenticated (with intent storage), instance loading, no instances, multi-instance selection, payload processing, and waiting for launchQueue. The conditional flow is clear and correct.


188-231: LGTM!

The InstanceSelector component provides a clean UI for multi-instance selection with clear connection status indicators and appropriate button styling.

Only store add intent when magnet or expectingFiles is present.
Redirect authenticated users to home if visiting /add with no payload.
Detect PWA standalone mode to distinguish file-handler launches from
manual /add navigation. Store hasFiles intent when in standalone mode
so files are processed after login. Add timeout fallback for both
authenticated and unauthenticated standalone users.

@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: 1

♻️ Duplicate comments (2)
web/src/routes/add.tsx (2)

20-25: Coerce expectingFiles from the URL.

URL query parameters are always strings. When a user navigates directly to /add?expectingFiles=true, the value is the string "true", not the boolean true. The current z.boolean().optional() cannot parse this string without coercion.

🔎 Proposed fix
 const addSearchSchema = z.object({
   magnet: z.string().optional(),
   url: z.string().optional(),
   instance: z.coerce.number().optional(),
-  expectingFiles: z.boolean().optional(),
+  expectingFiles: z.coerce.boolean().optional(),
 })

56-90: Add unmount guard to prevent state updates after component unmount.

The setConsumer callback captures setDropPayload and navigate but has no cleanup or mounted-state guard. If the component unmounts during auth redirect before the launch is delivered, the callback will still fire, causing a React warning about state updates on unmounted components.

🔎 Proposed fix using isMounted ref
   // Handle files from file handler (launchQueue API)
   useEffect(() => {
     if (!window.launchQueue) return
 
+    let isMounted = true
+
     window.launchQueue.setConsumer(async (launchParams) => {
+      if (!isMounted) return
+
       const launchedMagnet = extractMagnetFromTargetURL(launchParams.targetURL)
       if (launchedMagnet) {
         hasReceivedPayload.current = true
         setDropPayload({ type: "url", urls: [launchedMagnet] })
         return
       }
 
       if (!launchParams.files?.length) return
 
       const files: File[] = []
       for (const handle of launchParams.files) {
         try {
           const file = await handle.getFile()
           if (file.name.toLowerCase().endsWith(".torrent")) {
             files.push(file)
           }
         } catch (err) {
           console.error("Failed to get file from handle:", err)
         }
       }
 
+      if (!isMounted) return
+
       if (files.length > 0) {
         hasReceivedPayload.current = true
         setDropPayload({ type: "file", files })
       } else if (launchParams.files.length > 0) {
         // Files were provided but none were valid .torrent files
         toast.error("No valid .torrent files found")
         navigate({ to: "/" })
       }
     })
+
+    return () => {
+      isMounted = false
+    }
   }, [navigate])
🧹 Nitpick comments (2)
README.md (1)

203-206: Consider restructuring this section.

The "Magnet Link Handling" section is placed in the middle of the Configuration section (between the environment variables block and the Tracker Icons config). This placement is confusing since it's a ## heading that breaks the flow of configuration options.

Consider moving this section to appear after all Configuration-related content (after line 218) or making it a subsection (###) if it's intended to be part of Configuration.

web/src/lib/protocol-handler.ts (1)

10-22: Consider including Edge in Chromium detection for guidance.

The isChromium function explicitly excludes Edge (/edg/i), but Edge is Chromium-based and likely exhibits similar protocol handler behavior. Users on Edge would receive the generic guidance instead of the Chrome-specific address bar tip.

This is minor since the generic guidance still works, but you might want to either:

  • Include Edge in the Chromium check, or
  • Add a separate isEdge branch with appropriate guidance
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f58804a and 948ade7.

📒 Files selected for processing (7)
  • README.md
  • web/src/components/MagnetHandlerBanner.tsx
  • web/src/lib/magnet.ts
  • web/src/lib/protocol-handler.ts
  • web/src/pages/Settings.tsx
  • web/src/routes/add.tsx
  • web/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/vite.config.ts
  • web/src/pages/Settings.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.

Applied to files:

  • web/src/routes/add.tsx
🧬 Code graph analysis (3)
web/src/components/MagnetHandlerBanner.tsx (1)
web/src/lib/protocol-handler.ts (5)
  • isProtocolHandlerBannerDismissed (72-78)
  • canRegisterProtocolHandler (29-32)
  • registerMagnetHandler (55-67)
  • getMagnetHandlerRegistrationGuidance (34-48)
  • dismissProtocolHandlerBanner (83-89)
web/src/lib/protocol-handler.ts (1)
web/src/lib/base-url.ts (1)
  • withBasePath (24-29)
web/src/routes/add.tsx (5)
web/src/hooks/useAuth.ts (1)
  • useAuth (12-73)
web/src/hooks/useInstances.ts (1)
  • useInstances (10-188)
web/src/lib/magnet.ts (2)
  • normalizeMagnetLink (6-27)
  • extractMagnetFromTargetURL (29-42)
web/src/components/torrents/AddTorrentDialog.tsx (2)
  • AddTorrentDropPayload (104-106)
  • AddTorrentDialog (164-1671)
web/src/lib/add-intent.ts (1)
  • storeAddIntent (16-22)
🪛 markdownlint-cli2 (0.18.1)
README.md

16-16: Link fragments should be valid

(MD051, link-fragments)

🔇 Additional comments (11)
web/src/lib/magnet.ts (2)

6-27: LGTM!

The normalizeMagnetLink function has solid defensive coding: proper type guards, bounded decoding loop (max 2 iterations to handle double-encoding), and appropriate error handling. The case-insensitive regex for magnet: prefix validation is correct.


29-42: LGTM!

The extractMagnetFromTargetURL function correctly tries direct normalization first, then gracefully falls back to URL parsing for query parameter extraction. Using window.location.origin as the base URL for relative paths is a reasonable approach.

web/src/routes/add.tsx (4)

32-53: LGTM!

The component setup is well-structured: proper hook usage, magnet normalization chain with fallback, and state initialization. The hasReceivedPayload ref correctly prevents falling back to "waiting" state after consumption.


92-115: LGTM!

The standalone PWA detection pattern is correct, and the timeout logic appropriately handles the case where file payloads don't arrive. The 2-second timeout with proper cleanup via clearTimeout is reasonable.


136-220: LGTM!

The render logic handles all edge cases well: authentication loading, unauthenticated redirect with intent storage, instance loading, no-instances redirect, multi-instance selection, and payload-ready dialog rendering. The flow is clear and comprehensive.


228-263: LGTM!

The InstanceSelector component is clean and functional. The connection status indicator (green/red dot) provides good visual feedback, and the fallback name (Instance ${instance.id}) handles missing names gracefully.

web/src/components/MagnetHandlerBanner.tsx (2)

18-47: LGTM!

The component logic is well-structured with:

  • Lazy state initialization for dismissed status
  • Clean early returns for unsupported browsers and dismissed state
  • Proper success/error handling with toast notifications
  • Browser-specific guidance displayed on success

The separation between handleRegister and handleDismiss keeps the code readable.


49-64: LGTM!

The banner UI is clean and functional. The blue styling appropriately signals an informational/action item, and the layout with icon, message, and action buttons is intuitive.

web/src/lib/protocol-handler.ts (3)

29-32: LGTM!

The canRegisterProtocolHandler function correctly checks both API availability and secure context requirement. The optional chaining on navigator?.registerProtocolHandler is a good defensive pattern.


55-67: LGTM!

The registerMagnetHandler function correctly composes the handler URL using withBasePath and handles the deprecated third argument appropriately with a documented @ts-expect-error. The try-catch ensures graceful failure.


72-89: LGTM!

Both localStorage functions have proper try-catch blocks to handle cases where localStorage is unavailable (private browsing mode, storage quota exceeded). The warning log in dismissProtocolHandlerBanner appropriately signals non-critical failures.

Comment thread README.md Outdated
@s0up4200
s0up4200 changed the base branch from main to develop January 2, 2026 07:20
@s0up4200 s0up4200 removed this from the v1.12.0 milestone Jan 4, 2026
@s0up4200 s0up4200 added this to the v1.13.0 milestone Jan 4, 2026
@NekoShinobi

Copy link
Copy Markdown

Is there something that's holding this off? Would love to see this soon.

@s0up4200

Copy link
Copy Markdown
Collaborator Author

Is there something that's holding this off? Would love to see this soon.

Testing and time really. Been prioritizing other things. Is it something you want to work on? Contributions are very much appreciated 👍

@NekoShinobi

Copy link
Copy Markdown

If it's manual testing with just screenshot proof or something, I can probably help out. I'm not a TS dev so I can't really help out that much on the coding side.

@s0up4200 s0up4200 modified the milestones: v1.13.0, v1.14.0 Jan 30, 2026

@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: 1

🤖 Fix all issues with AI agents
In `@web/src/lib/launch-queue.ts`:
- Around line 22-25: The emitLaunchQueueEvent function is using
listeners.forEach with a callback that may be interpreted as returning values;
change it to explicitly invoke listeners without returning from the
callback—e.g., iterate with a for...of loop over the listeners or use a forEach
callback block that does not return—to call each listener(event) and set
pendingEvent = event in the emitLaunchQueueEvent function so no callback return
values are possible (refer to emitLaunchQueueEvent, pendingEvent, and
listeners).
🧹 Nitpick comments (2)
web/src/routes/add.tsx (2)

25-31: The expectingFiles preprocess logic is correct but could be simplified.

The manual string-to-boolean conversion works, but Zod's coercion handles common truthy/falsy string values automatically. However, I notice the current implementation is more explicit about only accepting "true"/"false" strings, which may be intentional for stricter validation.

♻️ Optional: Simpler approach using Zod coerce
-  expectingFiles: z.preprocess((value) => {
-    if (typeof value === "string") {
-      if (value.toLowerCase() === "true") return true
-      if (value.toLowerCase() === "false") return false
-    }
-    return value
-  }, z.boolean().optional()),
+  expectingFiles: z.coerce.boolean().optional(),

Note: z.coerce.boolean() treats any truthy value as true and falsy values as false, which may differ from the current behavior of only accepting exact "true"/"false" strings.


241-245: Consider adding accessible status text for connection indicator.

The connection status indicator relies solely on color (green/red dot), which may not be accessible to colorblind users. Consider adding a tooltip or sr-only text.

♻️ Optional: Add accessible status indication
 <div className="flex items-center gap-2">
   <div className={`h-2 w-2 rounded-full ${instance.connected ? "bg-green-500" : "bg-red-500"
-    }`} />
+    }`} 
+    aria-hidden="true" />
   <span>{instance.name || `Instance ${instance.id}`}</span>
+  <span className="sr-only">
+    {instance.connected ? "(connected)" : "(disconnected)"}
+  </span>
 </div>

Comment thread web/src/lib/launch-queue.ts

@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: 1

🤖 Fix all issues with AI agents
In `@web/src/routes/add.tsx`:
- Around line 39-58: Detect when a magnetParam or url is present but
normalizeMagnetLink returns null and surface an error instead of silently
redirecting: inside the existing useEffect that runs on [magnet], check the raw
params (magnetParam and url) and if either exists while normalizeMagnetLink
returned null, call the same toast/error routine used in handleLaunchQueueEvent
(or the component’s toast API) to show a user-facing error, set
hasReceivedPayload.current appropriately, and avoid calling setDropPayload;
reference normalizeMagnetLink, magnetParam, url, hasReceivedPayload,
setDropPayload, and the useEffect that initializes payload from magnet to locate
where to add this handling.
🧹 Nitpick comments (1)
web/src/routes/add.tsx (1)

141-150: Move add-intent storage out of the render path.
storeAddIntent is a side effect and will run multiple times in dev Strict Mode. Shifting it to an effect after auth state resolves avoids duplicate writes and keeps render pure.

♻️ Suggested refactor
+  const shouldStoreIntent = !authLoading && !isAuthenticated
+  useEffect(() => {
+    if (!shouldStoreIntent) return
+    if (magnet) {
+      storeAddIntent({ magnet, openAdd: true })
+    } else if (expectingFiles || isStandalone) {
+      storeAddIntent({ hasFiles: true, openAdd: true })
+    }
+  }, [shouldStoreIntent, magnet, expectingFiles, isStandalone])
+
   if (!isAuthenticated) {
-    // Store intent if there's a payload OR if we're in PWA mode (potential file-handler launch)
-    if (magnet) {
-      storeAddIntent({ magnet, openAdd: true })
-    } else if (expectingFiles || isStandalone) {
-      // In standalone mode without magnet, files may be coming via launchQueue
-      storeAddIntent({ hasFiles: true, openAdd: true })
-    }
     return <Navigate to="/login" />
   }

Comment thread web/src/routes/add.tsx Outdated
@s0up4200

s0up4200 commented Feb 3, 2026

Copy link
Copy Markdown
Collaborator Author

If it's manual testing with just screenshot proof or something, I can probably help out. I'm not a TS dev so I can't really help out that much on the coding side.

Are you running docker and want to test this PR? If so you can pull pr-783 👍

@NekoShinobi

NekoShinobi commented Feb 3, 2026

Copy link
Copy Markdown

Test Setup

  • Archlinux (hyprland setup) + LibreWolf 147.0.2-1 and Brave 1.86.146
  • Server is unraid w/ docker run --rm -p 7477:7476 --name quitest --network custom -it ghcr.io/autobrr/qui:pr-783 behind a caddy reverseproxy.

Test Firefox registerProtocolHandler from dashboard banner

  • Clicked banner Register and registered Handler
  • Magnet link from https://archlinux.org/releng/releases/2025.06.01/
  • Lead to Instances page. (I think an improvement here could be to have some sort of error message or banner. It's implied that the user needs to set an instance, but making this explicit would improve UX)
    • Side note: there seems to be another bug where if you have a "disabled" -> "disconnected" instance, but click test connection, it doesn't automatically refresh with connected status.
  • Clicked Magnet link again. Add New torrent page is shown. URI /add?magnet=magnet%3A%3Fxt%3Durn%3Abtih%3Aa492f8b92a25b0399c87715fc228c864ac5a7bfb%26dn%3Darchlinux-2025.06.01-x86_64.iso
image

Test Firefox registerProtocolHandler from Settings > Security

  • Removed handler in settings
  • Settings -> Security -> Register as handler
  • Click Magnet Link -> Add Torrent Page -> Imported.

Test while logged out - should redirect to login, then back to /add after auth

  • Log out
  • Open Manget link
  • Login screen shows (I think one improvement here would be to add a banner to indicate this flow? qbit doesn't have this either, but I think it'll look nicer.)
  • Add torrent screen after

Multiple Instances
image

  • Successfully added torrent

Test opening invalid file type - should show error toast and redirect home (firefox)

  • I tried testing with some invalid magnet links. It'll just prefill the page with the bad magnet. Same as qbit so it's fine.
    • magnet:?xt=urn:btih:
    • magnet:?xt=urn:btih:1234567890abcdefg
    • magnet:?xt=urn:btih:1234567890abcdef&dn=

Magnet handler also works from Brave. brave://settings/handlers

  • One caveat is that if you "x" the window, it treats that as "blocked" so you won't be able to request again until you remove it from the blocklist. Not qui's problem though.

PWA Installation

  • I think the magnet handler is just the URL, so it doesn't open them in PWA.
  • Opening torrent for the first time opens prompt to select an app to open it with with the qui app being selectable.
  • Opening torrent ✔️
  • Multi Instanced ✔️
image
  • Add Torrent Page ✔️
image

Found 1 Bug

  • Logout in PWA
  • Open Torrent in PWA App
  • Login to qui
  • Unable to open Torrent File

Edit: One more Issue. If you use a malformed torrent file, there's no error message in the PWA app.

  • Grab torrent file, Delete random characters in the middle of it.
  • Open in PWA App
  • It fails to add, and the server returns {"level":"error","error":"could not add torrent; status code: 415: unexpected status code","instanceID":1,"fileIndex":0,"time":"2026-02-03T20:02:30Z","message":"Failed to add torrent file"} but there's no front end error message for it.

@s0up4200

s0up4200 commented Feb 3, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @NekoShinobi 👍

- defer launch queue consume/subscribe until authenticated on /add

- show explicit add-torrent failure toast on hard API errors
@s0up4200
s0up4200 merged commit 6d1dac7 into develop Feb 5, 2026
12 checks passed
@s0up4200
s0up4200 deleted the feat/pwa-protocol-file-handlers branch February 5, 2026 21:20
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Feb 22, 2026
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/autobrr/qui](https://github.com/autobrr/qui) | minor | `v1.13.1` → `v1.14.0` |

---

### Release Notes

<details>
<summary>autobrr/qui (ghcr.io/autobrr/qui)</summary>

### [`v1.14.0`](https://github.com/autobrr/qui/releases/tag/v1.14.0)

[Compare Source](autobrr/qui@v1.13.1...v1.14.0)

#### Changelog

##### New Features

- [`6f8e6ed`](autobrr/qui@6f8e6ed): feat(api): add torrent field endpoint for select all copy ([#&#8203;1477](autobrr/qui#1477)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`2d9b4c7`](autobrr/qui@2d9b4c7): feat(automation): trigger external programs automatically via automation rules ([#&#8203;1284](autobrr/qui#1284)) ([@&#8203;0rkag](https://github.com/0rkag))
- [`32692a4`](autobrr/qui@32692a4): feat(automations): Add the ability to define the move automation with a templated path ([#&#8203;1376](autobrr/qui#1376)) ([@&#8203;ColinHebert](https://github.com/ColinHebert))
- [`61bbeb1`](autobrr/qui@61bbeb1): feat(automations): add Resume action to Automations ([#&#8203;1350](autobrr/qui#1350)) ([@&#8203;cy1der](https://github.com/cy1der))
- [`450b98f`](autobrr/qui@450b98f): feat(automations): grouping + release fields ([#&#8203;1467](autobrr/qui#1467)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`18d4a64`](autobrr/qui@18d4a64): feat(automations): match tracker conditions by display name ([#&#8203;1420](autobrr/qui#1420)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`7c67b82`](autobrr/qui@7c67b82): feat(automations): show activity run details ([#&#8203;1385](autobrr/qui#1385)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`177ef4d`](autobrr/qui@177ef4d): feat(crossseed): Multiple hard/reflink dirs ([#&#8203;1289](autobrr/qui#1289)) ([@&#8203;rybertm](https://github.com/rybertm))
- [`a72b673`](autobrr/qui@a72b673): feat(crossseed): gazelle-only OPS/RED ([#&#8203;1436](autobrr/qui#1436)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6a29384`](autobrr/qui@6a29384): feat(crossseed): match bit depth ([#&#8203;1427](autobrr/qui#1427)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c7fd5aa`](autobrr/qui@c7fd5aa): feat(dirscan): add max searchee age filter ([#&#8203;1486](autobrr/qui#1486)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d595a55`](autobrr/qui@d595a55): feat(documentation): add AI doc actions and llms discoverability ([#&#8203;1451](autobrr/qui#1451)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`562ab3f`](autobrr/qui@562ab3f): feat(metrics): add tracker metrics ([#&#8203;1073](autobrr/qui#1073)) ([@&#8203;Winter](https://github.com/Winter))
- [`1b9aa9d`](autobrr/qui@1b9aa9d): feat(notifications): add shoutrrr and notifiarr ([#&#8203;1371](autobrr/qui#1371)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6d1dac7`](autobrr/qui@6d1dac7): feat(pwa): add protocol and file handlers for magnet links and torrent files ([#&#8203;783](autobrr/qui#783)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`42fa501`](autobrr/qui@42fa501): feat(torrents): add unified cross-instance torrent table ([#&#8203;1481](autobrr/qui#1481)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`498eaca`](autobrr/qui@498eaca): feat(ui): show speeds in page title ([#&#8203;1292](autobrr/qui#1292)) ([@&#8203;NoLife141](https://github.com/NoLife141))
- [`94a506e`](autobrr/qui@94a506e): feat(unregistered): nem talalhato ([#&#8203;1483](autobrr/qui#1483)) ([@&#8203;KyleSanderson](https://github.com/KyleSanderson))
- [`8bf366c`](autobrr/qui@8bf366c): feat(web): add logs nav ([#&#8203;1458](autobrr/qui#1458)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`babc88d`](autobrr/qui@babc88d): feat(web): add responsive popover with mobile drawer support ([#&#8203;1398](autobrr/qui#1398)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`06d341b`](autobrr/qui@06d341b): feat(web): add torrent table selection quick wins ([#&#8203;1455](autobrr/qui#1455)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`56fbbec`](autobrr/qui@56fbbec): feat(web): hide selection column ([#&#8203;1460](autobrr/qui#1460)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`46814aa`](autobrr/qui@46814aa): feat(web): qBittorrent autorun preferences ([#&#8203;1430](autobrr/qui#1430)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`342643e`](autobrr/qui@342643e): feat(web): unify instance settings & qbit options dialog ([#&#8203;1257](autobrr/qui#1257)) ([@&#8203;0rkag](https://github.com/0rkag))
- [`e634d01`](autobrr/qui@e634d01): feat: add cross-seed blocklist ([#&#8203;1391](autobrr/qui#1391)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`13aaac8`](autobrr/qui@13aaac8): feat: add dry-run workflows ([#&#8203;1395](autobrr/qui#1395)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`f01101d`](autobrr/qui@f01101d): feat: add option to disable built-in authentication ([#&#8203;1464](autobrr/qui#1464)) ([@&#8203;libussa](https://github.com/libussa))
- [`6d1da50`](autobrr/qui@6d1da50): feat: download individual content files from context menu ([#&#8203;1465](autobrr/qui#1465)) ([@&#8203;libussa](https://github.com/libussa))
- [`77e9abf`](autobrr/qui@77e9abf): feat: migrate to dodopayments ([#&#8203;1407](autobrr/qui#1407)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`9f6c856`](autobrr/qui@9f6c856): feat: support basic auth for ARR and Torznab ([#&#8203;1442](autobrr/qui#1442)) ([@&#8203;s0up4200](https://github.com/s0up4200))

##### Bug Fixes

- [`8a06d4b`](autobrr/qui@8a06d4b): fix(api): correct add-torrent OpenAPI param names and add missing fields ([#&#8203;1426](autobrr/qui#1426)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b9a687c`](autobrr/qui@b9a687c): fix(api): honor explicit basic auth clear from URL userinfo ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`948ca67`](autobrr/qui@948ca67): fix(api): tighten CORS/auth routing and base URL joins ([#&#8203;1325](autobrr/qui#1325)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`12bea13`](autobrr/qui@12bea13): fix(automations): improve applied action summaries ([#&#8203;1478](autobrr/qui#1478)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8fe658b`](autobrr/qui@8fe658b): fix(automations): negate regex match for NotContains/NotEqual operators ([#&#8203;1441](autobrr/qui#1441)) ([@&#8203;andresatierf](https://github.com/andresatierf))
- [`8a808eb`](autobrr/qui@8a808eb): fix(automations): respect remove-only tag conditions ([#&#8203;1444](autobrr/qui#1444)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a72715e`](autobrr/qui@a72715e): fix(backups): add failure cooldown and export throttling ([#&#8203;1214](autobrr/qui#1214)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2e75c14`](autobrr/qui@2e75c14): fix(backups): skip exports missing metadata ([#&#8203;1362](autobrr/qui#1362)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5658421`](autobrr/qui@5658421): fix(config): update commented log settings in place ([#&#8203;1402](autobrr/qui#1402)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`62c50c0`](autobrr/qui@62c50c0): fix(crossseed): tighten TV title matching ([#&#8203;1445](autobrr/qui#1445)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e7cc489`](autobrr/qui@e7cc489): fix(dirscan): prevent immediate requeue after cancel ([#&#8203;1446](autobrr/qui#1446)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`36cbfcf`](autobrr/qui@36cbfcf): fix(docs): avoid mdx jsx parse error ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d8d6f62`](autobrr/qui@d8d6f62): fix(filters): stabilize dense sidebar layout ([#&#8203;1384](autobrr/qui#1384)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b959fc6`](autobrr/qui@b959fc6): fix(orphanscan): NFC-normalize paths to avoid false orphans ([#&#8203;1422](autobrr/qui#1422)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`598e994`](autobrr/qui@598e994): fix(reflink): retry EAGAIN clones ([#&#8203;1360](autobrr/qui#1360)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`aaa5ee0`](autobrr/qui@aaa5ee0): fix(reflinktree): retry transient FICLONE EINVAL and add diagnostics ([#&#8203;1487](autobrr/qui#1487)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`647af31`](autobrr/qui@647af31): fix(rss): enable rules list scrolling ([#&#8203;1359](autobrr/qui#1359)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c356a6f`](autobrr/qui@c356a6f): fix(sync): Optimize torrent sorting and reference management ([#&#8203;1474](autobrr/qui#1474)) ([@&#8203;KyleSanderson](https://github.com/KyleSanderson))
- [`cf4310e`](autobrr/qui@cf4310e): fix(ui): update placeholder text in ArrInstanceForm based on instance type ([#&#8203;1375](autobrr/qui#1375)) ([@&#8203;pashioya](https://github.com/pashioya))
- [`92b6748`](autobrr/qui@92b6748): fix(web): format IPv6 peer addresses and copy IP without port ([#&#8203;1417](autobrr/qui#1417)) ([@&#8203;sleepm](https://github.com/sleepm))
- [`25039bc`](autobrr/qui@25039bc): fix(web): handle SSO session expiry behind Cloudflare Access and other proxies ([#&#8203;1438](autobrr/qui#1438)) ([@&#8203;nitrobass24](https://github.com/nitrobass24))
- [`77fe310`](autobrr/qui@77fe310): fix(web): prevent category submenu re-render ([#&#8203;1357](autobrr/qui#1357)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`a42ab1e`](autobrr/qui@a42ab1e): fix(web): raise instance preferences max value from 999 to 99999 ([#&#8203;1311](autobrr/qui#1311)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`540168c`](autobrr/qui@540168c): fix(web): raise virtualization threshold ([#&#8203;1355](autobrr/qui#1355)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`8547dc6`](autobrr/qui@8547dc6): fix(web): remove column filters when column is hidden ([#&#8203;1418](autobrr/qui#1418)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`6b09b8d`](autobrr/qui@6b09b8d): fix(web): remove panel size bounds ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`db4cdc4`](autobrr/qui@db4cdc4): fix(web): show piece size in torrent details ([#&#8203;1365](autobrr/qui#1365)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`1f94a06`](autobrr/qui@1f94a06): fix(web): use absolute for scroll-to-top on desktop ([#&#8203;1419](autobrr/qui#1419)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`e31fe3a`](autobrr/qui@e31fe3a): fix: detect tracker health support after qBit upgrade ([#&#8203;909](autobrr/qui#909)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`52f01da`](autobrr/qui@52f01da): fix: disable update indicators when update checks are off ([#&#8203;1364](autobrr/qui#1364)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`f7e3fed`](autobrr/qui@f7e3fed): fix: normalize DD+ and DDP file keys ([#&#8203;1456](autobrr/qui#1456)) ([@&#8203;s0up4200](https://github.com/s0up4200))

##### Other Changes

- [`d914301`](autobrr/qui@d914301): chore(ci): fire Blacksmith (my wallet screamed) ([#&#8203;1408](autobrr/qui#1408)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b43327d`](autobrr/qui@b43327d): chore(deps): bump the golang group with 2 updates ([#&#8203;1378](autobrr/qui#1378)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`57747bd`](autobrr/qui@57747bd): chore(deps): bump the npm group across 1 directory with 27 updates ([#&#8203;1379](autobrr/qui#1379)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`a43850d`](autobrr/qui@a43850d): chore(docs): add BIMI SVG logo ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`914bede`](autobrr/qui@914bede): chore(funding): add Patreon to FUNDING.yml ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8b76f1e`](autobrr/qui@8b76f1e): docs(automations): clarify tag matching examples ([#&#8203;1457](autobrr/qui#1457)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2994054`](autobrr/qui@2994054): docs(readme): restore concise README ([#&#8203;1452](autobrr/qui#1452)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`51237d4`](autobrr/qui@51237d4): docs: Add configuration reference ([#&#8203;1440](autobrr/qui#1440)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`741462c`](autobrr/qui@741462c): docs: add Windows installation guide ([#&#8203;1463](autobrr/qui#1463)) ([@&#8203;soggy-cr0uton](https://github.com/soggy-cr0uton))
- [`6a11430`](autobrr/qui@6a11430): docs: clarify autobrr filter + apply troubleshooting ([#&#8203;1459](autobrr/qui#1459)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5a2edc2`](autobrr/qui@5a2edc2): docs: update 2 documentation files ([#&#8203;1454](autobrr/qui#1454)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`139ada9`](autobrr/qui@139ada9): docs: update contributing.md ([#&#8203;1470](autobrr/qui#1470)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`3909aa1`](autobrr/qui@3909aa1): docs: update docs/features/automations.md ([#&#8203;1447](autobrr/qui#1447)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5dc57ca`](autobrr/qui@5dc57ca): docs: update intro.md ([#&#8203;1453](autobrr/qui#1453)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5d9e986`](autobrr/qui@5d9e986): perf(web): memoize useDateTimeFormatters ([#&#8203;1403](autobrr/qui#1403)) ([@&#8203;jabloink](https://github.com/jabloink))

**Full Changelog**: <autobrr/qui@v1.13.1...v1.14.0>

#### Docker images

- `docker pull ghcr.io/autobrr/qui:v1.14.0`
- `docker pull ghcr.io/autobrr/qui:latest`

#### What to do next?

- Join our [Discord server](https://discord.autobrr.com/qui)

Thank you for using qui!

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNS43IiwidXBkYXRlZEluVmVyIjoiNDMuMjUuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=-->

Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/4154
Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net>
Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request pwa torrent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ability to register magnet links with qui

2 participants