From 4f565ebd6faa28484c8a5f84befc972aa492fb90 Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Mon, 19 Jan 2026 04:12:37 +1030 Subject: [PATCH] fix: use name+tmdbid primary key instead of auto incmremented id --- .../queries/customFormats/allConditions.ts | 3 +- .../pcd/queries/entityTests/createRelease.ts | 6 ++-- .../pcd/queries/entityTests/createReleases.ts | 14 +++++--- .../server/pcd/queries/entityTests/delete.ts | 24 +++++++++----- .../server/pcd/queries/entityTests/list.ts | 22 ++++++------- src/lib/server/pcd/schema.ts | 3 +- src/lib/server/utils/arr/releaseImport.ts | 33 ------------------- .../[databaseId]/+page.server.ts | 22 +++++++++---- .../entity-testing/[databaseId]/+page.svelte | 16 +++++---- .../components/EntityTable.svelte | 10 +++--- .../components/ImportReleasesModal.svelte | 3 +- .../components/ReleaseModal.svelte | 6 ++-- .../components/ReleaseTable.svelte | 10 +++--- 13 files changed, 87 insertions(+), 85 deletions(-) diff --git a/src/lib/server/pcd/queries/customFormats/allConditions.ts b/src/lib/server/pcd/queries/customFormats/allConditions.ts index 5d9fe3c..9663c65 100644 --- a/src/lib/server/pcd/queries/customFormats/allConditions.ts +++ b/src/lib/server/pcd/queries/customFormats/allConditions.ts @@ -33,7 +33,7 @@ export async function getAllConditionsForEvaluation( // Get all conditions for all formats const conditions = await db .selectFrom('custom_format_conditions') - .select(['id', 'custom_format_id', 'name', 'type', 'negate', 'required']) + .select(['id', 'custom_format_id', 'name', 'type', 'arr_type', 'negate', 'required']) .execute(); if (conditions.length === 0) { @@ -198,6 +198,7 @@ export async function getAllConditionsForEvaluation( id: c.id, name: c.name, type: c.type, + arrType: c.arr_type as 'all' | 'radarr' | 'sonarr', negate: c.negate === 1, required: c.required === 1, patterns: patternsMap.get(c.id), diff --git a/src/lib/server/pcd/queries/entityTests/createRelease.ts b/src/lib/server/pcd/queries/entityTests/createRelease.ts index b63d498..a87f611 100644 --- a/src/lib/server/pcd/queries/entityTests/createRelease.ts +++ b/src/lib/server/pcd/queries/entityTests/createRelease.ts @@ -6,7 +6,8 @@ import type { PCDCache } from '../../cache.ts'; import { writeOperation, type OperationLayer } from '../../writer.ts'; export interface CreateTestReleaseInput { - entityId: number; + entityType: 'movie' | 'series'; + entityTmdbId: number; title: string; size_bytes: number | null; languages: string[]; @@ -31,7 +32,8 @@ export async function createRelease(options: CreateTestReleaseOptions) { const insertRelease = db .insertInto('test_releases') .values({ - test_entity_id: input.entityId, + entity_type: input.entityType, + entity_tmdb_id: input.entityTmdbId, title: input.title, size_bytes: input.size_bytes, languages: JSON.stringify(input.languages), diff --git a/src/lib/server/pcd/queries/entityTests/createReleases.ts b/src/lib/server/pcd/queries/entityTests/createReleases.ts index 5352ebf..b55788e 100644 --- a/src/lib/server/pcd/queries/entityTests/createReleases.ts +++ b/src/lib/server/pcd/queries/entityTests/createReleases.ts @@ -6,7 +6,8 @@ import type { PCDCache } from '../../cache.ts'; import { writeOperation, type OperationLayer } from '../../writer.ts'; export interface CreateTestReleasesInput { - entityId: number; + entityType: 'movie' | 'series'; + entityTmdbId: number; title: string; size_bytes: number | null; languages: string[]; @@ -37,14 +38,16 @@ export async function createReleases(options: CreateTestReleasesOptions) { }; } - // Get the entity ID (all inputs should have the same entityId) - const entityId = inputs[0].entityId; + // Get the entity key (all inputs should have the same entity) + const entityType = inputs[0].entityType; + const entityTmdbId = inputs[0].entityTmdbId; // Check for existing releases for this entity const existingReleases = await db .selectFrom('test_releases') .select(['title']) - .where('test_entity_id', '=', entityId) + .where('entity_type', '=', entityType) + .where('entity_tmdb_id', '=', entityTmdbId) .execute(); const existingTitles = new Set(existingReleases.map((r) => r.title)); @@ -69,7 +72,8 @@ export async function createReleases(options: CreateTestReleasesOptions) { const insertRelease = db .insertInto('test_releases') .values({ - test_entity_id: input.entityId, + entity_type: input.entityType, + entity_tmdb_id: input.entityTmdbId, title: input.title, size_bytes: input.size_bytes, languages: JSON.stringify(input.languages), diff --git a/src/lib/server/pcd/queries/entityTests/delete.ts b/src/lib/server/pcd/queries/entityTests/delete.ts index 5a0baa2..e526140 100644 --- a/src/lib/server/pcd/queries/entityTests/delete.ts +++ b/src/lib/server/pcd/queries/entityTests/delete.ts @@ -9,34 +9,42 @@ export interface DeleteTestEntityOptions { databaseId: number; cache: PCDCache; layer: OperationLayer; - entityId: number; + entityType: 'movie' | 'series'; + entityTmdbId: number; + entityTitle: string; // For metadata } /** * Delete a test entity and its releases by writing an operation to the specified layer + * Uses stable composite key (type, tmdb_id) instead of auto-generated id */ export async function remove(options: DeleteTestEntityOptions) { - const { databaseId, cache, layer, entityId } = options; + const { databaseId, cache, layer, entityType, entityTmdbId, entityTitle } = options; const db = cache.kb; - // Delete releases first (foreign key constraint) + // Delete releases first (uses composite FK) const deleteReleases = db .deleteFrom('test_releases') - .where('test_entity_id', '=', entityId) + .where('entity_type', '=', entityType) + .where('entity_tmdb_id', '=', entityTmdbId) .compile(); - // Delete the entity - const deleteEntity = db.deleteFrom('test_entities').where('id', '=', entityId).compile(); + // Delete the entity using stable composite key + const deleteEntity = db + .deleteFrom('test_entities') + .where('type', '=', entityType) + .where('tmdb_id', '=', entityTmdbId) + .compile(); const result = await writeOperation({ databaseId, layer, - description: `delete-test-entity-${entityId}`, + description: `delete-test-entity-${entityTitle.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`, queries: [deleteReleases, deleteEntity], metadata: { operation: 'delete', entity: 'test_entity', - name: `id:${entityId}` + name: entityTitle } }); diff --git a/src/lib/server/pcd/queries/entityTests/list.ts b/src/lib/server/pcd/queries/entityTests/list.ts index 40c1496..ca67ff5 100644 --- a/src/lib/server/pcd/queries/entityTests/list.ts +++ b/src/lib/server/pcd/queries/entityTests/list.ts @@ -28,14 +28,13 @@ export async function list(cache: PCDCache) { if (entities.length === 0) return []; - const entityIds = entities.map((e) => e.id); - // 2. Get all releases for all entities const allReleases = await db .selectFrom('test_releases') .select([ 'id', - 'test_entity_id', + 'entity_type', + 'entity_tmdb_id', 'title', 'size_bytes', 'languages', @@ -44,24 +43,25 @@ export async function list(cache: PCDCache) { 'created_at', 'updated_at' ]) - .where('test_entity_id', 'in', entityIds) - .orderBy('test_entity_id') + .orderBy('entity_type') + .orderBy('entity_tmdb_id') .orderBy('title') .execute(); - // Build releases map - const releasesMap = new Map(); + // Build releases map using composite key + const releasesMap = new Map(); for (const release of allReleases) { - if (!releasesMap.has(release.test_entity_id)) { - releasesMap.set(release.test_entity_id, []); + const key = `${release.entity_type}-${release.entity_tmdb_id}`; + if (!releasesMap.has(key)) { + releasesMap.set(key, []); } - releasesMap.get(release.test_entity_id)!.push(release); + releasesMap.get(key)!.push(release); } // Build the final result return entities.map((entity) => ({ ...entity, - releases: (releasesMap.get(entity.id) || []).map((r) => ({ + releases: (releasesMap.get(`${entity.type}-${entity.tmdb_id}`) || []).map((r) => ({ id: r.id, title: r.title, size_bytes: r.size_bytes !== null ? Number(r.size_bytes) : null, diff --git a/src/lib/server/pcd/schema.ts b/src/lib/server/pcd/schema.ts index e9a3aa7..55e0884 100644 --- a/src/lib/server/pcd/schema.ts +++ b/src/lib/server/pcd/schema.ts @@ -221,7 +221,8 @@ export interface TestEntitiesTable { export interface TestReleasesTable { id: Generated; - test_entity_id: number; + entity_type: 'movie' | 'series'; + entity_tmdb_id: number; title: string; size_bytes: number | null; languages: string; // JSON array diff --git a/src/lib/server/utils/arr/releaseImport.ts b/src/lib/server/utils/arr/releaseImport.ts index 3e68a02..c5f4ada 100644 --- a/src/lib/server/utils/arr/releaseImport.ts +++ b/src/lib/server/utils/arr/releaseImport.ts @@ -237,36 +237,3 @@ export function groupSonarrReleases( return groups; } - -// ============================================================================= -// Test Release Conversion -// ============================================================================= - -/** - * Shape for PCD test release creation - */ -export interface TestReleaseInput { - entityId: number; - title: string; - size_bytes: number | null; - languages: string[]; - indexers: string[]; - flags: string[]; -} - -/** - * Convert grouped releases to test release inputs - */ -export function groupedReleasesToTestInputs( - groupedReleases: GroupedRelease[], - entityId: number -): TestReleaseInput[] { - return groupedReleases.map((r) => ({ - entityId, - title: r.title, - size_bytes: r.size, - languages: r.languages, - indexers: r.indexers, - flags: r.flags - })); -} diff --git a/src/routes/quality-profiles/entity-testing/[databaseId]/+page.server.ts b/src/routes/quality-profiles/entity-testing/[databaseId]/+page.server.ts index 1d17464..0266684 100644 --- a/src/routes/quality-profiles/entity-testing/[databaseId]/+page.server.ts +++ b/src/routes/quality-profiles/entity-testing/[databaseId]/+page.server.ts @@ -163,11 +163,17 @@ export const actions: Actions = { } const formData = await request.formData(); - const entityId = parseInt(formData.get('entityId') as string, 10); + const entityType = formData.get('entityType') as 'movie' | 'series'; + const entityTmdbId = parseInt(formData.get('entityTmdbId') as string, 10); + const entityTitle = formData.get('entityTitle') as string; const layer = (formData.get('layer') as 'user' | 'base') || 'user'; - if (isNaN(entityId)) { - return fail(400, { error: 'Invalid entity ID' }); + if (!entityType || !['movie', 'series'].includes(entityType)) { + return fail(400, { error: 'Invalid entity type' }); + } + + if (isNaN(entityTmdbId)) { + return fail(400, { error: 'Invalid entity TMDB ID' }); } if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { @@ -178,7 +184,9 @@ export const actions: Actions = { databaseId: currentDatabaseId, cache, layer, - entityId + entityType, + entityTmdbId, + entityTitle: entityTitle || 'Unknown' }); if (!result.success) { @@ -214,7 +222,8 @@ export const actions: Actions = { } let release: { - entityId: number; + entityType: 'movie' | 'series'; + entityTmdbId: number; title: string; size_bytes: number | null; languages: string[]; @@ -377,7 +386,8 @@ export const actions: Actions = { } let releases: Array<{ - entityId: number; + entityType: 'movie' | 'series'; + entityTmdbId: number; title: string; size_bytes: number | null; languages: string[]; diff --git a/src/routes/quality-profiles/entity-testing/[databaseId]/+page.svelte b/src/routes/quality-profiles/entity-testing/[databaseId]/+page.svelte index 343a480..8cd37e0 100644 --- a/src/routes/quality-profiles/entity-testing/[databaseId]/+page.svelte +++ b/src/routes/quality-profiles/entity-testing/[databaseId]/+page.svelte @@ -173,7 +173,8 @@ // Release modal state let showReleaseModal = false; let releaseModalMode: 'create' | 'edit' = 'create'; - let releaseEntityId: number = 0; + let releaseEntityType: 'movie' | 'series' = 'movie'; + let releaseEntityTmdbId: number = 0; let currentRelease: TestRelease | null = null; // Release delete modal state @@ -284,15 +285,17 @@ } // Release modal handlers - function handleAddRelease(e: CustomEvent<{ entityId: number }>) { - releaseEntityId = e.detail.entityId; + function handleAddRelease(e: CustomEvent<{ entityType: 'movie' | 'series'; entityTmdbId: number }>) { + releaseEntityType = e.detail.entityType; + releaseEntityTmdbId = e.detail.entityTmdbId; releaseModalMode = 'create'; currentRelease = null; showReleaseModal = true; } - function handleEditRelease(e: CustomEvent<{ entityId: number; release: TestRelease }>) { - releaseEntityId = e.detail.entityId; + function handleEditRelease(e: CustomEvent<{ entityType: 'movie' | 'series'; entityTmdbId: number; release: TestRelease }>) { + releaseEntityType = e.detail.entityType; + releaseEntityTmdbId = e.detail.entityTmdbId; releaseModalMode = 'edit'; currentRelease = e.detail.release; showReleaseModal = true; @@ -490,7 +493,8 @@ diff --git a/src/routes/quality-profiles/entity-testing/[databaseId]/components/EntityTable.svelte b/src/routes/quality-profiles/entity-testing/[databaseId]/components/EntityTable.svelte index 83639ca..06700e6 100644 --- a/src/routes/quality-profiles/entity-testing/[databaseId]/components/EntityTable.svelte +++ b/src/routes/quality-profiles/entity-testing/[databaseId]/components/EntityTable.svelte @@ -24,9 +24,9 @@ const dispatch = createEventDispatcher<{ confirmDelete: { entity: TestEntity; formRef: HTMLFormElement }; - addRelease: { entityId: number }; + addRelease: { entityType: 'movie' | 'series'; entityTmdbId: number }; importReleases: { entity: TestEntity }; - editRelease: { entityId: number; release: TestRelease }; + editRelease: { entityType: 'movie' | 'series'; entityTmdbId: number; release: TestRelease }; confirmDeleteRelease: { release: TestRelease; formRef: HTMLFormElement }; expand: { entity: TestEntity }; }>(); @@ -160,7 +160,9 @@ }; }} > - + + + {:else} selectedReleases.has(r.title)) .map((r) => ({ - entityId: entity?.id ?? 0, + entityType: entity?.type ?? 'movie', + entityTmdbId: entity?.tmdb_id ?? 0, title: r.title, size_bytes: r.size, languages: r.languages, diff --git a/src/routes/quality-profiles/entity-testing/[databaseId]/components/ReleaseModal.svelte b/src/routes/quality-profiles/entity-testing/[databaseId]/components/ReleaseModal.svelte index 20c29c6..96ff5b2 100644 --- a/src/routes/quality-profiles/entity-testing/[databaseId]/components/ReleaseModal.svelte +++ b/src/routes/quality-profiles/entity-testing/[databaseId]/components/ReleaseModal.svelte @@ -9,7 +9,8 @@ export let open = false; export let mode: 'create' | 'edit' = 'create'; - export let entityId: number; + export let entityType: 'movie' | 'series'; + export let entityTmdbId: number; export let release: { id?: number; title: string; @@ -86,7 +87,8 @@ // Build JSON for form submission $: releaseJson = JSON.stringify({ id: release?.id, - entityId, + entityType, + entityTmdbId, title, size_bytes: gbToBytes(sizeGb), languages, diff --git a/src/routes/quality-profiles/entity-testing/[databaseId]/components/ReleaseTable.svelte b/src/routes/quality-profiles/entity-testing/[databaseId]/components/ReleaseTable.svelte index 61f1ca7..b76bedf 100644 --- a/src/routes/quality-profiles/entity-testing/[databaseId]/components/ReleaseTable.svelte +++ b/src/routes/quality-profiles/entity-testing/[databaseId]/components/ReleaseTable.svelte @@ -14,8 +14,8 @@ type ReleaseEvaluation = components['schemas']['ReleaseEvaluation']; - export let entityId: number; export let entityType: 'movie' | 'series'; + export let entityTmdbId: number; export let releases: TestRelease[]; export let evaluations: Record; export let selectedProfileId: number | null; @@ -57,8 +57,8 @@ } const dispatch = createEventDispatcher<{ - add: { entityId: number }; - edit: { entityId: number; release: TestRelease }; + add: { entityType: 'movie' | 'series'; entityTmdbId: number }; + edit: { entityType: 'movie' | 'series'; entityTmdbId: number; release: TestRelease }; confirmDelete: { release: TestRelease; formRef: HTMLFormElement }; }>(); @@ -165,7 +165,7 @@ title="Edit release" variant="accent" size="sm" - on:click={() => dispatch('edit', { entityId, release })} + on:click={() => dispatch('edit', { entityType, entityTmdbId, release })} />