├── web ├── dist │ └── .gitkeep ├── public │ ├── qui.png │ ├── icon.png │ ├── favicon.png │ ├── napster.png │ ├── swizzin.png │ ├── pwa-192x192.png │ ├── pwa-512x512.png │ ├── apple-touch-icon.png │ ├── vite.svg │ └── icons.svg ├── tsconfig.json ├── src │ ├── routes │ │ ├── login.tsx │ │ ├── setup.tsx │ │ ├── _authenticated │ │ │ ├── instances.tsx │ │ │ ├── dashboard.tsx │ │ │ ├── search.tsx │ │ │ ├── services.tsx │ │ │ ├── backups.tsx │ │ │ ├── instances.index.tsx │ │ │ ├── cross-seed.tsx │ │ │ └── settings.tsx │ │ ├── __root.tsx │ │ ├── index.tsx │ │ └── _authenticated.tsx │ ├── vite-env.d.ts │ ├── main.tsx │ ├── lib │ │ ├── build-info.ts │ │ ├── polar-constants.ts │ │ ├── search-id-parsing.ts │ │ ├── torrent-task-polling.ts │ │ ├── base-url.ts │ │ ├── torrent-state-utils.ts │ │ ├── torrent-peer-flags.ts │ │ ├── category-utils.ts │ │ ├── license-errors.ts │ │ └── linkUtils.tsx │ ├── components │ │ ├── ui │ │ │ ├── collapsible.tsx │ │ │ ├── sort-icon.tsx │ │ │ ├── SwizzinLogo.tsx │ │ │ ├── NapsterLogo.tsx │ │ │ ├── visually-hidden.tsx │ │ │ ├── sonner.tsx │ │ │ ├── label.tsx │ │ │ ├── separator.tsx │ │ │ ├── textarea.tsx │ │ │ ├── progress.tsx │ │ │ ├── input.tsx │ │ │ ├── tracker-icon.tsx │ │ │ ├── switch.tsx │ │ │ ├── popover.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── SearchInput.tsx │ │ │ ├── badge.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── alert.tsx │ │ │ ├── Logo.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── scroll-to-top-button.tsx │ │ │ └── slider.tsx │ │ ├── torrents │ │ │ ├── details │ │ │ │ └── index.ts │ │ │ ├── piece-size.ts │ │ │ └── DeleteFilesPreference.tsx │ │ ├── Footer.tsx │ │ ├── instances │ │ │ ├── PasswordIssuesBanner.tsx │ │ │ └── InstanceSettingsButton.tsx │ │ └── magicui │ │ │ ├── shine-border.tsx │ │ │ └── blur-fade.tsx │ ├── hooks │ │ ├── useDebounce.ts │ │ ├── usePersistedAccordion.ts │ │ ├── useSearchHistory.ts │ │ ├── useTrackerIcons.ts │ │ ├── usePersistedThemeVariation.ts │ │ ├── useInstanceTrackers.ts │ │ ├── useInstanceCapabilities.ts │ │ ├── usePersistedTabState.ts │ │ ├── usePersistedSidebarState.ts │ │ ├── usePersistedInstanceSelection.ts │ │ ├── usePersistedColumnFilters.ts │ │ ├── useItemPartition.ts │ │ ├── usePersistedShowEmptyState.ts │ │ ├── usePersistedStartPaused.ts │ │ ├── useInstanceMetadata.ts │ │ ├── useTheme.ts │ │ ├── useDateTimeFormatters.ts │ │ ├── usePersistedColumnSorting.ts │ │ ├── usePersistedColumnVisibility.ts │ │ ├── usePersistedCollapsedCategories.ts │ │ ├── useAlternativeSpeedLimits.ts │ │ ├── usePersistedFilterSidebarState.ts │ │ └── useAuth.ts │ ├── router.tsx │ ├── config │ │ └── themes.ts │ ├── App.tsx │ ├── contexts │ │ ├── LayoutRouteContext.tsx │ │ └── MobileScrollContext.tsx │ └── pages │ │ └── NotFound.tsx ├── components.json ├── tsconfig.node.json ├── tsconfig.app.json ├── eslint.config.js └── build.go ├── .github ├── FUNDING.yml ├── assets │ ├── qui.png │ └── qui_old.png ├── dependabot.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── internal ├── database │ └── migrations │ │ ├── 008_add_tls_skip_verify.sql │ │ ├── 003_add_basic_auth.sql │ │ ├── 024_add_reannounce_max_retries.sql │ │ ├── 006_sessions_table.sql │ │ ├── 023_add_use_cross_category_suffix.sql │ │ ├── 034_add_tracker_customization_included_stats.sql │ │ ├── 022_remove_completion_delay_settings.sql │ │ ├── 021_add_completion_delay_settings.sql │ │ ├── 032_add_rss_source_filters.sql │ │ ├── 031_add_skip_auto_resume_settings.sql │ │ ├── 033_add_webhook_source_filters.sql │ │ ├── 017_add_cross_seed_completion_settings.sql │ │ ├── 028_fix_cross_seed_mutual_exclusivity.sql │ │ ├── 004_add_client_api_keys.sql │ │ ├── 013_add_empty_string_to_pool.sql │ │ ├── 020_add_source_specific_tags.sql │ │ ├── 011_add_external_programs.sql │ │ ├── 025_add_tracker_customizations.sql │ │ ├── 002_theme_licenses.sql │ │ ├── 018_add_cross_seed_search_settings.sql │ │ ├── 029_add_instance_crossseed_completion_settings.sql │ │ ├── 026_add_dashboard_settings.sql │ │ ├── 005_add_instance_errors.sql │ │ ├── 015_add_instance_activation_flag.sql │ │ ├── 019_add_tracker_rules.sql │ │ ├── 007_rename_theme_licenses_table_to_licenses.sql │ │ ├── 016_add_instance_reannounce_settings.sql │ │ ├── 001_initial_schema.sql │ │ ├── 012_add_instance_sort_order.sql │ │ └── 027_remove_dashboard_settings_fk.sql ├── backups │ └── testdata │ │ └── qbittorrent_4_6.torrent ├── services │ ├── crossseed │ │ ├── release_cache.go │ │ ├── layout_test.go │ │ └── search_query_test.go │ ├── license │ │ ├── license_service_test.go │ │ └── checker.go │ └── filesmanager │ │ └── models.go ├── domain │ ├── secrets.go │ └── config.go ├── qbittorrent │ ├── filter_types.go │ ├── context.go │ ├── client_test.go │ ├── metrics.go │ └── invalidation_test.go ├── proxy │ └── buffer_pool.go ├── api │ ├── server_cors_test.go │ ├── handlers │ │ ├── external_programs_test.go │ │ ├── version.go │ │ ├── crossseed_webhook_status_test.go │ │ ├── helpers.go │ │ ├── tracker_icons.go │ │ ├── dashboard_settings.go │ │ └── auth_setup_test.go │ └── middleware │ │ ├── logger.go │ │ └── chi.go ├── buildinfo │ └── buildinfo.go ├── config │ └── generate_test.go ├── metrics │ └── metrics.go ├── pkg │ └── timeouts │ │ └── timeouts.go ├── update │ └── updater.go └── models │ ├── license.go │ ├── helpers.go │ └── testing_helpers_test.go ├── pkg ├── httphelpers │ ├── response.go │ └── basepath.go ├── gojackett │ └── jackett.go ├── releases │ └── parser.go └── stringutils │ └── normalize.go ├── .air.toml ├── docker-compose.yml ├── .gitignore ├── Dockerfile └── ci.Dockerfile /web/dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [s0up4200, zze0s] 2 | buy_me_a_coffee: s0up4200 -------------------------------------------------------------------------------- /web/public/qui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autobrr/qui/HEAD/web/public/qui.png -------------------------------------------------------------------------------- /web/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autobrr/qui/HEAD/web/public/icon.png -------------------------------------------------------------------------------- /.github/assets/qui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autobrr/qui/HEAD/.github/assets/qui.png -------------------------------------------------------------------------------- /web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autobrr/qui/HEAD/web/public/favicon.png -------------------------------------------------------------------------------- /web/public/napster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autobrr/qui/HEAD/web/public/napster.png -------------------------------------------------------------------------------- /web/public/swizzin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autobrr/qui/HEAD/web/public/swizzin.png -------------------------------------------------------------------------------- /.github/assets/qui_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autobrr/qui/HEAD/.github/assets/qui_old.png -------------------------------------------------------------------------------- /web/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autobrr/qui/HEAD/web/public/pwa-192x192.png -------------------------------------------------------------------------------- /web/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autobrr/qui/HEAD/web/public/pwa-512x512.png -------------------------------------------------------------------------------- /web/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/autobrr/qui/HEAD/web/public/apple-touch-icon.png -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /internal/database/migrations/008_add_tls_skip_verify.sql: -------------------------------------------------------------------------------- 1 | -- Add TLS skip verification option to qBittorrent instances 2 | ALTER TABLE instances ADD COLUMN tls_skip_verify BOOLEAN NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /internal/backups/testdata/qbittorrent_4_6.torrent: -------------------------------------------------------------------------------- 1 | d8:announce36:https://tracker.example.com/announce10:created by18:qBittorrent v4.6.24:infod6:lengthi12345e4:name13:test-file.txt12:piece lengthi16384e6:pieces20:aaaaaaaaaaaaaaaaaaaaee -------------------------------------------------------------------------------- /internal/database/migrations/003_add_basic_auth.sql: -------------------------------------------------------------------------------- 1 | -- Add HTTP Basic Authentication fields to instances table 2 | ALTER TABLE instances ADD COLUMN basic_username TEXT; 3 | ALTER TABLE instances ADD COLUMN basic_password_encrypted TEXT; -------------------------------------------------------------------------------- /internal/database/migrations/024_add_reannounce_max_retries.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2025, s0up and the autobrr contributors. 2 | -- SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | ALTER TABLE instance_reannounce_settings ADD COLUMN max_retries INTEGER NOT NULL DEFAULT 50; 5 | -------------------------------------------------------------------------------- /web/src/routes/login.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { createFileRoute } from "@tanstack/react-router" 7 | import { Login } from "@/pages/Login" 8 | 9 | export const Route = createFileRoute("/login")({ 10 | component: Login, 11 | }) -------------------------------------------------------------------------------- /web/src/routes/setup.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { createFileRoute } from "@tanstack/react-router" 7 | import { Setup } from "@/pages/Setup" 8 | 9 | export const Route = createFileRoute("/setup")({ 10 | component: Setup, 11 | }) -------------------------------------------------------------------------------- /web/src/routes/_authenticated/instances.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { createFileRoute, Outlet } from "@tanstack/react-router" 7 | 8 | export const Route = createFileRoute("/_authenticated/instances")({ 9 | component: () => , 10 | }) -------------------------------------------------------------------------------- /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | /// 7 | /// 8 | 9 | declare global { 10 | interface Window { 11 | __QUI_VERSION__?: string 12 | } 13 | } 14 | 15 | export {} 16 | -------------------------------------------------------------------------------- /internal/database/migrations/006_sessions_table.sql: -------------------------------------------------------------------------------- 1 | -- Create sessions table for database-backed session storage 2 | -- This replaces cookie-based sessions with secure database storage 3 | 4 | CREATE TABLE sessions ( 5 | token TEXT PRIMARY KEY, 6 | data BLOB NOT NULL, 7 | expiry REAL NOT NULL 8 | ); 9 | 10 | CREATE INDEX sessions_expiry_idx ON sessions(expiry); -------------------------------------------------------------------------------- /internal/database/migrations/023_add_use_cross_category_suffix.sql: -------------------------------------------------------------------------------- 1 | -- Add setting to control .cross category suffix behavior 2 | -- Default TRUE preserves existing behavior where cross-seeds get .cross suffix 3 | -- When FALSE, cross-seeds use the same category as the matched torrent 4 | 5 | ALTER TABLE cross_seed_settings ADD COLUMN use_cross_category_suffix BOOLEAN NOT NULL DEFAULT TRUE; 6 | -------------------------------------------------------------------------------- /web/src/routes/_authenticated/dashboard.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { createFileRoute } from "@tanstack/react-router" 7 | import { Dashboard } from "@/pages/Dashboard" 8 | 9 | export const Route = createFileRoute("/_authenticated/dashboard")({ 10 | component: Dashboard, 11 | }) -------------------------------------------------------------------------------- /web/src/routes/_authenticated/search.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { createFileRoute } from '@tanstack/react-router' 7 | import { Search } from '@/pages/Search' 8 | 9 | export const Route = createFileRoute('/_authenticated/search')({ 10 | component: Search, 11 | }) 12 | -------------------------------------------------------------------------------- /web/src/routes/_authenticated/services.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { Services } from "@/pages/Services" 7 | import { createFileRoute } from "@tanstack/react-router" 8 | 9 | export const Route = createFileRoute("/_authenticated/services")({ 10 | component: Services, 11 | }) 12 | -------------------------------------------------------------------------------- /web/src/main.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { StrictMode } from "react" 7 | import { createRoot } from "react-dom/client" 8 | import App from "./App.tsx" 9 | import "./index.css" 10 | createRoot(document.getElementById("root")!).render( 11 | 12 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /web/src/routes/__root.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { createRootRoute, Outlet } from "@tanstack/react-router" 7 | import { NotFound } from "@/pages/NotFound" 8 | 9 | export const Route = createRootRoute({ 10 | component: () => ( 11 | <> 12 | 13 | 14 | ), 15 | notFoundComponent: NotFound, 16 | }) -------------------------------------------------------------------------------- /web/src/routes/_authenticated/backups.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { InstanceBackups } from "@/pages/InstanceBackups" 7 | import { createFileRoute } from "@tanstack/react-router" 8 | 9 | export const Route = createFileRoute("/_authenticated/backups")({ 10 | component: BackupsRoute, 11 | }) 12 | 13 | function BackupsRoute() { 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /pkg/httphelpers/response.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025, s0up and the autobrr contributors. 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | package httphelpers 5 | 6 | import ( 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | // DrainAndClose consumes the remaining response body and closes it to allow connection reuse. 12 | func DrainAndClose(resp *http.Response) { 13 | if resp == nil || resp.Body == nil { 14 | return 15 | } 16 | _, _ = io.Copy(io.Discard, resp.Body) 17 | resp.Body.Close() 18 | } 19 | -------------------------------------------------------------------------------- /web/src/lib/build-info.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | const FALLBACK_VERSION = "0.0.0-dev" 7 | 8 | export function getAppVersion(): string { 9 | if (typeof window === "undefined") { 10 | return FALLBACK_VERSION 11 | } 12 | 13 | const version = window.__QUI_VERSION__ 14 | if (!version || version.trim() === "") { 15 | return FALLBACK_VERSION 16 | } 17 | 18 | return version 19 | } 20 | -------------------------------------------------------------------------------- /web/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 7 | 8 | const Collapsible = CollapsiblePrimitive.Root 9 | 10 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 11 | 12 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 13 | 14 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 15 | -------------------------------------------------------------------------------- /internal/database/migrations/034_add_tracker_customization_included_stats.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2025, s0up and the autobrr contributors. 2 | -- SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | -- Add included_in_stats column to tracker_customizations 5 | -- Stores comma-separated list of secondary domains whose stats SHOULD be included in combined totals 6 | -- Empty = only primary domain stats shown (backwards compatible with pre-feature behavior) 7 | ALTER TABLE tracker_customizations ADD COLUMN included_in_stats TEXT DEFAULT ''; 8 | -------------------------------------------------------------------------------- /web/src/components/torrents/details/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | export { CrossSeedTable } from "./CrossSeedTable" 7 | export { GeneralTabHorizontal } from "./GeneralTabHorizontal" 8 | export { PeersTable } from "./PeersTable" 9 | export { StatRow } from "./StatRow" 10 | export { TorrentFileTable } from "./TorrentFileTable" 11 | export { TrackersTable } from "./TrackersTable" 12 | export { WebSeedsTable } from "./WebSeedsTable" 13 | -------------------------------------------------------------------------------- /internal/database/migrations/022_remove_completion_delay_settings.sql: -------------------------------------------------------------------------------- 1 | -- Remove completion delay settings (no longer needed with .cross category suffixing) 2 | -- Cross-seeded torrents now go into .cross suffixed categories (e.g., movies.cross) 3 | -- which prevents *arr applications from importing them, making delay unnecessary. 4 | 5 | -- Note: SQLite 3.35.0+ supports DROP COLUMN 6 | ALTER TABLE cross_seed_settings 7 | DROP COLUMN completion_delay_minutes; 8 | 9 | ALTER TABLE cross_seed_settings 10 | DROP COLUMN completion_pre_import_categories; 11 | -------------------------------------------------------------------------------- /web/src/components/ui/sort-icon.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react" 7 | 8 | interface SortIconProps { 9 | sorted: false | "asc" | "desc" 10 | } 11 | 12 | export function SortIcon({ sorted }: SortIconProps) { 13 | if (sorted === "asc") return 14 | if (sorted === "desc") return 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /internal/services/crossseed/release_cache.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025, s0up and the autobrr contributors. 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | package crossseed 5 | 6 | import "github.com/autobrr/qui/pkg/releases" 7 | 8 | // ReleaseCache is preserved for backwards compatibility within the crossseed package. 9 | // It is an alias to the shared releases.Parser. 10 | type ReleaseCache = releases.Parser 11 | 12 | // NewReleaseCache creates a cached parser for release metadata. 13 | func NewReleaseCache() *ReleaseCache { 14 | return releases.NewDefaultParser() 15 | } 16 | -------------------------------------------------------------------------------- /internal/domain/secrets.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025, s0up and the autobrr contributors. 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | package domain 5 | 6 | const RedactedStr = "" 7 | 8 | // RedactString replaces a string with redacted placeholder 9 | func RedactString(s string) string { 10 | if len(s) == 0 { 11 | return "" 12 | } 13 | 14 | return RedactedStr 15 | } 16 | 17 | // IsRedactedString checks if a value is the redacted placeholder 18 | func IsRedactedString(s string) bool { 19 | if s == "" { 20 | return false 21 | } 22 | return s == RedactedStr 23 | } 24 | -------------------------------------------------------------------------------- /web/src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { useState, useEffect } from "react" 7 | 8 | export function useDebounce(value: T, delay: number): T { 9 | const [debouncedValue, setDebouncedValue] = useState(value) 10 | 11 | useEffect(() => { 12 | const handler = setTimeout(() => { 13 | setDebouncedValue(value) 14 | }, delay) 15 | 16 | return () => { 17 | clearTimeout(handler) 18 | } 19 | }, [value, delay]) 20 | 21 | return debouncedValue 22 | } -------------------------------------------------------------------------------- /web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true 11 | }, 12 | "iconLibrary": "lucide", 13 | "aliases": { 14 | "components": "src/components", 15 | "utils": "src/lib/utils" 16 | }, 17 | "registries": { 18 | "@aceternity": "https://ui.aceternity.com/registry/{name}.json", 19 | "@magicui": "https://magicui.design/r/{name}.json" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/src/components/ui/SwizzinLogo.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { cn } from "@/lib/utils" 7 | import { withBasePath } from "@/lib/base-url" 8 | 9 | interface SwizzinLogoProps { 10 | className?: string 11 | } 12 | 13 | export function SwizzinLogo({ className }: SwizzinLogoProps) { 14 | return ( 15 | Swizzin 20 | ) 21 | } -------------------------------------------------------------------------------- /web/src/components/ui/NapsterLogo.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { withBasePath } from "@/lib/base-url" 7 | import { cn } from "@/lib/utils" 8 | 9 | interface NapsterLogoProps { 10 | className?: string 11 | } 12 | 13 | export function NapsterLogo({ className }: NapsterLogoProps) { 14 | return ( 15 | Napster 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /web/src/lib/polar-constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | /** 7 | * Polar.sh related constants 8 | */ 9 | 10 | // Customer portal URL for managing license activations 11 | export const POLAR_PORTAL_URL = "https://polar.sh/qbitwebui/portal/request" 12 | export const POLAR_CHECKOUT_URL = "https://buy.polar.sh/polar_cl_yyXJesVM9pFVfAPIplspbfCukgVgXzXjXIc2N0I8WcL" 13 | export const POLAR_SANDBOX_CHECKOUT_URL = "https://sandbox-api.polar.sh/v1/checkout-links/polar_cl_5dhqiQBBNaPiCc0sniCocHAJI84X1JCGGI98Y0B7zg5/redirect" 14 | -------------------------------------------------------------------------------- /internal/database/migrations/021_add_completion_delay_settings.sql: -------------------------------------------------------------------------------- 1 | -- Add completion delay settings for *arr import timing 2 | -- delay_minutes: time to wait after completion before triggering cross-seed search 3 | -- pre_import_categories: categories that indicate torrent is waiting for *arr import 4 | -- If category changes from a pre-import category, search triggers immediately (skipping delay) 5 | 6 | ALTER TABLE cross_seed_settings 7 | ADD COLUMN completion_delay_minutes INTEGER NOT NULL DEFAULT 0; 8 | 9 | ALTER TABLE cross_seed_settings 10 | ADD COLUMN completion_pre_import_categories TEXT NOT NULL DEFAULT '[]'; 11 | -------------------------------------------------------------------------------- /internal/database/migrations/032_add_rss_source_filters.sql: -------------------------------------------------------------------------------- 1 | -- Add RSS source filtering columns for cross-seed automation 2 | -- These filter which LOCAL torrents are considered when matching RSS feed items 3 | -- Empty arrays mean "all" (no filtering) 4 | 5 | ALTER TABLE cross_seed_settings ADD COLUMN rss_source_categories TEXT NOT NULL DEFAULT '[]'; 6 | ALTER TABLE cross_seed_settings ADD COLUMN rss_source_tags TEXT NOT NULL DEFAULT '[]'; 7 | ALTER TABLE cross_seed_settings ADD COLUMN rss_source_exclude_categories TEXT NOT NULL DEFAULT '[]'; 8 | ALTER TABLE cross_seed_settings ADD COLUMN rss_source_exclude_tags TEXT NOT NULL DEFAULT '[]'; 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | day: saturday 8 | time: "07:00" 9 | groups: 10 | github: 11 | patterns: 12 | - "*" 13 | 14 | - package-ecosystem: gomod 15 | directory: / 16 | schedule: 17 | interval: monthly 18 | groups: 19 | golang: 20 | patterns: 21 | - "*" 22 | 23 | - package-ecosystem: npm 24 | directory: /web 25 | schedule: 26 | interval: monthly 27 | groups: 28 | npm: 29 | patterns: 30 | - "*" 31 | -------------------------------------------------------------------------------- /internal/database/migrations/031_add_skip_auto_resume_settings.sql: -------------------------------------------------------------------------------- 1 | -- Add per-mode skip auto-resume settings for cross-seed 2 | -- When enabled, torrents remain paused after hash check instead of auto-resuming 3 | -- Default to false to preserve existing behavior 4 | 5 | ALTER TABLE cross_seed_settings ADD COLUMN skip_auto_resume_rss BOOLEAN NOT NULL DEFAULT 0; 6 | ALTER TABLE cross_seed_settings ADD COLUMN skip_auto_resume_seeded_search BOOLEAN NOT NULL DEFAULT 0; 7 | ALTER TABLE cross_seed_settings ADD COLUMN skip_auto_resume_completion BOOLEAN NOT NULL DEFAULT 0; 8 | ALTER TABLE cross_seed_settings ADD COLUMN skip_auto_resume_webhook BOOLEAN NOT NULL DEFAULT 0; 9 | -------------------------------------------------------------------------------- /internal/database/migrations/033_add_webhook_source_filters.sql: -------------------------------------------------------------------------------- 1 | -- Add webhook source filtering columns for cross-seed automation. 2 | -- These filter which LOCAL torrents are considered when checking webhook requests. 3 | -- Empty arrays mean "all" (no filtering). 4 | 5 | ALTER TABLE cross_seed_settings ADD COLUMN webhook_source_categories TEXT NOT NULL DEFAULT '[]'; 6 | ALTER TABLE cross_seed_settings ADD COLUMN webhook_source_tags TEXT NOT NULL DEFAULT '[]'; 7 | ALTER TABLE cross_seed_settings ADD COLUMN webhook_source_exclude_categories TEXT NOT NULL DEFAULT '[]'; 8 | ALTER TABLE cross_seed_settings ADD COLUMN webhook_source_exclude_tags TEXT NOT NULL DEFAULT '[]'; 9 | -------------------------------------------------------------------------------- /web/src/lib/search-id-parsing.ts: -------------------------------------------------------------------------------- 1 | const IMDB_REGEX = /tt(\d{7,})/i 2 | const TVDB_REGEX = /(?:the)?tvdb(?:id)?\s*[:#=]?\s*(\d{5,})/i 3 | 4 | export function extractImdbId(rawQuery: string): string | null { 5 | if (!rawQuery) { 6 | return null 7 | } 8 | 9 | const match = rawQuery.match(IMDB_REGEX) 10 | if (!match) { 11 | return null 12 | } 13 | 14 | return `tt${match[1]}` 15 | } 16 | 17 | export function extractTvdbId(rawQuery: string): string | null { 18 | if (!rawQuery) { 19 | return null 20 | } 21 | 22 | const match = rawQuery.match(TVDB_REGEX) 23 | if (!match) { 24 | return null 25 | } 26 | 27 | return match[1] 28 | } 29 | -------------------------------------------------------------------------------- /web/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { createFileRoute, Navigate } from "@tanstack/react-router" 7 | import { useAuth } from "@/hooks/useAuth" 8 | 9 | export const Route = createFileRoute("/")({ 10 | component: IndexComponent, 11 | }) 12 | 13 | function IndexComponent() { 14 | const { isAuthenticated, isLoading } = useAuth() 15 | 16 | if (isLoading) { 17 | return
Loading...
18 | } 19 | 20 | if (!isAuthenticated) { 21 | return 22 | } 23 | 24 | return 25 | } -------------------------------------------------------------------------------- /internal/database/migrations/017_add_cross_seed_completion_settings.sql: -------------------------------------------------------------------------------- 1 | -- Add completion-triggered cross-seed automation fields 2 | ALTER TABLE cross_seed_settings 3 | ADD COLUMN completion_enabled BOOLEAN NOT NULL DEFAULT 0; 4 | 5 | ALTER TABLE cross_seed_settings 6 | ADD COLUMN completion_categories TEXT NOT NULL DEFAULT '[]'; 7 | 8 | ALTER TABLE cross_seed_settings 9 | ADD COLUMN completion_tags TEXT NOT NULL DEFAULT '[]'; 10 | 11 | ALTER TABLE cross_seed_settings 12 | ADD COLUMN completion_exclude_categories TEXT NOT NULL DEFAULT '[]'; 13 | 14 | ALTER TABLE cross_seed_settings 15 | ADD COLUMN completion_exclude_tags TEXT NOT NULL DEFAULT '[]'; 16 | -------------------------------------------------------------------------------- /internal/database/migrations/028_fix_cross_seed_mutual_exclusivity.sql: -------------------------------------------------------------------------------- 1 | -- Fix lockout for users who had use_category_from_indexer enabled before migration 023. 2 | -- Migration 023 added use_cross_category_suffix with DEFAULT TRUE, which caused both 3 | -- settings to be enabled simultaneously. Since these settings are mutually exclusive 4 | -- in the UI, users became locked out of both toggles. 5 | -- 6 | -- Resolution: Preserve the user's original choice (use_category_from_indexer) and 7 | -- disable the new setting that was auto-enabled by the migration default. 8 | 9 | UPDATE cross_seed_settings 10 | SET use_cross_category_suffix = 0 11 | WHERE use_category_from_indexer = 1; 12 | -------------------------------------------------------------------------------- /web/src/components/ui/visually-hidden.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import * as React from "react" 7 | import { cn } from "@/lib/utils" 8 | 9 | interface VisuallyHiddenProps extends React.HTMLAttributes {} 10 | 11 | function VisuallyHidden({ className, ...props }: VisuallyHiddenProps) { 12 | return ( 13 | 20 | ) 21 | } 22 | 23 | export { VisuallyHidden } -------------------------------------------------------------------------------- /web/src/hooks/usePersistedAccordion.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { useState, useEffect } from "react" 7 | 8 | export function usePersistedAccordion() { 9 | const [expandedItems, setExpandedItems] = useState(() => { 10 | const stored = localStorage.getItem("qui-accordion") 11 | return stored ? JSON.parse(stored) : ["status", "categories", "tags", "trackers"] 12 | }) 13 | 14 | useEffect(() => { 15 | localStorage.setItem("qui-accordion", JSON.stringify(expandedItems)) 16 | }, [expandedItems]) 17 | 18 | return [expandedItems, setExpandedItems] as const 19 | } -------------------------------------------------------------------------------- /web/src/routes/_authenticated/instances.index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { createFileRoute, Navigate } from "@tanstack/react-router" 7 | 8 | export const Route = createFileRoute("/_authenticated/instances/")({ 9 | component: InstancesRedirect, 10 | }) 11 | 12 | function InstancesRedirect() { 13 | const search = Route.useSearch() as Record 14 | const nextSearch = { 15 | ...search, 16 | tab: "instances" as const, 17 | } 18 | 19 | return ( 20 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /internal/database/migrations/004_add_client_api_keys.sql: -------------------------------------------------------------------------------- 1 | -- Create client_api_keys table for proxy authentication 2 | CREATE TABLE client_api_keys ( 3 | id INTEGER PRIMARY KEY AUTOINCREMENT, 4 | key_hash TEXT NOT NULL UNIQUE, 5 | client_name TEXT NOT NULL, 6 | instance_id INTEGER NOT NULL, 7 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 8 | last_used_at TIMESTAMP, 9 | FOREIGN KEY (instance_id) REFERENCES instances(id) ON DELETE CASCADE 10 | ); 11 | 12 | -- Create index for faster lookups by key_hash 13 | CREATE INDEX idx_client_api_keys_key_hash ON client_api_keys(key_hash); 14 | 15 | -- Create index for instance_id lookups 16 | CREATE INDEX idx_client_api_keys_instance_id ON client_api_keys(instance_id); -------------------------------------------------------------------------------- /web/src/routes/_authenticated.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { useAuth } from "@/hooks/useAuth" 7 | import { AppLayout } from "@/layouts/AppLayout" 8 | import { createFileRoute, Navigate } from "@tanstack/react-router" 9 | 10 | export const Route = createFileRoute("/_authenticated")({ 11 | component: AuthLayout, 12 | }) 13 | 14 | function AuthLayout() { 15 | const { isAuthenticated, isLoading } = useAuth() 16 | 17 | if (isLoading) { 18 | return
Loading...
19 | } 20 | 21 | if (!isAuthenticated) { 22 | return 23 | } 24 | 25 | return 26 | } -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /web/src/router.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { createRouter } from "@tanstack/react-router" 7 | import { routeTree } from "./routeTree.gen" 8 | import { getBaseUrl } from "./lib/base-url" 9 | 10 | // Get the base path from the injected global variable 11 | // Remove trailing slash for TanStack Router 12 | const basepath = getBaseUrl().slice(0, -1) || undefined 13 | 14 | export const router = createRouter({ 15 | routeTree, 16 | basepath, 17 | defaultPreload: "intent", 18 | context: { 19 | auth: undefined!, 20 | }, 21 | }) 22 | 23 | declare module "@tanstack/react-router" { 24 | interface Register { 25 | router: typeof router 26 | } 27 | } -------------------------------------------------------------------------------- /internal/qbittorrent/filter_types.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025, s0up and the autobrr contributors. 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | package qbittorrent 5 | 6 | // FilterOptions represents the filter options from the frontend 7 | type FilterOptions struct { 8 | Hashes []string `json:"hashes"` 9 | Status []string `json:"status"` 10 | ExcludeStatus []string `json:"excludeStatus"` 11 | Categories []string `json:"categories"` 12 | ExcludeCategories []string `json:"excludeCategories"` 13 | Tags []string `json:"tags"` 14 | ExcludeTags []string `json:"excludeTags"` 15 | Trackers []string `json:"trackers"` 16 | ExcludeTrackers []string `json:"excludeTrackers"` 17 | Expr string `json:"expr"` 18 | } 19 | -------------------------------------------------------------------------------- /internal/qbittorrent/context.go: -------------------------------------------------------------------------------- 1 | package qbittorrent 2 | 3 | import "context" 4 | 5 | type contextKey string 6 | 7 | const skipTrackerHydrationKey contextKey = "qui_skip_tracker_hydration" 8 | 9 | // WithSkipTrackerHydration marks the context so tracker enrichment/hydration is skipped. 10 | func WithSkipTrackerHydration(ctx context.Context) context.Context { 11 | if ctx == nil { 12 | ctx = context.Background() 13 | } 14 | return context.WithValue(ctx, skipTrackerHydrationKey, true) 15 | } 16 | 17 | // shouldSkipTrackerHydration returns true when the context requests tracker enrichment to be skipped. 18 | func shouldSkipTrackerHydration(ctx context.Context) bool { 19 | if ctx == nil { 20 | return false 21 | } 22 | val, ok := ctx.Value(skipTrackerHydrationKey).(bool) 23 | return ok && val 24 | } 25 | -------------------------------------------------------------------------------- /internal/database/migrations/013_add_empty_string_to_pool.sql: -------------------------------------------------------------------------------- 1 | -- Migration 013: Add empty string to string_pool for localhost bypass support 2 | -- This fixes issue #573 where localhost bypass authentication fails in v1.7.0+ 3 | -- The empty string is needed when creating instances with empty username (localhost bypass) 4 | -- 5 | -- NOTE: This migration compensates for a retroactive change made to migration 010 after 6 | -- its release (commit 9480692). Migration 010 originally inserted only '(unknown)' and 7 | -- '(unnamed)' but was later modified to include '' which broke migration immutability. 8 | -- Migration 010 has been reverted to its original form, and this migration provides the 9 | -- forward-safe fix for already-deployed databases. 10 | 11 | INSERT OR IGNORE INTO string_pool (value) VALUES (''); 12 | -------------------------------------------------------------------------------- /web/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { useTheme } from "next-themes" 7 | import { Toaster as Sonner, type ToasterProps } from "sonner" 8 | 9 | const Toaster = ({ ...props }: ToasterProps) => { 10 | const { theme = "system" } = useTheme() 11 | 12 | return ( 13 | 25 | ) 26 | } 27 | 28 | export { Toaster } 29 | -------------------------------------------------------------------------------- /web/src/hooks/useSearchHistory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { api } from "@/lib/api" 7 | import { useQuery } from "@tanstack/react-query" 8 | 9 | interface UseSearchHistoryOptions { 10 | limit?: number 11 | enabled?: boolean 12 | refetchInterval?: number | false 13 | } 14 | 15 | export const useSearchHistory = (options: UseSearchHistoryOptions = {}) => { 16 | const { limit = 50, enabled = true, refetchInterval = false } = options 17 | 18 | return useQuery({ 19 | queryKey: ["searchHistory", limit], 20 | queryFn: () => api.getSearchHistory(limit), 21 | enabled, 22 | refetchInterval, 23 | staleTime: 5 * 1000, // 5 seconds 24 | refetchOnWindowFocus: false, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /internal/database/migrations/020_add_source_specific_tags.sql: -------------------------------------------------------------------------------- 1 | -- Add source-specific tagging columns for cross-seed automation 2 | -- Each source type (RSS, Seeded Search, Completion, Webhook) can have its own tags 3 | -- Default is ["cross-seed"] for all sources 4 | 5 | ALTER TABLE cross_seed_settings 6 | ADD COLUMN rss_automation_tags TEXT NOT NULL DEFAULT '["cross-seed"]'; 7 | 8 | ALTER TABLE cross_seed_settings 9 | ADD COLUMN seeded_search_tags TEXT NOT NULL DEFAULT '["cross-seed"]'; 10 | 11 | ALTER TABLE cross_seed_settings 12 | ADD COLUMN completion_search_tags TEXT NOT NULL DEFAULT '["cross-seed"]'; 13 | 14 | ALTER TABLE cross_seed_settings 15 | ADD COLUMN webhook_tags TEXT NOT NULL DEFAULT '["cross-seed"]'; 16 | 17 | ALTER TABLE cross_seed_settings 18 | ADD COLUMN inherit_source_tags BOOLEAN NOT NULL DEFAULT 0; 19 | -------------------------------------------------------------------------------- /internal/database/migrations/011_add_external_programs.sql: -------------------------------------------------------------------------------- 1 | -- Migration 011: Add external_programs table 2 | -- This table stores configurations for external programs that can be executed from the torrent context menu 3 | 4 | CREATE TABLE IF NOT EXISTS external_programs ( 5 | id INTEGER PRIMARY KEY AUTOINCREMENT, 6 | name TEXT NOT NULL UNIQUE, 7 | path TEXT NOT NULL, 8 | args_template TEXT NOT NULL DEFAULT '', 9 | enabled INTEGER NOT NULL DEFAULT 1, 10 | use_terminal INTEGER NOT NULL DEFAULT 1, 11 | path_mappings TEXT NOT NULL DEFAULT '[]', 12 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 13 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 14 | ); 15 | 16 | -- Create index on enabled status for faster filtering 17 | CREATE INDEX IF NOT EXISTS idx_external_programs_enabled ON external_programs(enabled); 18 | -------------------------------------------------------------------------------- /internal/database/migrations/025_add_tracker_customizations.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2025, s0up and the autobrr contributors. 2 | -- SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | -- Tracker customizations: nicknames and merged tracker domains 5 | CREATE TABLE IF NOT EXISTS tracker_customizations ( 6 | id INTEGER PRIMARY KEY AUTOINCREMENT, 7 | display_name TEXT NOT NULL, 8 | domains TEXT NOT NULL, 9 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 10 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 11 | ); 12 | 13 | CREATE INDEX IF NOT EXISTS idx_tracker_customizations_domains ON tracker_customizations(domains); 14 | 15 | CREATE TRIGGER IF NOT EXISTS trg_tracker_customizations_updated 16 | AFTER UPDATE ON tracker_customizations 17 | BEGIN 18 | UPDATE tracker_customizations SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; 19 | END; 20 | -------------------------------------------------------------------------------- /web/src/hooks/useTrackerIcons.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { api } from "@/lib/api" 7 | import { useQuery } from "@tanstack/react-query" 8 | 9 | /** 10 | * Hook for fetching all cached tracker icons 11 | * Returns a map of tracker hostnames to base64-encoded data URLs 12 | */ 13 | export function useTrackerIcons() { 14 | const query = useQuery>({ 15 | queryKey: ["tracker-icons"], 16 | queryFn: () => api.getTrackerIcons(), 17 | staleTime: 60000, // 1 minute 18 | gcTime: 1800000, // Keep in cache for 30 minutes 19 | refetchInterval: 60000, // Refetch every 1 minute 20 | refetchIntervalInBackground: false, 21 | placeholderData: (previousData) => previousData, 22 | }) 23 | 24 | return query 25 | } 26 | -------------------------------------------------------------------------------- /internal/qbittorrent/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025, s0up and the autobrr contributors. 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | package qbittorrent 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | qbt "github.com/autobrr/go-qbittorrent" 11 | ) 12 | 13 | func TestClientUpdateServerStateDoesNotBlockOnClientMutex(t *testing.T) { 14 | t.Parallel() 15 | 16 | client := &Client{} 17 | client.mu.RLock() 18 | defer client.mu.RUnlock() 19 | 20 | done := make(chan struct{}) 21 | go func() { 22 | defer close(done) 23 | client.updateServerState(&qbt.MainData{ 24 | ServerState: qbt.ServerState{ 25 | ConnectionStatus: "connected", 26 | }, 27 | }) 28 | }() 29 | 30 | select { 31 | case <-done: 32 | case <-time.After(200 * time.Millisecond): 33 | t.Fatal("updateServerState blocked waiting for Client.mu write lock") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /web/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | "use client" 7 | 8 | import * as React from "react" 9 | import * as LabelPrimitive from "@radix-ui/react-label" 10 | 11 | import { cn } from "@/lib/utils" 12 | 13 | function Label({ 14 | className, 15 | ...props 16 | }: React.ComponentProps) { 17 | return ( 18 | 26 | ) 27 | } 28 | 29 | export { Label } 30 | -------------------------------------------------------------------------------- /web/src/lib/torrent-task-polling.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import type { TorrentCreationTask } from "@/types" 7 | 8 | export const ACTIVE_TORRENT_TASK_POLL_INTERVAL = 5000 9 | export const IDLE_TORRENT_TASK_POLL_INTERVAL = 30000 10 | 11 | type PollingOptions = { 12 | activeInterval?: number 13 | idleInterval?: number 14 | } 15 | 16 | export function getTorrentTaskPollInterval( 17 | tasks: TorrentCreationTask[] | undefined, 18 | options: PollingOptions = {} 19 | ): number { 20 | const { activeInterval = ACTIVE_TORRENT_TASK_POLL_INTERVAL, idleInterval = IDLE_TORRENT_TASK_POLL_INTERVAL } = options 21 | 22 | if (tasks?.some((task) => task.status === "Running" || task.status === "Queued")) { 23 | return activeInterval 24 | } 25 | 26 | return idleInterval 27 | } 28 | -------------------------------------------------------------------------------- /internal/database/migrations/002_theme_licenses.sql: -------------------------------------------------------------------------------- 1 | -- Add theme licenses table for Polar SDK integration 2 | CREATE TABLE theme_licenses ( 3 | id INTEGER PRIMARY KEY AUTOINCREMENT, 4 | license_key TEXT UNIQUE NOT NULL, 5 | theme_name TEXT NOT NULL, 6 | status TEXT NOT NULL DEFAULT 'active', -- 'active', 'expired', 'invalid' 7 | activated_at DATETIME DEFAULT CURRENT_TIMESTAMP, 8 | expires_at DATETIME, 9 | last_validated DATETIME DEFAULT CURRENT_TIMESTAMP, 10 | polar_customer_id TEXT, 11 | polar_product_id TEXT, 12 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 13 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 14 | ); 15 | 16 | -- Index for performance 17 | CREATE INDEX idx_theme_licenses_status ON theme_licenses(status); 18 | CREATE INDEX idx_theme_licenses_theme ON theme_licenses(theme_name); 19 | CREATE INDEX idx_theme_licenses_key ON theme_licenses(license_key); -------------------------------------------------------------------------------- /internal/proxy/buffer_pool.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025, s0up and the autobrr contributors. 2 | // SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | package proxy 5 | 6 | import "sync" 7 | 8 | // BufferPool provides a thread-safe pool of byte slices for the reverse proxy 9 | type BufferPool struct { 10 | pool sync.Pool 11 | } 12 | 13 | // NewBufferPool creates a new buffer pool with 32KB buffers 14 | func NewBufferPool() *BufferPool { 15 | return &BufferPool{ 16 | pool: sync.Pool{ 17 | New: func() any { 18 | // Create 32KB buffers - good balance between memory usage and performance 19 | return make([]byte, 32*1024) 20 | }, 21 | }, 22 | } 23 | } 24 | 25 | // Get returns a buffer from the pool 26 | func (p *BufferPool) Get() []byte { 27 | return p.pool.Get().([]byte) 28 | } 29 | 30 | // Put returns a buffer to the pool 31 | func (p *BufferPool) Put(buf []byte) { 32 | p.pool.Put(buf) 33 | } 34 | -------------------------------------------------------------------------------- /web/src/hooks/usePersistedThemeVariation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | const VARIATIONS_THEME_KEY = "variations-theme"; 7 | 8 | export const getStoredVariation = (themeId: string): string | null => { 9 | try { 10 | const stored = localStorage.getItem(VARIATIONS_THEME_KEY); 11 | if (!stored) return null; 12 | const parsed = JSON.parse(stored); 13 | return parsed[themeId] || null; 14 | } catch { 15 | return null; 16 | } 17 | }; 18 | 19 | export const setStoredVariation = (themeId: string, variationId: string): void => { 20 | try { 21 | const stored = localStorage.getItem(VARIATIONS_THEME_KEY); 22 | const parsed = stored ? JSON.parse(stored) : {}; 23 | parsed[themeId] = variationId; 24 | localStorage.setItem(VARIATIONS_THEME_KEY, JSON.stringify(parsed)); 25 | } catch { 26 | // Ignore 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /web/src/hooks/useInstanceTrackers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import { useQuery } from "@tanstack/react-query" 7 | import { api } from "@/lib/api" 8 | 9 | interface UseInstanceTrackersOptions { 10 | enabled?: boolean 11 | staleTimeMs?: number 12 | } 13 | 14 | /** 15 | * Fetches and caches the active tracker domains for an instance. 16 | * This hook centralizes the query so multiple consumers share the same cache entry. 17 | */ 18 | export function useInstanceTrackers(instanceId: number, options: UseInstanceTrackersOptions = {}) { 19 | const { enabled = true, staleTimeMs = 1000 * 60 * 5 } = options 20 | 21 | return useQuery({ 22 | queryKey: ["instance-trackers", instanceId], 23 | queryFn: () => api.getActiveTrackers(instanceId), 24 | staleTime: staleTimeMs, 25 | enabled: Boolean(enabled && instanceId), 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /web/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "erasableSyntaxOnly": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true, 25 | 26 | /* Import alias */ 27 | "baseUrl": ".", 28 | "paths": { 29 | "@/*": ["./src/*"] 30 | } 31 | }, 32 | "include": ["src"] 33 | } 34 | -------------------------------------------------------------------------------- /internal/database/migrations/018_add_cross_seed_search_settings.sql: -------------------------------------------------------------------------------- 1 | -- Persist seeded torrent search defaults 2 | CREATE TABLE IF NOT EXISTS cross_seed_search_settings ( 3 | id INTEGER PRIMARY KEY CHECK (id = 1), 4 | instance_id INTEGER, 5 | categories TEXT NOT NULL DEFAULT '[]', 6 | tags TEXT NOT NULL DEFAULT '[]', 7 | indexer_ids TEXT NOT NULL DEFAULT '[]', 8 | interval_seconds INTEGER NOT NULL DEFAULT 60, 9 | cooldown_minutes INTEGER NOT NULL DEFAULT 720, 10 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | FOREIGN KEY (instance_id) REFERENCES instances(id) ON DELETE SET NULL 13 | ); 14 | 15 | CREATE TRIGGER IF NOT EXISTS cross_seed_search_settings_updated_at 16 | AFTER UPDATE ON cross_seed_search_settings 17 | FOR EACH ROW 18 | BEGIN 19 | UPDATE cross_seed_search_settings 20 | SET updated_at = CURRENT_TIMESTAMP 21 | WHERE id = NEW.id; 22 | END; 23 | -------------------------------------------------------------------------------- /internal/database/migrations/029_add_instance_crossseed_completion_settings.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2025, s0up and the autobrr contributors. 2 | -- SPDX-License-Identifier: GPL-2.0-or-later 3 | 4 | CREATE TABLE IF NOT EXISTS instance_crossseed_completion_settings ( 5 | instance_id INTEGER PRIMARY KEY REFERENCES instances(id) ON DELETE CASCADE, 6 | enabled INTEGER NOT NULL DEFAULT 0, 7 | categories_json TEXT NOT NULL DEFAULT '[]', 8 | tags_json TEXT NOT NULL DEFAULT '[]', 9 | exclude_categories_json TEXT NOT NULL DEFAULT '[]', 10 | exclude_tags_json TEXT NOT NULL DEFAULT '[]', 11 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP 12 | ); 13 | 14 | CREATE TRIGGER IF NOT EXISTS trg_instance_crossseed_completion_settings_updated 15 | AFTER UPDATE ON instance_crossseed_completion_settings 16 | BEGIN 17 | UPDATE instance_crossseed_completion_settings 18 | SET updated_at = CURRENT_TIMESTAMP 19 | WHERE instance_id = NEW.instance_id; 20 | END; 21 | -------------------------------------------------------------------------------- /web/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | "use client" 7 | 8 | import * as React from "react" 9 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 10 | 11 | import { cn } from "@/lib/utils" 12 | 13 | function Separator({ 14 | className, 15 | orientation = "horizontal", 16 | decorative = true, 17 | ...props 18 | }: React.ComponentProps) { 19 | return ( 20 | 30 | ) 31 | } 32 | 33 | export { Separator } 34 | -------------------------------------------------------------------------------- /web/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, s0up and the autobrr contributors. 3 | * SPDX-License-Identifier: GPL-2.0-or-later 4 | */ 5 | 6 | import * as React from "react" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 11 | return ( 12 |