From e7fac4896259d7c7d79d688c37c3a3a13f6f6b2a Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Wed, 5 Nov 2025 07:30:42 +1030 Subject: [PATCH] feat(quality-profiles): add quality profile management with views and queries --- src/lib/server/pcd/queries/qualityProfiles.ts | 245 ++++++++++++++++++ src/routes/api/databases/+server.ts | 21 ++ src/routes/arr/[type]/[id]/+page.svelte | 30 +-- src/routes/quality-profiles/+page.server.ts | 18 ++ src/routes/quality-profiles/+page.svelte | 17 ++ .../[databaseId]/+page.server.ts | 39 +++ .../[databaseId]/+page.svelte | 105 ++++++++ .../[databaseId]/views/CardView.svelte | 118 +++++++++ .../[databaseId]/views/TableView.svelte | 145 +++++++++++ 9 files changed, 720 insertions(+), 18 deletions(-) create mode 100644 src/lib/server/pcd/queries/qualityProfiles.ts create mode 100644 src/routes/api/databases/+server.ts create mode 100644 src/routes/quality-profiles/+page.server.ts create mode 100644 src/routes/quality-profiles/+page.svelte create mode 100644 src/routes/quality-profiles/[databaseId]/+page.server.ts create mode 100644 src/routes/quality-profiles/[databaseId]/+page.svelte create mode 100644 src/routes/quality-profiles/[databaseId]/views/CardView.svelte create mode 100644 src/routes/quality-profiles/[databaseId]/views/TableView.svelte diff --git a/src/lib/server/pcd/queries/qualityProfiles.ts b/src/lib/server/pcd/queries/qualityProfiles.ts new file mode 100644 index 0000000..f1b059c --- /dev/null +++ b/src/lib/server/pcd/queries/qualityProfiles.ts @@ -0,0 +1,245 @@ +/** + * 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 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(); + 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>(); + 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(); + 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(); + 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; + }); +} \ No newline at end of file diff --git a/src/routes/api/databases/+server.ts b/src/routes/api/databases/+server.ts new file mode 100644 index 0000000..f431e1b --- /dev/null +++ b/src/routes/api/databases/+server.ts @@ -0,0 +1,21 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from '@sveltejs/kit'; +import { pcdManager } from '$pcd/pcd.ts'; + +/** + * GET /api/databases + * Returns all linked database instances + */ +export const GET: RequestHandler = () => { + try { + const databases = pcdManager.getAll(); + return json(databases); + } catch (error) { + return json( + { + error: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +}; diff --git a/src/routes/arr/[type]/[id]/+page.svelte b/src/routes/arr/[type]/[id]/+page.svelte index 7f2e9d8..048793b 100644 --- a/src/routes/arr/[type]/[id]/+page.svelte +++ b/src/routes/arr/[type]/[id]/+page.svelte @@ -1,29 +1,23 @@
-
- -
+ +
diff --git a/src/routes/quality-profiles/+page.server.ts b/src/routes/quality-profiles/+page.server.ts new file mode 100644 index 0000000..f729633 --- /dev/null +++ b/src/routes/quality-profiles/+page.server.ts @@ -0,0 +1,18 @@ +import { redirect } from '@sveltejs/kit'; +import type { ServerLoad } from '@sveltejs/kit'; +import { pcdManager } from '$pcd/pcd.ts'; + +export const load: ServerLoad = () => { + // Get all databases + const databases = pcdManager.getAll(); + + // If there are databases, redirect to the first one + if (databases.length > 0) { + throw redirect(303, `/quality-profiles/${databases[0].id}`); + } + + // If no databases, return empty array (page will show empty state) + return { + databases + }; +}; diff --git a/src/routes/quality-profiles/+page.svelte b/src/routes/quality-profiles/+page.svelte new file mode 100644 index 0000000..94d7aa3 --- /dev/null +++ b/src/routes/quality-profiles/+page.svelte @@ -0,0 +1,17 @@ + + + + Quality Profiles - Profilarr + + + diff --git a/src/routes/quality-profiles/[databaseId]/+page.server.ts b/src/routes/quality-profiles/[databaseId]/+page.server.ts new file mode 100644 index 0000000..dbdf11b --- /dev/null +++ b/src/routes/quality-profiles/[databaseId]/+page.server.ts @@ -0,0 +1,39 @@ +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'; + +export const load: ServerLoad = ({ params }) => { + const { databaseId } = params; + + // Get all databases for tabs + const databases = pcdManager.getAll(); + + // Parse and validate the database ID + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + // Get the current database instance + const currentDatabase = databases.find((db) => db.id === currentDatabaseId); + + if (!currentDatabase) { + throw error(404, 'Database not found'); + } + + // Get the cache for the database + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + // Load quality profiles for the current database + const qualityProfiles = qualityProfileQueries.list(cache); + + return { + databases, + currentDatabase, + qualityProfiles + }; +}; diff --git a/src/routes/quality-profiles/[databaseId]/+page.svelte b/src/routes/quality-profiles/[databaseId]/+page.svelte new file mode 100644 index 0000000..edb48ef --- /dev/null +++ b/src/routes/quality-profiles/[databaseId]/+page.svelte @@ -0,0 +1,105 @@ + + + + Quality Profiles - {data.currentDatabase.name} - Profilarr + + +
+ + + + + + + + + (currentView = 'cards')} + /> + (currentView = 'table')} + /> + + + + + +
+ {#if data.qualityProfiles.length === 0} +
+

+ No quality profiles found for {data.currentDatabase.name} +

+
+ {:else if filteredProfiles.length === 0} +
+

+ No quality profiles match your search +

+
+ {:else if currentView === 'table'} + + {:else} + + {/if} +
+
diff --git a/src/routes/quality-profiles/[databaseId]/views/CardView.svelte b/src/routes/quality-profiles/[databaseId]/views/CardView.svelte new file mode 100644 index 0000000..1e6d6d2 --- /dev/null +++ b/src/routes/quality-profiles/[databaseId]/views/CardView.svelte @@ -0,0 +1,118 @@ + + +
+ {#each profiles as profile} + + {/each} +
+ + \ No newline at end of file diff --git a/src/routes/quality-profiles/[databaseId]/views/TableView.svelte b/src/routes/quality-profiles/[databaseId]/views/TableView.svelte new file mode 100644 index 0000000..93fa043 --- /dev/null +++ b/src/routes/quality-profiles/[databaseId]/views/TableView.svelte @@ -0,0 +1,145 @@ + + +