From 8a3f26659389d0a7f11e3ce1bf2c98d20e52acbf Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Tue, 30 Dec 2025 08:23:36 +1030 Subject: [PATCH] feat(custom-formats): implement custom formats management with list, detail views, and search functionality --- src/lib/client/ui/badge/Badge.svelte | 31 +++ .../server/pcd/queries/customFormats/index.ts | 9 + .../server/pcd/queries/customFormats/list.ts | 85 ++++++++ .../server/pcd/queries/customFormats/types.ts | 22 +++ src/routes/custom-formats/+page.server.ts | 18 ++ src/routes/custom-formats/+page.svelte | 17 ++ .../[databaseId]/+page.server.ts | 44 +++++ .../custom-formats/[databaseId]/+page.svelte | 181 ++++++++++++++++++ .../components/SearchFilterAction.svelte | 87 +++++++++ .../[databaseId]/views/CardView.svelte | 96 ++++++++++ .../[databaseId]/views/TableView.svelte | 134 +++++++++++++ 11 files changed, 724 insertions(+) create mode 100644 src/lib/client/ui/badge/Badge.svelte create mode 100644 src/lib/server/pcd/queries/customFormats/index.ts create mode 100644 src/lib/server/pcd/queries/customFormats/list.ts create mode 100644 src/lib/server/pcd/queries/customFormats/types.ts create mode 100644 src/routes/custom-formats/+page.server.ts create mode 100644 src/routes/custom-formats/+page.svelte create mode 100644 src/routes/custom-formats/[databaseId]/+page.server.ts create mode 100644 src/routes/custom-formats/[databaseId]/+page.svelte create mode 100644 src/routes/custom-formats/[databaseId]/components/SearchFilterAction.svelte create mode 100644 src/routes/custom-formats/[databaseId]/views/CardView.svelte create mode 100644 src/routes/custom-formats/[databaseId]/views/TableView.svelte diff --git a/src/lib/client/ui/badge/Badge.svelte b/src/lib/client/ui/badge/Badge.svelte new file mode 100644 index 0000000..e7a4cdc --- /dev/null +++ b/src/lib/client/ui/badge/Badge.svelte @@ -0,0 +1,31 @@ + + + + {#if icon} + + {/if} + + diff --git a/src/lib/server/pcd/queries/customFormats/index.ts b/src/lib/server/pcd/queries/customFormats/index.ts new file mode 100644 index 0000000..6f86c0e --- /dev/null +++ b/src/lib/server/pcd/queries/customFormats/index.ts @@ -0,0 +1,9 @@ +/** + * Custom Format queries and mutations + */ + +// Export all types +export type { CustomFormatTableRow, ConditionRef } from './types.ts'; + +// Export query functions +export { list } from './list.ts'; diff --git a/src/lib/server/pcd/queries/customFormats/list.ts b/src/lib/server/pcd/queries/customFormats/list.ts new file mode 100644 index 0000000..3d95b0f --- /dev/null +++ b/src/lib/server/pcd/queries/customFormats/list.ts @@ -0,0 +1,85 @@ +/** + * Custom format list queries + */ + +import type { PCDCache } from '../../cache.ts'; +import type { Tag } from '../../types.ts'; +import type { CustomFormatTableRow, ConditionRef } from './types.ts'; + +/** + * Get custom formats with full data for table/card views + */ +export async function list(cache: PCDCache): Promise { + const db = cache.kb; + + // 1. Get all custom formats + const formats = await db + .selectFrom('custom_formats') + .select(['id', 'name', 'description']) + .orderBy('name') + .execute(); + + if (formats.length === 0) return []; + + const formatIds = formats.map((f) => f.id); + + // 2. Get all tags for all custom formats + const allTags = await db + .selectFrom('custom_format_tags as cft') + .innerJoin('tags as t', 't.id', 'cft.tag_id') + .select([ + 'cft.custom_format_id', + 't.id as tag_id', + 't.name as tag_name', + 't.created_at as tag_created_at' + ]) + .where('cft.custom_format_id', 'in', formatIds) + .orderBy('cft.custom_format_id') + .orderBy('t.name') + .execute(); + + // 3. Get all conditions for all custom formats + const allConditions = await db + .selectFrom('custom_format_conditions') + .select(['id', 'custom_format_id', 'name', 'required', 'negate']) + .where('custom_format_id', 'in', formatIds) + .orderBy('custom_format_id') + .orderBy('name') + .execute(); + + // Build tags map + const tagsMap = new Map(); + for (const tag of allTags) { + if (!tagsMap.has(tag.custom_format_id)) { + tagsMap.set(tag.custom_format_id, []); + } + tagsMap.get(tag.custom_format_id)!.push({ + id: tag.tag_id, + name: tag.tag_name, + created_at: tag.tag_created_at + }); + } + + // Build conditions map + const conditionsMap = new Map(); + for (const condition of allConditions) { + if (!conditionsMap.has(condition.custom_format_id)) { + conditionsMap.set(condition.custom_format_id, []); + } + conditionsMap.get(condition.custom_format_id)!.push({ + id: condition.id, + name: condition.name, + required: condition.required === 1, + negate: condition.negate === 1 + }); + } + + // Build the final result + return formats.map((format) => ({ + id: format.id, + name: format.name, + description: format.description, + tags: tagsMap.get(format.id) || [], + conditions: conditionsMap.get(format.id) || [] + })); +} diff --git a/src/lib/server/pcd/queries/customFormats/types.ts b/src/lib/server/pcd/queries/customFormats/types.ts new file mode 100644 index 0000000..f260fa3 --- /dev/null +++ b/src/lib/server/pcd/queries/customFormats/types.ts @@ -0,0 +1,22 @@ +/** + * Custom Format query-specific types + */ + +import type { Tag } from '../../types.ts'; + +/** Condition reference for display */ +export interface ConditionRef { + id: number; + name: string; + required: boolean; + negate: boolean; +} + +/** Custom format data for table/card views */ +export interface CustomFormatTableRow { + id: number; + name: string; + description: string | null; + tags: Tag[]; + conditions: ConditionRef[]; +} diff --git a/src/routes/custom-formats/+page.server.ts b/src/routes/custom-formats/+page.server.ts new file mode 100644 index 0000000..0d7e877 --- /dev/null +++ b/src/routes/custom-formats/+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, `/custom-formats/${databases[0].id}`); + } + + // If no databases, return empty array (page will show empty state) + return { + databases + }; +}; diff --git a/src/routes/custom-formats/+page.svelte b/src/routes/custom-formats/+page.svelte new file mode 100644 index 0000000..d31caa7 --- /dev/null +++ b/src/routes/custom-formats/+page.svelte @@ -0,0 +1,17 @@ + + + + Custom Formats - Profilarr + + + diff --git a/src/routes/custom-formats/[databaseId]/+page.server.ts b/src/routes/custom-formats/[databaseId]/+page.server.ts new file mode 100644 index 0000000..03645d5 --- /dev/null +++ b/src/routes/custom-formats/[databaseId]/+page.server.ts @@ -0,0 +1,44 @@ +import { error } from '@sveltejs/kit'; +import type { ServerLoad } from '@sveltejs/kit'; +import { pcdManager } from '$pcd/pcd.ts'; +import * as customFormatQueries from '$pcd/queries/customFormats/index.ts'; + +export const load: ServerLoad = async ({ params }) => { + const { databaseId } = params; + + // Validate params exist + if (!databaseId) { + throw error(400, 'Missing database ID'); + } + + // 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 custom formats for the current database + const customFormats = await customFormatQueries.list(cache); + + return { + databases, + currentDatabase, + customFormats + }; +}; diff --git a/src/routes/custom-formats/[databaseId]/+page.svelte b/src/routes/custom-formats/[databaseId]/+page.svelte new file mode 100644 index 0000000..c186ae4 --- /dev/null +++ b/src/routes/custom-formats/[databaseId]/+page.svelte @@ -0,0 +1,181 @@ + + + + Custom Formats - {data.currentDatabase.name} - Profilarr + + +
+ + + + + + + goto(`/custom-formats/${data.currentDatabase.id}/new`)} + /> + + + (infoModalOpen = true)} /> + + + +
+ {#if data.customFormats.length === 0} +
+

+ No custom formats found for {data.currentDatabase.name} +

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

+ No custom formats match your search +

+
+ {:else if $view === 'table'} + + {:else} + + {/if} +
+
+ + + +
+
+

What Are Custom Formats?

+

+ Custom formats are rules that match specific release characteristics like codec, resolution, + source, or release group. They're used to score releases and guide quality decisions. +

+
+ +
+

How They Work

+

+ Each custom format contains one or more conditions. A release must match all required + conditions (or at least one non-required condition) to be assigned the custom format. + Quality profiles then assign scores to determine which releases are preferred. +

+
+ +
+

Condition Types

+
    +
  • Release Title - Match patterns in the release name
  • +
  • Release Group - Match specific release groups
  • +
  • Edition - Match edition names (Director's Cut, etc.)
  • +
  • Language - Match audio language
  • +
  • Source - Match release source (BluRay, WEB, etc.)
  • +
  • Resolution - Match video resolution
  • +
+
+
+
diff --git a/src/routes/custom-formats/[databaseId]/components/SearchFilterAction.svelte b/src/routes/custom-formats/[databaseId]/components/SearchFilterAction.svelte new file mode 100644 index 0000000..e1c59a7 --- /dev/null +++ b/src/routes/custom-formats/[databaseId]/components/SearchFilterAction.svelte @@ -0,0 +1,87 @@ + + +
+ + + {#if isHovered} +
+
+
+
+ Search in... +
+ {#each options as option} + + {/each} +
+
+ {/if} +
diff --git a/src/routes/custom-formats/[databaseId]/views/CardView.svelte b/src/routes/custom-formats/[databaseId]/views/CardView.svelte new file mode 100644 index 0000000..54de6fb --- /dev/null +++ b/src/routes/custom-formats/[databaseId]/views/CardView.svelte @@ -0,0 +1,96 @@ + + +
+ {#each formats as format} + + {/each} +
+ + diff --git a/src/routes/custom-formats/[databaseId]/views/TableView.svelte b/src/routes/custom-formats/[databaseId]/views/TableView.svelte new file mode 100644 index 0000000..6158f2c --- /dev/null +++ b/src/routes/custom-formats/[databaseId]/views/TableView.svelte @@ -0,0 +1,134 @@ + + + + +