mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feat: Add Kysely integration for quality profile queries and refactor database interactions
- Updated package.json to include Kysely and Deno Vite plugin dependencies. - Introduced new types for sorting in table component. - Refactored PCDCache to utilize Kysely for type-safe database queries. - Created new query files for quality profiles, including general information, languages, and list queries. - Removed outdated qualityProfiles.ts file and replaced it with modular query structure. - Updated routes to use new query functions for loading quality profile data. - Enhanced Vite configuration to include Deno plugin and improved watch settings.
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"$http/": "./src/lib/server/utils/http/",
|
||||
"$utils/": "./src/lib/server/utils/",
|
||||
"$notifications/": "./src/lib/server/notifications/",
|
||||
"@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.2.0",
|
||||
"@std/assert": "jsr:@std/assert@^1.0.0",
|
||||
"marked": "npm:marked@^15.0.6",
|
||||
"simple-icons": "npm:simple-icons@^15.17.0"
|
||||
|
||||
84
deno.lock
generated
84
deno.lock
generated
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"jsr:@soapbox/kysely-deno-sqlite@*": "2.2.0",
|
||||
"jsr:@soapbox/kysely-deno-sqlite@^2.2.0": "2.2.0",
|
||||
"jsr:@std/assert@*": "1.0.15",
|
||||
"jsr:@std/assert@1": "1.0.15",
|
||||
"jsr:@std/internal@^1.0.12": "1.0.12",
|
||||
"npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.12__@types+node@22.19.0__picomatch@4.0.3_@types+node@22.19.0",
|
||||
"npm:@eslint/compat@^1.4.0": "1.4.1_eslint@9.39.1",
|
||||
"npm:@eslint/js@^9.36.0": "9.39.1",
|
||||
"npm:@jsr/db__sqlite@0.12": "0.12.0",
|
||||
@@ -16,6 +19,8 @@
|
||||
"npm:eslint-plugin-svelte@^3.12.4": "3.13.0_eslint@9.39.1_svelte@5.43.3__acorn@8.15.0_postcss@8.5.6",
|
||||
"npm:eslint@^9.36.0": "9.39.1",
|
||||
"npm:globals@^16.4.0": "16.5.0",
|
||||
"npm:kysely@~0.27.2": "0.27.6",
|
||||
"npm:kysely@~0.28.8": "0.28.8",
|
||||
"npm:lucide-svelte@0.546": "0.546.0_svelte@5.43.3__acorn@8.15.0",
|
||||
"npm:marked@^15.0.6": "15.0.12",
|
||||
"npm:prettier-plugin-svelte@^3.4.0": "3.4.0_prettier@3.6.2_svelte@5.43.3__acorn@8.15.0",
|
||||
@@ -31,6 +36,12 @@
|
||||
"npm:vite@^7.1.7": "7.1.12_@types+node@22.19.0_picomatch@4.0.3"
|
||||
},
|
||||
"jsr": {
|
||||
"@soapbox/kysely-deno-sqlite@2.2.0": {
|
||||
"integrity": "668ec94600bc4b4d7bd618dd7ca65d4ef30ee61c46ffcb379b6f45203c08517a",
|
||||
"dependencies": [
|
||||
"npm:kysely@~0.27.2"
|
||||
]
|
||||
},
|
||||
"@std/assert@1.0.15": {
|
||||
"integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b",
|
||||
"dependencies": [
|
||||
@@ -42,6 +53,12 @@
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"@deno/vite-plugin@1.0.5_vite@7.1.12__@types+node@22.19.0__picomatch@4.0.3_@types+node@22.19.0": {
|
||||
"integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==",
|
||||
"dependencies": [
|
||||
"vite"
|
||||
]
|
||||
},
|
||||
"@esbuild/aix-ppc64@0.24.2": {
|
||||
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||
"os": ["aix"],
|
||||
@@ -991,31 +1008,6 @@
|
||||
"devalue@5.4.2": {
|
||||
"integrity": "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw=="
|
||||
},
|
||||
"dom-serializer@2.0.0": {
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dependencies": [
|
||||
"domelementtype",
|
||||
"domhandler",
|
||||
"entities"
|
||||
]
|
||||
},
|
||||
"domelementtype@2.3.0": {
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
|
||||
},
|
||||
"domhandler@5.0.3": {
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dependencies": [
|
||||
"domelementtype"
|
||||
]
|
||||
},
|
||||
"domutils@3.2.2": {
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"dependencies": [
|
||||
"dom-serializer",
|
||||
"domelementtype",
|
||||
"domhandler"
|
||||
]
|
||||
},
|
||||
"enhanced-resolve@5.18.3": {
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"dependencies": [
|
||||
@@ -1023,9 +1015,6 @@
|
||||
"tapable"
|
||||
]
|
||||
},
|
||||
"entities@4.5.0": {
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||
},
|
||||
"esbuild@0.24.2": {
|
||||
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||
"optionalDependencies": [
|
||||
@@ -1304,15 +1293,6 @@
|
||||
"has-flag@4.0.0": {
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"htmlparser2@8.0.2": {
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"dependencies": [
|
||||
"domelementtype",
|
||||
"domhandler",
|
||||
"domutils",
|
||||
"entities"
|
||||
]
|
||||
},
|
||||
"ignore@5.3.2": {
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="
|
||||
},
|
||||
@@ -1341,9 +1321,6 @@
|
||||
"is-number@7.0.0": {
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||
},
|
||||
"is-plain-object@5.0.0": {
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
|
||||
},
|
||||
"is-reference@3.0.3": {
|
||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||
"dependencies": [
|
||||
@@ -1385,6 +1362,12 @@
|
||||
"known-css-properties@0.37.0": {
|
||||
"integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="
|
||||
},
|
||||
"kysely@0.27.6": {
|
||||
"integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="
|
||||
},
|
||||
"kysely@0.28.8": {
|
||||
"integrity": "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="
|
||||
},
|
||||
"levn@0.4.1": {
|
||||
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
||||
"dependencies": [
|
||||
@@ -1493,10 +1476,6 @@
|
||||
"@jridgewell/sourcemap-codec"
|
||||
]
|
||||
},
|
||||
"marked@12.0.2": {
|
||||
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
|
||||
"bin": true
|
||||
},
|
||||
"marked@15.0.12": {
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"bin": true
|
||||
@@ -1572,9 +1551,6 @@
|
||||
"callsites"
|
||||
]
|
||||
},
|
||||
"parse-srcset@1.0.2": {
|
||||
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
|
||||
},
|
||||
"path-exists@4.0.0": {
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
|
||||
},
|
||||
@@ -1711,17 +1687,6 @@
|
||||
"mri"
|
||||
]
|
||||
},
|
||||
"sanitize-html@2.17.0": {
|
||||
"integrity": "sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==",
|
||||
"dependencies": [
|
||||
"deepmerge",
|
||||
"escape-string-regexp",
|
||||
"htmlparser2",
|
||||
"is-plain-object",
|
||||
"parse-srcset",
|
||||
"postcss"
|
||||
]
|
||||
},
|
||||
"semver@7.7.3": {
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"bin": true
|
||||
@@ -1926,12 +1891,14 @@
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@soapbox/kysely-deno-sqlite@^2.2.0",
|
||||
"jsr:@std/assert@1",
|
||||
"npm:marked@^15.0.6",
|
||||
"npm:simple-icons@^15.17.0"
|
||||
],
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@deno/vite-plugin@^1.0.5",
|
||||
"npm:@eslint/compat@^1.4.0",
|
||||
"npm:@eslint/js@^9.36.0",
|
||||
"npm:@jsr/db__sqlite@0.12",
|
||||
@@ -1944,6 +1911,7 @@
|
||||
"npm:eslint-plugin-svelte@^3.12.4",
|
||||
"npm:eslint@^9.36.0",
|
||||
"npm:globals@^16.4.0",
|
||||
"npm:kysely@~0.28.8",
|
||||
"npm:lucide-svelte@0.546",
|
||||
"npm:marked@^15.0.6",
|
||||
"npm:prettier-plugin-svelte@^3.4.0",
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"version": "2.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@deno/vite-plugin": "^1.0.5",
|
||||
"@jsr/db__sqlite": "^0.12.0",
|
||||
"kysely": "^0.28.8",
|
||||
"lucide-svelte": "^0.546.0",
|
||||
"marked": "^15.0.6",
|
||||
"simple-icons": "^15.17.0",
|
||||
|
||||
@@ -3,6 +3,13 @@ import type { ComponentType } from 'svelte';
|
||||
/**
|
||||
* Column definition for table
|
||||
*/
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export interface SortState {
|
||||
key: string;
|
||||
direction: SortDirection;
|
||||
}
|
||||
|
||||
export interface Column<T> {
|
||||
/** Unique key for the column */
|
||||
key: string;
|
||||
@@ -16,6 +23,12 @@ export interface Column<T> {
|
||||
align?: 'left' | 'center' | 'right';
|
||||
/** Whether column is sortable */
|
||||
sortable?: boolean;
|
||||
/** Optional accessor used for sorting (defaults to column key lookup) */
|
||||
sortAccessor?: (row: T) => string | number | boolean | Date | null | undefined;
|
||||
/** Optional comparator when sorter needs full row context */
|
||||
sortComparator?: (a: T, b: T) => number;
|
||||
/** Default sort direction when column is first sorted */
|
||||
defaultSortDirection?: SortDirection;
|
||||
/** Custom cell renderer - receives the full row object */
|
||||
cell?: (row: T) => string | ComponentType | { html: string };
|
||||
}
|
||||
|
||||
@@ -3,15 +3,19 @@
|
||||
*/
|
||||
|
||||
import { Database } from '@jsr/db__sqlite';
|
||||
import { Kysely } from 'kysely';
|
||||
import { DenoSqlite3Dialect } from 'jsr:@soapbox/kysely-deno-sqlite';
|
||||
import { logger } from '$logger/logger.ts';
|
||||
import { loadAllOperations, validateOperations } from './ops.ts';
|
||||
import { disableDatabaseInstance } from '$db/queries/databaseInstances.ts';
|
||||
import type { PCDDatabase } from './schema.ts';
|
||||
|
||||
/**
|
||||
* PCDCache - Manages an in-memory compiled database for a single PCD
|
||||
*/
|
||||
export class PCDCache {
|
||||
private db: Database | null = null;
|
||||
private kysely: Kysely<PCDDatabase> | null = null;
|
||||
private pcdPath: string;
|
||||
private databaseInstanceId: number;
|
||||
private built = false;
|
||||
@@ -37,6 +41,13 @@ export class PCDCache {
|
||||
// Enable foreign keys
|
||||
this.db.exec('PRAGMA foreign_keys = ON');
|
||||
|
||||
// Initialize Kysely query builder
|
||||
this.kysely = new Kysely<PCDDatabase>({
|
||||
dialect: new DenoSqlite3Dialect({
|
||||
database: this.db
|
||||
})
|
||||
});
|
||||
|
||||
// 2. Register helper functions
|
||||
this.registerHelperFunctions();
|
||||
|
||||
@@ -125,6 +136,10 @@ export class PCDCache {
|
||||
* Close the database connection
|
||||
*/
|
||||
close(): void {
|
||||
if (this.kysely) {
|
||||
this.kysely.destroy();
|
||||
this.kysely = null;
|
||||
}
|
||||
if (this.db) {
|
||||
this.db.close();
|
||||
this.db = null;
|
||||
@@ -132,6 +147,17 @@ export class PCDCache {
|
||||
this.built = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Kysely query builder
|
||||
* Use this for type-safe queries
|
||||
*/
|
||||
get kb(): Kysely<PCDDatabase> {
|
||||
if (!this.kysely) {
|
||||
throw new Error('Cache not built');
|
||||
}
|
||||
return this.kysely;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// QUERY API
|
||||
// ============================================================================
|
||||
|
||||
@@ -35,6 +35,15 @@ class PCDManager {
|
||||
* Link a new PCD repository
|
||||
*/
|
||||
async link(options: LinkOptions): Promise<DatabaseInstance> {
|
||||
await logger.debug('Starting database link operation', {
|
||||
source: 'PCDManager',
|
||||
meta: {
|
||||
name: options.name,
|
||||
repositoryUrl: options.repositoryUrl,
|
||||
branch: options.branch
|
||||
}
|
||||
});
|
||||
|
||||
// Generate UUID for storage
|
||||
const uuid = crypto.randomUUID();
|
||||
const localPath = getPCDPath(uuid);
|
||||
|
||||
@@ -1,472 +0,0 @@
|
||||
/**
|
||||
* Quality Profile queries for PCD cache
|
||||
*/
|
||||
|
||||
import type { PCDCache } from '../cache.ts';
|
||||
import type { Tag } from '../types.ts';
|
||||
import { parseMarkdown } from '$utils/markdown/markdown.ts';
|
||||
|
||||
// Type for quality/group items in the hierarchy
|
||||
interface QualityItem {
|
||||
position: number;
|
||||
type: 'quality' | 'group';
|
||||
id: number;
|
||||
name: string;
|
||||
is_upgrade_until: boolean;
|
||||
}
|
||||
|
||||
// Type for language configuration
|
||||
interface ProfileLanguage {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'must' | 'only' | 'not' | 'simple';
|
||||
}
|
||||
|
||||
// Type for custom format counts
|
||||
interface CustomFormatCounts {
|
||||
all: number;
|
||||
radarr: number;
|
||||
sonarr: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality profile general information
|
||||
*/
|
||||
export interface QualityProfileGeneral {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string; // Raw markdown
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Language configuration for a quality profile
|
||||
*/
|
||||
export interface QualityProfileLanguage {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'must' | 'only' | 'not' | 'simple';
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality profile languages information
|
||||
*/
|
||||
export interface QualityProfileLanguages {
|
||||
languages: QualityProfileLanguage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Single quality item
|
||||
*/
|
||||
export interface QualitySingle {
|
||||
id: number;
|
||||
name: string;
|
||||
position: number;
|
||||
enabled: boolean;
|
||||
isUpgradeUntil: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality group with members
|
||||
*/
|
||||
export interface QualityGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
position: number;
|
||||
enabled: boolean;
|
||||
isUpgradeUntil: boolean;
|
||||
members: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality profile qualities information
|
||||
*/
|
||||
export interface QualityProfileQualities {
|
||||
singles: QualitySingle[];
|
||||
groups: QualityGroup[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality profile data for table view with all relationships
|
||||
*/
|
||||
export interface QualityProfileTableRow {
|
||||
// Basic info
|
||||
id: number;
|
||||
name: string;
|
||||
description: string; // Parsed HTML from markdown
|
||||
|
||||
// Tags
|
||||
tags: Tag[];
|
||||
|
||||
// Upgrade settings
|
||||
upgrades_allowed: boolean;
|
||||
minimum_custom_format_score: number;
|
||||
upgrade_until_score?: number; // Only if upgrades_allowed
|
||||
upgrade_score_increment?: number; // Only if upgrades_allowed
|
||||
|
||||
// Custom format counts by arr type
|
||||
custom_formats: CustomFormatCounts;
|
||||
|
||||
// Quality hierarchy (in order)
|
||||
qualities: QualityItem[];
|
||||
|
||||
// Single language configuration
|
||||
language?: ProfileLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quality profiles with full data for table/card views
|
||||
* Optimized to minimize database queries
|
||||
*/
|
||||
export function list(cache: PCDCache): QualityProfileTableRow[] {
|
||||
// 1. Get all quality profiles
|
||||
const profiles = cache.query<{
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
upgrades_allowed: number;
|
||||
minimum_custom_format_score: number;
|
||||
upgrade_until_score: number;
|
||||
upgrade_score_increment: number;
|
||||
}>(`
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
upgrades_allowed,
|
||||
minimum_custom_format_score,
|
||||
upgrade_until_score,
|
||||
upgrade_score_increment
|
||||
FROM quality_profiles
|
||||
ORDER BY name
|
||||
`);
|
||||
|
||||
if (profiles.length === 0) return [];
|
||||
|
||||
const profileIds = profiles.map(p => p.id);
|
||||
const idPlaceholders = profileIds.map(() => '?').join(',');
|
||||
|
||||
// 2. Get all tags for all profiles in one query
|
||||
const allTags = cache.query<{
|
||||
quality_profile_id: number;
|
||||
tag_id: number;
|
||||
tag_name: string;
|
||||
tag_created_at: string;
|
||||
}>(`
|
||||
SELECT
|
||||
qpt.quality_profile_id,
|
||||
t.id as tag_id,
|
||||
t.name as tag_name,
|
||||
t.created_at as tag_created_at
|
||||
FROM quality_profile_tags qpt
|
||||
JOIN tags t ON qpt.tag_id = t.id
|
||||
WHERE qpt.quality_profile_id IN (${idPlaceholders})
|
||||
ORDER BY qpt.quality_profile_id, t.name
|
||||
`, ...profileIds);
|
||||
|
||||
// 3. Get custom format counts grouped by arr_type for all profiles
|
||||
const formatCounts = cache.query<{
|
||||
quality_profile_id: number;
|
||||
arr_type: string;
|
||||
count: number;
|
||||
}>(`
|
||||
SELECT
|
||||
quality_profile_id,
|
||||
arr_type,
|
||||
COUNT(*) as count
|
||||
FROM quality_profile_custom_formats
|
||||
WHERE quality_profile_id IN (${idPlaceholders})
|
||||
GROUP BY quality_profile_id, arr_type
|
||||
`, ...profileIds);
|
||||
|
||||
// 4. Get all qualities for all profiles with names
|
||||
const allQualities = cache.query<{
|
||||
quality_profile_id: number;
|
||||
position: number;
|
||||
upgrade_until: number;
|
||||
quality_id: number | null;
|
||||
quality_group_id: number | null;
|
||||
quality_name: string | null;
|
||||
group_name: string | null;
|
||||
}>(`
|
||||
SELECT
|
||||
qpq.quality_profile_id,
|
||||
qpq.position,
|
||||
qpq.upgrade_until,
|
||||
qpq.quality_id,
|
||||
qpq.quality_group_id,
|
||||
q.name as quality_name,
|
||||
qg.name as group_name
|
||||
FROM quality_profile_qualities qpq
|
||||
LEFT JOIN qualities q ON qpq.quality_id = q.id
|
||||
LEFT JOIN quality_groups qg ON qpq.quality_group_id = qg.id
|
||||
WHERE qpq.quality_profile_id IN (${idPlaceholders})
|
||||
ORDER BY qpq.quality_profile_id, qpq.position
|
||||
`, ...profileIds);
|
||||
|
||||
// 5. Get languages for all profiles (one per profile)
|
||||
const allLanguages = cache.query<{
|
||||
quality_profile_id: number;
|
||||
language_id: number;
|
||||
language_name: string;
|
||||
type: string;
|
||||
}>(`
|
||||
SELECT
|
||||
qpl.quality_profile_id,
|
||||
l.id as language_id,
|
||||
l.name as language_name,
|
||||
qpl.type
|
||||
FROM quality_profile_languages qpl
|
||||
JOIN languages l ON qpl.language_id = l.id
|
||||
WHERE qpl.quality_profile_id IN (${idPlaceholders})
|
||||
`, ...profileIds);
|
||||
|
||||
// Build maps for efficient lookup
|
||||
const tagsMap = new Map<number, Tag[]>();
|
||||
for (const tag of allTags) {
|
||||
if (!tagsMap.has(tag.quality_profile_id)) {
|
||||
tagsMap.set(tag.quality_profile_id, []);
|
||||
}
|
||||
tagsMap.get(tag.quality_profile_id)!.push({
|
||||
id: tag.tag_id,
|
||||
name: tag.tag_name,
|
||||
created_at: tag.tag_created_at
|
||||
});
|
||||
}
|
||||
|
||||
const formatCountsMap = new Map<number, Omit<CustomFormatCounts, 'total'>>();
|
||||
for (const fc of formatCounts) {
|
||||
if (!formatCountsMap.has(fc.quality_profile_id)) {
|
||||
formatCountsMap.set(fc.quality_profile_id, {all: 0, radarr: 0, sonarr: 0});
|
||||
}
|
||||
const counts = formatCountsMap.get(fc.quality_profile_id)!;
|
||||
if (fc.arr_type === 'all') counts.all = fc.count;
|
||||
else if (fc.arr_type === 'radarr') counts.radarr = fc.count;
|
||||
else if (fc.arr_type === 'sonarr') counts.sonarr = fc.count;
|
||||
}
|
||||
|
||||
const qualitiesMap = new Map<number, QualityItem[]>();
|
||||
for (const qual of allQualities) {
|
||||
if (!qualitiesMap.has(qual.quality_profile_id)) {
|
||||
qualitiesMap.set(qual.quality_profile_id, []);
|
||||
}
|
||||
|
||||
qualitiesMap.get(qual.quality_profile_id)!.push({
|
||||
position: qual.position,
|
||||
type: qual.quality_id ? 'quality' : 'group',
|
||||
id: qual.quality_id || qual.quality_group_id!,
|
||||
name: qual.quality_name || qual.group_name!,
|
||||
is_upgrade_until: qual.upgrade_until === 1
|
||||
});
|
||||
}
|
||||
|
||||
const languagesMap = new Map<number, ProfileLanguage>();
|
||||
for (const lang of allLanguages) {
|
||||
languagesMap.set(lang.quality_profile_id, {
|
||||
id: lang.language_id,
|
||||
name: lang.language_name,
|
||||
type: lang.type as 'must' | 'only' | 'not' | 'simple'
|
||||
});
|
||||
}
|
||||
|
||||
// Build the final result
|
||||
return profiles.map(profile => {
|
||||
const counts = formatCountsMap.get(profile.id) || {all: 0, radarr: 0, sonarr: 0};
|
||||
|
||||
const result: QualityProfileTableRow = {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
description: parseMarkdown(profile.description),
|
||||
tags: tagsMap.get(profile.id) || [],
|
||||
upgrades_allowed: profile.upgrades_allowed === 1,
|
||||
minimum_custom_format_score: profile.minimum_custom_format_score,
|
||||
custom_formats: {
|
||||
all: counts.all,
|
||||
radarr: counts.radarr,
|
||||
sonarr: counts.sonarr,
|
||||
total: counts.all + counts.radarr + counts.sonarr
|
||||
},
|
||||
qualities: qualitiesMap.get(profile.id) || [],
|
||||
language: languagesMap.get(profile.id)
|
||||
};
|
||||
|
||||
// Only include upgrade settings if upgrades are allowed
|
||||
if (profile.upgrades_allowed === 1) {
|
||||
result.upgrade_until_score = profile.upgrade_until_score;
|
||||
result.upgrade_score_increment = profile.upgrade_score_increment;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get general information for a single quality profile
|
||||
*/
|
||||
export function general(cache: PCDCache, profileId: number): QualityProfileGeneral | null {
|
||||
// Get the quality profile
|
||||
const profiles = cache.query<{
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
}>(`
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description
|
||||
FROM quality_profiles
|
||||
WHERE id = ?
|
||||
`, profileId);
|
||||
|
||||
if (profiles.length === 0) return null;
|
||||
|
||||
const profile = profiles[0];
|
||||
|
||||
// Get tags for this profile
|
||||
const tags = cache.query<{
|
||||
tag_id: number;
|
||||
tag_name: string;
|
||||
tag_created_at: string;
|
||||
}>(`
|
||||
SELECT
|
||||
t.id as tag_id,
|
||||
t.name as tag_name,
|
||||
t.created_at as tag_created_at
|
||||
FROM quality_profile_tags qpt
|
||||
JOIN tags t ON qpt.tag_id = t.id
|
||||
WHERE qpt.quality_profile_id = ?
|
||||
ORDER BY t.name
|
||||
`, profileId);
|
||||
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
description: profile.description || '',
|
||||
tags: tags.map(tag => ({
|
||||
id: tag.tag_id,
|
||||
name: tag.tag_name,
|
||||
created_at: tag.tag_created_at
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get languages for a quality profile
|
||||
*/
|
||||
export function languages(cache: PCDCache, profileId: number): QualityProfileLanguages {
|
||||
const profileLanguages = cache.query<{
|
||||
language_id: number;
|
||||
language_name: string;
|
||||
type: string;
|
||||
}>(`
|
||||
SELECT
|
||||
l.id as language_id,
|
||||
l.name as language_name,
|
||||
qpl.type
|
||||
FROM quality_profile_languages qpl
|
||||
JOIN languages l ON qpl.language_id = l.id
|
||||
WHERE qpl.quality_profile_id = ?
|
||||
ORDER BY l.name
|
||||
`, profileId);
|
||||
|
||||
return {
|
||||
languages: profileLanguages.map(lang => ({
|
||||
id: lang.language_id,
|
||||
name: lang.language_name,
|
||||
type: lang.type as 'must' | 'only' | 'not' | 'simple'
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get qualities for a quality profile (singles, groups, and available)
|
||||
*/
|
||||
export function qualities(cache: PCDCache, profileId: number): QualityProfileQualities {
|
||||
// 1. Get single qualities
|
||||
const singles = cache.query<{
|
||||
quality_id: number;
|
||||
quality_name: string;
|
||||
position: number;
|
||||
enabled: number;
|
||||
upgrade_until: number;
|
||||
}>(`
|
||||
SELECT
|
||||
qpq.quality_id,
|
||||
q.name as quality_name,
|
||||
qpq.position,
|
||||
qpq.enabled,
|
||||
qpq.upgrade_until
|
||||
FROM quality_profile_qualities qpq
|
||||
JOIN qualities q ON qpq.quality_id = q.id
|
||||
WHERE qpq.quality_profile_id = ? AND qpq.quality_id IS NOT NULL
|
||||
ORDER BY qpq.position
|
||||
`, profileId);
|
||||
|
||||
// 2. Get groups with their IDs
|
||||
const groups = cache.query<{
|
||||
group_id: number;
|
||||
group_name: string;
|
||||
position: number;
|
||||
enabled: number;
|
||||
upgrade_until: number;
|
||||
}>(`
|
||||
SELECT
|
||||
qpq.quality_group_id as group_id,
|
||||
qg.name as group_name,
|
||||
qpq.position,
|
||||
qpq.enabled,
|
||||
qpq.upgrade_until
|
||||
FROM quality_profile_qualities qpq
|
||||
JOIN quality_groups qg ON qpq.quality_group_id = qg.id
|
||||
WHERE qpq.quality_profile_id = ? AND qpq.quality_group_id IS NOT NULL
|
||||
ORDER BY qpq.position
|
||||
`, profileId);
|
||||
|
||||
// 3. Get all group members for all groups in this profile
|
||||
const groupIds = groups.map(g => g.group_id);
|
||||
const groupMembers = groupIds.length > 0 ? cache.query<{
|
||||
group_id: number;
|
||||
quality_id: number;
|
||||
quality_name: string;
|
||||
}>(`
|
||||
SELECT
|
||||
qgm.quality_group_id as group_id,
|
||||
qgm.quality_id,
|
||||
q.name as quality_name
|
||||
FROM quality_group_members qgm
|
||||
JOIN qualities q ON qgm.quality_id = q.id
|
||||
WHERE qgm.quality_group_id IN (${groupIds.map(() => '?').join(',')})
|
||||
ORDER BY q.name
|
||||
`, ...groupIds) : [];
|
||||
|
||||
// Build groups with members
|
||||
const groupsWithMembers: QualityGroup[] = groups.map(g => {
|
||||
const members = groupMembers
|
||||
.filter(gm => gm.group_id === g.group_id)
|
||||
.map(gm => ({ id: gm.quality_id, name: gm.quality_name }));
|
||||
|
||||
return {
|
||||
id: g.group_id,
|
||||
name: g.group_name,
|
||||
position: g.position,
|
||||
enabled: g.enabled === 1,
|
||||
isUpgradeUntil: g.upgrade_until === 1,
|
||||
members
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
singles: singles.map(s => ({
|
||||
id: s.quality_id,
|
||||
name: s.quality_name,
|
||||
position: s.position,
|
||||
enabled: s.enabled === 1,
|
||||
isUpgradeUntil: s.upgrade_until === 1
|
||||
})),
|
||||
groups: groupsWithMembers
|
||||
};
|
||||
}
|
||||
42
src/lib/server/pcd/queries/qualityProfiles/general.ts
Normal file
42
src/lib/server/pcd/queries/qualityProfiles/general.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Quality profile general queries
|
||||
*/
|
||||
|
||||
import type { PCDCache } from '../../cache.ts';
|
||||
import type { QualityProfileGeneral } from './types.ts';
|
||||
|
||||
/**
|
||||
* Get general information for a single quality profile
|
||||
*/
|
||||
export async function general(cache: PCDCache, profileId: number): Promise<QualityProfileGeneral | null> {
|
||||
const db = cache.kb;
|
||||
|
||||
// Get the quality profile
|
||||
const profile = await db
|
||||
.selectFrom('quality_profiles')
|
||||
.select(['id', 'name', 'description'])
|
||||
.where('id', '=', profileId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!profile) return null;
|
||||
|
||||
// Get tags for this profile
|
||||
const tags = await db
|
||||
.selectFrom('quality_profile_tags as qpt')
|
||||
.innerJoin('tags as t', 't.id', 'qpt.tag_id')
|
||||
.select(['t.id as tag_id', 't.name as tag_name', 't.created_at as tag_created_at'])
|
||||
.where('qpt.quality_profile_id', '=', profileId)
|
||||
.orderBy('t.name')
|
||||
.execute();
|
||||
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
description: profile.description || '',
|
||||
tags: tags.map((tag) => ({
|
||||
id: tag.tag_id,
|
||||
name: tag.tag_name,
|
||||
created_at: tag.tag_created_at
|
||||
}))
|
||||
};
|
||||
}
|
||||
24
src/lib/server/pcd/queries/qualityProfiles/index.ts
Normal file
24
src/lib/server/pcd/queries/qualityProfiles/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Quality Profile queries
|
||||
*/
|
||||
|
||||
// Export all types
|
||||
export type {
|
||||
QualityItem,
|
||||
ProfileLanguage,
|
||||
CustomFormatCounts,
|
||||
QualityProfileGeneral,
|
||||
QualityProfileLanguage,
|
||||
QualityProfileLanguages,
|
||||
QualitySingle,
|
||||
QualityGroup,
|
||||
QualityProfileQualities,
|
||||
QualityProfileTableRow
|
||||
} from './types.ts';
|
||||
|
||||
// Export query functions
|
||||
export { list } from './list.ts';
|
||||
export { general } from './general.ts';
|
||||
export { languages } from './languages.ts';
|
||||
export { scoring } from './scoring.ts';
|
||||
// TODO: qualities function needs to be rewritten
|
||||
29
src/lib/server/pcd/queries/qualityProfiles/languages.ts
Normal file
29
src/lib/server/pcd/queries/qualityProfiles/languages.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Quality profile languages queries
|
||||
*/
|
||||
|
||||
import type { PCDCache } from '../../cache.ts';
|
||||
import type { QualityProfileLanguages } from './types.ts';
|
||||
|
||||
/**
|
||||
* Get languages for a quality profile
|
||||
*/
|
||||
export async function languages(cache: PCDCache, profileId: number): Promise<QualityProfileLanguages> {
|
||||
const db = cache.kb;
|
||||
|
||||
const profileLanguages = await db
|
||||
.selectFrom('quality_profile_languages as qpl')
|
||||
.innerJoin('languages as l', 'qpl.language_id', 'l.id')
|
||||
.select(['l.id as language_id', 'l.name as language_name', 'qpl.type'])
|
||||
.where('qpl.quality_profile_id', '=', profileId)
|
||||
.orderBy('l.name')
|
||||
.execute();
|
||||
|
||||
return {
|
||||
languages: profileLanguages.map((lang) => ({
|
||||
id: lang.language_id,
|
||||
name: lang.language_name,
|
||||
type: lang.type as 'must' | 'only' | 'not' | 'simple'
|
||||
}))
|
||||
};
|
||||
}
|
||||
171
src/lib/server/pcd/queries/qualityProfiles/list.ts
Normal file
171
src/lib/server/pcd/queries/qualityProfiles/list.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Quality profile list queries
|
||||
*/
|
||||
|
||||
import type { PCDCache } from '../../cache.ts';
|
||||
import type { Tag } from '../../types.ts';
|
||||
import type { QualityProfileTableRow, QualityItem, ProfileLanguage, CustomFormatCounts } from './types.ts';
|
||||
import { parseMarkdown } from '$utils/markdown/markdown.ts';
|
||||
import { logger } from '$logger/logger.ts';
|
||||
|
||||
/**
|
||||
* Get quality profiles with full data for table/card views
|
||||
* Optimized to minimize database queries
|
||||
*/
|
||||
export async function list(cache: PCDCache): Promise<QualityProfileTableRow[]> {
|
||||
const db = cache.kb;
|
||||
|
||||
// 1. Get all quality profiles
|
||||
const profiles = await db
|
||||
.selectFrom('quality_profiles')
|
||||
.select([
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'upgrades_allowed',
|
||||
'minimum_custom_format_score',
|
||||
'upgrade_until_score',
|
||||
'upgrade_score_increment'
|
||||
])
|
||||
.orderBy('name')
|
||||
.execute();
|
||||
|
||||
if (profiles.length === 0) return [];
|
||||
|
||||
const profileIds = profiles.map(p => p.id);
|
||||
|
||||
// 2. Get all tags for all profiles
|
||||
const allTags = await db
|
||||
.selectFrom('quality_profile_tags as qpt')
|
||||
.innerJoin('tags as t', 't.id', 'qpt.tag_id')
|
||||
.select([
|
||||
'qpt.quality_profile_id',
|
||||
't.id as tag_id',
|
||||
't.name as tag_name',
|
||||
't.created_at as tag_created_at'
|
||||
])
|
||||
.where('qpt.quality_profile_id', 'in', profileIds)
|
||||
.orderBy(['qpt.quality_profile_id', 't.name'])
|
||||
.execute();
|
||||
|
||||
// 3. Get custom format counts grouped by arr_type
|
||||
const formatCounts = await db
|
||||
.selectFrom('quality_profile_custom_formats')
|
||||
.select(['quality_profile_id', 'arr_type'])
|
||||
.select((eb) => eb.fn.count('quality_profile_id').as('count'))
|
||||
.where('quality_profile_id', 'in', profileIds)
|
||||
.groupBy(['quality_profile_id', 'arr_type'])
|
||||
.execute();
|
||||
|
||||
// 4. Get all qualities for all profiles with names
|
||||
const allQualities = await db
|
||||
.selectFrom('quality_profile_qualities as qpq')
|
||||
.leftJoin('qualities as q', 'qpq.quality_id', 'q.id')
|
||||
.leftJoin('quality_groups as qg', 'qpq.quality_group_id', 'qg.id')
|
||||
.select([
|
||||
'qpq.quality_profile_id',
|
||||
'qpq.position',
|
||||
'qpq.upgrade_until',
|
||||
'qpq.quality_id',
|
||||
'qpq.quality_group_id',
|
||||
'q.name as quality_name',
|
||||
'qg.name as group_name'
|
||||
])
|
||||
.where('qpq.quality_profile_id', 'in', profileIds)
|
||||
.orderBy(['qpq.quality_profile_id', 'qpq.position'])
|
||||
.execute();
|
||||
|
||||
// 5. Get languages for all profiles
|
||||
const allLanguages = await db
|
||||
.selectFrom('quality_profile_languages as qpl')
|
||||
.innerJoin('languages as l', 'qpl.language_id', 'l.id')
|
||||
.select([
|
||||
'qpl.quality_profile_id',
|
||||
'l.id as language_id',
|
||||
'l.name as language_name',
|
||||
'qpl.type'
|
||||
])
|
||||
.where('qpl.quality_profile_id', 'in', profileIds)
|
||||
.execute();
|
||||
|
||||
// Build maps for efficient lookup
|
||||
const tagsMap = new Map<number, Tag[]>();
|
||||
for (const tag of allTags) {
|
||||
if (!tagsMap.has(tag.quality_profile_id)) {
|
||||
tagsMap.set(tag.quality_profile_id, []);
|
||||
}
|
||||
tagsMap.get(tag.quality_profile_id)!.push({
|
||||
id: tag.tag_id,
|
||||
name: tag.tag_name,
|
||||
created_at: tag.tag_created_at
|
||||
});
|
||||
}
|
||||
|
||||
const formatCountsMap = new Map<number, Omit<CustomFormatCounts, 'total'>>();
|
||||
for (const fc of formatCounts) {
|
||||
if (!formatCountsMap.has(fc.quality_profile_id)) {
|
||||
formatCountsMap.set(fc.quality_profile_id, { all: 0, radarr: 0, sonarr: 0 });
|
||||
}
|
||||
const counts = formatCountsMap.get(fc.quality_profile_id)!;
|
||||
const count = Number(fc.count);
|
||||
if (fc.arr_type === 'all') counts.all = count;
|
||||
else if (fc.arr_type === 'radarr') counts.radarr = count;
|
||||
else if (fc.arr_type === 'sonarr') counts.sonarr = count;
|
||||
}
|
||||
|
||||
const qualitiesMap = new Map<number, QualityItem[]>();
|
||||
for (const qual of allQualities) {
|
||||
if (!qualitiesMap.has(qual.quality_profile_id)) {
|
||||
qualitiesMap.set(qual.quality_profile_id, []);
|
||||
}
|
||||
|
||||
qualitiesMap.get(qual.quality_profile_id)!.push({
|
||||
position: qual.position,
|
||||
type: qual.quality_id ? 'quality' : 'group',
|
||||
id: qual.quality_id || qual.quality_group_id!,
|
||||
name: qual.quality_name || qual.group_name!,
|
||||
is_upgrade_until: qual.upgrade_until === 1
|
||||
});
|
||||
}
|
||||
|
||||
const languagesMap = new Map<number, ProfileLanguage>();
|
||||
for (const lang of allLanguages) {
|
||||
languagesMap.set(lang.quality_profile_id, {
|
||||
id: lang.language_id,
|
||||
name: lang.language_name,
|
||||
type: lang.type as 'must' | 'only' | 'not' | 'simple'
|
||||
});
|
||||
}
|
||||
|
||||
// Build the final result
|
||||
const results = profiles.map((profile) => {
|
||||
const counts = formatCountsMap.get(profile.id) || { all: 0, radarr: 0, sonarr: 0 };
|
||||
|
||||
const result: QualityProfileTableRow = {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
description: parseMarkdown(profile.description),
|
||||
tags: tagsMap.get(profile.id) || [],
|
||||
upgrades_allowed: profile.upgrades_allowed === 1,
|
||||
minimum_custom_format_score: profile.minimum_custom_format_score,
|
||||
custom_formats: {
|
||||
all: counts.all,
|
||||
radarr: counts.radarr,
|
||||
sonarr: counts.sonarr,
|
||||
total: counts.all + counts.radarr + counts.sonarr
|
||||
},
|
||||
qualities: qualitiesMap.get(profile.id) || [],
|
||||
language: languagesMap.get(profile.id)
|
||||
};
|
||||
|
||||
// Only include upgrade settings if upgrades are allowed
|
||||
if (profile.upgrades_allowed === 1) {
|
||||
result.upgrade_until_score = profile.upgrade_until_score;
|
||||
result.upgrade_score_increment = profile.upgrade_score_increment;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
111
src/lib/server/pcd/queries/qualityProfiles/types.ts
Normal file
111
src/lib/server/pcd/queries/qualityProfiles/types.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Quality Profile query-specific types
|
||||
*/
|
||||
|
||||
import type { Tag } from '../../types.ts';
|
||||
|
||||
// ============================================================================
|
||||
// INTERNAL TYPES (used within queries)
|
||||
// ============================================================================
|
||||
|
||||
/** Quality/group item in the hierarchy */
|
||||
export interface QualityItem {
|
||||
position: number;
|
||||
type: 'quality' | 'group';
|
||||
id: number;
|
||||
name: string;
|
||||
is_upgrade_until: boolean;
|
||||
}
|
||||
|
||||
/** Language configuration */
|
||||
export interface ProfileLanguage {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'must' | 'only' | 'not' | 'simple';
|
||||
}
|
||||
|
||||
/** Custom format counts by arr type */
|
||||
export interface CustomFormatCounts {
|
||||
all: number;
|
||||
radarr: number;
|
||||
sonarr: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// QUERY RESULT TYPES
|
||||
// ============================================================================
|
||||
|
||||
/** Quality profile general information */
|
||||
export interface QualityProfileGeneral {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string; // Raw markdown
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
/** Language configuration for a quality profile */
|
||||
export interface QualityProfileLanguage {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'must' | 'only' | 'not' | 'simple';
|
||||
}
|
||||
|
||||
/** Quality profile languages information */
|
||||
export interface QualityProfileLanguages {
|
||||
languages: QualityProfileLanguage[];
|
||||
}
|
||||
|
||||
/** Single quality item */
|
||||
export interface QualitySingle {
|
||||
id: number;
|
||||
name: string;
|
||||
position: number;
|
||||
enabled: boolean;
|
||||
isUpgradeUntil: boolean;
|
||||
}
|
||||
|
||||
/** Quality group with members */
|
||||
export interface QualityGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
position: number;
|
||||
enabled: boolean;
|
||||
isUpgradeUntil: boolean;
|
||||
members: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
/** Quality profile qualities information */
|
||||
export interface QualityProfileQualities {
|
||||
singles: QualitySingle[];
|
||||
groups: QualityGroup[];
|
||||
}
|
||||
|
||||
/** Quality profile data for table view with all relationships */
|
||||
export interface QualityProfileTableRow {
|
||||
// Basic info
|
||||
id: number;
|
||||
name: string;
|
||||
description: string; // Parsed HTML from markdown
|
||||
|
||||
// Tags
|
||||
tags: Tag[];
|
||||
|
||||
// Upgrade settings
|
||||
upgrades_allowed: boolean;
|
||||
minimum_custom_format_score: number;
|
||||
upgrade_until_score?: number; // Only if upgrades_allowed
|
||||
upgrade_score_increment?: number; // Only if upgrades_allowed
|
||||
|
||||
// Custom format counts by arr type
|
||||
custom_formats: CustomFormatCounts;
|
||||
|
||||
// Quality hierarchy (in order)
|
||||
qualities: QualityItem[];
|
||||
|
||||
// Single language configuration
|
||||
language?: ProfileLanguage;
|
||||
}
|
||||
214
src/lib/server/pcd/schema.ts
Normal file
214
src/lib/server/pcd/schema.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Kysely Database Schema Types for PCD (Profilarr Compliant Database)
|
||||
* Auto-generated columns use Generated<T>
|
||||
*/
|
||||
|
||||
import type { Generated } from 'kysely';
|
||||
|
||||
// ============================================================================
|
||||
// CORE ENTITY TABLES
|
||||
// ============================================================================
|
||||
|
||||
export interface TagsTable {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
created_at: Generated<string>;
|
||||
}
|
||||
|
||||
export interface LanguagesTable {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
created_at: Generated<string>;
|
||||
updated_at: Generated<string>;
|
||||
}
|
||||
|
||||
export interface RegularExpressionsTable {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
pattern: string;
|
||||
regex101_id: string | null;
|
||||
description: string | null;
|
||||
created_at: Generated<string>;
|
||||
updated_at: Generated<string>;
|
||||
}
|
||||
|
||||
export interface QualitiesTable {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
created_at: Generated<string>;
|
||||
updated_at: Generated<string>;
|
||||
}
|
||||
|
||||
export interface CustomFormatsTable {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
description: string | null;
|
||||
created_at: Generated<string>;
|
||||
updated_at: Generated<string>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DEPENDENT ENTITY TABLES
|
||||
// ============================================================================
|
||||
|
||||
export interface QualityProfilesTable {
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
description: string | null;
|
||||
upgrades_allowed: number;
|
||||
minimum_custom_format_score: number;
|
||||
upgrade_until_score: number;
|
||||
upgrade_score_increment: number;
|
||||
created_at: Generated<string>;
|
||||
updated_at: Generated<string>;
|
||||
}
|
||||
|
||||
export interface QualityGroupsTable {
|
||||
id: Generated<number>;
|
||||
quality_profile_id: number;
|
||||
name: string;
|
||||
created_at: Generated<string>;
|
||||
updated_at: Generated<string>;
|
||||
}
|
||||
|
||||
export interface CustomFormatConditionsTable {
|
||||
id: Generated<number>;
|
||||
custom_format_id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
arr_type: string;
|
||||
negate: number;
|
||||
required: number;
|
||||
created_at: Generated<string>;
|
||||
updated_at: Generated<string>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JUNCTION TABLES
|
||||
// ============================================================================
|
||||
|
||||
export interface RegularExpressionTagsTable {
|
||||
regular_expression_id: number;
|
||||
tag_id: number;
|
||||
}
|
||||
|
||||
export interface CustomFormatTagsTable {
|
||||
custom_format_id: number;
|
||||
tag_id: number;
|
||||
}
|
||||
|
||||
export interface QualityProfileTagsTable {
|
||||
quality_profile_id: number;
|
||||
tag_id: number;
|
||||
}
|
||||
|
||||
export interface QualityProfileLanguagesTable {
|
||||
quality_profile_id: number;
|
||||
language_id: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface QualityGroupMembersTable {
|
||||
quality_group_id: number;
|
||||
quality_id: number;
|
||||
}
|
||||
|
||||
export interface QualityProfileQualitiesTable {
|
||||
id: Generated<number>;
|
||||
quality_profile_id: number;
|
||||
quality_id: number | null;
|
||||
quality_group_id: number | null;
|
||||
position: number;
|
||||
enabled: number;
|
||||
upgrade_until: number;
|
||||
}
|
||||
|
||||
export interface QualityProfileCustomFormatsTable {
|
||||
quality_profile_id: number;
|
||||
custom_format_id: number;
|
||||
arr_type: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CUSTOM FORMAT CONDITION TYPE TABLES
|
||||
// ============================================================================
|
||||
|
||||
export interface ConditionPatternsTable {
|
||||
custom_format_condition_id: number;
|
||||
regular_expression_id: number;
|
||||
}
|
||||
|
||||
export interface ConditionLanguagesTable {
|
||||
custom_format_condition_id: number;
|
||||
language_id: number;
|
||||
except_language: number;
|
||||
}
|
||||
|
||||
export interface ConditionIndexerFlagsTable {
|
||||
custom_format_condition_id: number;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
export interface ConditionSourcesTable {
|
||||
custom_format_condition_id: number;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface ConditionResolutionsTable {
|
||||
custom_format_condition_id: number;
|
||||
resolution: string;
|
||||
}
|
||||
|
||||
export interface ConditionQualityModifiersTable {
|
||||
custom_format_condition_id: number;
|
||||
quality_modifier: string;
|
||||
}
|
||||
|
||||
export interface ConditionSizesTable {
|
||||
custom_format_condition_id: number;
|
||||
min_bytes: number | null;
|
||||
max_bytes: number | null;
|
||||
}
|
||||
|
||||
export interface ConditionReleaseTypesTable {
|
||||
custom_format_condition_id: number;
|
||||
release_type: string;
|
||||
}
|
||||
|
||||
export interface ConditionYearsTable {
|
||||
custom_format_condition_id: number;
|
||||
min_year: number | null;
|
||||
max_year: number | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DATABASE INTERFACE
|
||||
// ============================================================================
|
||||
|
||||
export interface PCDDatabase {
|
||||
tags: TagsTable;
|
||||
languages: LanguagesTable;
|
||||
regular_expressions: RegularExpressionsTable;
|
||||
qualities: QualitiesTable;
|
||||
custom_formats: CustomFormatsTable;
|
||||
quality_profiles: QualityProfilesTable;
|
||||
quality_groups: QualityGroupsTable;
|
||||
custom_format_conditions: CustomFormatConditionsTable;
|
||||
regular_expression_tags: RegularExpressionTagsTable;
|
||||
custom_format_tags: CustomFormatTagsTable;
|
||||
quality_profile_tags: QualityProfileTagsTable;
|
||||
quality_profile_languages: QualityProfileLanguagesTable;
|
||||
quality_group_members: QualityGroupMembersTable;
|
||||
quality_profile_qualities: QualityProfileQualitiesTable;
|
||||
quality_profile_custom_formats: QualityProfileCustomFormatsTable;
|
||||
condition_patterns: ConditionPatternsTable;
|
||||
condition_languages: ConditionLanguagesTable;
|
||||
condition_indexer_flags: ConditionIndexerFlagsTable;
|
||||
condition_sources: ConditionSourcesTable;
|
||||
condition_resolutions: ConditionResolutionsTable;
|
||||
condition_quality_modifiers: ConditionQualityModifiersTable;
|
||||
condition_sizes: ConditionSizesTable;
|
||||
condition_release_types: ConditionReleaseTypesTable;
|
||||
condition_years: ConditionYearsTable;
|
||||
}
|
||||
@@ -254,3 +254,22 @@ export interface QualityGroupMemberRow {
|
||||
quality_group_id: number;
|
||||
quality_id: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// QUALITY PROFILE SCORING TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface CustomFormatScoring {
|
||||
id: number; // custom format ID
|
||||
name: string; // custom format name
|
||||
scores: Record<string, number | null>; // arr_type -> score mapping
|
||||
}
|
||||
|
||||
export interface QualityProfileScoring {
|
||||
databaseId: number; // PCD database ID (for linking)
|
||||
arrTypes: string[]; // Display arr types: ['radarr', 'sonarr']. Note: 'all' scores are applied to each type
|
||||
customFormats: CustomFormatScoring[]; // All custom formats with their scores
|
||||
minimum_custom_format_score: number; // Minimum score to accept
|
||||
upgrade_until_score: number; // Stop upgrading when reached
|
||||
upgrade_score_increment: number; // Minimum score improvement needed
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { ServerLoad } from '@sveltejs/kit';
|
||||
import { pcdManager } from '$pcd/pcd.ts';
|
||||
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles.ts';
|
||||
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles/index.ts';
|
||||
|
||||
export const load: ServerLoad = ({ params }) => {
|
||||
export const load: ServerLoad = async ({ params }) => {
|
||||
const { databaseId } = params;
|
||||
|
||||
// Validate params exist
|
||||
@@ -34,7 +34,7 @@ export const load: ServerLoad = ({ params }) => {
|
||||
}
|
||||
|
||||
// Load quality profiles for the current database
|
||||
const qualityProfiles = qualityProfileQueries.list(cache);
|
||||
const qualityProfiles = await qualityProfileQueries.list(cache);
|
||||
|
||||
return {
|
||||
databases,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { ServerLoad } from '@sveltejs/kit';
|
||||
import { pcdManager } from '$pcd/pcd.ts';
|
||||
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles.ts';
|
||||
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles/index.ts';
|
||||
|
||||
export const load: ServerLoad = ({ params }) => {
|
||||
export const load: ServerLoad = async ({ params }) => {
|
||||
const { databaseId, id } = params;
|
||||
|
||||
// Validate params exist
|
||||
@@ -30,7 +30,7 @@ export const load: ServerLoad = ({ params }) => {
|
||||
}
|
||||
|
||||
// Load general information for the quality profile
|
||||
const profile = qualityProfileQueries.general(cache, profileId);
|
||||
const profile = await qualityProfileQueries.general(cache, profileId);
|
||||
|
||||
if (!profile) {
|
||||
throw error(404, 'Quality profile not found');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { ServerLoad } from '@sveltejs/kit';
|
||||
import { pcdManager } from '$pcd/pcd.ts';
|
||||
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles.ts';
|
||||
import * as qualityProfileQueries from '$pcd/queries/qualityProfiles/index.ts';
|
||||
import * as languageQueries from '$pcd/queries/languages.ts';
|
||||
|
||||
export const load: ServerLoad = ({ params }) => {
|
||||
export const load: ServerLoad = async ({ params }) => {
|
||||
const { databaseId, id } = params;
|
||||
|
||||
// Validate params exist
|
||||
@@ -31,7 +31,7 @@ export const load: ServerLoad = ({ params }) => {
|
||||
}
|
||||
|
||||
// Load languages for the quality profile
|
||||
const languagesData = qualityProfileQueries.languages(cache, profileId);
|
||||
const languagesData = await qualityProfileQueries.languages(cache, profileId);
|
||||
|
||||
// Load all available languages
|
||||
const availableLanguages = languageQueries.list(cache);
|
||||
|
||||
@@ -2,13 +2,18 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import deno from '@deno/vite-plugin';
|
||||
|
||||
const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8'));
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
plugins: [deno(), tailwindcss(), sveltekit()],
|
||||
server: {
|
||||
port: 6969
|
||||
port: 6969,
|
||||
watch: {
|
||||
// Ignore temporary files created by editors
|
||||
ignored: ['**/*.tmp.*', '**/*~', '**/.#*']
|
||||
}
|
||||
},
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(packageJson.version)
|
||||
|
||||
Reference in New Issue
Block a user