├── .npmrc ├── static ├── logo.png └── robots.txt ├── CinephageLogo.png ├── .vscode └── settings.json ├── src ├── lib │ ├── index.ts │ ├── server │ │ ├── downloadClients │ │ │ ├── nzbget │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── sabnzbd │ │ │ │ └── index.ts │ │ │ ├── monitoring │ │ │ │ └── index.ts │ │ │ ├── import │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── monitoring │ │ │ ├── TESTING.md │ │ │ ├── MONITORING.md │ │ │ ├── tasks │ │ │ │ ├── index.ts │ │ │ │ └── SmartListRefreshTask.ts │ │ │ └── specifications │ │ │ │ └── index.ts │ │ ├── downloads │ │ │ ├── nzb │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── indexers │ │ │ ├── http │ │ │ │ ├── index.ts │ │ │ │ └── browser │ │ │ │ │ └── index.ts │ │ │ ├── newznab │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── engine │ │ │ │ └── index.ts │ │ │ ├── registry │ │ │ │ └── index.ts │ │ │ ├── ratelimit │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── status │ │ │ │ ├── index.ts │ │ │ │ └── BackoffCalculator.ts │ │ │ ├── categories │ │ │ │ └── index.ts │ │ │ ├── search │ │ │ │ └── index.ts │ │ │ ├── auth │ │ │ │ └── providers │ │ │ │ │ ├── index.ts │ │ │ │ │ └── NoAuthProvider.ts │ │ │ ├── runtime │ │ │ │ └── index.ts │ │ │ ├── parser │ │ │ │ └── index.ts │ │ │ └── loader │ │ │ │ └── index.ts │ │ ├── library │ │ │ ├── naming │ │ │ │ ├── template │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── normalization │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── videoCodecs.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── sources.ts │ │ │ │ │ └── audioCodecs.ts │ │ │ │ └── tokens │ │ │ │ │ ├── definitions │ │ │ │ │ ├── release.ts │ │ │ │ │ ├── audio.ts │ │ │ │ │ ├── video.ts │ │ │ │ │ └── core.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ └── index.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ └── background-service.ts │ │ ├── streaming │ │ │ ├── validation │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ └── index.ts │ │ │ ├── types.ts │ │ │ ├── cache │ │ │ │ └── index.ts │ │ │ ├── anilist │ │ │ │ └── index.ts │ │ │ ├── errors │ │ │ │ └── index.ts │ │ │ ├── providers │ │ │ │ └── types.ts │ │ │ ├── lookup │ │ │ │ ├── index.ts │ │ │ │ └── providers │ │ │ │ │ └── index.ts │ │ │ ├── enc-dec │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── subtitles │ │ │ ├── index.ts │ │ │ ├── providers │ │ │ │ ├── index.ts │ │ │ │ └── yifysubtitles │ │ │ │ │ └── types.ts │ │ │ └── services │ │ │ │ └── index.ts │ │ ├── smartlists │ │ │ └── index.ts │ │ ├── tasks │ │ │ └── TaskCancelledException.ts │ │ ├── workers │ │ │ └── index.ts │ │ ├── quality │ │ │ └── index.ts │ │ └── db │ │ │ └── index.ts │ ├── components │ │ ├── library │ │ │ ├── tv │ │ │ │ ├── index.ts │ │ │ │ └── BulkActionBar.svelte │ │ │ ├── AutoSearchStatus.svelte │ │ │ ├── MonitorToggle.svelte │ │ │ ├── index.ts │ │ │ ├── QualityBadge.svelte │ │ │ └── StatusIndicator.svelte │ │ ├── rootFolders │ │ │ └── index.ts │ │ ├── downloadClients │ │ │ └── index.ts │ │ ├── search │ │ │ └── index.ts │ │ ├── ui │ │ │ ├── modal │ │ │ │ ├── index.ts │ │ │ │ ├── SectionHeader.svelte │ │ │ │ ├── ModalHeader.svelte │ │ │ │ ├── ToggleSetting.svelte │ │ │ │ ├── TestResult.svelte │ │ │ │ ├── ModalWrapper.svelte │ │ │ │ └── ModalFooter.svelte │ │ │ ├── StatsSkeleton.svelte │ │ │ ├── TmdbConfigRequired.svelte │ │ │ ├── MediaTypeBadge.svelte │ │ │ ├── CardSkeleton.svelte │ │ │ ├── form │ │ │ │ ├── FormField.svelte │ │ │ │ └── LoadingButton.svelte │ │ │ ├── Skeleton.svelte │ │ │ ├── AsyncState.svelte │ │ │ └── Toasts.svelte │ │ ├── formats │ │ │ └── index.ts │ │ ├── subtitles │ │ │ └── index.ts │ │ ├── profiles │ │ │ ├── index.ts │ │ │ └── ProfileList.svelte │ │ ├── queue │ │ │ ├── index.ts │ │ │ ├── QueueProgressBar.svelte │ │ │ └── QueueStatusBadge.svelte │ │ ├── subtitleProviders │ │ │ ├── index.ts │ │ │ └── SubtitleProviderStatusBadge.svelte │ │ ├── indexers │ │ │ ├── IndexerTestResult.svelte │ │ │ ├── IndexerSearchSettings.svelte │ │ │ └── IndexerBulkActions.svelte │ │ ├── tmdb │ │ │ ├── MetadataFact.svelte │ │ │ ├── NetworkLogos.svelte │ │ │ ├── TmdbImage.svelte │ │ │ ├── ProductionCompanies.svelte │ │ │ ├── SeasonList.svelte │ │ │ ├── CrewList.svelte │ │ │ ├── PersonCard.svelte │ │ │ └── WatchProviders.svelte │ │ ├── ThemeSelector.svelte │ │ └── discover │ │ │ └── SearchBar.svelte │ ├── layout.svelte.ts │ ├── themes.ts │ ├── utils │ │ ├── routing.ts │ │ └── format.ts │ ├── theme.svelte.ts │ ├── types │ │ └── task.ts │ └── config │ │ └── trackers.ts ├── demo.spec.ts ├── routes │ ├── settings │ │ ├── profiles │ │ │ ├── +page.svelte │ │ │ └── +page.server.ts │ │ └── integrations │ │ │ └── +layout.svelte │ ├── api │ │ ├── health │ │ │ └── +server.ts │ │ ├── subtitles │ │ │ ├── providers │ │ │ │ ├── definitions │ │ │ │ │ └── +server.ts │ │ │ │ └── test │ │ │ │ │ └── +server.ts │ │ │ ├── scan │ │ │ │ └── +server.ts │ │ │ └── language-profiles │ │ │ │ └── +server.ts │ │ ├── system │ │ │ └── status │ │ │ │ └── +server.ts │ │ ├── monitoring │ │ │ ├── status │ │ │ │ └── +server.ts │ │ │ └── search │ │ │ │ ├── upgrade │ │ │ │ └── +server.ts │ │ │ │ ├── new-episodes │ │ │ │ └── +server.ts │ │ │ │ ├── cutoff-unmet │ │ │ │ └── +server.ts │ │ │ │ ├── missing │ │ │ │ └── +server.ts │ │ │ │ ├── subtitle-upgrade │ │ │ │ └── +server.ts │ │ │ │ └── missing-subtitles │ │ │ │ └── +server.ts │ │ ├── rate-limits │ │ │ └── +server.ts │ │ ├── smartlists │ │ │ └── [id] │ │ │ │ └── refresh │ │ │ │ └── +server.ts │ │ ├── library │ │ │ ├── backfill-quality │ │ │ │ └── +server.ts │ │ │ └── seasons │ │ │ │ └── [id] │ │ │ │ └── +server.ts │ │ ├── streaming │ │ │ └── proxy │ │ │ │ └── [...path] │ │ │ │ └── +server.ts │ │ ├── workers │ │ │ ├── +server.ts │ │ │ └── [id] │ │ │ │ └── +server.ts │ │ ├── indexers │ │ │ ├── definitions │ │ │ │ ├── [id] │ │ │ │ │ └── +server.ts │ │ │ │ └── +server.ts │ │ │ └── test │ │ │ │ └── +server.ts │ │ ├── rename │ │ │ └── preview │ │ │ │ ├── movie │ │ │ │ └── [id] │ │ │ │ │ └── +server.ts │ │ │ │ └── series │ │ │ │ └── [id] │ │ │ │ └── +server.ts │ │ ├── queue │ │ │ └── cleanup │ │ │ │ └── +server.ts │ │ ├── root-folders │ │ │ ├── validate │ │ │ │ └── +server.ts │ │ │ ├── +server.ts │ │ │ └── [id] │ │ │ │ └── +server.ts │ │ ├── download-clients │ │ │ └── test │ │ │ │ └── +server.ts │ │ ├── naming │ │ │ ├── presets │ │ │ │ └── [id] │ │ │ │ │ └── apply │ │ │ │ │ └── +server.ts │ │ │ └── validate │ │ │ │ └── +server.ts │ │ └── tasks │ │ │ └── [taskId] │ │ │ ├── history │ │ │ └── +server.ts │ │ │ └── cancel │ │ │ └── +server.ts │ ├── smartlists │ │ ├── +page.server.ts │ │ ├── new │ │ │ ├── +page.svelte │ │ │ └── +page.server.ts │ │ └── [id] │ │ │ ├── edit │ │ │ ├── +page.svelte │ │ │ └── +page.server.ts │ │ │ └── +page.server.ts │ ├── page.svelte.spec.ts │ ├── layout.css │ ├── library │ │ └── unmatched │ │ │ └── +page.server.ts │ ├── health │ │ └── +server.ts │ ├── person │ │ └── [id] │ │ │ └── +page.server.ts │ ├── tv │ │ └── [id] │ │ │ └── +page.svelte │ └── movie │ │ └── [id] │ │ └── +page.svelte ├── test │ └── setup.ts ├── app.d.ts └── app.html ├── docs └── images │ ├── dashboard.png │ ├── discover.png │ ├── library-tv.png │ ├── library-movies.png │ ├── movie-details.png │ ├── discover-filters.png │ ├── library-tv-details.png │ ├── tv-discover-details.png │ └── library-movie-details.png ├── .gitattributes ├── .dockerignore ├── vitest.config.ts ├── .prettierignore ├── .prettierrc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── config.yml ├── dependabot.yml └── PULL_REQUEST_TEMPLATE.md ├── tsconfig.json ├── svelte.config.js ├── .gitignore ├── scripts └── remove-monitoring-enabled.ts ├── docker-entrypoint.sh ├── deploy └── cinephage.service ├── data └── indexers │ └── definitions │ ├── cinephage-stream.yaml │ ├── eztv.yaml │ └── yts.yaml ├── docker-compose.yaml └── CODE_OF_CONDUCT.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/static/logo.png -------------------------------------------------------------------------------- /CinephageLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/CinephageLogo.png -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | # allow crawling everything by default 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.css": "tailwind" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /docs/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/docs/images/dashboard.png -------------------------------------------------------------------------------- /docs/images/discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/docs/images/discover.png -------------------------------------------------------------------------------- /docs/images/library-tv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/docs/images/library-tv.png -------------------------------------------------------------------------------- /src/lib/server/downloadClients/nzbget/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NZBGetClient'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /docs/images/library-movies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/docs/images/library-movies.png -------------------------------------------------------------------------------- /docs/images/movie-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/docs/images/movie-details.png -------------------------------------------------------------------------------- /docs/images/discover-filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/docs/images/discover-filters.png -------------------------------------------------------------------------------- /docs/images/library-tv-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/docs/images/library-tv-details.png -------------------------------------------------------------------------------- /docs/images/tv-discover-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/docs/images/tv-discover-details.png -------------------------------------------------------------------------------- /docs/images/library-movie-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoldyTaint/Cinephage/HEAD/docs/images/library-movie-details.png -------------------------------------------------------------------------------- /src/lib/components/library/tv/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TVSeriesSidebar } from './TVSeriesSidebar.svelte'; 2 | export { default as BulkActionBar } from './BulkActionBar.svelte'; 3 | -------------------------------------------------------------------------------- /src/lib/components/rootFolders/index.ts: -------------------------------------------------------------------------------- 1 | export { default as RootFolderModal } from './RootFolderModal.svelte'; 2 | export { default as RootFolderList } from './RootFolderList.svelte'; 3 | -------------------------------------------------------------------------------- /src/lib/layout.svelte.ts: -------------------------------------------------------------------------------- 1 | export const layoutState = $state({ 2 | isSidebarExpanded: true, 3 | toggleSidebar() { 4 | this.isSidebarExpanded = !this.isSidebarExpanded; 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /src/demo.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/lib/components/downloadClients/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DownloadClientModal } from './DownloadClientModal.svelte'; 2 | export { default as DownloadClientTable } from './DownloadClientTable.svelte'; 3 | -------------------------------------------------------------------------------- /src/lib/server/monitoring/TESTING.md: -------------------------------------------------------------------------------- 1 | # Moved 2 | 3 | This documentation has been moved to `/docs/development/testing.md`. 4 | 5 | See the [Testing Guide](../../../../docs/development/testing.md) documentation. 6 | -------------------------------------------------------------------------------- /src/routes/settings/profiles/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |

Redirecting...

7 | -------------------------------------------------------------------------------- /src/lib/components/search/index.ts: -------------------------------------------------------------------------------- 1 | // Search components 2 | export { default as InteractiveSearchModal } from './InteractiveSearchModal.svelte'; 3 | export { default as SearchResultRow } from './SearchResultRow.svelte'; 4 | -------------------------------------------------------------------------------- /src/routes/api/health/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | 4 | export const GET: RequestHandler = async () => { 5 | return json({ status: 'ok' }); 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/server/downloads/nzb/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NZB handling module exports. 3 | */ 4 | 5 | export { 6 | NzbValidationService, 7 | getNzbValidationService, 8 | type NzbValidationResult 9 | } from './NzbValidationService'; 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Force LF line endings for shell scripts (critical for Docker) 5 | *.sh text eol=lf 6 | 7 | # Force LF for Dockerfile 8 | Dockerfile text eol=lf 9 | -------------------------------------------------------------------------------- /src/lib/server/indexers/http/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP utilities for indexers. 3 | */ 4 | 5 | export * from './CloudflareDetection'; 6 | export * from './CaptchaHandler'; 7 | export * from './RetryPolicy'; 8 | export * from './IndexerHttp'; 9 | -------------------------------------------------------------------------------- /src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { vi } from 'vitest'; 3 | 4 | config(); // Load .env file 5 | 6 | // Mock $env/dynamic/private 7 | vi.mock('$env/dynamic/private', () => ({ 8 | env: process.env 9 | })); 10 | -------------------------------------------------------------------------------- /src/lib/server/monitoring/MONITORING.md: -------------------------------------------------------------------------------- 1 | # Moved 2 | 3 | This documentation has been moved to `/docs/development/monitoring-internals.md`. 4 | 5 | See the [Monitoring Internals](../../../../docs/development/monitoring-internals.md) documentation. 6 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/template/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Template module exports 3 | */ 4 | 5 | export { TemplateEngine } from './TemplateEngine'; 6 | export type { TemplateParseResult, TemplateError, TemplateWarning, ParsedToken } from './types'; 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | .git 5 | .gitignore 6 | .gitattributes 7 | logs 8 | data/*.db 9 | data/*.db-* 10 | .env 11 | .env.* 12 | !.env.example 13 | *.log 14 | .DS_Store 15 | .vscode 16 | .idea 17 | coverage 18 | .nyc_output 19 | *.tgz 20 | -------------------------------------------------------------------------------- /src/lib/server/downloadClients/sabnzbd/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SABnzbd Download Client module exports. 3 | */ 4 | 5 | export { SABnzbdClient, type SABnzbdConfig } from './SABnzbdClient'; 6 | export { SABnzbdProxy, SabnzbdApiError } from './SABnzbdProxy'; 7 | export * from './types'; 8 | -------------------------------------------------------------------------------- /src/lib/server/services/index.ts: -------------------------------------------------------------------------------- 1 | export type { BackgroundService, ServiceStatus, ServiceStatusInfo } from './background-service.js'; 2 | export { serviceManager } from './service-manager.js'; 3 | export { ExternalIdService, getExternalIdService, ensureExternalIds } from './ExternalIdService.js'; 4 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | test: { 7 | include: ['src/**/*.{test,spec}.{js,ts}'], 8 | setupFiles: ['src/test/setup.ts'] 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /src/lib/components/ui/modal/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ConfirmationModal } from './ConfirmationModal.svelte'; 2 | export { default as SectionHeader } from './SectionHeader.svelte'; 3 | export { default as ToggleSetting } from './ToggleSetting.svelte'; 4 | export { default as TestResult } from './TestResult.svelte'; 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | bun.lock 6 | bun.lockb 7 | 8 | # Miscellaneous 9 | /static/ 10 | /drizzle/ 11 | .claude/ 12 | 13 | # External/separate projects 14 | Flyx-main/ 15 | Cinephage-Streamer/ 16 | 17 | # Build artifacts 18 | .svelte-kit/ 19 | -------------------------------------------------------------------------------- /src/lib/server/indexers/newznab/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Newznab indexer module exports. 3 | */ 4 | 5 | export * from './types'; 6 | export { 7 | NewznabCapabilitiesProvider, 8 | getNewznabCapabilitiesProvider, 9 | CapabilitiesFetchError, 10 | DEFAULT_CAPABILITIES 11 | } from './NewznabCapabilitiesProvider'; 12 | -------------------------------------------------------------------------------- /src/routes/settings/profiles/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | 4 | export const load: PageServerLoad = async () => { 5 | // Redirect to the new unified quality settings page 6 | throw redirect(301, '/settings/quality?tab=profiles'); 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/server/streaming/validation/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Stream Validation Module 3 | * 4 | * Provides stream validation capabilities for ensuring streams are playable. 5 | */ 6 | 7 | export { 8 | StreamValidator, 9 | getStreamValidator, 10 | createStreamValidator, 11 | quickValidateStream 12 | } from './StreamValidator'; 13 | -------------------------------------------------------------------------------- /src/lib/components/formats/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FormatConditionBuilder } from './FormatConditionBuilder.svelte'; 2 | export { default as CustomFormatModal } from './CustomFormatModal.svelte'; 3 | export { default as FormatList } from './FormatList.svelte'; 4 | 5 | export type { CustomFormatFormData } from './CustomFormatModal.svelte'; 6 | -------------------------------------------------------------------------------- /src/lib/components/subtitles/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SubtitleBadge } from './SubtitleBadge.svelte'; 2 | export { default as SubtitleDisplay } from './SubtitleDisplay.svelte'; 3 | export { default as SubtitleSearchResultRow } from './SubtitleSearchResultRow.svelte'; 4 | export { default as SubtitleSearchModal } from './SubtitleSearchModal.svelte'; 5 | -------------------------------------------------------------------------------- /src/lib/components/profiles/index.ts: -------------------------------------------------------------------------------- 1 | // Profile components barrel export 2 | export { default as ProfileList } from './ProfileList.svelte'; 3 | export { default as ProfileModal } from './ProfileModal.svelte'; 4 | export { default as ProfileTable } from './ProfileTable.svelte'; 5 | export { default as FormatScoreAccordion } from './FormatScoreAccordion.svelte'; 6 | -------------------------------------------------------------------------------- /src/lib/components/queue/index.ts: -------------------------------------------------------------------------------- 1 | export { default as QueueTable } from './QueueTable.svelte'; 2 | export { default as QueueStats } from './QueueStats.svelte'; 3 | export { default as QueueStatusBadge } from './QueueStatusBadge.svelte'; 4 | export { default as QueueProgressBar } from './QueueProgressBar.svelte'; 5 | export { default as HistoryTable } from './HistoryTable.svelte'; 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ], 15 | "tailwindStylesheet": "src/routes/layout.css" 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/server/subtitles/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Subtitle Management System 3 | * 4 | * Comprehensive subtitle handling inspired by Bazarr, 5 | * with multiple providers, language profiles, and auto-sync. 6 | */ 7 | 8 | // Core types 9 | export * from './types'; 10 | 11 | // Provider layer 12 | export * from './providers'; 13 | 14 | // Services 15 | export * from './services'; 16 | -------------------------------------------------------------------------------- /src/lib/components/subtitleProviders/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SubtitleProviderTable } from './SubtitleProviderTable.svelte'; 2 | export { default as SubtitleProviderRow } from './SubtitleProviderRow.svelte'; 3 | export { default as SubtitleProviderModal } from './SubtitleProviderModal.svelte'; 4 | export { default as SubtitleProviderStatusBadge } from './SubtitleProviderStatusBadge.svelte'; 5 | -------------------------------------------------------------------------------- /src/lib/components/ui/modal/SectionHeader.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |

13 | {title} 14 |

15 | -------------------------------------------------------------------------------- /src/lib/server/streaming/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Streaming Utilities 3 | * 4 | * Shared utility functions for the streaming module. 5 | */ 6 | 7 | export { 8 | fetchWithTimeout, 9 | checkStreamAvailability, 10 | checkHlsAvailability, 11 | fetchPlaylist, 12 | fetchAndRewritePlaylist, 13 | rewritePlaylistUrls, 14 | ensureVodPlaylist, 15 | type FetchOptions 16 | } from './http'; 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owner for everything 2 | * @MoldyTaint 3 | 4 | # Indexer system 5 | /src/lib/server/indexers/ @MoldyTaint 6 | /data/indexers/ @MoldyTaint 7 | 8 | # Documentation 9 | /docs/ @MoldyTaint 10 | /*.md @MoldyTaint 11 | 12 | # Database schema 13 | /src/lib/server/db/schema.ts @MoldyTaint 14 | /drizzle/ @MoldyTaint 15 | 16 | # GitHub configuration 17 | /.github/ @MoldyTaint 18 | -------------------------------------------------------------------------------- /src/lib/server/downloadClients/monitoring/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Download monitoring module exports 3 | */ 4 | 5 | export { 6 | DownloadMonitorService, 7 | downloadMonitor, 8 | getDownloadMonitor, 9 | resetDownloadMonitor 10 | } from './DownloadMonitorService'; 11 | export { 12 | mapClientPathToLocal, 13 | getContentPath, 14 | needsPathMapping, 15 | type PathMappingConfig 16 | } from './PathMapping'; 17 | -------------------------------------------------------------------------------- /src/lib/server/indexers/engine/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Engine exports 3 | */ 4 | 5 | export { TemplateEngine, createTemplateEngine } from './TemplateEngine'; 6 | export { FilterEngine, createFilterEngine } from './FilterEngine'; 7 | export { 8 | SelectorEngine, 9 | createSelectorEngine, 10 | type JsonValue, 11 | type JsonObject, 12 | type JsonArray, 13 | type SelectorResult 14 | } from './SelectorEngine'; 15 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | interface Locals { 7 | /** Unique identifier for request tracing */ 8 | correlationId: string; 9 | } 10 | // interface PageData {} 11 | // interface PageState {} 12 | // interface Platform {} 13 | } 14 | } 15 | 16 | export {}; 17 | -------------------------------------------------------------------------------- /src/routes/smartlists/+page.server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Smart Lists Page - Server Load 3 | */ 4 | 5 | import type { PageServerLoad } from './$types'; 6 | import { getSmartListService } from '$lib/server/smartlists/index.js'; 7 | 8 | export const load: PageServerLoad = async () => { 9 | const service = getSmartListService(); 10 | const lists = await service.getAllSmartLists(); 11 | 12 | return { 13 | lists 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/server/streaming/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Common types for stream extraction 3 | * 4 | * This file re-exports types from the unified types module for backward compatibility. 5 | * New code should import from './types/' directly. 6 | */ 7 | 8 | // Re-export all types from the unified types module 9 | export * from './types/index'; 10 | 11 | // Also export the errors module for convenience 12 | export * from './errors/index'; 13 | -------------------------------------------------------------------------------- /src/lib/server/monitoring/tasks/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Monitoring Tasks 3 | * 4 | * Export all task executors 5 | */ 6 | 7 | export * from './MissingContentTask.js'; 8 | export * from './UpgradeMonitorTask.js'; 9 | export * from './NewEpisodeMonitorTask.js'; 10 | export * from './CutoffUnmetTask.js'; 11 | export * from './PendingReleaseTask.js'; 12 | export * from './MissingSubtitlesTask.js'; 13 | export * from './SubtitleUpgradeTask.js'; 14 | -------------------------------------------------------------------------------- /src/routes/smartlists/new/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | Create Smart List - Cinephage 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/server/streaming/cache/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Cache Module Exports 3 | * 4 | * Multi-level caching for stream extraction with: 5 | * - Stream URL cache (successful extractions) 6 | * - Validation cache (stream validation results) 7 | * - Negative cache (failed extractions - prevents hammering) 8 | */ 9 | 10 | export { 11 | MultiLevelStreamCache, 12 | getStreamCache, 13 | createStreamCache, 14 | type CacheStats 15 | } from './StreamCache'; 16 | -------------------------------------------------------------------------------- /src/lib/server/indexers/registry/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Indexer Registry Module 3 | * 4 | * Provides the unified indexer definition registry system. 5 | */ 6 | 7 | export { 8 | UnifiedDefinitionRegistry, 9 | getUnifiedRegistry, 10 | resetUnifiedRegistry, 11 | createDefinition, 12 | convertLegacyDefinition, 13 | type RegisteredDefinition, 14 | type DefinitionSource, 15 | type DefinitionFilter, 16 | type IndexerFactory 17 | } from './UnifiedDefinitionRegistry'; 18 | -------------------------------------------------------------------------------- /src/routes/smartlists/[id]/edit/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | Edit {data.list.name} - Cinephage 10 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/normalization/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Normalization module for media naming 3 | * 4 | * Provides consistent string transformations for sources, codecs, and audio formats. 5 | */ 6 | 7 | export { normalizeSource, sourceNormalizer } from './sources'; 8 | export { normalizeVideoCodec, videoCodecNormalizer } from './videoCodecs'; 9 | export { normalizeAudioCodec, audioCodecNormalizer } from './audioCodecs'; 10 | export { createNormalizationMap, type NormalizationMap } from './types'; 11 | -------------------------------------------------------------------------------- /src/lib/components/ui/modal/ModalHeader.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 |

{title}

14 | 17 |
18 | -------------------------------------------------------------------------------- /src/lib/components/ui/StatsSkeleton.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 | 12 |
13 | 14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /src/lib/server/indexers/ratelimit/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Rate limiting module exports. 3 | */ 4 | 5 | export { type RateLimitConfig, DEFAULT_RATE_LIMIT, fromYamlRateLimit } from './types'; 6 | 7 | export { RateLimiter } from './RateLimiter'; 8 | 9 | export { 10 | RateLimitRegistry, 11 | getRateLimitRegistry, 12 | resetRateLimitRegistry 13 | } from './RateLimitRegistry'; 14 | 15 | export { 16 | HostRateLimiter, 17 | getHostRateLimiter, 18 | resetHostRateLimiter, 19 | checkRateLimits 20 | } from './HostRateLimiter'; 21 | -------------------------------------------------------------------------------- /src/routes/smartlists/new/+page.server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Smart List Page - Server Load 3 | */ 4 | 5 | import type { PageServerLoad } from './$types'; 6 | import { db } from '$lib/server/db/index.js'; 7 | import { rootFolders, scoringProfiles } from '$lib/server/db/schema.js'; 8 | 9 | export const load: PageServerLoad = async () => { 10 | const folders = await db.select().from(rootFolders); 11 | const profiles = await db.select().from(scoringProfiles); 12 | 13 | return { 14 | rootFolders: folders, 15 | scoringProfiles: profiles 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/routes/api/subtitles/providers/definitions/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { SubtitleProviderFactory } from '$lib/server/subtitles/providers/SubtitleProviderFactory'; 4 | 5 | /** 6 | * GET /api/subtitles/providers/definitions 7 | * Get all available subtitle provider definitions. 8 | */ 9 | export const GET: RequestHandler = async () => { 10 | const factory = new SubtitleProviderFactory(); 11 | const definitions = factory.getDefinitions(); 12 | 13 | return json(definitions); 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/server/smartlists/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Smart Lists Module 3 | * 4 | * TMDB-based smart lists with dynamic filter-based content discovery. 5 | */ 6 | 7 | export { SmartListService, getSmartListService } from './SmartListService.js'; 8 | export type { 9 | SmartListFilters, 10 | SmartListRecord, 11 | SmartListItemRecord, 12 | SmartListRefreshHistoryRecord, 13 | SmartListSortBy, 14 | AutoAddBehavior, 15 | SmartListMediaType, 16 | RefreshStatus, 17 | CreateSmartListInput, 18 | UpdateSmartListInput, 19 | RefreshResult, 20 | ItemQueryOptions, 21 | BulkAddResult 22 | } from './types.js'; 23 | -------------------------------------------------------------------------------- /src/routes/page.svelte.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | // Browser-mode Svelte component tests are not yet configured. 4 | // This is a placeholder for future browser-based component testing. 5 | // To enable, configure vitest with browser mode and use vitest-browser-svelte. 6 | 7 | describe('/+page.svelte', () => { 8 | it.skip('should render h1 (requires browser mode)', () => { 9 | // This test requires browser mode which is not configured for unit tests. 10 | // Enable browser mode in vitest.config.ts to run this test. 11 | expect(true).toBe(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/lib/server/indexers/status/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Status tracking module exports. 3 | */ 4 | 5 | export { 6 | type HealthStatus, 7 | type FailureRecord, 8 | type IndexerStatus, 9 | type StatusTrackerConfig, 10 | DEFAULT_STATUS_CONFIG, 11 | createDefaultStatus 12 | } from './types'; 13 | 14 | export { BackoffCalculator, defaultBackoffCalculator } from './BackoffCalculator'; 15 | 16 | // Persistent status tracker (database-backed, survives restarts) 17 | export { 18 | PersistentStatusTracker, 19 | getPersistentStatusTracker, 20 | resetPersistentStatusTracker 21 | } from './PersistentStatusTracker'; 22 | -------------------------------------------------------------------------------- /src/lib/server/indexers/categories/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Category module exports. 3 | */ 4 | 5 | export { 6 | type CategoryInfo, 7 | NEWZNAB_CATEGORIES, 8 | getCategoryById, 9 | getCategoryName, 10 | getParentCategoryId, 11 | getSubcategories, 12 | isSubcategoryOf, 13 | getRootCategory, 14 | toCategory 15 | } from './newznabCategories'; 16 | 17 | export { 18 | mapYtsCategory, 19 | map1337xCategory, 20 | mapEztvCategory, 21 | detectQualityCategories, 22 | filterMovieCategories, 23 | filterTvCategories, 24 | hasMovieCategory, 25 | hasTvCategory, 26 | normalizeCategories 27 | } from './CategoryMapper'; 28 | -------------------------------------------------------------------------------- /src/lib/components/indexers/IndexerTestResult.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if result} 12 |
13 | {#if result.success} 14 | 15 | Connection successful! 16 | {:else} 17 | 18 | {result.error ?? 'Connection failed'} 19 | {/if} 20 |
21 | {/if} 22 | -------------------------------------------------------------------------------- /src/routes/layout.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @plugin 'daisyui' { 3 | themes: 4 | light --default, 5 | dark --prefersdark, 6 | cupcake, 7 | bumblebee, 8 | emerald, 9 | corporate, 10 | synthwave, 11 | retro, 12 | cyberpunk, 13 | valentine, 14 | halloween, 15 | garden, 16 | forest, 17 | aqua, 18 | lofi, 19 | pastel, 20 | fantasy, 21 | wireframe, 22 | black, 23 | luxury, 24 | dracula, 25 | cmyk, 26 | autumn, 27 | business, 28 | acid, 29 | lemonade, 30 | night, 31 | coffee, 32 | winter, 33 | dim, 34 | nord, 35 | sunset, 36 | caramellatte, 37 | abyss, 38 | silk; 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/api/system/status/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * System Status API Endpoint 3 | * 4 | * Returns the status of all background services. 5 | * Useful for monitoring startup progress and health checks. 6 | */ 7 | 8 | import { json } from '@sveltejs/kit'; 9 | import type { RequestHandler } from './$types.js'; 10 | import { serviceManager } from '$lib/server/services/index.js'; 11 | 12 | export const GET: RequestHandler = async () => { 13 | const services = serviceManager.getStatus(); 14 | const allReady = serviceManager.allReady(); 15 | 16 | return json({ 17 | success: true, 18 | ready: allReady, 19 | services 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/TmdbConfigRequired.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /src/lib/server/downloadClients/import/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Import module exports 3 | */ 4 | 5 | export { 6 | ImportService, 7 | importService, 8 | getImportService, 9 | resetImportService 10 | } from './ImportService'; 11 | export type { ImportResult, ImportJobResult } from './ImportService'; 12 | 13 | export { 14 | transferFile, 15 | moveFile, 16 | transferDirectory, 17 | findVideoFiles, 18 | ensureDirectory, 19 | fileExists, 20 | getFileSize, 21 | isVideoFile, 22 | VIDEO_EXTENSIONS, 23 | type TransferMode, 24 | type TransferResult, 25 | type BatchTransferOptions, 26 | type BatchTransferResult 27 | } from './FileTransfer'; 28 | -------------------------------------------------------------------------------- /src/lib/server/tasks/TaskCancelledException.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Exception thrown when a task is cancelled. 3 | * This allows distinguishing cancellation from other errors. 4 | */ 5 | export class TaskCancelledException extends Error { 6 | readonly taskId: string; 7 | 8 | constructor(taskId: string) { 9 | super(`Task '${taskId}' was cancelled`); 10 | this.name = 'TaskCancelledException'; 11 | this.taskId = taskId; 12 | } 13 | 14 | /** 15 | * Check if an error is a TaskCancelledException 16 | */ 17 | static isTaskCancelled(error: unknown): error is TaskCancelledException { 18 | return error instanceof TaskCancelledException; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | %sveltekit.head% 16 | 17 | 18 |
%sveltekit.body%
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/lib/themes.ts: -------------------------------------------------------------------------------- 1 | export const themes = [ 2 | 'light', 3 | 'dark', 4 | 'cupcake', 5 | 'bumblebee', 6 | 'emerald', 7 | 'corporate', 8 | 'synthwave', 9 | 'retro', 10 | 'cyberpunk', 11 | 'valentine', 12 | 'halloween', 13 | 'garden', 14 | 'forest', 15 | 'aqua', 16 | 'lofi', 17 | 'pastel', 18 | 'fantasy', 19 | 'wireframe', 20 | 'black', 21 | 'luxury', 22 | 'dracula', 23 | 'cmyk', 24 | 'autumn', 25 | 'business', 26 | 'acid', 27 | 'lemonade', 28 | 'night', 29 | 'coffee', 30 | 'winter', 31 | 'dim', 32 | 'nord', 33 | 'sunset', 34 | 'caramellatte', 35 | 'abyss', 36 | 'silk' 37 | ] as const; 38 | 39 | export type Theme = (typeof themes)[number]; 40 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/tokens/definitions/release.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Release tokens - ReleaseGroup, Edition 3 | */ 4 | 5 | import type { TokenDefinition } from '../types'; 6 | 7 | export const releaseTokens: TokenDefinition[] = [ 8 | { 9 | name: 'ReleaseGroup', 10 | aliases: ['Group'], 11 | category: 'release', 12 | description: 'Release group name', 13 | applicability: ['movie', 'episode'], 14 | render: (info) => info.releaseGroup || '' 15 | }, 16 | { 17 | name: 'Edition', 18 | category: 'release', 19 | description: 'Edition (Directors Cut, Extended, etc.)', 20 | applicability: ['movie'], 21 | render: (info) => info.edition || '' 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /src/lib/server/monitoring/specifications/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Monitoring Specifications 3 | * 4 | * Export all specification classes for easy importing 5 | */ 6 | 7 | export * from './types.js'; 8 | export * from './MonitoredSpecification.js'; 9 | export * from './UpgradeableSpecification.js'; 10 | export * from './CutoffUnmetSpecification.js'; 11 | export * from './MissingContentSpecification.js'; 12 | export * from './NewEpisodeSpecification.js'; 13 | export * from './AvailabilitySpecification.js'; 14 | export * from './SearchCooldownSpecification.js'; 15 | export * from './BlocklistSpecification.js'; 16 | export * from './DelaySpecification.js'; 17 | export * from './ReadOnlyFolderSpecification.js'; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowImportingTsExtensions": true, 5 | "allowJs": true, 6 | "checkJs": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "resolveJsonModule": true, 10 | "skipLibCheck": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "moduleResolution": "bundler" 14 | } 15 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 16 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 17 | // 18 | // To make changes to top-level options such as include and exclude, we recommend extending 19 | // the generated config; see https://svelte.dev/docs/kit/configuration#typescript 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/utils/routing.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Routing Utilities 3 | * 4 | * Helper functions for SvelteKit routing with dynamic paths. 5 | */ 6 | 7 | import { resolve } from '$app/paths'; 8 | 9 | /** 10 | * Resolve a dynamic route path. 11 | * 12 | * SvelteKit's resolve() expects typed route strings, but we often need to 13 | * use dynamically constructed paths. This wrapper handles the type 14 | * coercion safely. 15 | * 16 | * @param path - A dynamic path string (e.g., `/movies/${id}`) 17 | * @returns The resolved path with proper base handling 18 | */ 19 | export function resolvePath(path: string): string { 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | return resolve(path as any); 22 | } 23 | 24 | // Re-export resolve for ESLint compatibility 25 | export { resolve }; 26 | -------------------------------------------------------------------------------- /src/lib/server/streaming/anilist/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AniList Module 3 | * 4 | * Exports for the AniList ID resolution service. 5 | * Used to bridge TMDB metadata to MAL/AniList IDs for anime content. 6 | */ 7 | 8 | // Resolver (main export) 9 | export { AniListResolver, getAniListResolver, anilistResolver } from './AniListResolver'; 10 | 11 | // Client (for direct API access if needed) 12 | export { AniListClient, getAniListClient } from './client'; 13 | 14 | // Types 15 | export type { 16 | // API response types 17 | AniListMedia, 18 | AniListTitle, 19 | AniListFormat, 20 | AniListStatus, 21 | AniListMediaResponse, 22 | AniListPageResponse, 23 | 24 | // Resolver types 25 | AniListResolveResult, 26 | 27 | // Cache types 28 | AniListCacheEntry, 29 | AniListCacheKey 30 | } from './types'; 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions & Support 4 | url: https://github.com/MoldyTaint/Cinephage/discussions/categories/q-a 5 | about: Ask questions and get help from the community. Please search existing discussions first. 6 | - name: Ideas & Brainstorming 7 | url: https://github.com/MoldyTaint/Cinephage/discussions/categories/ideas 8 | about: Share early-stage ideas before creating a formal feature request. 9 | - name: Discord Community 10 | url: https://discord.gg/scGCBTSWEt 11 | about: Join our Discord for real-time chat and community support. 12 | - name: Documentation 13 | url: https://github.com/MoldyTaint/Cinephage/tree/main/docs 14 | about: Check the documentation - your answer might already be there. 15 | -------------------------------------------------------------------------------- /src/lib/server/subtitles/providers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Subtitle Providers - Module exports 3 | */ 4 | 5 | // Core interfaces 6 | export * from './interfaces'; 7 | 8 | // Base class 9 | export { BaseSubtitleProvider } from './BaseProvider'; 10 | 11 | // Factory 12 | export { SubtitleProviderFactory, getSubtitleProviderFactory } from './SubtitleProviderFactory'; 13 | 14 | // Provider implementations 15 | export { OpenSubtitlesProvider } from './opensubtitles/OpenSubtitlesProvider'; 16 | export { PodnapisiProvider } from './podnapisi/PodnapisiProvider'; 17 | export { SubsceneProvider } from './subscene/SubsceneProvider'; 18 | export { Addic7edProvider } from './addic7ed/Addic7edProvider'; 19 | 20 | // OpenSubtitles utilities 21 | export { calculateOpenSubtitlesHash, canHashFile } from './opensubtitles/hash'; 22 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // Explicit Node.js adapter for self-hosted deployment 12 | adapter: adapter({ 13 | // Externalize native modules that can't be bundled 14 | external: ['better-sqlite3'] 15 | }), 16 | // Trust all origins for self-hosted access via IP/LAN 17 | csrf: { 18 | trustedOrigins: ['*'] 19 | } 20 | }, 21 | 22 | vitePlugin: { 23 | // Externalize native modules from Vite's SSR bundling 24 | inspector: false 25 | } 26 | }; 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /src/lib/components/ui/modal/ToggleSetting.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 34 | -------------------------------------------------------------------------------- /src/lib/components/tmdb/MetadataFact.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if value} 14 |
15 | {label} 16 | {#if href} 17 | 18 | 24 | {value} 25 | 26 | 27 | {:else} 28 | {value} 29 | {/if} 30 |
31 | {/if} 32 | -------------------------------------------------------------------------------- /src/lib/server/downloadClients/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Download Clients Module 3 | * 4 | * Provides services for managing download clients (QBittorrent, etc.) 5 | * and root folders (media library destinations). 6 | */ 7 | 8 | export { 9 | DownloadClientManager, 10 | getDownloadClientManager, 11 | resetDownloadClientManager 12 | } from './DownloadClientManager'; 13 | export type { DownloadClientInput } from './DownloadClientManager'; 14 | 15 | export { 16 | RootFolderService, 17 | getRootFolderService, 18 | resetRootFolderService 19 | } from './RootFolderService'; 20 | export type { RootFolderInput } from './RootFolderService'; 21 | 22 | export { QBittorrentClient } from './qbittorrent/QBittorrentClient'; 23 | 24 | export type { 25 | IDownloadClient, 26 | DownloadClientConfig, 27 | AddDownloadOptions, 28 | DownloadInfo 29 | } from './core/interfaces'; 30 | -------------------------------------------------------------------------------- /src/lib/components/ui/MediaTypeBadge.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
34 | {labels[type]} 35 |
36 | -------------------------------------------------------------------------------- /src/lib/server/indexers/search/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Search System 3 | * 4 | * Provides search orchestration, result processing, and caching: 5 | * - SearchOrchestrator: Tiered search across multiple indexers 6 | * - ReleaseDeduplicator: Merge duplicate releases 7 | * - ReleaseRanker: Score and rank releases 8 | * - ReleaseCache: Cache search results with TTL 9 | */ 10 | 11 | // Search orchestration (new) 12 | export { 13 | SearchOrchestrator, 14 | getSearchOrchestrator, 15 | resetSearchOrchestrator, 16 | type SearchOrchestratorOptions 17 | } from './SearchOrchestrator'; 18 | 19 | // Result processing 20 | export { ReleaseDeduplicator } from './ReleaseDeduplicator'; 21 | export { ReleaseRanker, QualityLevel, type RankingWeights } from './ReleaseRanker'; 22 | 23 | // Caching 24 | export { ReleaseCache, getReleaseCache, resetReleaseCache } from './ReleaseCache'; 25 | -------------------------------------------------------------------------------- /src/routes/api/monitoring/status/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { monitoringScheduler } from '$lib/server/monitoring/MonitoringScheduler.js'; 4 | import { logger } from '$lib/logging'; 5 | 6 | /** 7 | * GET /api/monitoring/status 8 | * Returns monitoring scheduler status 9 | */ 10 | export const GET: RequestHandler = async () => { 11 | try { 12 | const status = await monitoringScheduler.getStatus(); 13 | 14 | return json({ 15 | success: true, 16 | ...status 17 | }); 18 | } catch (error) { 19 | logger.error( 20 | '[API] Failed to get monitoring status', 21 | error instanceof Error ? error : undefined 22 | ); 23 | return json( 24 | { 25 | success: false, 26 | error: 'Failed to get monitoring status' 27 | }, 28 | { status: 500 } 29 | ); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/routes/api/rate-limits/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { getRateLimitRegistry } from '$lib/server/indexers/ratelimit'; 4 | 5 | /** 6 | * GET /api/rate-limits 7 | * Returns current rate limit status for all indexers 8 | */ 9 | export const GET: RequestHandler = async () => { 10 | const registry = getRateLimitRegistry(); 11 | return json({ 12 | limiters: registry.getSummary() 13 | }); 14 | }; 15 | 16 | /** 17 | * DELETE /api/rate-limits 18 | * Clears all rate limiters (they'll be recreated with fresh configs) 19 | */ 20 | export const DELETE: RequestHandler = async () => { 21 | const registry = getRateLimitRegistry(); 22 | registry.clear(); // Fully remove limiters so they get recreated with new config 23 | return json({ success: true, message: 'All rate limiters cleared' }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/components/tmdb/NetworkLogos.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {#if networks.length > 0} 9 |
10 | {#each networks as network (network.id)} 11 |
15 | {#if network.logo_path} 16 | 22 | {:else} 23 | {network.name} 24 | {/if} 25 |
26 | {/each} 27 |
28 | {/if} 29 | -------------------------------------------------------------------------------- /src/routes/smartlists/[id]/edit/+page.server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Edit Smart List Page - Server Load 3 | */ 4 | 5 | import type { PageServerLoad } from './$types'; 6 | import { getSmartListService } from '$lib/server/smartlists/index.js'; 7 | import { db } from '$lib/server/db/index.js'; 8 | import { rootFolders, scoringProfiles } from '$lib/server/db/schema.js'; 9 | import { error } from '@sveltejs/kit'; 10 | 11 | export const load: PageServerLoad = async ({ params }) => { 12 | const service = getSmartListService(); 13 | const list = await service.getSmartList(params.id); 14 | 15 | if (!list) { 16 | throw error(404, 'Smart list not found'); 17 | } 18 | 19 | const folders = await db.select().from(rootFolders); 20 | const profiles = await db.select().from(scoringProfiles); 21 | 22 | return { 23 | list, 24 | rootFolders: folders, 25 | scoringProfiles: profiles 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/server/indexers/ratelimit/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Rate limiting types. 3 | */ 4 | 5 | /** Rate limit configuration */ 6 | export interface RateLimitConfig { 7 | /** Maximum requests in the period */ 8 | requests: number; 9 | /** Period in milliseconds */ 10 | periodMs: number; 11 | /** Optional burst allowance */ 12 | burst?: number; 13 | } 14 | 15 | /** Default rate limit (60 requests per minute - generous for development) */ 16 | export const DEFAULT_RATE_LIMIT: RateLimitConfig = { 17 | requests: 60, 18 | periodMs: 60_000, 19 | burst: 10 20 | }; 21 | 22 | /** Convert from YAML format (seconds) to internal format (ms) */ 23 | export function fromYamlRateLimit(yaml: { 24 | requests: number; 25 | period: number; 26 | burst?: number; 27 | }): RateLimitConfig { 28 | return { 29 | requests: yaml.requests, 30 | periodMs: yaml.period * 1000, 31 | burst: yaml.burst 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/routes/api/smartlists/[id]/refresh/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Smart List Refresh API 3 | * POST /api/smartlists/[id]/refresh - Manually refresh a smart list 4 | */ 5 | 6 | import { json } from '@sveltejs/kit'; 7 | import type { RequestHandler } from './$types'; 8 | import { getSmartListService } from '$lib/server/smartlists/index.js'; 9 | 10 | export const POST: RequestHandler = async ({ params }) => { 11 | const service = getSmartListService(); 12 | 13 | const list = await service.getSmartList(params.id); 14 | if (!list) { 15 | return json({ error: 'Smart list not found' }, { status: 404 }); 16 | } 17 | 18 | try { 19 | const result = await service.refreshSmartList(params.id, 'manual'); 20 | return json(result); 21 | } catch (error) { 22 | const message = error instanceof Error ? error.message : 'Unknown error'; 23 | return json({ error: message }, { status: 500 }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/template/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Template engine types 3 | */ 4 | 5 | /** 6 | * Error found during template parsing 7 | */ 8 | export interface TemplateError { 9 | position: number; 10 | length: number; 11 | message: string; 12 | token?: string; 13 | } 14 | 15 | /** 16 | * Warning found during template parsing 17 | */ 18 | export interface TemplateWarning { 19 | position: number; 20 | message: string; 21 | suggestion?: string; 22 | } 23 | 24 | /** 25 | * Parsed token from a template 26 | */ 27 | export interface ParsedToken { 28 | name: string; 29 | formatSpec?: string; 30 | position: number; 31 | length: number; 32 | isConditional: boolean; 33 | } 34 | 35 | /** 36 | * Result of parsing a template 37 | */ 38 | export interface TemplateParseResult { 39 | valid: boolean; 40 | errors: TemplateError[]; 41 | warnings: TemplateWarning[]; 42 | tokens: ParsedToken[]; 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.docker.example 20 | !.env.test 21 | 22 | # Vite 23 | vite.config.js.timestamp-* 24 | vite.config.ts.timestamp-* 25 | 26 | # SQLite 27 | *.db 28 | 29 | # Logs 30 | logs/ 31 | *.log 32 | 33 | # Claude Code local settings 34 | .claude/ 35 | CLAUDE.md 36 | 37 | # Backup files 38 | *.bak 39 | *.backup 40 | 41 | # Reference/inspiration repos (local only) 42 | Inspiration/ 43 | 44 | # Additional IDE/Editor files 45 | .idea/ 46 | *.swp 47 | *.swo 48 | *~ 49 | 50 | # Additional database patterns 51 | *.sqlite 52 | *.sqlite3 53 | 54 | # Temporary directories 55 | tmp/ 56 | temp/ 57 | .tmp/ 58 | 59 | # Build artifacts 60 | dist/ 61 | *.tsbuildinfo 62 | 63 | # Local environment overrides 64 | .env.local 65 | .env.*.local 66 | -------------------------------------------------------------------------------- /src/lib/server/streaming/errors/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Streaming Errors 3 | * 4 | * Re-exports error classes from the types module. 5 | * Import from here for cleaner imports in error handling code. 6 | */ 7 | 8 | export { 9 | // Error codes 10 | StreamErrorCode, 11 | type StreamErrorCode as StreamErrorCodeType, 12 | 13 | // Base error 14 | StreamError, 15 | 16 | // Provider errors 17 | ProviderUnavailableError, 18 | ProviderTimeoutError, 19 | ProviderRateLimitedError, 20 | 21 | // Validation errors 22 | StreamValidationError, 23 | PlaylistParseError, 24 | 25 | // EncDec API errors 26 | EncDecApiError, 27 | type EncDecOperation, 28 | 29 | // Content ID lookup errors 30 | ContentNotFoundError, 31 | ContentIdLookupError, 32 | 33 | // Proxy errors 34 | ProxyError, 35 | 36 | // Circuit breaker 37 | CircuitBreakerOpenError, 38 | 39 | // Utilities 40 | isStreamError, 41 | isRecoverableError, 42 | wrapError 43 | } from '../types/error'; 44 | -------------------------------------------------------------------------------- /scripts/remove-monitoring-enabled.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Migration Script: Remove 'enabled' key from monitoring_settings 3 | * 4 | * This script removes the master monitoring toggle from the database. 5 | * Run this after deploying the code changes. 6 | * 7 | * Usage: tsx scripts/remove-monitoring-enabled.ts 8 | */ 9 | 10 | import { db } from '../src/lib/server/db/index.js'; 11 | import { monitoringSettings } from '../src/lib/server/db/schema.js'; 12 | import { eq } from 'drizzle-orm'; 13 | 14 | async function main() { 15 | console.log('🗑️ Removing "enabled" key from monitoring_settings...'); 16 | 17 | try { 18 | await db.delete(monitoringSettings).where(eq(monitoringSettings.key, 'enabled')); 19 | 20 | console.log('✅ Successfully removed "enabled" key'); 21 | console.log(' Monitoring will now always run on server startup'); 22 | } catch (error) { 23 | console.error('❌ Failed to remove "enabled" key:', error); 24 | process.exit(1); 25 | } 26 | } 27 | 28 | main(); 29 | -------------------------------------------------------------------------------- /src/lib/server/indexers/auth/providers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Authentication Providers 3 | * 4 | * Exports all authentication providers and utilities. 5 | */ 6 | 7 | // Provider interface and base 8 | export { 9 | type IAuthProvider, 10 | type AuthContext, 11 | type AuthRequestOptions, 12 | type AuthRequestResponse, 13 | type IAuthHttpClient, 14 | BaseAuthProvider 15 | } from './IAuthProvider'; 16 | 17 | // Concrete providers 18 | export { NoAuthProvider } from './NoAuthProvider'; 19 | export { CookieAuthProvider } from './CookieAuthProvider'; 20 | export { ApiKeyAuthProvider } from './ApiKeyAuthProvider'; 21 | export { PasskeyAuthProvider } from './PasskeyAuthProvider'; 22 | export { FormAuthProvider } from './FormAuthProvider'; 23 | export { BasicAuthProvider } from './BasicAuthProvider'; 24 | 25 | // Factory 26 | export { 27 | AuthProviderFactory, 28 | getAuthProvider, 29 | getAuthProviderFactory, 30 | createAuthProviderForMethod 31 | } from './AuthProviderFactory'; 32 | -------------------------------------------------------------------------------- /src/routes/api/monitoring/search/upgrade/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { monitoringScheduler } from '$lib/server/monitoring/MonitoringScheduler.js'; 4 | import { logger } from '$lib/logging'; 5 | 6 | /** 7 | * POST /api/monitoring/search/upgrade 8 | * Manually trigger upgrade search 9 | */ 10 | export const POST: RequestHandler = async () => { 11 | try { 12 | const result = await monitoringScheduler.runUpgradeSearch(); 13 | 14 | return json({ 15 | success: true, 16 | message: 'Upgrade search completed', 17 | result 18 | }); 19 | } catch (error) { 20 | logger.error('[API] Failed to run upgrade search', error instanceof Error ? error : undefined); 21 | return json( 22 | { 23 | success: false, 24 | error: 'Failed to run upgrade search', 25 | message: error instanceof Error ? error.message : 'Unknown error' 26 | }, 27 | { status: 500 } 28 | ); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/tokens/definitions/audio.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Audio tokens - AudioCodec, AudioChannels, AudioLanguages 3 | */ 4 | 5 | import type { TokenDefinition } from '../types'; 6 | import { normalizeAudioCodec } from '../../normalization'; 7 | 8 | export const audioTokens: TokenDefinition[] = [ 9 | { 10 | name: 'AudioCodec', 11 | category: 'audio', 12 | description: 'Audio codec (TrueHD, DTS-HD MA, etc.)', 13 | applicability: ['movie', 'episode'], 14 | render: (info) => normalizeAudioCodec(info.audioCodec) || '' 15 | }, 16 | { 17 | name: 'AudioChannels', 18 | category: 'audio', 19 | description: 'Audio channels (5.1, 7.1, etc.)', 20 | applicability: ['movie', 'episode'], 21 | render: (info) => info.audioChannels || '' 22 | }, 23 | { 24 | name: 'AudioLanguages', 25 | category: 'audio', 26 | description: 'Audio languages in file', 27 | applicability: ['movie', 'episode'], 28 | render: (info) => info.audioLanguages?.join(' ') || '' 29 | } 30 | ]; 31 | -------------------------------------------------------------------------------- /src/lib/server/streaming/providers/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Provider Types 3 | * 4 | * Re-exports types from the unified types module for backward compatibility. 5 | * New code should import from '../types/' directly. 6 | */ 7 | 8 | // Re-export all provider-related types from unified module 9 | export type { 10 | // Provider identifiers 11 | StreamingProviderId, 12 | MediaType, 13 | ContentCategory, 14 | 15 | // Provider configuration 16 | ProviderCapabilities, 17 | RetryConfig, 18 | TimeoutConfig, 19 | ProviderConfig, 20 | ServerConfig, 21 | 22 | // Search parameters 23 | SearchParams, 24 | 25 | // Results 26 | StreamResult, 27 | SubtitleTrack, 28 | ProviderResult, 29 | 30 | // Provider interface 31 | IStreamProvider 32 | } from '../types/provider'; 33 | 34 | // Re-export extraction types 35 | export type { 36 | ExtractOptions, 37 | CircuitBreakerState, 38 | ExtendedCircuitState, 39 | CircuitBreakerConfig, 40 | ProviderHealth, 41 | ProviderStatus 42 | } from '../types/extraction'; 43 | -------------------------------------------------------------------------------- /src/lib/components/indexers/IndexerSearchSettings.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
Search Capabilities
12 | 13 |
14 | 22 | 30 |
31 | -------------------------------------------------------------------------------- /src/lib/theme.svelte.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '$app/environment'; 2 | import type { Theme } from './themes'; 3 | 4 | const THEME_KEY = 'theme'; 5 | 6 | class ThemeStore { 7 | current = $state('dark'); 8 | 9 | constructor() { 10 | if (browser) { 11 | const stored = localStorage.getItem(THEME_KEY) as Theme | null; 12 | if (stored) { 13 | this.current = stored; 14 | this.apply(stored); 15 | } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) { 16 | this.current = 'dark'; 17 | this.apply('dark'); 18 | } else { 19 | this.current = 'light'; 20 | this.apply('light'); 21 | } 22 | } 23 | } 24 | 25 | set(theme: Theme) { 26 | this.current = theme; 27 | this.apply(theme); 28 | if (browser) { 29 | localStorage.setItem(THEME_KEY, theme); 30 | } 31 | } 32 | 33 | private apply(theme: Theme) { 34 | if (browser) { 35 | document.documentElement.setAttribute('data-theme', theme); 36 | } 37 | } 38 | } 39 | 40 | export const theme = new ThemeStore(); 41 | -------------------------------------------------------------------------------- /src/routes/api/library/backfill-quality/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { backfillMissingQuality } from '$lib/server/library/quality-backfill.js'; 4 | import { logger } from '$lib/logging'; 5 | 6 | /** 7 | * POST /api/library/backfill-quality 8 | * 9 | * Backfills quality data for files that have NULL quality. 10 | * Parses quality information from the filename and updates the database. 11 | */ 12 | export const POST: RequestHandler = async () => { 13 | try { 14 | logger.info('[API] Starting quality backfill'); 15 | const result = await backfillMissingQuality(); 16 | 17 | return json({ 18 | success: true, 19 | ...result 20 | }); 21 | } catch (error) { 22 | logger.error('[API] Quality backfill failed', error instanceof Error ? error : undefined); 23 | return json( 24 | { 25 | success: false, 26 | error: error instanceof Error ? error.message : 'Unknown error' 27 | }, 28 | { status: 500 } 29 | ); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/routes/api/monitoring/search/new-episodes/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { monitoringScheduler } from '$lib/server/monitoring/MonitoringScheduler.js'; 4 | import { logger } from '$lib/logging'; 5 | 6 | /** 7 | * POST /api/monitoring/search/new-episodes 8 | * Manually trigger new episode check 9 | */ 10 | export const POST: RequestHandler = async () => { 11 | try { 12 | const result = await monitoringScheduler.runNewEpisodeCheck(); 13 | 14 | return json({ 15 | success: true, 16 | message: 'New episode check completed', 17 | result 18 | }); 19 | } catch (error) { 20 | logger.error( 21 | '[API] Failed to run new episode check', 22 | error instanceof Error ? error : undefined 23 | ); 24 | return json( 25 | { 26 | success: false, 27 | error: 'Failed to run new episode check', 28 | message: error instanceof Error ? error.message : 'Unknown error' 29 | }, 30 | { status: 500 } 31 | ); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/server/library/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Library Module 3 | * 4 | * Re-exports all library services for easy importing. 5 | */ 6 | 7 | export { mediaInfoService, MediaInfoService, isVideoFile } from './media-info.js'; 8 | export { 9 | diskScanService, 10 | DiskScanService, 11 | type ScanProgress, 12 | type ScanResult, 13 | type DiscoveredFile 14 | } from './disk-scan.js'; 15 | export { mediaMatcherService, MediaMatcherService, type MatchResult } from './media-matcher.js'; 16 | export { libraryWatcherService, LibraryWatcherService } from './library-watcher.js'; 17 | export { 18 | librarySchedulerService, 19 | LibrarySchedulerService, 20 | getLibraryScheduler, 21 | resetLibraryScheduler 22 | } from './library-scheduler.js'; 23 | export { 24 | validateRootFolder, 25 | getEffectiveScoringProfileId, 26 | getLanguageProfileId, 27 | fetchMovieDetails, 28 | fetchMovieExternalIds, 29 | fetchSeriesDetails, 30 | fetchSeriesExternalIds, 31 | triggerMovieSearch, 32 | triggerSeriesSearch 33 | } from './LibraryAddService.js'; 34 | -------------------------------------------------------------------------------- /src/routes/api/streaming/proxy/[...path]/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HLS Stream Proxy - Path-based routing 3 | * 4 | * Handles proxy requests with path suffixes like /segment.ts or /playlist.m3u8 5 | * This is needed because FFmpeg's HLS parser requires URLs to end with 6 | * recognized extensions (.ts, .m3u8, etc.) 7 | * 8 | * GET /api/streaming/proxy/segment.ts?url=&referer= 9 | * GET /api/streaming/proxy/playlist.m3u8?url=&referer= 10 | */ 11 | 12 | import type { RequestHandler } from './$types'; 13 | import { GET as proxyHandler, OPTIONS as optionsHandler } from '../+server'; 14 | 15 | // Forward all requests to the main proxy handler 16 | // The path suffix (segment.ts, playlist.m3u8, etc.) is just for FFmpeg compatibility 17 | // Cast needed because routes have different path signatures but same logic 18 | export const GET: RequestHandler = proxyHandler as unknown as RequestHandler; 19 | export const OPTIONS: RequestHandler = optionsHandler as unknown as RequestHandler; 20 | -------------------------------------------------------------------------------- /src/lib/components/ThemeSelector.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 34 | -------------------------------------------------------------------------------- /src/routes/api/monitoring/search/cutoff-unmet/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { monitoringScheduler } from '$lib/server/monitoring/MonitoringScheduler.js'; 4 | import { logger } from '$lib/logging'; 5 | 6 | /** 7 | * POST /api/monitoring/search/cutoff-unmet 8 | * Manually trigger cutoff unmet search 9 | */ 10 | export const POST: RequestHandler = async () => { 11 | try { 12 | const result = await monitoringScheduler.runCutoffUnmetSearch(); 13 | 14 | return json({ 15 | success: true, 16 | message: 'Cutoff unmet search completed', 17 | result 18 | }); 19 | } catch (error) { 20 | logger.error( 21 | '[API] Failed to run cutoff unmet search', 22 | error instanceof Error ? error : undefined 23 | ); 24 | return json( 25 | { 26 | success: false, 27 | error: 'Failed to run cutoff unmet search', 28 | message: error instanceof Error ? error.message : 'Unknown error' 29 | }, 30 | { status: 500 } 31 | ); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/routes/api/monitoring/search/missing/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { monitoringScheduler } from '$lib/server/monitoring/MonitoringScheduler.js'; 4 | import { logger } from '$lib/logging'; 5 | 6 | /** 7 | * POST /api/monitoring/search/missing 8 | * Manually trigger missing content search 9 | */ 10 | export const POST: RequestHandler = async () => { 11 | try { 12 | const result = await monitoringScheduler.runMissingContentSearch(); 13 | 14 | return json({ 15 | success: true, 16 | message: 'Missing content search completed', 17 | result 18 | }); 19 | } catch (error) { 20 | logger.error( 21 | '[API] Failed to run missing content search', 22 | error instanceof Error ? error : undefined 23 | ); 24 | return json( 25 | { 26 | success: false, 27 | error: 'Failed to run missing content search', 28 | message: error instanceof Error ? error.message : 'Unknown error' 29 | }, 30 | { status: 500 } 31 | ); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/routes/api/monitoring/search/subtitle-upgrade/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { monitoringScheduler } from '$lib/server/monitoring/MonitoringScheduler.js'; 4 | import { logger } from '$lib/logging'; 5 | 6 | /** 7 | * POST /api/monitoring/search/subtitle-upgrade 8 | * Manually trigger subtitle upgrade search 9 | */ 10 | export const POST: RequestHandler = async () => { 11 | try { 12 | const result = await monitoringScheduler.runSubtitleUpgradeSearch(); 13 | 14 | return json({ 15 | success: true, 16 | message: 'Subtitle upgrade search completed', 17 | result 18 | }); 19 | } catch (error) { 20 | logger.error( 21 | '[API] Failed to run subtitle upgrade search', 22 | error instanceof Error ? error : undefined 23 | ); 24 | return json( 25 | { 26 | success: false, 27 | error: 'Failed to run subtitle upgrade search', 28 | message: error instanceof Error ? error.message : 'Unknown error' 29 | }, 30 | { status: 500 } 31 | ); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/normalization/videoCodecs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Video codec normalization for media naming 3 | * 4 | * Maps various video codec name variants to standardized formats. 5 | */ 6 | 7 | import { createNormalizationMap, type NormalizationMap } from './types'; 8 | 9 | const VIDEO_CODEC_MAPPINGS: Record = { 10 | // H.264/AVC 11 | x264: 'x264', 12 | h264: 'x264', 13 | 'h.264': 'x264', 14 | avc: 'x264', 15 | 16 | // H.265/HEVC 17 | x265: 'x265', 18 | h265: 'x265', 19 | 'h.265': 'x265', 20 | hevc: 'x265', 21 | 22 | // Legacy codecs 23 | xvid: 'XviD', 24 | divx: 'DivX', 25 | 26 | // Modern codecs 27 | av1: 'AV1', 28 | vp9: 'VP9', 29 | mpeg2: 'MPEG2' 30 | }; 31 | 32 | export const videoCodecNormalizer: NormalizationMap = createNormalizationMap(VIDEO_CODEC_MAPPINGS); 33 | 34 | /** 35 | * Normalize a video codec name to standard format 36 | */ 37 | export function normalizeVideoCodec(codec: string | undefined): string | undefined { 38 | if (!codec) return undefined; 39 | return videoCodecNormalizer.normalize(codec); 40 | } 41 | -------------------------------------------------------------------------------- /src/routes/api/monitoring/search/missing-subtitles/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { monitoringScheduler } from '$lib/server/monitoring/MonitoringScheduler.js'; 4 | import { logger } from '$lib/logging'; 5 | 6 | /** 7 | * POST /api/monitoring/search/missing-subtitles 8 | * Manually trigger missing subtitles search 9 | */ 10 | export const POST: RequestHandler = async () => { 11 | try { 12 | const result = await monitoringScheduler.runMissingSubtitlesSearch(); 13 | 14 | return json({ 15 | success: true, 16 | message: 'Missing subtitles search completed', 17 | result 18 | }); 19 | } catch (error) { 20 | logger.error( 21 | '[API] Failed to run missing subtitles search', 22 | error instanceof Error ? error : undefined 23 | ); 24 | return json( 25 | { 26 | success: false, 27 | error: 'Failed to run missing subtitles search', 28 | message: error instanceof Error ? error.message : 'Unknown error' 29 | }, 30 | { status: 500 } 31 | ); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # PUID/PGID handling (like linuxserver.io images) 5 | PUID=${PUID:-1000} 6 | PGID=${PGID:-1000} 7 | 8 | echo "Starting Cinephage with UID:$PUID GID:$PGID" 9 | 10 | # Update node user/group to match PUID/PGID 11 | if [ "$PGID" != "$(id -g node)" ]; then 12 | groupmod -o -g "$PGID" node 13 | fi 14 | 15 | if [ "$PUID" != "$(id -u node)" ]; then 16 | usermod -o -u "$PUID" node 17 | fi 18 | 19 | # Ensure data and logs directories exist with correct ownership 20 | mkdir -p data logs 21 | chown -R node:node data logs 22 | 23 | # Copy bundled indexers if data/indexers is empty or missing 24 | INDEXER_DIR="data/indexers" 25 | BUNDLED_DIR="/app/bundled-indexers" 26 | 27 | if [ -d "$BUNDLED_DIR" ]; then 28 | if [ ! -d "$INDEXER_DIR" ] || [ -z "$(ls -A "$INDEXER_DIR" 2>/dev/null)" ]; then 29 | echo "Initializing indexer definitions from bundled files..." 30 | cp -r "$BUNDLED_DIR" "$INDEXER_DIR" 31 | chown -R node:node "$INDEXER_DIR" 32 | fi 33 | fi 34 | 35 | echo "Starting application..." 36 | exec su-exec node "$@" 37 | -------------------------------------------------------------------------------- /src/lib/server/downloads/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Downloads module - services for handling download resolution and management. 3 | */ 4 | 5 | export { 6 | getDownloadResolutionService, 7 | resetDownloadResolutionService, 8 | type ResolveDownloadInput, 9 | type ResolvedDownload 10 | } from './DownloadResolutionService'; 11 | 12 | export { 13 | releaseDecisionService, 14 | ReleaseDecisionService, 15 | type ReleaseDecisionResult, 16 | type DecisionOptions, 17 | type ReleaseInfo, 18 | type UpgradeStats, 19 | type UpgradeStatus 20 | } from './ReleaseDecisionService'; 21 | 22 | export { 23 | getReleaseGrabService, 24 | resetReleaseGrabService, 25 | ReleaseGrabService, 26 | type GrabOptions, 27 | type GrabResult 28 | } from './ReleaseGrabService'; 29 | 30 | export { 31 | getCascadingSearchStrategy, 32 | resetCascadingSearchStrategy, 33 | CascadingSearchStrategy, 34 | type EpisodeToSearch, 35 | type SeriesData, 36 | type EpisodeSearchResult, 37 | type SeasonPackGrab, 38 | type CascadingSearchOptions, 39 | type CascadingSearchResult 40 | } from './CascadingSearchStrategy'; 41 | -------------------------------------------------------------------------------- /src/lib/server/streaming/lookup/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Content ID Lookup Module 3 | * 4 | * Provides content ID resolution for streaming providers that require 5 | * provider-specific content IDs rather than TMDB IDs. 6 | */ 7 | 8 | // Types 9 | export type { 10 | CacheEntry, 11 | CacheKey, 12 | IContentIdLookupProvider, 13 | LookupMediaType, 14 | LookupParams, 15 | LookupProviderId, 16 | LookupResult, 17 | AnimeKaiSearchResult, 18 | YFlixSearchResult, 19 | OneTouchTVSearchResult, 20 | KissKHSearchResult 21 | } from './types'; 22 | 23 | // Cache 24 | export { 25 | ContentIdCache, 26 | contentIdCache, 27 | CONTENT_ID_CACHE_TTL_SUCCESS_MS, 28 | CONTENT_ID_CACHE_TTL_FAILURE_MS, 29 | CONTENT_ID_CACHE_MAX_SIZE 30 | } from './ContentIdCache'; 31 | 32 | // Service 33 | export { 34 | ContentIdLookupService, 35 | contentIdLookupService, 36 | LOOKUP_TIMEOUT_MS 37 | } from './ContentIdLookupService'; 38 | 39 | // Lookup Providers 40 | export { 41 | AnimeKaiLookup, 42 | KissKHLookup, 43 | YFlixLookup, 44 | OneTouchTVLookup, 45 | registerAllLookupProviders 46 | } from './providers'; 47 | -------------------------------------------------------------------------------- /src/lib/components/library/AutoSearchStatus.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | {#if status === 'searching'} 23 |
24 | 25 |
26 | {:else if status === 'success'} 27 |
28 | 29 |
30 | {:else if status === 'failed'} 31 |
32 | 33 |
34 | {/if} 35 | -------------------------------------------------------------------------------- /src/lib/server/indexers/runtime/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Runtime module exports. 3 | * 4 | * Provides indexer runtime components: 5 | * - YamlIndexer: Full-featured YAML-based indexer implementation 6 | * - RequestBuilder: HTTP request construction 7 | * - ResponseParser: HTML/JSON/XML parsing 8 | * - DownloadHandler: Download URL resolution 9 | * - SearchCapabilityChecker: Search criteria validation 10 | */ 11 | 12 | // YAML-based indexer runtime 13 | export { YamlIndexer, createYamlIndexer, type YamlIndexerConfig } from './YamlIndexer'; 14 | export { 15 | RequestBuilder, 16 | createRequestBuilder, 17 | CategoryMapper, 18 | type HttpRequest 19 | } from './RequestBuilder'; 20 | export { 21 | ResponseParser, 22 | createResponseParser, 23 | type ParseResult, 24 | type ParseContext 25 | } from './ResponseParser'; 26 | export { 27 | DownloadHandler, 28 | createDownloadHandler, 29 | type DownloadContext, 30 | type DownloadRequest, 31 | type DownloadResult 32 | } from './DownloadHandler'; 33 | 34 | // Search capability checking 35 | export { SearchCapabilityChecker } from './SearchCapabilityChecker'; 36 | -------------------------------------------------------------------------------- /src/lib/components/ui/modal/TestResult.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | {#if result} 19 |
20 | {#if result.success} 21 | 22 |
23 | {successMessage} 24 | {#if successDetails} 25 |

{successDetails}

26 | {/if} 27 |
28 | {:else} 29 | 30 |
31 | Connection test failed 32 | {#if result.error} 33 |

{result.error}

34 | {/if} 35 |
36 | {/if} 37 |
38 | {/if} 39 | -------------------------------------------------------------------------------- /src/routes/library/unmatched/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { PageServerLoad } from './$types'; 2 | import { db } from '$lib/server/db'; 3 | import { unmatchedFiles, rootFolders } from '$lib/server/db/schema'; 4 | import { eq } from 'drizzle-orm'; 5 | 6 | export const load: PageServerLoad = async () => { 7 | const files = await db 8 | .select({ 9 | id: unmatchedFiles.id, 10 | path: unmatchedFiles.path, 11 | rootFolderId: unmatchedFiles.rootFolderId, 12 | rootFolderPath: rootFolders.path, 13 | mediaType: unmatchedFiles.mediaType, 14 | size: unmatchedFiles.size, 15 | parsedTitle: unmatchedFiles.parsedTitle, 16 | parsedYear: unmatchedFiles.parsedYear, 17 | parsedSeason: unmatchedFiles.parsedSeason, 18 | parsedEpisode: unmatchedFiles.parsedEpisode, 19 | suggestedMatches: unmatchedFiles.suggestedMatches, 20 | reason: unmatchedFiles.reason, 21 | discoveredAt: unmatchedFiles.discoveredAt 22 | }) 23 | .from(unmatchedFiles) 24 | .leftJoin(rootFolders, eq(unmatchedFiles.rootFolderId, rootFolders.id)); 25 | 26 | return { 27 | files, 28 | total: files.length 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/components/library/MonitorToggle.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 45 | -------------------------------------------------------------------------------- /src/lib/components/library/index.ts: -------------------------------------------------------------------------------- 1 | // Library management components 2 | export { default as QualityBadge } from './QualityBadge.svelte'; 3 | export { default as MonitorToggle } from './MonitorToggle.svelte'; 4 | export { default as StatusIndicator } from './StatusIndicator.svelte'; 5 | export { default as AutoSearchStatus } from './AutoSearchStatus.svelte'; 6 | export { default as FileCard } from './FileCard.svelte'; 7 | export { default as MediaInfoPopover } from './MediaInfoPopover.svelte'; 8 | export { default as MovieFilesTab } from './MovieFilesTab.svelte'; 9 | export { default as LibraryMovieHeader } from './LibraryMovieHeader.svelte'; 10 | export { default as MovieEditModal } from './MovieEditModal.svelte'; 11 | export { default as RenamePreviewModal } from './RenamePreviewModal.svelte'; 12 | 13 | // TV Series components 14 | export { default as LibrarySeriesHeader } from './LibrarySeriesHeader.svelte'; 15 | export { default as SeasonAccordion } from './SeasonAccordion.svelte'; 16 | export { default as EpisodeRow } from './EpisodeRow.svelte'; 17 | export { default as SeriesEditModal } from './SeriesEditModal.svelte'; 18 | -------------------------------------------------------------------------------- /src/lib/components/ui/modal/ModalWrapper.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | 31 | {#if open} 32 | 43 | {/if} 44 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/tokens/definitions/video.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Video tokens - VideoCodec, HDR, BitDepth, 3D 3 | */ 4 | 5 | import type { TokenDefinition } from '../types'; 6 | import { normalizeVideoCodec } from '../../normalization'; 7 | 8 | export const videoTokens: TokenDefinition[] = [ 9 | { 10 | name: 'VideoCodec', 11 | aliases: ['Codec'], 12 | category: 'video', 13 | description: 'Video codec (x264, x265, AV1)', 14 | applicability: ['movie', 'episode'], 15 | render: (info) => normalizeVideoCodec(info.codec) || '' 16 | }, 17 | { 18 | name: 'HDR', 19 | category: 'video', 20 | description: 'HDR format (DV, HDR10, HDR10+)', 21 | applicability: ['movie', 'episode'], 22 | render: (info) => info.hdr || '' 23 | }, 24 | { 25 | name: 'BitDepth', 26 | category: 'video', 27 | description: 'Bit depth (8, 10, 12)', 28 | applicability: ['movie', 'episode'], 29 | render: (info) => info.bitDepth || '' 30 | }, 31 | { 32 | name: '3D', 33 | category: 'video', 34 | description: '"3D" if applicable', 35 | applicability: ['movie', 'episode'], 36 | render: (info) => (info.is3D ? '3D' : '') 37 | } 38 | ]; 39 | -------------------------------------------------------------------------------- /src/routes/smartlists/[id]/+page.server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Smart List Detail Page - Server Load 3 | */ 4 | 5 | import type { PageServerLoad } from './$types'; 6 | import { getSmartListService } from '$lib/server/smartlists/index.js'; 7 | import { error } from '@sveltejs/kit'; 8 | 9 | export const load: PageServerLoad = async ({ params, url }) => { 10 | const service = getSmartListService(); 11 | const list = await service.getSmartList(params.id); 12 | 13 | if (!list) { 14 | throw error(404, 'Smart list not found'); 15 | } 16 | 17 | const page = parseInt(url.searchParams.get('page') ?? '1'); 18 | const inLibrary = url.searchParams.get('inLibrary'); 19 | const includeExcluded = url.searchParams.get('includeExcluded') === 'true'; 20 | 21 | const items = await service.getSmartListItems(params.id, { 22 | page, 23 | limit: 50, 24 | inLibrary: inLibrary === 'true' ? true : inLibrary === 'false' ? false : null, 25 | includeExcluded 26 | }); 27 | 28 | return { 29 | list, 30 | items: items.items, 31 | pagination: { 32 | page: items.page, 33 | totalPages: items.totalPages, 34 | totalItems: items.totalItems 35 | } 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | day: 'monday' 8 | open-pull-requests-limit: 10 9 | labels: 10 | - 'Type: Chore' 11 | - 'Dependencies' 12 | assignees: 13 | - 'MoldyTaint' 14 | commit-message: 15 | prefix: 'chore(deps)' 16 | groups: 17 | minor-and-patch: 18 | patterns: 19 | - '*' 20 | update-types: 21 | - 'minor' 22 | - 'patch' 23 | svelte: 24 | patterns: 25 | - 'svelte*' 26 | - '@sveltejs/*' 27 | tailwind: 28 | patterns: 29 | - 'tailwindcss*' 30 | - 'daisyui*' 31 | - '@tailwindcss/*' 32 | drizzle: 33 | patterns: 34 | - 'drizzle-*' 35 | 36 | - package-ecosystem: 'github-actions' 37 | directory: '/' 38 | schedule: 39 | interval: 'weekly' 40 | day: 'monday' 41 | labels: 42 | - 'Type: Chore' 43 | - 'Dependencies' 44 | assignees: 45 | - 'MoldyTaint' 46 | commit-message: 47 | prefix: 'chore(ci)' 48 | -------------------------------------------------------------------------------- /src/lib/server/workers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Worker System 3 | * Export all worker-related types, classes, and utilities. 4 | */ 5 | 6 | // Types 7 | export type { 8 | WorkerType, 9 | WorkerStatus, 10 | WorkerLogEntry, 11 | WorkerState, 12 | WorkerManagerConfig, 13 | WorkerEvent, 14 | StreamWorkerMetadata, 15 | ImportWorkerMetadata, 16 | ScanWorkerMetadata, 17 | MonitoringWorkerMetadata, 18 | SearchWorkerMetadata, 19 | SubtitleSearchWorkerMetadata 20 | } from './types.js'; 21 | 22 | export { DEFAULT_WORKER_CONFIG, workerTypeToLogCategory } from './types.js'; 23 | 24 | // Base class 25 | export { TaskWorker } from './TaskWorker.js'; 26 | 27 | // Manager 28 | export { workerManager, ConcurrencyLimitError } from './WorkerManager.js'; 29 | 30 | // Worker implementations 31 | export { StreamWorker, streamWorkerRegistry, type StreamWorkerOptions } from './StreamWorker.js'; 32 | export { ImportWorker, type ImportWorkerOptions } from './ImportWorker.js'; 33 | export { SearchWorker, type SearchWorkerOptions, type SearchResult } from './SearchWorker.js'; 34 | export { SubtitleSearchWorker, type SubtitleSearchWorkerOptions } from './SubtitleSearchWorker.js'; 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/CardSkeleton.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 | {#if showDetails} 35 |
36 | 37 | 38 | 39 | 40 |
41 | {/if} 42 |
43 | -------------------------------------------------------------------------------- /src/lib/components/queue/QueueProgressBar.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 |
36 | 37 | {percentage}% 38 |
39 | -------------------------------------------------------------------------------- /src/lib/server/indexers/auth/providers/NoAuthProvider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * No Authentication Provider 3 | * 4 | * Handles public indexers that require no authentication. 5 | */ 6 | 7 | import { BaseAuthProvider, type AuthContext, type AuthRequestResponse } from './IAuthProvider'; 8 | import type { AuthConfig, AuthResult, AuthTestResult } from '../../types'; 9 | 10 | /** 11 | * Provider for indexers that require no authentication 12 | */ 13 | export class NoAuthProvider extends BaseAuthProvider { 14 | readonly method = 'none'; 15 | 16 | /** 17 | * No authentication needed - always succeeds 18 | */ 19 | async authenticate(_context: AuthContext, _config: AuthConfig): Promise { 20 | return this.successResult(); 21 | } 22 | 23 | /** 24 | * No authentication to test - always valid 25 | */ 26 | async test(_context: AuthContext, _config: AuthConfig): Promise { 27 | return { 28 | valid: true, 29 | message: 'Public indexer - no authentication required' 30 | }; 31 | } 32 | 33 | /** 34 | * Public indexers don't require login 35 | */ 36 | checkLoginNeeded(_response: AuthRequestResponse): boolean { 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/server/indexers/parser/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Release Parser Module 3 | * 4 | * Exports for parsing release titles into structured metadata 5 | */ 6 | 7 | // Main parser 8 | export { ReleaseParser, releaseParser, parseRelease } from './ReleaseParser.js'; 9 | 10 | // Types 11 | export type { 12 | ParsedRelease, 13 | EpisodeInfo, 14 | Resolution, 15 | Source, 16 | Codec, 17 | HdrFormat, 18 | AudioFormat 19 | } from './types.js'; 20 | 21 | export { RESOLUTION_ORDER, SOURCE_ORDER, CODEC_ORDER, AUDIO_ORDER } from './types.js'; 22 | 23 | // Individual pattern extractors (for advanced use) 24 | export { extractResolution, hasResolutionInfo } from './patterns/resolution.js'; 25 | export { extractSource, hasSourceInfo } from './patterns/source.js'; 26 | export { extractCodec, hasCodecInfo } from './patterns/codec.js'; 27 | export { extractAudio, extractHdr, hasAudioInfo, hasHdrInfo } from './patterns/audio.js'; 28 | export { extractLanguages, hasExplicitLanguage } from './patterns/language.js'; 29 | export { extractEpisode, isTvRelease, extractTitleBeforeEpisode } from './patterns/episode.js'; 30 | export { extractReleaseGroup, hasReleaseGroup } from './patterns/releaseGroup.js'; 31 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/normalization/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Normalization map interface for consistent string transformations 3 | */ 4 | export interface NormalizationMap { 5 | /** 6 | * Map of lowercase input to normalized output 7 | */ 8 | readonly mappings: Record; 9 | 10 | /** 11 | * Normalize an input string 12 | * @param input - The string to normalize 13 | * @returns The normalized string, or the original if no mapping exists 14 | */ 15 | normalize(input: string): string; 16 | 17 | /** 18 | * Check if input is a recognized value 19 | * @param input - The string to check 20 | * @returns True if the input has a known mapping 21 | */ 22 | isKnown(input: string): boolean; 23 | } 24 | 25 | /** 26 | * Create a normalization map from a mappings object 27 | */ 28 | export function createNormalizationMap(mappings: Record): NormalizationMap { 29 | return { 30 | mappings, 31 | normalize(input: string): string { 32 | const normalized = input.toLowerCase(); 33 | return mappings[normalized] ?? input; 34 | }, 35 | isKnown(input: string): boolean { 36 | return input.toLowerCase() in mappings; 37 | } 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/api/workers/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Workers API - List and manage workers 3 | * 4 | * GET /api/workers - List all workers (optionally filter by type) 5 | * DELETE /api/workers - Clear all completed workers 6 | */ 7 | 8 | import { json } from '@sveltejs/kit'; 9 | import type { RequestHandler } from './$types'; 10 | import { workerManager } from '$lib/server/workers'; 11 | import type { WorkerType } from '$lib/server/workers'; 12 | 13 | export const GET: RequestHandler = async ({ url }) => { 14 | const typeParam = url.searchParams.get('type') as WorkerType | null; 15 | const activeOnly = url.searchParams.get('active') === 'true'; 16 | 17 | let workers = workerManager.listStates(typeParam || undefined); 18 | 19 | if (activeOnly) { 20 | workers = workers.filter((w) => w.status === 'pending' || w.status === 'running'); 21 | } 22 | 23 | const stats = workerManager.getStats(); 24 | 25 | return json({ 26 | workers, 27 | stats, 28 | config: workerManager.getConfig() 29 | }); 30 | }; 31 | 32 | export const DELETE: RequestHandler = async () => { 33 | const cleared = workerManager.clearCompleted(); 34 | 35 | return json({ 36 | success: true, 37 | cleared 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/routes/health/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Health check endpoint. 3 | * Returns service health status for monitoring and load balancers. 4 | */ 5 | 6 | import { json } from '@sveltejs/kit'; 7 | import type { RequestHandler } from './$types'; 8 | import { db } from '$lib/server/db'; 9 | import { settings } from '$lib/server/db/schema'; 10 | 11 | export const GET: RequestHandler = async () => { 12 | const checks: Record = {}; 13 | let overallHealthy = true; 14 | 15 | // Database check 16 | const dbStart = performance.now(); 17 | try { 18 | await db.select().from(settings).limit(1); 19 | checks.database = { 20 | status: 'healthy', 21 | latencyMs: Math.round(performance.now() - dbStart) 22 | }; 23 | } catch { 24 | checks.database = { status: 'unhealthy' }; 25 | overallHealthy = false; 26 | } 27 | 28 | return json( 29 | { 30 | status: overallHealthy ? 'healthy' : 'unhealthy', 31 | version: process.env.npm_package_version ?? '0.0.1', 32 | uptime: Math.round(process.uptime()), 33 | timestamp: new Date().toISOString(), 34 | checks 35 | }, 36 | { status: overallHealthy ? 200 : 503 } 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/lib/components/library/QualityBadge.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | {#if qualityText || hdrText || sourceText} 25 |
26 | {#if qualityText} 27 | {qualityText} 28 | {/if} 29 | {#if sourceText} 30 | {sourceText} 31 | {/if} 32 | {#if hdrText} 33 | {hdrText} 34 | {/if} 35 |
36 | {/if} 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/FormField.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 34 | 35 | {@render children()} 36 | 37 | {#if error} 38 |
39 | {error} 40 |
41 | {:else if helpText} 42 |
43 | {helpText} 44 |
45 | {/if} 46 |
47 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/tokens/definitions/core.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Core tokens - Title, Year, CleanTitle 3 | */ 4 | 5 | import type { TokenDefinition } from '../types'; 6 | 7 | /** 8 | * Generate a clean title by removing special characters for filesystem compatibility 9 | */ 10 | function generateCleanTitle(title: string): string { 11 | return title 12 | .replace(/[:/\\?*"<>|]/g, '') 13 | .replace(/\s+/g, ' ') 14 | .trim(); 15 | } 16 | 17 | export const coreTokens: TokenDefinition[] = [ 18 | { 19 | name: 'Title', 20 | aliases: ['SeriesTitle'], 21 | category: 'core', 22 | description: 'Title as-is', 23 | applicability: ['movie', 'series', 'episode'], 24 | render: (info) => info.title || '' 25 | }, 26 | { 27 | name: 'CleanTitle', 28 | aliases: ['MovieCleanTitle', 'SeriesCleanTitle'], 29 | category: 'core', 30 | description: 'Title with special characters removed', 31 | applicability: ['movie', 'series', 'episode'], 32 | render: (info) => (info.title ? generateCleanTitle(info.title) : '') 33 | }, 34 | { 35 | name: 'Year', 36 | category: 'core', 37 | description: 'Release year', 38 | applicability: ['movie', 'series', 'episode'], 39 | render: (info) => (info.year ? String(info.year) : '') 40 | } 41 | ]; 42 | -------------------------------------------------------------------------------- /src/lib/server/indexers/loader/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Indexer Definition System 3 | * 4 | * Provides loading and factory functionality for indexer definitions. 5 | */ 6 | 7 | // Types 8 | export type { 9 | IndexerDefinition, 10 | IndexerDefinitionSummary, 11 | SettingField, 12 | SettingFieldType, 13 | CategoryMapping, 14 | DefinitionSource, 15 | CreateIndexerConfig, 16 | UIDefinitionSetting, 17 | UIIndexerDefinition 18 | } from './types'; 19 | 20 | export { 21 | getDefaultSettings, 22 | getRequiredSettings, 23 | requiresAuth, 24 | toDefinitionSummary, 25 | toUIDefinition 26 | } from './types'; 27 | 28 | // Definition Loader (unified) 29 | export { 30 | DefinitionLoader, 31 | getDefinitionLoader, 32 | initializeDefinitions, 33 | type DefinitionLoadError 34 | } from './DefinitionLoader'; 35 | 36 | // Factory (unified) 37 | export { IndexerFactory, getIndexerFactory } from './IndexerFactory'; 38 | 39 | // YAML Definition Loader 40 | export { 41 | YamlDefinitionLoader, 42 | getYamlDefinitionLoader, 43 | resetYamlDefinitionLoader, 44 | type DefinitionLoadResult 45 | } from './YamlDefinitionLoader'; 46 | 47 | // YAML Indexer Factory 48 | export { 49 | YamlIndexerFactory, 50 | getYamlIndexerFactory, 51 | resetYamlIndexerFactory 52 | } from './YamlIndexerFactory'; 53 | -------------------------------------------------------------------------------- /src/lib/components/library/tv/BulkActionBar.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#if selectedCount > 0} 15 |
16 |
19 | 20 | {selectedCount} episode{selectedCount !== 1 ? 's' : ''} selected 21 | 22 | 23 |
24 | 33 | 34 | 37 |
38 |
39 |
40 | {/if} 41 | -------------------------------------------------------------------------------- /src/lib/types/task.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Task System Types 3 | * 4 | * Type definitions for the system tasks feature. 5 | * Tasks are manual maintenance operations that users can trigger from the Tasks page. 6 | */ 7 | 8 | /** 9 | * Definition of a system task that can be run manually 10 | */ 11 | export interface TaskDefinition { 12 | /** Unique identifier for the task (e.g., 'update-strm-urls') */ 13 | id: string; 14 | /** Human-readable name displayed in UI */ 15 | name: string; 16 | /** Description of what the task does */ 17 | description: string; 18 | /** API endpoint to call when running the task (POST) */ 19 | endpoint: string; 20 | /** Category for grouping tasks */ 21 | category: 'maintenance' | 'housekeeping' | 'sync'; 22 | } 23 | 24 | /** 25 | * A record of a task execution 26 | */ 27 | export interface TaskHistoryEntry { 28 | id: string; 29 | taskId: string; 30 | status: 'running' | 'completed' | 'failed' | 'cancelled'; 31 | results: Record | null; 32 | errors: string[] | null; 33 | startedAt: string; 34 | completedAt: string | null; 35 | } 36 | 37 | /** 38 | * Combined task status including definition and last run info 39 | */ 40 | export interface TaskStatus { 41 | taskId: string; 42 | lastRun: TaskHistoryEntry | null; 43 | isRunning: boolean; 44 | } 45 | -------------------------------------------------------------------------------- /src/routes/api/indexers/definitions/[id]/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { 4 | getDefinitionLoader, 5 | initializeDefinitions, 6 | toUIDefinition 7 | } from '$lib/server/indexers/loader'; 8 | 9 | /** 10 | * GET /api/indexers/definitions/:id 11 | * Returns a specific indexer definition (native or Cardigann). 12 | */ 13 | export const GET: RequestHandler = async ({ params }) => { 14 | // Get definition loader 15 | const loader = getDefinitionLoader(); 16 | if (!loader.isLoaded()) { 17 | await initializeDefinitions(); 18 | } 19 | 20 | const definition = loader.get(params.id); 21 | 22 | if (!definition) { 23 | return json({ error: 'Definition not found' }, { status: 404 }); 24 | } 25 | 26 | // Convert to UI format for consistent response 27 | const uiDef = toUIDefinition(definition); 28 | 29 | return json({ 30 | id: uiDef.id, 31 | name: uiDef.name, 32 | description: uiDef.description ?? `${uiDef.name} torrent indexer`, 33 | type: uiDef.type, 34 | language: definition.language, 35 | encoding: 'UTF-8', 36 | protocol: uiDef.protocol, 37 | siteUrl: uiDef.siteUrl, 38 | alternateUrls: uiDef.alternateUrls, 39 | capabilities: uiDef.capabilities, 40 | settings: uiDef.settings 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/lib/server/quality/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Quality Module 3 | * 4 | * Provides quality filtering, TMDB matching, and release enrichment 5 | */ 6 | 7 | // Types 8 | export type { QualityPreset, QualityMatchResult, ScoreComponents } from './types.js'; 9 | export { DEFAULT_PRESETS } from './types.js'; 10 | 11 | // Quality Filter 12 | export { QualityFilter, qualityFilter, type EnhancedQualityResult } from './QualityFilter.js'; 13 | 14 | // TMDB Matcher 15 | export { TmdbMatcher, tmdbMatcher, type TmdbMatch, type TmdbHint } from './TmdbMatcher.js'; 16 | 17 | // Release Enricher 18 | export { 19 | ReleaseEnricher, 20 | releaseEnricher, 21 | type EnrichmentOptions, 22 | type EnrichmentResult 23 | } from './ReleaseEnricher.js'; 24 | 25 | // Re-export scoring engine types for convenience 26 | export type { 27 | ScoringProfile, 28 | ScoringResult, 29 | CustomFormat, 30 | FormatCategory, 31 | ReleaseAttributes, 32 | ScoreBreakdown, 33 | CategoryBreakdown, 34 | ScoredFormat 35 | } from '../scoring/index.js'; 36 | 37 | export { 38 | DEFAULT_PROFILES, 39 | QUALITY_PROFILE, 40 | BALANCED_PROFILE, 41 | COMPACT_PROFILE, 42 | STREAMER_PROFILE, 43 | scoreRelease, 44 | rankReleases, 45 | isUpgrade, 46 | getProfile, 47 | isBuiltInProfile, 48 | getBuiltInProfileIds 49 | } from '../scoring/index.js'; 50 | -------------------------------------------------------------------------------- /src/lib/components/tmdb/TmdbImage.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 | {#if src} 21 | (loaded = true)} 27 | class="h-full w-full object-cover transition-opacity duration-200 {loaded 28 | ? 'opacity-100' 29 | : 'opacity-0'}" 30 | /> 31 | {#if !loaded} 32 |
33 | {/if} 34 | {:else} 35 | 36 |
37 | No Image 38 |
39 | {/if} 40 |
41 | -------------------------------------------------------------------------------- /src/lib/config/trackers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * BitTorrent tracker URLs for magnet link construction. 3 | * These are public trackers that help with peer discovery. 4 | */ 5 | export const DEFAULT_TRACKERS = [ 6 | 'udp://open.demonii.com:1337/announce', 7 | 'udp://tracker.openbittorrent.com:80', 8 | 'udp://tracker.coppersurfer.tk:6969', 9 | 'udp://glotorrents.pw:6969/announce', 10 | 'udp://tracker.opentrackr.org:1337/announce', 11 | 'udp://torrent.gresille.org:80/announce', 12 | 'udp://p4p.arenabg.com:1337', 13 | 'udp://tracker.leechers-paradise.org:6969' 14 | ] as const; 15 | 16 | /** 17 | * Builds a magnet URL from an info hash and title. 18 | * 19 | * @param hash - The torrent info hash 20 | * @param name - The torrent name/title (will be URL encoded) 21 | * @param trackers - Optional array of tracker URLs (defaults to DEFAULT_TRACKERS) 22 | * @returns A properly formatted magnet URL 23 | */ 24 | export function buildMagnetUrl( 25 | hash: string, 26 | name: string, 27 | trackers: readonly string[] = DEFAULT_TRACKERS 28 | ): string { 29 | const params = new URLSearchParams(); 30 | params.set('xt', `urn:btih:${hash}`); 31 | params.set('dn', name); 32 | 33 | // Add trackers 34 | for (const tracker of trackers) { 35 | params.append('tr', tracker); 36 | } 37 | 38 | return `magnet:?${params.toString()}`; 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/components/subtitleProviders/SubtitleProviderStatusBadge.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 |
36 |
37 | 38 | {statusInfo.text} 39 |
40 |
41 | -------------------------------------------------------------------------------- /src/lib/components/tmdb/ProductionCompanies.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | {#if companies.length > 0} 19 |
20 | {#each displayCompanies as company (company.id)} 21 |
22 | {#if company.logo_path} 23 | 29 | {:else} 30 | {company.name} 31 | {/if} 32 |
33 | {/each} 34 | 35 | {#if hasMore && !showAll} 36 | 39 | {/if} 40 |
41 | {/if} 42 | -------------------------------------------------------------------------------- /src/routes/api/rename/preview/movie/[id]/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Single Movie Rename Preview API 3 | * 4 | * GET /api/rename/preview/movie/:id 5 | * Returns a preview of how files for a specific movie would be renamed. 6 | */ 7 | 8 | import { json } from '@sveltejs/kit'; 9 | import type { RequestHandler } from './$types'; 10 | import { RenamePreviewService } from '$lib/server/library/naming/RenamePreviewService'; 11 | import { logger } from '$lib/logging'; 12 | 13 | /** 14 | * GET /api/rename/preview/movie/:id 15 | * Get preview of rename for a single movie's files 16 | */ 17 | export const GET: RequestHandler = async ({ params }) => { 18 | try { 19 | const { id } = params; 20 | 21 | if (!id) { 22 | return json({ error: 'Movie ID is required' }, { status: 400 }); 23 | } 24 | 25 | const service = new RenamePreviewService(); 26 | const result = await service.previewMovie(id); 27 | 28 | return json(result); 29 | } catch (error) { 30 | logger.error('[RenamePreview API] Failed to preview movie rename', { 31 | movieId: params.id, 32 | error: error instanceof Error ? error.message : String(error) 33 | }); 34 | 35 | return json( 36 | { 37 | error: 'Failed to generate movie rename preview', 38 | details: error instanceof Error ? error.message : 'Unknown error' 39 | }, 40 | { status: 500 } 41 | ); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/routes/api/rename/preview/series/[id]/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Series Rename Preview API 3 | * 4 | * GET /api/rename/preview/series/:id 5 | * Returns a preview of how files for a specific series would be renamed. 6 | */ 7 | 8 | import { json } from '@sveltejs/kit'; 9 | import type { RequestHandler } from './$types'; 10 | import { RenamePreviewService } from '$lib/server/library/naming/RenamePreviewService'; 11 | import { logger } from '$lib/logging'; 12 | 13 | /** 14 | * GET /api/rename/preview/series/:id 15 | * Get preview of rename for all files in a series 16 | */ 17 | export const GET: RequestHandler = async ({ params }) => { 18 | try { 19 | const { id } = params; 20 | 21 | if (!id) { 22 | return json({ error: 'Series ID is required' }, { status: 400 }); 23 | } 24 | 25 | const service = new RenamePreviewService(); 26 | const result = await service.previewSeries(id); 27 | 28 | return json(result); 29 | } catch (error) { 30 | logger.error('[RenamePreview API] Failed to preview series rename', { 31 | seriesId: params.id, 32 | error: error instanceof Error ? error.message : String(error) 33 | }); 34 | 35 | return json( 36 | { 37 | error: 'Failed to generate series rename preview', 38 | details: error instanceof Error ? error.message : 'Unknown error' 39 | }, 40 | { status: 500 } 41 | ); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/normalization/sources.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Source name normalization for media naming 3 | * 4 | * Maps various source name variants to standardized formats 5 | * following TRaSH Guides conventions. 6 | */ 7 | 8 | import { createNormalizationMap, type NormalizationMap } from './types'; 9 | 10 | const SOURCE_MAPPINGS: Record = { 11 | // Blu-ray variants 12 | bluray: 'Bluray', 13 | 'blu-ray': 'Bluray', 14 | bdrip: 'Bluray', 15 | brrip: 'Bluray', 16 | 17 | // Remux 18 | remux: 'Remux', 19 | 20 | // Web sources 21 | webdl: 'WEB-DL', 22 | 'web-dl': 'WEB-DL', 23 | 'web dl': 'WEB-DL', 24 | webrip: 'WEBRip', 25 | 'web-rip': 'WEBRip', 26 | web: 'WEB', 27 | 28 | // TV sources 29 | hdtv: 'HDTV', 30 | pdtv: 'PDTV', 31 | dsr: 'DSR', 32 | 33 | // DVD sources 34 | dvdrip: 'DVDRip', 35 | dvd: 'DVD', 36 | 37 | // Low quality sources 38 | hdcam: 'HDCAM', 39 | hdrip: 'HDRip', 40 | cam: 'CAM', 41 | telesync: 'TS', 42 | ts: 'TS', 43 | telecine: 'TC', 44 | tc: 'TC', 45 | screener: 'SCR', 46 | scr: 'SCR', 47 | r5: 'R5' 48 | }; 49 | 50 | export const sourceNormalizer: NormalizationMap = createNormalizationMap(SOURCE_MAPPINGS); 51 | 52 | /** 53 | * Normalize a source name to standard format 54 | */ 55 | export function normalizeSource(source: string): string { 56 | return sourceNormalizer.normalize(source); 57 | } 58 | -------------------------------------------------------------------------------- /src/lib/server/streaming/lookup/providers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Content ID Lookup Providers 3 | * 4 | * Provider-specific adapters for resolving TMDB IDs to provider content IDs. 5 | */ 6 | 7 | export { AnimeKaiLookup } from './AnimeKaiLookup'; 8 | export { KissKHLookup } from './KissKHLookup'; 9 | export { YFlixLookup } from './YFlixLookup'; 10 | export { OneTouchTVLookup } from './OneTouchTVLookup'; 11 | 12 | // ============================================================================ 13 | // Provider Registration 14 | // ============================================================================ 15 | 16 | import { contentIdLookupService } from '../ContentIdLookupService'; 17 | import { AnimeKaiLookup } from './AnimeKaiLookup'; 18 | import { KissKHLookup } from './KissKHLookup'; 19 | import { YFlixLookup } from './YFlixLookup'; 20 | import { OneTouchTVLookup } from './OneTouchTVLookup'; 21 | 22 | /** 23 | * Register all lookup providers with the service 24 | */ 25 | export function registerAllLookupProviders(): void { 26 | contentIdLookupService.registerProvider(new AnimeKaiLookup()); 27 | contentIdLookupService.registerProvider(new KissKHLookup()); 28 | contentIdLookupService.registerProvider(new YFlixLookup()); 29 | contentIdLookupService.registerProvider(new OneTouchTVLookup()); 30 | } 31 | 32 | // Auto-register on import 33 | registerAllLookupProviders(); 34 | -------------------------------------------------------------------------------- /src/routes/person/[id]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { tmdb } from '$lib/server/tmdb'; 2 | import { error } from '@sveltejs/kit'; 3 | import type { PageServerLoad } from './$types'; 4 | import { logger } from '$lib/logging'; 5 | 6 | export const load: PageServerLoad = async ({ params }) => { 7 | const id = parseInt(params.id); 8 | if (isNaN(id)) { 9 | throw error(400, 'Invalid person ID'); 10 | } 11 | 12 | // Check if TMDB is configured 13 | const tmdbConfigured = await tmdb.isConfigured(); 14 | if (!tmdbConfigured) { 15 | throw error(503, { 16 | message: 17 | 'TMDB API key not configured. Please configure your TMDB API key in Settings > Integrations.' 18 | }); 19 | } 20 | 21 | try { 22 | // Fetch ONLY person details - no credits 23 | // This is ~5KB vs 150KB+ with combined_credits 24 | // Credits are loaded lazily by SectionRow via /api/tmdb/person/{id}/credits 25 | const person = await tmdb.getPersonBasic(id); 26 | 27 | // Handle null response (shouldn't happen since we checked config, but be safe) 28 | if (!person) { 29 | throw error(503, { 30 | message: 31 | 'TMDB API key not configured. Please configure your TMDB API key in Settings > Integrations.' 32 | }); 33 | } 34 | 35 | return { person }; 36 | } catch (e) { 37 | logger.error('Failed to fetch person', e, { personId: id }); 38 | throw error(404, 'Person not found'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/server/subtitles/services/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Subtitle Services - Module exports 3 | */ 4 | 5 | export { 6 | SubtitleProviderManager, 7 | getSubtitleProviderManager, 8 | initializeSubtitleProviderManager 9 | } from './SubtitleProviderManager'; 10 | 11 | export { SubtitleScoringService, getSubtitleScoringService } from './SubtitleScoringService'; 12 | 13 | export { 14 | SubtitleSearchService, 15 | getSubtitleSearchService, 16 | type SubtitleSearchOptions 17 | } from './SubtitleSearchService'; 18 | 19 | export { SubtitleDownloadService, getSubtitleDownloadService } from './SubtitleDownloadService'; 20 | 21 | export { 22 | LanguageProfileService, 23 | getLanguageProfileService, 24 | type LanguageProfile, 25 | type CreateLanguageProfile, 26 | type UpdateLanguageProfile 27 | } from './LanguageProfileService'; 28 | 29 | export { 30 | SubtitleSyncService, 31 | getSubtitleSyncService, 32 | type SyncOptions 33 | } from './SubtitleSyncService'; 34 | 35 | export { 36 | searchSubtitlesForNewMedia, 37 | searchSubtitlesForMediaBatch, 38 | type ImportSearchResult, 39 | type BatchSearchResult 40 | } from './SubtitleImportService'; 41 | 42 | export { SubtitleScannerService, getSubtitleScannerService } from './SubtitleScannerService'; 43 | 44 | export { 45 | SubtitleSettingsService, 46 | getSubtitleSettingsService, 47 | type SubtitleSettingsData 48 | } from './SubtitleSettingsService'; 49 | -------------------------------------------------------------------------------- /src/routes/api/queue/cleanup/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Queue Cleanup API 3 | * 4 | * POST /api/queue/cleanup - Clean up orphaned completed torrents from download clients 5 | * 6 | * Query params: 7 | * dryRun=true - Preview what would be removed without actually removing 8 | */ 9 | 10 | import { json } from '@sveltejs/kit'; 11 | import type { RequestHandler } from './$types'; 12 | import { downloadMonitor } from '$lib/server/downloadClients/monitoring'; 13 | import { logger } from '$lib/logging'; 14 | 15 | export const POST: RequestHandler = async ({ url }) => { 16 | const dryRun = url.searchParams.get('dryRun') === 'true'; 17 | 18 | logger.info('Queue cleanup requested', { dryRun }); 19 | 20 | try { 21 | const result = await downloadMonitor.cleanupOrphanedDownloads(dryRun); 22 | 23 | return json({ 24 | success: true, 25 | dryRun, 26 | summary: { 27 | removed: result.removed.length, 28 | skipped: result.skipped.length, 29 | errors: result.errors.length 30 | }, 31 | removed: result.removed, 32 | skipped: result.skipped, 33 | errors: result.errors 34 | }); 35 | } catch (error) { 36 | const message = error instanceof Error ? error.message : 'Unknown error'; 37 | logger.error('Queue cleanup failed', { error: message }); 38 | 39 | return json( 40 | { 41 | success: false, 42 | error: message 43 | }, 44 | { status: 500 } 45 | ); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/lib/server/streaming/enc-dec/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * EncDec Module 3 | * 4 | * Exports for the encryption-as-a-service API client. 5 | */ 6 | 7 | export { 8 | EncDecClient, 9 | getEncDecClient, 10 | createEncDecClient, 11 | type EncDecClientConfig 12 | } from './client'; 13 | 14 | export { 15 | // Error class 16 | EncDecApiError, 17 | 18 | // Provider type unions 19 | type EncryptionProvider, 20 | type DecryptionProvider, 21 | 22 | // Request payload types 23 | type DecryptPayload, 24 | type VideasyDecryptPayload, 25 | type VidstackDecryptPayload, 26 | type HexaDecryptPayload, 27 | type KissKHDecryptPayload, 28 | type KissKHEncryptParams, 29 | type ParseHtmlPayload, 30 | 31 | // Response types 32 | type EncDecResponse, 33 | type StringResult, 34 | type VidstackTokenResult, 35 | type MappleSessionResult, 36 | type ParsedHtmlResult, 37 | 38 | // Provider-specific response types 39 | type VideasyStream, 40 | type VidlinkResponse, 41 | type XPrimeStream, 42 | type HexaStream, 43 | type SmashyStreamPlayerResponse, 44 | type SmashyStreamType2Data, 45 | type OneTouchTVStream, 46 | type KissKHVideoResponse, 47 | type KissKHSubtitle, 48 | 49 | // AnimeKai database types 50 | type AnimeKaiSearchParams, 51 | type AnimeKaiFindParams, 52 | type AnimeKaiEntry, 53 | type AnimeKaiSearchResponse, 54 | type AnimeKaiFindResponse, 55 | 56 | // Error types 57 | type EncDecError 58 | } from './types'; 59 | -------------------------------------------------------------------------------- /src/routes/api/indexers/definitions/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import { 3 | getDefinitionLoader, 4 | initializeDefinitions, 5 | toUIDefinition 6 | } from '$lib/server/indexers/loader'; 7 | 8 | /** 9 | * GET /api/indexers/definitions 10 | * Returns all available indexer definitions from the unified YAML-based system. 11 | * Internal/auto-managed indexers (like streaming) are excluded from this list. 12 | */ 13 | export async function GET() { 14 | // Ensure definitions are loaded 15 | const loader = getDefinitionLoader(); 16 | if (!loader.isLoaded()) { 17 | await initializeDefinitions(); 18 | } 19 | 20 | // Get all definitions and convert to UI format 21 | const allDefinitions = loader.getAll(); 22 | 23 | // Map to API response format, excluding internal indexers 24 | const definitions = allDefinitions 25 | .filter((def) => def.protocol !== 'streaming') // Exclude streaming indexers from public list 26 | .map((def) => { 27 | const uiDef = toUIDefinition(def); 28 | return { 29 | id: uiDef.id, 30 | name: uiDef.name, 31 | description: uiDef.description, 32 | type: uiDef.type, 33 | protocol: uiDef.protocol, 34 | siteUrl: uiDef.siteUrl, 35 | alternateUrls: uiDef.alternateUrls, 36 | capabilities: uiDef.capabilities, 37 | settings: uiDef.settings 38 | }; 39 | }) 40 | .sort((a, b) => a.name.localeCompare(b.name)); 41 | 42 | return json(definitions); 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/server/indexers/http/browser/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Browser Solver Module 3 | * 4 | * Provides Cloudflare bypass capabilities using headless Chromium. 5 | * Handles JavaScript challenges and Turnstile widgets. 6 | * 7 | * @example 8 | * ```typescript 9 | * import { getBrowserSolver } from '$lib/server/indexers/http/browser'; 10 | * 11 | * // Initialize at startup 12 | * const solver = getBrowserSolver(); 13 | * await solver.initialize(); 14 | * 15 | * // Solve a challenge 16 | * const result = await solver.solve({ 17 | * url: 'https://example.com', 18 | * indexerId: 'my-indexer' 19 | * }); 20 | * 21 | * if (result.success) { 22 | * console.log('Cookies:', result.cookies); 23 | * } 24 | * ``` 25 | */ 26 | 27 | // Main service 28 | export { BrowserSolver, getBrowserSolver, resetBrowserSolver } from './BrowserSolver'; 29 | 30 | // Components 31 | export { BrowserPool } from './BrowserPool'; 32 | export { ChallengeSolver } from './ChallengeSolver'; 33 | export { TurnstileSolver } from './TurnstileSolver'; 34 | export { CookieExtractor } from './CookieExtractor'; 35 | 36 | // Configuration 37 | export { DEFAULT_CONFIG, getConfig, loadConfigFromEnv } from './config'; 38 | 39 | // Types 40 | export type { 41 | BrowserSolverConfig, 42 | BrowserInstance, 43 | BrowserPoolHealth, 44 | CachedSolution, 45 | ChallengeType, 46 | ProxyConfig, 47 | QueuedRequest, 48 | SolveOptions, 49 | SolveResult, 50 | SolverMetrics 51 | } from './types'; 52 | -------------------------------------------------------------------------------- /src/lib/server/services/background-service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Background Service Interface 3 | * 4 | * Defines the contract for services that run in the background 5 | * without blocking the main event loop or HTTP request handling. 6 | */ 7 | 8 | export type ServiceStatus = 'pending' | 'starting' | 'ready' | 'error'; 9 | 10 | /** 11 | * BackgroundService interface 12 | * 13 | * All background services should implement this interface. 14 | * Key principle: start() must return immediately and perform 15 | * work asynchronously via setImmediate() or similar mechanisms. 16 | */ 17 | export interface BackgroundService { 18 | /** 19 | * Unique name for the service 20 | */ 21 | readonly name: string; 22 | 23 | /** 24 | * Current status of the service 25 | */ 26 | readonly status: ServiceStatus; 27 | 28 | /** 29 | * Error if status is 'error' 30 | */ 31 | readonly error?: Error; 32 | 33 | /** 34 | * Start the service (non-blocking) 35 | * 36 | * This method MUST return immediately without blocking. 37 | * Use setImmediate() or queueMicrotask() to defer actual work. 38 | */ 39 | start(): void; 40 | 41 | /** 42 | * Stop the service gracefully 43 | * 44 | * This can be async and should clean up resources. 45 | */ 46 | stop(): Promise; 47 | } 48 | 49 | /** 50 | * Service status info returned by the manager 51 | */ 52 | export interface ServiceStatusInfo { 53 | name: string; 54 | status: ServiceStatus; 55 | error?: string; 56 | } 57 | -------------------------------------------------------------------------------- /deploy/cinephage.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Cinephage - Self-hosted Media Acquisition System 3 | Documentation=https://github.com/your-username/cinephage 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | User=cinephage 9 | Group=cinephage 10 | WorkingDirectory=/opt/cinephage 11 | 12 | # Environment configuration 13 | # Option 1: Use environment file (recommended) 14 | EnvironmentFile=-/opt/cinephage/.env 15 | 16 | # Option 2: Set directly (uncomment and modify as needed) 17 | # Environment=HOST=127.0.0.1 18 | # Environment=PORT=3000 19 | 20 | # Always set production mode 21 | Environment=NODE_ENV=production 22 | 23 | # Start the application 24 | ExecStart=/usr/bin/node build/index.js 25 | 26 | # Reload configuration (sends HUP signal) 27 | ExecReload=/bin/kill -HUP $MAINPID 28 | 29 | # Graceful shutdown 30 | KillMode=mixed 31 | KillSignal=SIGTERM 32 | TimeoutStopSec=30 33 | 34 | # Restart policy 35 | Restart=always 36 | RestartSec=5 37 | 38 | # Security hardening 39 | NoNewPrivileges=true 40 | ProtectSystem=strict 41 | ProtectHome=true 42 | PrivateTmp=true 43 | ProtectKernelTunables=true 44 | ProtectKernelModules=true 45 | ProtectControlGroups=true 46 | 47 | # Allow write access to required directories 48 | ReadWritePaths=/opt/cinephage/data 49 | ReadWritePaths=/opt/cinephage/logs 50 | 51 | # Logging to systemd journal 52 | StandardOutput=journal 53 | StandardError=journal 54 | SyslogIdentifier=cinephage 55 | 56 | [Install] 57 | WantedBy=multi-user.target 58 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | 5 | ## Related Issues 6 | 7 | 8 | 9 | ## Type of Change 10 | 11 | - [ ] Bug fix 12 | - [ ] New feature 13 | - [ ] Breaking change 14 | - [ ] Refactoring (no functional changes) 15 | - [ ] Documentation 16 | - [ ] Dependency update 17 | - [ ] Other: 18 | 19 | ## Changes Made 20 | 21 | 22 | 23 | - 24 | - 25 | - 26 | 27 | ## Areas Affected 28 | 29 | 30 | 31 | - [ ] UI/Frontend 32 | - [ ] API/Backend 33 | - [ ] Indexers 34 | - [ ] Download Clients 35 | - [ ] Library Management 36 | - [ ] Database/Schema 37 | - [ ] Subtitles 38 | - [ ] Documentation 39 | 40 | ## Testing 41 | 42 | - [ ] Tested locally 43 | - [ ] Added/updated tests 44 | - [ ] All tests pass (`npm run test`) 45 | - [ ] Type checking passes (`npm run check`) 46 | 47 | ## Checklist 48 | 49 | - [ ] Code follows project conventions 50 | - [ ] Ran `npm run format` 51 | - [ ] Commit messages follow [conventional commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, etc.) 52 | - [ ] Svelte 5 runes used correctly (`$state`, `$derived`, `$effect`, `$props`) 53 | - [ ] No new warnings in console or build output 54 | - [ ] Documentation updated (if applicable) 55 | 56 | ## Screenshots 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/lib/components/ui/Skeleton.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 33 | 34 | 55 | -------------------------------------------------------------------------------- /src/routes/api/root-folders/validate/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { getRootFolderService } from '$lib/server/downloadClients'; 4 | import { z } from 'zod'; 5 | 6 | const validatePathSchema = z.object({ 7 | path: z.string().min(1, 'Path is required'), 8 | readOnly: z.boolean().optional().default(false) 9 | }); 10 | 11 | /** 12 | * POST /api/root-folders/validate 13 | * Validate a path exists and is accessible. 14 | */ 15 | export const POST: RequestHandler = async ({ request }) => { 16 | let data: unknown; 17 | try { 18 | data = await request.json(); 19 | } catch { 20 | return json({ error: 'Invalid JSON body' }, { status: 400 }); 21 | } 22 | 23 | const result = validatePathSchema.safeParse(data); 24 | 25 | if (!result.success) { 26 | return json( 27 | { 28 | error: 'Validation failed', 29 | details: result.error.flatten() 30 | }, 31 | { status: 400 } 32 | ); 33 | } 34 | 35 | const { path, readOnly } = result.data; 36 | const service = getRootFolderService(); 37 | 38 | try { 39 | const validation = await service.validatePath(path, readOnly); 40 | return json(validation); 41 | } catch (error) { 42 | const message = error instanceof Error ? error.message : 'Unknown error'; 43 | return json( 44 | { 45 | valid: false, 46 | exists: false, 47 | writable: false, 48 | error: message 49 | }, 50 | { status: 500 } 51 | ); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/lib/server/subtitles/providers/yifysubtitles/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * YIFY Subtitles Types 3 | * 4 | * YIFY Subtitles is a movie subtitle database (movies only, no TV). 5 | * Uses HTML scraping since there's no public API. 6 | */ 7 | 8 | /** YIFY language mapping - ISO 639-1 to YIFY language names */ 9 | export const YIFY_LANGUAGES: Record = { 10 | en: 'english', 11 | es: 'spanish', 12 | fr: 'french', 13 | de: 'german', 14 | it: 'italian', 15 | pt: 'portuguese', 16 | 'pt-br': 'brazilian-portuguese', 17 | nl: 'dutch', 18 | pl: 'polish', 19 | ru: 'russian', 20 | ar: 'arabic', 21 | he: 'hebrew', 22 | tr: 'turkish', 23 | el: 'greek', 24 | hu: 'hungarian', 25 | ro: 'romanian', 26 | cs: 'czech', 27 | sv: 'swedish', 28 | da: 'danish', 29 | fi: 'finnish', 30 | no: 'norwegian', 31 | ja: 'japanese', 32 | ko: 'korean', 33 | zh: 'chinese', 34 | vi: 'vietnamese', 35 | th: 'thai', 36 | id: 'indonesian', 37 | ms: 'malay', 38 | fa: 'farsi', 39 | hi: 'hindi', 40 | bn: 'bengali', 41 | uk: 'ukrainian', 42 | bg: 'bulgarian', 43 | hr: 'croatian', 44 | sr: 'serbian', 45 | sk: 'slovak', 46 | sl: 'slovenian', 47 | et: 'estonian', 48 | lv: 'latvian', 49 | lt: 'lithuanian' 50 | }; 51 | 52 | /** Reverse mapping for parsing HTML */ 53 | export const YIFY_LANGUAGE_REVERSE: Record = Object.entries(YIFY_LANGUAGES).reduce( 54 | (acc, [iso, name]) => { 55 | acc[name.toLowerCase()] = iso; 56 | return acc; 57 | }, 58 | {} as Record 59 | ); 60 | -------------------------------------------------------------------------------- /src/lib/server/streaming/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Streaming Module 3 | * 4 | * Provides streaming functionality for Cinephage including: 5 | * - Stream extraction from multiple providers via EncDec API 6 | * - HLS playlist parsing and best quality selection 7 | * - Stream validation for playability verification 8 | * - Caching for stream URLs 9 | * - STRM file generation and management 10 | * - Shared HTTP utilities for providers 11 | */ 12 | 13 | // Core types 14 | export * from './types'; 15 | 16 | // Configuration constants 17 | export * from './constants'; 18 | 19 | // Caching 20 | export * from './cache'; 21 | 22 | // HLS parsing 23 | export * from './hls'; 24 | 25 | // Stream validation 26 | export { getStreamValidator, createStreamValidator, quickValidateStream } from './validation'; 27 | 28 | // Stream providers (replaces extractors) 29 | export { extractStreams, getAvailableProviders, getProviderById, clearCaches } from './providers'; 30 | 31 | // EncDec API client 32 | export { getEncDecClient, EncDecClient } from './enc-dec'; 33 | 34 | // STRM file service 35 | export * from './StrmService'; 36 | 37 | // URL utilities 38 | export * from './url'; 39 | 40 | // Settings helper 41 | export * from './settings'; 42 | 43 | // Shared HTTP utilities (also available via ./utils) 44 | export { 45 | fetchWithTimeout, 46 | fetchPlaylist, 47 | fetchAndRewritePlaylist, 48 | rewritePlaylistUrls, 49 | ensureVodPlaylist, 50 | checkStreamAvailability, 51 | checkHlsAvailability 52 | } from './utils'; 53 | -------------------------------------------------------------------------------- /src/lib/components/tmdb/SeasonList.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | {#each seasons as season (season.season_number)} 13 |
14 |
15 |
16 | 22 |
23 |
24 |
25 |

{season.name}

26 |
27 | {#if season.air_date} 28 | {new Date(season.air_date).getFullYear()} 29 | 30 | {/if} 31 | {season.episode_count} Episodes 32 |
33 | {#if season.overview} 34 |

{season.overview}

35 | {:else} 36 |

No overview available.

37 | {/if} 38 |
39 |
40 | {/each} 41 |
42 | -------------------------------------------------------------------------------- /src/lib/components/ui/form/LoadingButton.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 | 69 | -------------------------------------------------------------------------------- /src/lib/components/profiles/ProfileList.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 |
30 | {#if onAdd} 31 | 35 | {/if} 36 |
37 | 38 |
39 |
40 | {})} 43 | onDelete={onDelete ?? (() => {})} 44 | onSetDefault={onSetDefault ?? (() => {})} 45 | /> 46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /src/routes/api/subtitles/scan/+server.ts: -------------------------------------------------------------------------------- 1 | import { json, type RequestEvent } from '@sveltejs/kit'; 2 | import { getSubtitleScannerService } from '$lib/server/subtitles/services/SubtitleScannerService'; 3 | 4 | type RequestHandler = (event: RequestEvent) => Promise; 5 | 6 | /** 7 | * POST /api/subtitles/scan 8 | * Trigger subtitle discovery scan. 9 | */ 10 | export const POST: RequestHandler = async ({ request }) => { 11 | let data: { movieId?: string; seriesId?: string; scanAll?: boolean }; 12 | try { 13 | data = await request.json(); 14 | } catch { 15 | data = {}; 16 | } 17 | 18 | const scanner = getSubtitleScannerService(); 19 | 20 | try { 21 | if (data.movieId) { 22 | const result = await scanner.scanMovieSubtitles(data.movieId); 23 | return json({ 24 | success: true, 25 | type: 'movie', 26 | ...result 27 | }); 28 | } 29 | 30 | if (data.seriesId) { 31 | const result = await scanner.scanSeriesSubtitles(data.seriesId); 32 | return json({ 33 | success: true, 34 | type: 'series', 35 | ...result 36 | }); 37 | } 38 | 39 | if (data.scanAll) { 40 | const result = await scanner.scanAll(); 41 | return json({ 42 | success: true, 43 | type: 'all', 44 | movies: result.movies, 45 | series: result.series 46 | }); 47 | } 48 | 49 | return json({ error: 'Specify movieId, seriesId, or scanAll: true' }, { status: 400 }); 50 | } catch (error) { 51 | const message = error instanceof Error ? error.message : 'Unknown error'; 52 | return json({ error: message }, { status: 500 }); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/lib/components/queue/QueueStatusBadge.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 | 45 | 46 | {config.label} 47 | 48 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/normalization/audioCodecs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Audio codec normalization for media naming 3 | * 4 | * Maps various audio codec name variants to standardized formats. 5 | */ 6 | 7 | import { createNormalizationMap, type NormalizationMap } from './types'; 8 | 9 | /** 10 | * Pre-process audio codec input by removing non-alphanumeric characters 11 | */ 12 | function preprocessAudioCodec(codec: string): string { 13 | return codec.toLowerCase().replace(/[^a-z0-9]/g, ''); 14 | } 15 | 16 | const AUDIO_CODEC_MAPPINGS: Record = { 17 | // Lossless formats 18 | truehd: 'TrueHD', 19 | 'truehd atmos': 'TrueHD Atmos', 20 | truhdatmos: 'TrueHD Atmos', 21 | truehdatmos: 'TrueHD Atmos', 22 | dtshd: 'DTS-HD', 23 | dtshdma: 'DTS-HD MA', 24 | 'dtshd ma': 'DTS-HD MA', 25 | dtsx: 'DTS-X', 26 | flac: 'FLAC', 27 | pcm: 'PCM', 28 | lpcm: 'LPCM', 29 | 30 | // Lossy formats 31 | dts: 'DTS', 32 | 'dolby digital': 'DD', 33 | dolbydigital: 'DD', 34 | dd: 'DD', 35 | ddp: 'DD+', 36 | 'dd+': 'DD+', 37 | ddplus: 'DD+', 38 | eac3: 'EAC3', 39 | ac3: 'AC3', 40 | aac: 'AAC', 41 | mp3: 'MP3', 42 | opus: 'Opus', 43 | vorbis: 'Vorbis' 44 | }; 45 | 46 | export const audioCodecNormalizer: NormalizationMap = createNormalizationMap(AUDIO_CODEC_MAPPINGS); 47 | 48 | /** 49 | * Normalize an audio codec name to standard format 50 | */ 51 | export function normalizeAudioCodec(codec: string | undefined): string | undefined { 52 | if (!codec) return undefined; 53 | const preprocessed = preprocessAudioCodec(codec); 54 | return audioCodecNormalizer.normalize(preprocessed); 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/tokens/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Token module - Single source of truth for all naming tokens 3 | */ 4 | 5 | import { tokenRegistry } from './registry'; 6 | import { coreTokens } from './definitions/core'; 7 | import { qualityTokens } from './definitions/quality'; 8 | import { videoTokens } from './definitions/video'; 9 | import { audioTokens } from './definitions/audio'; 10 | import { releaseTokens } from './definitions/release'; 11 | import { mediaIdTokens } from './definitions/mediaId'; 12 | import { episodeTokens } from './definitions/episode'; 13 | 14 | // Register all tokens 15 | tokenRegistry.registerAll(coreTokens); 16 | tokenRegistry.registerAll(qualityTokens); 17 | tokenRegistry.registerAll(videoTokens); 18 | tokenRegistry.registerAll(audioTokens); 19 | tokenRegistry.registerAll(releaseTokens); 20 | tokenRegistry.registerAll(mediaIdTokens); 21 | tokenRegistry.registerAll(episodeTokens); 22 | 23 | // Export registry and types 24 | export { tokenRegistry, TokenRegistry } from './registry'; 25 | export type { 26 | TokenDefinition, 27 | TokenCategory, 28 | TokenApplicability, 29 | TokenMetadata, 30 | TokenValidationResult 31 | } from './types'; 32 | 33 | // Export individual token sets for testing 34 | export { coreTokens } from './definitions/core'; 35 | export { qualityTokens } from './definitions/quality'; 36 | export { videoTokens } from './definitions/video'; 37 | export { audioTokens } from './definitions/audio'; 38 | export { releaseTokens } from './definitions/release'; 39 | export { mediaIdTokens } from './definitions/mediaId'; 40 | export { episodeTokens } from './definitions/episode'; 41 | -------------------------------------------------------------------------------- /src/routes/api/download-clients/test/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { getDownloadClientManager } from '$lib/server/downloadClients'; 4 | import { downloadClientTestSchema } from '$lib/validation/schemas'; 5 | 6 | /** 7 | * POST /api/download-clients/test 8 | * Test a download client connection before saving. 9 | */ 10 | export const POST: RequestHandler = async ({ request }) => { 11 | let data: unknown; 12 | try { 13 | data = await request.json(); 14 | } catch { 15 | return json({ error: 'Invalid JSON body' }, { status: 400 }); 16 | } 17 | 18 | const result = downloadClientTestSchema.safeParse(data); 19 | 20 | if (!result.success) { 21 | return json( 22 | { 23 | error: 'Validation failed', 24 | details: result.error.flatten() 25 | }, 26 | { status: 400 } 27 | ); 28 | } 29 | 30 | const validated = result.data; 31 | const manager = getDownloadClientManager(); 32 | 33 | try { 34 | const testResult = await manager.testClient({ 35 | host: validated.host, 36 | port: validated.port, 37 | useSsl: validated.useSsl, 38 | username: validated.username, 39 | password: validated.password, 40 | implementation: validated.implementation, 41 | apiKey: validated.implementation === 'sabnzbd' ? validated.password : undefined 42 | }); 43 | 44 | return json(testResult); 45 | } catch (error) { 46 | const message = error instanceof Error ? error.message : 'Unknown error'; 47 | return json( 48 | { 49 | success: false, 50 | error: message 51 | }, 52 | { status: 500 } 53 | ); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/lib/components/ui/modal/ModalFooter.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 | 64 | -------------------------------------------------------------------------------- /src/routes/api/workers/[id]/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Worker API - Individual worker operations 3 | * 4 | * GET /api/workers/[id] - Get worker details and logs 5 | * DELETE /api/workers/[id] - Cancel/remove a worker 6 | */ 7 | 8 | import { json, error } from '@sveltejs/kit'; 9 | import type { RequestHandler } from './$types'; 10 | import { workerManager } from '$lib/server/workers'; 11 | 12 | export const GET: RequestHandler = async ({ params, url }) => { 13 | const { id } = params; 14 | const logsLimit = parseInt(url.searchParams.get('logs') || '100', 10); 15 | 16 | const worker = workerManager.get(id); 17 | 18 | if (!worker) { 19 | throw error(404, { message: 'Worker not found' }); 20 | } 21 | 22 | return json({ 23 | ...worker.getState(), 24 | logs: workerManager.getLogs(id, logsLimit) 25 | }); 26 | }; 27 | 28 | export const DELETE: RequestHandler = async ({ params, url }) => { 29 | const { id } = params; 30 | const force = url.searchParams.get('force') === 'true'; 31 | 32 | const worker = workerManager.get(id); 33 | 34 | if (!worker) { 35 | throw error(404, { message: 'Worker not found' }); 36 | } 37 | 38 | if (worker.isActive && !force) { 39 | // Cancel the worker first 40 | const cancelled = workerManager.cancel(id); 41 | return json({ 42 | success: cancelled, 43 | action: 'cancelled', 44 | message: cancelled ? 'Worker cancelled' : 'Failed to cancel worker' 45 | }); 46 | } 47 | 48 | // Remove the worker 49 | const removed = workerManager.remove(id); 50 | return json({ 51 | success: removed, 52 | action: 'removed', 53 | message: removed ? 'Worker removed' : 'Failed to remove worker' 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /src/routes/tv/[id]/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | {data.tv.name} - Cinephage 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | {#if data.tv.credits.cast.length > 0} 21 | 22 | {#snippet cardSnippet(person)} 23 | 24 | {/snippet} 25 | 26 | {/if} 27 | 28 | 29 | {#if data.tv.seasons.length > 0} 30 |
31 |

Seasons

32 | 33 |
34 | {/if} 35 | 36 | 37 | {#if data.tv.recommendations.results.length > 0} 38 | 43 | {/if} 44 | 45 | 46 | {#if data.tv.similar.results.length > 0} 47 | 52 | {/if} 53 |
54 | -------------------------------------------------------------------------------- /src/lib/server/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/better-sqlite3'; 2 | import Database from 'better-sqlite3'; 3 | import { existsSync, mkdirSync } from 'node:fs'; 4 | import * as schema from './schema'; 5 | import { logger } from '$lib/logging'; 6 | import { syncSchema } from './schema-sync'; 7 | 8 | // Ensure data directory exists before creating database connection 9 | const DATA_DIR = 'data'; 10 | if (!existsSync(DATA_DIR)) { 11 | mkdirSync(DATA_DIR, { recursive: true }); 12 | } 13 | 14 | const sqlite = new Database('data/cinephage.db'); 15 | export const db = drizzle(sqlite, { schema }); 16 | 17 | // Export sqlite for direct access when needed (schema sync uses it) 18 | export { sqlite }; 19 | 20 | let initialized = false; 21 | 22 | /** 23 | * Initialize database using embedded schema synchronization. 24 | * 25 | * This replaces the previous migration-file-based system with an embedded 26 | * schema versioning approach (similar to Radarr/Sonarr). 27 | * 28 | * Handles: 29 | * 1. Fresh install - Creates all tables, sets schema version 30 | * 2. Existing database - Ensures all tables exist, runs incremental updates 31 | * 3. Migration-era database - Backward compatible with old migration system 32 | */ 33 | export async function initializeDatabase(): Promise { 34 | if (initialized) return; 35 | 36 | try { 37 | logger.info('Initializing database...'); 38 | 39 | // Use embedded schema sync (no external migration files needed) 40 | syncSchema(sqlite); 41 | 42 | initialized = true; 43 | logger.info('Database initialization complete'); 44 | } catch (error) { 45 | logger.error('Database initialization failed', error); 46 | throw error; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/routes/api/root-folders/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { getRootFolderService } from '$lib/server/downloadClients'; 4 | import { rootFolderCreateSchema } from '$lib/validation/schemas'; 5 | 6 | /** 7 | * GET /api/root-folders 8 | * List all configured root folders with free space info. 9 | */ 10 | export const GET: RequestHandler = async () => { 11 | const service = getRootFolderService(); 12 | const folders = await service.getFolders(); 13 | return json(folders); 14 | }; 15 | 16 | /** 17 | * POST /api/root-folders 18 | * Create a new root folder. 19 | */ 20 | export const POST: RequestHandler = async ({ request }) => { 21 | let data: unknown; 22 | try { 23 | data = await request.json(); 24 | } catch { 25 | return json({ error: 'Invalid JSON body' }, { status: 400 }); 26 | } 27 | 28 | const result = rootFolderCreateSchema.safeParse(data); 29 | 30 | if (!result.success) { 31 | return json( 32 | { 33 | error: 'Validation failed', 34 | details: result.error.flatten() 35 | }, 36 | { status: 400 } 37 | ); 38 | } 39 | 40 | const validated = result.data; 41 | const service = getRootFolderService(); 42 | 43 | try { 44 | const created = await service.createFolder({ 45 | name: validated.name, 46 | path: validated.path, 47 | mediaType: validated.mediaType, 48 | isDefault: validated.isDefault, 49 | readOnly: validated.readOnly 50 | }); 51 | 52 | return json({ success: true, folder: created }); 53 | } catch (error) { 54 | const message = error instanceof Error ? error.message : 'Unknown error'; 55 | return json({ error: message }, { status: 500 }); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/lib/components/library/StatusIndicator.svelte: -------------------------------------------------------------------------------- 1 | 56 | 57 |
62 | 63 | 64 | {#if status === 'downloaded' && qualityText} 65 | {qualityText} 66 | {:else} 67 | {config.label} 68 | {/if} 69 | 70 |
71 | -------------------------------------------------------------------------------- /src/routes/api/naming/presets/[id]/apply/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { db } from '$lib/server/db'; 4 | import { namingPresets } from '$lib/server/db/schema'; 5 | import { getBuiltInPreset, type NamingPreset } from '$lib/server/library/naming/presets'; 6 | import { namingSettingsService } from '$lib/server/library/naming/NamingSettingsService'; 7 | import { eq } from 'drizzle-orm'; 8 | 9 | /** 10 | * POST /api/naming/presets/[id]/apply 11 | * Apply a preset's config to the current naming settings 12 | */ 13 | export const POST: RequestHandler = async ({ params }) => { 14 | try { 15 | const { id } = params; 16 | let presetConfig: NamingPreset['config']; 17 | 18 | // Check built-in presets first 19 | const builtIn = getBuiltInPreset(id); 20 | if (builtIn) { 21 | presetConfig = builtIn.config; 22 | } else { 23 | // Check custom presets 24 | const [customPreset] = await db.select().from(namingPresets).where(eq(namingPresets.id, id)); 25 | 26 | if (!customPreset) { 27 | return json({ error: 'Preset not found' }, { status: 404 }); 28 | } 29 | 30 | presetConfig = customPreset.config as NamingPreset['config']; 31 | } 32 | 33 | // Apply the preset config to naming settings 34 | await namingSettingsService.updateConfig(presetConfig); 35 | 36 | // Get the updated config to return 37 | const updatedConfig = await namingSettingsService.getConfig(); 38 | 39 | return json({ 40 | success: true, 41 | config: updatedConfig 42 | }); 43 | } catch (err) { 44 | console.error('Error applying naming preset:', err); 45 | return json({ error: 'Failed to apply preset' }, { status: 500 }); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/routes/movie/[id]/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | {data.movie.title} - Cinephage 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | {#if data.movie.credits.cast.length > 0} 20 | 21 | {#snippet cardSnippet(person)} 22 | 23 | {/snippet} 24 | 25 | {/if} 26 | 27 | 28 | {#if data.collection && data.collection.parts} 29 | new Date(a.release_date).getTime() - new Date(b.release_date).getTime() 33 | )} 34 | /> 35 | {/if} 36 | 37 | 38 | {#if data.movie.recommendations.results.length > 0} 39 | 44 | {/if} 45 | 46 | 47 | {#if data.movie.similar.results.length > 0} 48 | 53 | {/if} 54 |
55 | -------------------------------------------------------------------------------- /src/lib/components/indexers/IndexerBulkActions.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | 18 | {selectedCount} selected 19 | 20 | 21 |
22 | 23 | 31 | 32 | 40 | 41 | 49 | 50 | 58 |
59 | -------------------------------------------------------------------------------- /data/indexers/definitions/cinephage-stream.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Cinephage Library Streaming Indexer 3 | # This internal indexer queries the local library database to provide 4 | # streaming URLs for content in your library. 5 | # 6 | # Quality is determined at playback time by the streaming proxy, 7 | # which extracts streams from providers and selects the best available quality. 8 | 9 | id: cinephage-stream 10 | name: Cinephage Library 11 | description: Stream content from your local media library via .strm files 12 | type: private 13 | protocol: streaming 14 | language: en-US 15 | encoding: UTF-8 16 | 17 | links: 18 | - http://localhost 19 | 20 | settings: 21 | - name: baseUrl 22 | type: text 23 | label: Base URL 24 | default: http://localhost:5173 25 | helpText: External URL for streaming access. Used in .strm files to point to your Cinephage instance. Set this to your public URL or reverse proxy address (e.g., https://media.example.com). 26 | 27 | # Protocol-specific configuration for streaming 28 | protocolConfig: 29 | streaming: 30 | type: internal 31 | dataSource: database 32 | 33 | # Search capabilities 34 | caps: 35 | modes: 36 | search: [q] 37 | movie-search: [q, tmdbid, imdbid] 38 | tv-search: [q, tmdbid, tvdbid, imdbid, season, ep] 39 | categories: 40 | '2000': Movies 41 | '5000': TV 42 | categorymappings: 43 | - id: Movies 44 | cat: '2000' 45 | desc: Movies 46 | default: true 47 | - id: TV 48 | cat: '5000' 49 | desc: TV Shows 50 | default: true 51 | 52 | # Search configuration (uses DatabaseQueryExecutor defaults) 53 | # The executor handles movie/TV queries against the local database 54 | search: 55 | response: 56 | type: json 57 | -------------------------------------------------------------------------------- /src/lib/utils/format.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Format a number as USD currency 3 | */ 4 | export function formatCurrency(amount: number, locale = 'en-US'): string { 5 | if (amount <= 0) return ''; 6 | return new Intl.NumberFormat(locale, { 7 | style: 'currency', 8 | currency: 'USD', 9 | maximumFractionDigits: 0, 10 | notation: amount >= 1_000_000_000 ? 'compact' : 'standard' 11 | }).format(amount); 12 | } 13 | 14 | /** 15 | * Format a date string to readable format 16 | */ 17 | export function formatDate(dateString: string, locale = 'en-US'): string { 18 | if (!dateString) return ''; 19 | return new Intl.DateTimeFormat(locale, { 20 | year: 'numeric', 21 | month: 'long', 22 | day: 'numeric' 23 | }).format(new Date(dateString)); 24 | } 25 | 26 | /** 27 | * Format a date string to short format (MMM D, YYYY) 28 | */ 29 | export function formatDateShort(dateString: string, locale = 'en-US'): string { 30 | if (!dateString) return ''; 31 | return new Intl.DateTimeFormat(locale, { 32 | year: 'numeric', 33 | month: 'short', 34 | day: 'numeric' 35 | }).format(new Date(dateString)); 36 | } 37 | 38 | /** 39 | * Get display name for a language code (ISO 639-1) 40 | */ 41 | export function formatLanguage(code: string, locale = 'en-US'): string { 42 | if (!code) return ''; 43 | try { 44 | return new Intl.DisplayNames([locale], { type: 'language' }).of(code) || code; 45 | } catch { 46 | return code; 47 | } 48 | } 49 | 50 | /** 51 | * Get display name for a country code (ISO 3166-1) 52 | */ 53 | export function formatCountry(code: string, locale = 'en-US'): string { 54 | if (!code) return ''; 55 | try { 56 | return new Intl.DisplayNames([locale], { type: 'region' }).of(code) || code; 57 | } catch { 58 | return code; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/server/monitoring/tasks/SmartListRefreshTask.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Smart List Refresh Task 3 | * 4 | * Automated task for refreshing smart lists that are due. 5 | * Runs hourly to check which lists need to be refreshed based on their individual intervals. 6 | */ 7 | 8 | import { getSmartListService } from '$lib/server/smartlists/index.js'; 9 | import { logger } from '$lib/logging'; 10 | import type { TaskResult } from '../MonitoringScheduler.js'; 11 | import type { TaskExecutionContext } from '$lib/server/tasks/TaskExecutionContext.js'; 12 | 13 | /** 14 | * Execute smart list refresh task 15 | * @param ctx - Execution context for cancellation support and activity tracking 16 | */ 17 | export async function executeSmartListRefreshTask( 18 | ctx: TaskExecutionContext | null 19 | ): Promise { 20 | const taskHistoryId = ctx?.historyId; 21 | logger.info('[SmartListRefreshTask] Starting smart list refresh task', { taskHistoryId }); 22 | 23 | // Check for cancellation before starting 24 | ctx?.checkCancelled(); 25 | 26 | const service = getSmartListService(); 27 | 28 | try { 29 | const results = await service.refreshAllDueLists(); 30 | 31 | const itemsProcessed = results.length; 32 | const itemsGrabbed = results.reduce((sum, r) => sum + r.itemsAutoAdded, 0); 33 | const errors = results.filter((r) => r.status === 'failed').length; 34 | 35 | logger.info('[SmartListRefreshTask] Completed', { 36 | listsRefreshed: itemsProcessed, 37 | itemsAutoAdded: itemsGrabbed, 38 | errors 39 | }); 40 | 41 | return { 42 | taskType: 'smartListRefresh', 43 | itemsProcessed, 44 | itemsGrabbed, 45 | errors, 46 | executedAt: new Date() 47 | }; 48 | } catch (error) { 49 | logger.error('[SmartListRefreshTask] Failed', error); 50 | throw error; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | cinephage: 3 | build: . 4 | image: cinephage:latest 5 | container_name: ${CINEPHAGE_CONTAINER_NAME:-cinephage} 6 | restart: ${CINEPHAGE_RESTART_POLICY:-unless-stopped} 7 | init: true 8 | stop_grace_period: 30s 9 | ports: 10 | - "${CINEPHAGE_PORT:-3000}:3000" 11 | volumes: 12 | # Persist the SQLite database and settings 13 | - ${CINEPHAGE_DATA_PATH:-./data}:/app/data 14 | # Persist logs 15 | - ${CINEPHAGE_LOGS_PATH:-./logs}:/app/logs 16 | # Mount your media library (adjust the host path on the left) 17 | - ${CINEPHAGE_MEDIA_PATH:-/path/to/your/media}:/media 18 | environment: 19 | # User/Group ID - set these to match your host user (run 'id' to find yours) 20 | - PUID=${CINEPHAGE_PUID:-1000} 21 | - PGID=${CINEPHAGE_PGID:-1000} 22 | # REQUIRED: Set this to your server's access URL for CSRF protection 23 | # Examples: http://localhost:3000, https://cinephage.yourdomain.com 24 | - ORIGIN=${CINEPHAGE_ORIGIN:-http://localhost:3000} 25 | - LOG_TO_FILE=${CINEPHAGE_LOG_TO_FILE:-true} 26 | - TZ=${CINEPHAGE_TZ:-UTC} 27 | # Browser solver (Cloudflare bypass) - requires Chromium, not included in image 28 | - BROWSER_SOLVER_ENABLED=${CINEPHAGE_BROWSER_SOLVER_ENABLED:-false} 29 | # Worker limits (optional): 30 | - WORKER_MAX_STREAMS=${CINEPHAGE_WORKER_MAX_STREAMS:-10} 31 | - WORKER_MAX_IMPORTS=${CINEPHAGE_WORKER_MAX_IMPORTS:-5} 32 | - WORKER_MAX_SCANS=${CINEPHAGE_WORKER_MAX_SCANS:-2} 33 | # Uncomment to set resource limits: 34 | # deploy: 35 | # resources: 36 | # limits: 37 | # cpus: '2' 38 | # memory: 2G 39 | # reservations: 40 | # cpus: '0.5' 41 | # memory: 512M 42 | -------------------------------------------------------------------------------- /src/lib/components/tmdb/CrewList.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | {#if hasContent} 35 |
36 | {#if creators.length > 0} 37 |
38 | Created by 39 | 40 | {creators.map((c) => c.name).join(', ')} 41 | 42 |
43 | {/if} 44 | 45 | {#each Object.entries(groupedCrew) as [job, members] (job)} 46 |
47 | {job} 48 | 49 | {members.map((m) => m.name).join(', ')} 50 | 51 |
52 | {/each} 53 |
54 | {/if} 55 | -------------------------------------------------------------------------------- /src/lib/components/tmdb/PersonCard.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 18 |
19 | {#if person.profile_path} 20 | 26 | {:else} 27 |
28 | 41 |
42 | {/if} 43 |
44 |
45 |

{person.name}

46 |

47 | {getRole(person)} 48 |

49 |
50 |
51 | -------------------------------------------------------------------------------- /src/routes/api/root-folders/[id]/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { getRootFolderService } from '$lib/server/downloadClients'; 4 | import { rootFolderUpdateSchema } from '$lib/validation/schemas'; 5 | import { assertFound, parseBody } from '$lib/server/api/validate'; 6 | import { NotFoundError } from '$lib/errors'; 7 | 8 | /** 9 | * GET /api/root-folders/[id] 10 | * Get a single root folder with current free space. 11 | */ 12 | export const GET: RequestHandler = async ({ params }) => { 13 | const service = getRootFolderService(); 14 | const folder = await service.getFolder(params.id); 15 | 16 | return json(assertFound(folder, 'Root folder', params.id)); 17 | }; 18 | 19 | /** 20 | * PUT /api/root-folders/[id] 21 | * Update a root folder. 22 | */ 23 | export const PUT: RequestHandler = async ({ params, request }) => { 24 | const data = await parseBody(request, rootFolderUpdateSchema); 25 | const service = getRootFolderService(); 26 | 27 | try { 28 | const updated = await service.updateFolder(params.id, data); 29 | return json({ success: true, folder: updated }); 30 | } catch (error) { 31 | if (error instanceof Error && error.message.includes('not found')) { 32 | throw new NotFoundError('Root folder', params.id); 33 | } 34 | throw error; 35 | } 36 | }; 37 | 38 | /** 39 | * DELETE /api/root-folders/[id] 40 | * Delete a root folder. 41 | */ 42 | export const DELETE: RequestHandler = async ({ params }) => { 43 | const service = getRootFolderService(); 44 | 45 | try { 46 | await service.deleteFolder(params.id); 47 | return json({ success: true }); 48 | } catch (error) { 49 | if (error instanceof Error && error.message.includes('not found')) { 50 | throw new NotFoundError('Root folder', params.id); 51 | } 52 | throw error; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Standards 4 | 5 | We are committed to providing a welcoming and inclusive environment for everyone who wants to contribute to or participate in this project. 6 | 7 | ### Expected Behavior 8 | 9 | - Be respectful and considerate in all interactions 10 | - Provide constructive feedback and accept feedback gracefully 11 | - Focus on what is best for the community and project 12 | - Show empathy and kindness toward other community members 13 | - Respect differing viewpoints and experiences 14 | 15 | ### Unacceptable Behavior 16 | 17 | - Personal attacks, insults, or derogatory comments 18 | - Trolling or deliberately inflammatory remarks 19 | - Publishing others' private information without permission 20 | - Any conduct that would be considered inappropriate in a professional setting 21 | 22 | ## Scope 23 | 24 | This Code of Conduct applies to all project spaces, including: 25 | 26 | - GitHub issues and pull requests 27 | - Project documentation 28 | - Community discussions 29 | - Any other forums created by the project team 30 | 31 | ## Enforcement 32 | 33 | Project maintainers are responsible for clarifying and enforcing standards of acceptable behavior. They may take appropriate action in response to any behavior they deem inappropriate, including: 34 | 35 | - Removing, editing, or rejecting comments, commits, or other contributions 36 | - Temporarily or permanently restricting participation 37 | 38 | ## Reporting 39 | 40 | If you experience or witness unacceptable behavior, please report it by contacting the project maintainers through GitHub. All reports will be reviewed and investigated promptly and fairly. 41 | 42 | ## Attribution 43 | 44 | This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 45 | -------------------------------------------------------------------------------- /src/lib/components/ui/AsyncState.svelte: -------------------------------------------------------------------------------- 1 | 52 | 53 | {#if status !== 'idle'} 54 |
55 | {#if showIcon && config.icon} 56 | 60 | {/if} 61 | {config.text} 62 |
63 | {/if} 64 | -------------------------------------------------------------------------------- /data/indexers/definitions/eztv.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: eztv 3 | name: EZTV 4 | description: TV show torrents via JSON API 5 | type: public 6 | language: en-US 7 | encoding: UTF-8 8 | requestdelay: 2 9 | 10 | links: 11 | - https://eztv.yt 12 | legacylinks: 13 | - https://eztvx.to 14 | - https://eztv.re 15 | - https://eztv.wf 16 | - https://eztv.tf 17 | - https://eztv.ch 18 | 19 | caps: 20 | modes: 21 | search: [q] 22 | tv-search: [q, imdbid] 23 | categories: 24 | '5000': TV 25 | categorymappings: 26 | - id: TV 27 | cat: '5000' 28 | desc: TV Shows 29 | default: true 30 | 31 | search: 32 | paths: 33 | # IMDB lookup via API - if imdb_id is empty, returns recent torrents 34 | - path: /api/get-torrents 35 | method: get 36 | inputs: 37 | imdb_id: '{{ .Query.IMDBIDShort }}' 38 | limit: '100' 39 | page: '1' 40 | - path: /api/get-torrents 41 | method: get 42 | inputs: 43 | imdb_id: '{{ .Query.IMDBIDShort }}' 44 | limit: '100' 45 | page: '2' 46 | 47 | response: 48 | type: json 49 | 50 | rows: 51 | selector: torrents 52 | 53 | fields: 54 | title: 55 | selector: title 56 | details: 57 | selector: id 58 | filters: 59 | - name: prepend 60 | args: 'https://eztv.yt/ep/' 61 | download: 62 | selector: torrent_url 63 | magnet: 64 | selector: magnet_url 65 | infohash: 66 | selector: hash 67 | date: 68 | selector: date_released_unix 69 | filters: 70 | - name: dateparse 71 | args: unix 72 | size: 73 | selector: size_bytes 74 | seeders: 75 | selector: seeds 76 | leechers: 77 | selector: peers 78 | imdbid: 79 | selector: imdb_id 80 | filters: 81 | - name: prepend 82 | args: 'tt' 83 | category: 84 | text: TV 85 | -------------------------------------------------------------------------------- /src/routes/api/naming/validate/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { tokenRegistry } from '$lib/server/library/naming/tokens'; 4 | import { TemplateEngine } from '$lib/server/library/naming/template'; 5 | 6 | const templateEngine = new TemplateEngine(tokenRegistry); 7 | 8 | /** 9 | * POST /api/naming/validate 10 | * Validates naming format strings and returns errors/warnings 11 | */ 12 | export const POST: RequestHandler = async ({ request }) => { 13 | try { 14 | const body = await request.json(); 15 | const { formats } = body as { formats: Record }; 16 | 17 | if (!formats || typeof formats !== 'object') { 18 | return json({ error: 'formats object is required' }, { status: 400 }); 19 | } 20 | 21 | const results: Record< 22 | string, 23 | { 24 | valid: boolean; 25 | errors: Array<{ position: number; message: string; token?: string }>; 26 | warnings: Array<{ position: number; message: string; suggestion?: string }>; 27 | tokens: string[]; 28 | } 29 | > = {}; 30 | 31 | for (const [key, format] of Object.entries(formats)) { 32 | if (typeof format !== 'string') continue; 33 | 34 | const parseResult = templateEngine.parse(format); 35 | results[key] = { 36 | valid: parseResult.valid, 37 | errors: parseResult.errors.map((e) => ({ 38 | position: e.position, 39 | message: e.message, 40 | token: e.token 41 | })), 42 | warnings: parseResult.warnings.map((w) => ({ 43 | position: w.position, 44 | message: w.message, 45 | suggestion: w.suggestion 46 | })), 47 | tokens: templateEngine.getUsedTokens(format) 48 | }; 49 | } 50 | 51 | return json({ results }); 52 | } catch (err) { 53 | console.error('Error validating naming formats:', err); 54 | return json({ error: 'Failed to validate formats' }, { status: 500 }); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/lib/components/discover/SearchBar.svelte: -------------------------------------------------------------------------------- 1 | 42 | 43 |
44 |
45 | {#if isLoading} 46 | 47 | {:else} 48 | 49 | {/if} 50 |
51 | 52 | 61 | 62 | {#if value} 63 | 70 | {/if} 71 |
72 | -------------------------------------------------------------------------------- /src/routes/api/tasks/[taskId]/history/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Task History API 3 | * 4 | * GET /api/tasks/[taskId]/history - Returns execution history for a specific task 5 | */ 6 | 7 | import { json, error } from '@sveltejs/kit'; 8 | import type { RequestHandler } from './$types'; 9 | import { getUnifiedTaskById } from '$lib/server/tasks/UnifiedTaskRegistry'; 10 | import { taskHistoryService } from '$lib/server/tasks/TaskHistoryService'; 11 | import { z } from 'zod'; 12 | 13 | const querySchema = z.object({ 14 | limit: z.coerce.number().int().positive().max(100).default(10), 15 | offset: z.coerce.number().int().nonnegative().default(0) 16 | }); 17 | 18 | /** 19 | * GET /api/tasks/[taskId]/history 20 | * 21 | * Returns paginated execution history for a specific task. 22 | * 23 | * Query params: 24 | * - limit: Number of entries to return (default: 10, max: 100) 25 | * - offset: Number of entries to skip (default: 0) 26 | */ 27 | export const GET: RequestHandler = async ({ params, url }) => { 28 | const { taskId } = params; 29 | 30 | // Verify task exists 31 | const task = getUnifiedTaskById(taskId); 32 | if (!task) { 33 | throw error(404, { message: `Task '${taskId}' not found` }); 34 | } 35 | 36 | // Parse query params 37 | const parseResult = querySchema.safeParse({ 38 | limit: url.searchParams.get('limit') ?? undefined, 39 | offset: url.searchParams.get('offset') ?? undefined 40 | }); 41 | 42 | if (!parseResult.success) { 43 | throw error(400, { message: 'Invalid query parameters' }); 44 | } 45 | 46 | const { limit, offset } = parseResult.data; 47 | 48 | // Get history for this task 49 | const { entries, total } = await taskHistoryService.getHistoryForTask(taskId, limit, offset); 50 | 51 | return json({ 52 | success: true, 53 | taskId, 54 | history: entries, 55 | pagination: { 56 | limit, 57 | offset, 58 | total, 59 | hasMore: offset + entries.length < total 60 | } 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /src/lib/server/indexers/status/BackoffCalculator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Exponential backoff calculator with jitter. 3 | * Used to calculate retry delays after failures. 4 | */ 5 | 6 | export class BackoffCalculator { 7 | constructor( 8 | private readonly baseMs: number, 9 | private readonly maxMs: number, 10 | private readonly multiplier: number 11 | ) {} 12 | 13 | /** 14 | * Calculate backoff time with jitter. 15 | * @param failureCount Number of consecutive failures (1-based) 16 | * @returns Backoff time in milliseconds 17 | */ 18 | calculate(failureCount: number): number { 19 | if (failureCount <= 0) return 0; 20 | 21 | // Exponential backoff: base * multiplier^(failures-1) 22 | const exponential = this.baseMs * Math.pow(this.multiplier, failureCount - 1); 23 | 24 | // Cap at maximum 25 | const capped = Math.min(exponential, this.maxMs); 26 | 27 | // Add jitter (0-25% of calculated time) to prevent thundering herd 28 | const jitter = Math.random() * 0.25 * capped; 29 | 30 | return Math.round(capped + jitter); 31 | } 32 | 33 | /** 34 | * Calculate when retry should be attempted. 35 | * @param failureCount Number of consecutive failures 36 | * @returns Date when retry can be attempted 37 | */ 38 | calculateRetryTime(failureCount: number): Date { 39 | const backoffMs = this.calculate(failureCount); 40 | return new Date(Date.now() + backoffMs); 41 | } 42 | 43 | /** 44 | * Check if enough time has passed since disable. 45 | * @param disabledUntil When the indexer can be retried 46 | * @returns Whether retry is allowed 47 | */ 48 | canRetry(disabledUntil: Date | undefined): boolean { 49 | if (!disabledUntil) return true; 50 | return new Date() >= disabledUntil; 51 | } 52 | } 53 | 54 | /** Default backoff calculator instance */ 55 | export const defaultBackoffCalculator = new BackoffCalculator( 56 | 60_000, // 1 minute base 57 | 3_600_000, // 1 hour max 58 | 2 // double each time 59 | ); 60 | -------------------------------------------------------------------------------- /src/routes/api/tasks/[taskId]/cancel/+server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Task Cancel Endpoint 3 | * 4 | * POST /api/tasks/[taskId]/cancel 5 | * 6 | * Cancels a running task. The task will stop at the next safe checkpoint 7 | * and preserve any work completed before cancellation. 8 | */ 9 | 10 | import { json } from '@sveltejs/kit'; 11 | import type { RequestHandler } from './$types'; 12 | import { getUnifiedTaskById } from '$lib/server/tasks/UnifiedTaskRegistry'; 13 | import { taskHistoryService } from '$lib/server/tasks/TaskHistoryService'; 14 | import { createChildLogger } from '$lib/logging'; 15 | 16 | const logger = createChildLogger({ module: 'TaskCancelAPI' }); 17 | 18 | export const POST: RequestHandler = async ({ params }) => { 19 | const { taskId } = params; 20 | 21 | // Validate task exists in registry 22 | const taskDef = getUnifiedTaskById(taskId); 23 | if (!taskDef) { 24 | return json({ success: false, error: `Task '${taskId}' not found` }, { status: 404 }); 25 | } 26 | 27 | // Check if task is running 28 | if (!taskHistoryService.isTaskRunning(taskId)) { 29 | return json({ success: false, error: `Task '${taskId}' is not running` }, { status: 400 }); 30 | } 31 | 32 | logger.info('[TaskCancelAPI] Cancelling task', { taskId }); 33 | 34 | try { 35 | const cancelled = await taskHistoryService.cancelTask(taskId); 36 | 37 | if (cancelled) { 38 | logger.info('[TaskCancelAPI] Task cancelled successfully', { taskId }); 39 | return json({ success: true, message: `Task '${taskId}' cancelled` }); 40 | } else { 41 | return json( 42 | { success: false, error: `Failed to cancel task '${taskId}'` }, 43 | { status: 500 } 44 | ); 45 | } 46 | } catch (error) { 47 | const message = error instanceof Error ? error.message : 'Failed to cancel task'; 48 | logger.error('[TaskCancelAPI] Error cancelling task', { taskId, error: message }); 49 | return json({ success: false, error: message }, { status: 500 }); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/routes/api/subtitles/language-profiles/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { LanguageProfileService } from '$lib/server/subtitles/services/LanguageProfileService'; 4 | import { languageProfileCreateSchema } from '$lib/validation/schemas'; 5 | import type { LanguagePreference } from '$lib/server/db/schema'; 6 | 7 | /** 8 | * GET /api/subtitles/language-profiles 9 | * List all language profiles. 10 | */ 11 | export const GET: RequestHandler = async () => { 12 | const service = LanguageProfileService.getInstance(); 13 | const profiles = await service.getProfiles(); 14 | 15 | return json(profiles); 16 | }; 17 | 18 | /** 19 | * POST /api/subtitles/language-profiles 20 | * Create a new language profile. 21 | */ 22 | export const POST: RequestHandler = async ({ request }) => { 23 | let data: unknown; 24 | try { 25 | data = await request.json(); 26 | } catch { 27 | return json({ error: 'Invalid JSON body' }, { status: 400 }); 28 | } 29 | 30 | const result = languageProfileCreateSchema.safeParse(data); 31 | 32 | if (!result.success) { 33 | return json( 34 | { 35 | error: 'Validation failed', 36 | details: result.error.flatten() 37 | }, 38 | { status: 400 } 39 | ); 40 | } 41 | 42 | const validated = result.data; 43 | const service = LanguageProfileService.getInstance(); 44 | 45 | try { 46 | const created = await service.createProfile({ 47 | name: validated.name, 48 | languages: validated.languages as LanguagePreference[], 49 | upgradesAllowed: validated.upgradesAllowed, 50 | isDefault: validated.isDefault, 51 | cutoffIndex: validated.cutoffIndex, 52 | minimumScore: validated.minimumScore 53 | }); 54 | 55 | return json({ success: true, profile: created }); 56 | } catch (error) { 57 | const message = error instanceof Error ? error.message : 'Unknown error'; 58 | return json({ error: message }, { status: 500 }); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /data/indexers/definitions/yts.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: yts 3 | name: YTS 4 | description: YIFY Movies - High quality movie torrents 5 | type: public 6 | language: en-US 7 | encoding: UTF-8 8 | requestdelay: 2 9 | 10 | links: 11 | - https://yts.lt 12 | legacylinks: 13 | - https://yts.mx 14 | - https://yts.gg 15 | - https://yts.am 16 | - https://yts.ag 17 | 18 | caps: 19 | modes: 20 | search: [q] 21 | movie-search: [q, imdbid] 22 | categories: 23 | '2000': Movies 24 | '2040': Movies/HD 25 | '2045': Movies/UHD 26 | categorymappings: 27 | - id: Movies 28 | cat: '2000' 29 | default: true 30 | - id: '720p' 31 | cat: '2040' 32 | - id: '1080p' 33 | cat: '2040' 34 | - id: '2160p' 35 | cat: '2045' 36 | 37 | search: 38 | paths: 39 | - path: /api/v2/list_movies.json 40 | method: get 41 | inputs: 42 | query_term: '{{ or .Query.IMDBID .Keywords }}' 43 | limit: '50' 44 | sort_by: seeds 45 | 46 | response: 47 | type: json 48 | 49 | rows: 50 | selector: data.movies 51 | # YTS returns an array of movies, each with nested torrents array 52 | # We need to flatten this - iterate over each torrent per movie 53 | multiple: torrents 54 | 55 | fields: 56 | title: 57 | # Build title from movie fields + torrent quality + codec 58 | text: '{{ .Result.title }} ({{ .Result.year }}) {{ .Result.quality }} {{ .Result.type }} {{ .Result.video_codec }}' 59 | details: 60 | selector: url 61 | download: 62 | selector: url 63 | infohash: 64 | selector: hash 65 | date: 66 | selector: date_uploaded_unix 67 | filters: 68 | - name: dateparse 69 | args: unix 70 | size: 71 | selector: size_bytes 72 | seeders: 73 | selector: seeds 74 | leechers: 75 | selector: peers 76 | imdbid: 77 | selector: imdb_code 78 | optional: true 79 | category: 80 | text: Movies 81 | -------------------------------------------------------------------------------- /src/lib/server/indexers/newznab/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type definitions for Newznab API capabilities and responses. 3 | */ 4 | 5 | /** 6 | * Newznab search mode with supported parameters. 7 | */ 8 | export interface NewznabSearchMode { 9 | /** Whether this search mode is available */ 10 | available: boolean; 11 | /** Supported parameters (e.g., 'q', 'imdbid', 'tvdbid', 'season', 'ep') */ 12 | supportedParams: string[]; 13 | } 14 | 15 | /** 16 | * Newznab category from capabilities response. 17 | */ 18 | export interface NewznabCategory { 19 | /** Category ID (e.g., '2000', '5040') */ 20 | id: string; 21 | /** Category name (e.g., 'Movies', 'TV/HD') */ 22 | name: string; 23 | /** Subcategories */ 24 | subCategories?: NewznabCategory[]; 25 | } 26 | 27 | /** 28 | * Newznab indexer capabilities. 29 | */ 30 | export interface NewznabCapabilities { 31 | /** Server information */ 32 | server: { 33 | version?: string; 34 | title?: string; 35 | email?: string; 36 | url?: string; 37 | }; 38 | /** Result limits */ 39 | limits: { 40 | /** Default results per request */ 41 | default: number; 42 | /** Maximum results per request */ 43 | max: number; 44 | }; 45 | /** Search modes and their supported parameters */ 46 | searching: { 47 | /** Basic search (?t=search) */ 48 | search: NewznabSearchMode; 49 | /** TV search (?t=tvsearch) */ 50 | tvSearch: NewznabSearchMode; 51 | /** Movie search (?t=movie) */ 52 | movieSearch: NewznabSearchMode; 53 | /** Audio search (?t=audio) */ 54 | audioSearch: NewznabSearchMode; 55 | /** Book search (?t=book) */ 56 | bookSearch: NewznabSearchMode; 57 | }; 58 | /** Available categories */ 59 | categories: NewznabCategory[]; 60 | /** Raw XML for reference */ 61 | rawXml?: string; 62 | } 63 | 64 | /** 65 | * Cached capabilities entry. 66 | */ 67 | export interface CachedCapabilities { 68 | /** The capabilities */ 69 | capabilities: NewznabCapabilities; 70 | /** Cache expiry timestamp */ 71 | expiresAt: number; 72 | } 73 | -------------------------------------------------------------------------------- /src/routes/api/library/seasons/[id]/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types.js'; 3 | import { db } from '$lib/server/db/index.js'; 4 | import { seasons, episodes } from '$lib/server/db/schema.js'; 5 | import { eq } from 'drizzle-orm'; 6 | import { logger } from '$lib/logging'; 7 | 8 | /** 9 | * PATCH /api/library/seasons/[id] 10 | * Update season settings (primarily monitoring) 11 | */ 12 | export const PATCH: RequestHandler = async ({ params, request }) => { 13 | try { 14 | const body = await request.json(); 15 | const { monitored } = body; 16 | 17 | // Validate season exists 18 | const [season] = await db.select().from(seasons).where(eq(seasons.id, params.id)).limit(1); 19 | 20 | if (!season) { 21 | return json({ success: false, error: 'Season not found' }, { status: 404 }); 22 | } 23 | 24 | const updateData: Record = {}; 25 | 26 | if (typeof monitored === 'boolean') { 27 | updateData.monitored = monitored; 28 | } 29 | 30 | if (Object.keys(updateData).length === 0) { 31 | return json({ success: false, error: 'No valid fields to update' }, { status: 400 }); 32 | } 33 | 34 | // Update season 35 | await db.update(seasons).set(updateData).where(eq(seasons.id, params.id)); 36 | 37 | // If toggling monitoring, optionally update all episodes in this season 38 | if (typeof monitored === 'boolean' && body.updateEpisodes === true) { 39 | await db.update(episodes).set({ monitored }).where(eq(episodes.seasonId, params.id)); 40 | } 41 | 42 | return json({ success: true }); 43 | } catch (error) { 44 | logger.error('[API] Error updating season', error instanceof Error ? error : undefined); 45 | return json( 46 | { 47 | success: false, 48 | error: error instanceof Error ? error.message : 'Failed to update season' 49 | }, 50 | { status: 500 } 51 | ); 52 | } 53 | }; 54 | 55 | // Alias PUT to PATCH for convenience 56 | export const PUT: RequestHandler = PATCH; 57 | -------------------------------------------------------------------------------- /src/lib/server/library/naming/tokens/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Token types for the naming system 3 | */ 4 | 5 | import type { MediaNamingInfo, NamingConfig } from '../NamingService'; 6 | 7 | /** 8 | * Categories for organizing tokens in the UI 9 | */ 10 | export type TokenCategory = 11 | | 'core' 12 | | 'quality' 13 | | 'video' 14 | | 'audio' 15 | | 'release' 16 | | 'mediaId' 17 | | 'episode'; 18 | 19 | /** 20 | * Media types a token can apply to 21 | */ 22 | export type TokenApplicability = 'movie' | 'series' | 'episode'; 23 | 24 | /** 25 | * Definition for a single naming token 26 | */ 27 | export interface TokenDefinition { 28 | /** 29 | * Token name (case-insensitive) e.g., "CleanTitle" 30 | */ 31 | name: string; 32 | 33 | /** 34 | * Alternative names that map to the same token 35 | */ 36 | aliases?: string[]; 37 | 38 | /** 39 | * Category for UI organization 40 | */ 41 | category: TokenCategory; 42 | 43 | /** 44 | * Human-readable description 45 | */ 46 | description: string; 47 | 48 | /** 49 | * Example usage in a format string 50 | */ 51 | example?: string; 52 | 53 | /** 54 | * What media types this token applies to 55 | */ 56 | applicability: TokenApplicability[]; 57 | 58 | /** 59 | * Whether the token supports format specifiers like :00 60 | */ 61 | supportsFormatSpec?: boolean; 62 | 63 | /** 64 | * Render function that produces the token value 65 | */ 66 | render: (info: MediaNamingInfo, config: NamingConfig, formatSpec?: string) => string; 67 | } 68 | 69 | /** 70 | * Token metadata for UI display (without render function) 71 | */ 72 | export interface TokenMetadata { 73 | name: string; 74 | aliases?: string[]; 75 | category: TokenCategory; 76 | description: string; 77 | example?: string; 78 | applicability: TokenApplicability[]; 79 | supportsFormatSpec?: boolean; 80 | } 81 | 82 | /** 83 | * Result of token validation 84 | */ 85 | export interface TokenValidationResult { 86 | valid: boolean; 87 | suggestion?: string; 88 | } 89 | -------------------------------------------------------------------------------- /src/lib/components/ui/Toasts.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 |
24 | {#each toasts.toasts as toast (toast.id)} 25 | {@const Icon = icons[toast.type]} 26 | 59 | {/each} 60 |
61 | -------------------------------------------------------------------------------- /src/lib/server/downloadClients/nzbget/types.ts: -------------------------------------------------------------------------------- 1 | export interface NzbgetGroup { 2 | Name: string; 3 | DestDir: string; 4 | } 5 | 6 | export interface NzbgetStatus { 7 | Version: string; 8 | DownloadRate: number; 9 | } 10 | 11 | export interface NzbgetList { 12 | /** NZB ID */ 13 | FirstID: number; 14 | /** NZB ID */ 15 | LastID: number; 16 | /** NZB Name */ 17 | NZBName: string; 18 | /** NZB Filename */ 19 | NZBFilename: string; 20 | /** Category */ 21 | Category: string; 22 | /** Size in Bytes (Lo part) */ 23 | FileSizeLo: number; 24 | /** Size in Bytes (Hi part) */ 25 | FileSizeHi: number; 26 | /** Remaning Size in Bytes (Lo part) */ 27 | RemainingSizeLo: number; 28 | /** Remaning Size in Bytes (Hi part) */ 29 | RemainingSizeHi: number; 30 | /** Download Status (QUEUED, PAUSED, DOWNLOADING, SUCCESS, FAILURE, DELETED) */ 31 | Status: string; 32 | /** Destination Directory */ 33 | DestDir: string; 34 | /** Download Priority (Start, Pforce, Force, High, Normal, Low, VeryLow) */ 35 | Priority: string; 36 | } 37 | 38 | export interface NzbgetHistory { 39 | /** NZB ID */ 40 | ID: number; 41 | /** NZB Name */ 42 | Name: string; 43 | /** NZB Filename */ 44 | NZBFilename: string; 45 | /** Category */ 46 | Category: string; 47 | /** Size in Bytes (Lo part) */ 48 | FileSizeLo: number; 49 | /** Size in Bytes (Hi part) */ 50 | FileSizeHi: number; 51 | /** Download Status (SUCCESS, FAILURE, DELETED) */ 52 | Status: string; 53 | /** Destination Directory */ 54 | DestDir: string; 55 | /** Download Priority (Start, Pforce, Force, High, Normal, Low, VeryLow) */ 56 | Priority: string; 57 | /** Completed Time (Low part) */ 58 | UnixTimeLo: number; 59 | /** Completed Time (Hi part) */ 60 | UnixTimeHi: number; 61 | } 62 | 63 | export interface NzbgetConfigResponse { 64 | result: NzbgetConfigItem[]; 65 | } 66 | 67 | export interface NzbgetConfigItem { 68 | Name: string; 69 | Value: string; 70 | } 71 | 72 | export interface JsonRpcResponse { 73 | version: string; 74 | result: T; 75 | error: { 76 | name: string; 77 | code: number; 78 | message: string; 79 | } | null; 80 | } 81 | -------------------------------------------------------------------------------- /src/routes/settings/integrations/+layout.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 |
46 | 47 |
48 |
49 | 66 |
67 |
68 | 69 | 70 |
71 | {@render children()} 72 |
73 |
74 | -------------------------------------------------------------------------------- /src/routes/api/subtitles/providers/test/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { subtitleProviderTestSchema } from '$lib/validation/schemas'; 4 | import { SubtitleProviderFactory } from '$lib/server/subtitles/providers/SubtitleProviderFactory'; 5 | 6 | /** 7 | * POST /api/subtitles/providers/test 8 | * Test a subtitle provider configuration. 9 | */ 10 | export const POST: RequestHandler = async ({ request }) => { 11 | let data: unknown; 12 | try { 13 | data = await request.json(); 14 | } catch { 15 | return json({ error: 'Invalid JSON body' }, { status: 400 }); 16 | } 17 | 18 | const result = subtitleProviderTestSchema.safeParse(data); 19 | 20 | if (!result.success) { 21 | return json( 22 | { 23 | error: 'Validation failed', 24 | details: result.error.flatten() 25 | }, 26 | { status: 400 } 27 | ); 28 | } 29 | 30 | const validated = result.data; 31 | const factory = new SubtitleProviderFactory(); 32 | 33 | // Create a temporary provider config for testing 34 | const testConfig = { 35 | id: 'test', 36 | name: 'Test Provider', 37 | implementation: validated.implementation, 38 | enabled: true, 39 | priority: 1, 40 | apiKey: validated.apiKey ?? undefined, 41 | username: validated.username ?? undefined, 42 | password: validated.password ?? undefined, 43 | settings: (validated.settings as Record) ?? undefined, 44 | requestsPerMinute: 60, 45 | consecutiveFailures: 0 46 | }; 47 | 48 | try { 49 | const provider = factory.createProvider(testConfig); 50 | const testResult = await provider.test(); 51 | 52 | return json({ 53 | success: testResult.success, 54 | message: testResult.message, 55 | responseTime: testResult.responseTime 56 | }); 57 | } catch (error) { 58 | const message = error instanceof Error ? error.message : 'Unknown error'; 59 | return json( 60 | { 61 | success: false, 62 | message, 63 | responseTime: 0 64 | }, 65 | { status: 200 } 66 | ); // Return 200 since the test itself completed, just with a failure 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /src/routes/api/indexers/test/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from './$types'; 3 | import { getIndexerManager } from '$lib/server/indexers/IndexerManager'; 4 | import { indexerTestSchema } from '$lib/validation/schemas'; 5 | 6 | export const POST: RequestHandler = async ({ request }) => { 7 | let data: unknown; 8 | try { 9 | data = await request.json(); 10 | } catch { 11 | return json({ error: 'Invalid JSON body' }, { status: 400 }); 12 | } 13 | 14 | const result = indexerTestSchema.safeParse(data); 15 | 16 | if (!result.success) { 17 | return json( 18 | { 19 | success: false, 20 | error: 'Validation failed', 21 | details: result.error.flatten() 22 | }, 23 | { status: 400 } 24 | ); 25 | } 26 | 27 | const validated = result.data; 28 | 29 | const manager = await getIndexerManager(); 30 | 31 | // Verify the definition exists 32 | const definition = manager.getDefinition(validated.definitionId); 33 | if (!definition) { 34 | return json( 35 | { 36 | success: false, 37 | error: `Unknown indexer definition: ${validated.definitionId}` 38 | }, 39 | { status: 400 } 40 | ); 41 | } 42 | 43 | try { 44 | // Get protocol from YAML definition 45 | const protocol = definition.protocol; 46 | 47 | await manager.testIndexer({ 48 | name: validated.name, 49 | definitionId: validated.definitionId, 50 | baseUrl: validated.baseUrl, 51 | alternateUrls: validated.alternateUrls, 52 | enabled: true, 53 | priority: 25, 54 | protocol, 55 | settings: (validated.settings ?? {}) as Record, 56 | 57 | // Default values for test (not needed for connectivity test) 58 | enableAutomaticSearch: true, 59 | enableInteractiveSearch: true, 60 | minimumSeeders: 1, 61 | seedRatio: null, 62 | seedTime: null, 63 | packSeedTime: null, 64 | preferMagnetUrl: false 65 | }); 66 | 67 | return json({ success: true }); 68 | } catch (e) { 69 | const message = e instanceof Error ? e.message : 'Unknown error'; 70 | return json({ success: false, error: message }, { status: 400 }); 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /src/lib/components/tmdb/WatchProviders.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 | {#if allProviders.length > 0} 42 |
43 | {#each allProviders.slice(0, 8) as provider (provider.provider_id)} 44 |
48 | {#if provider.logo_path} 49 | 55 | {:else} 56 |
59 | {provider.provider_name.slice(0, 2)} 60 |
61 | {/if} 62 |
63 | {/each} 64 | {#if allProviders.length > 8} 65 | +{allProviders.length - 8} 66 | {/if} 67 |
68 | {:else} 69 | Not available 70 | {/if} 71 | --------------------------------------------------------------------------------