mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
fix: use name+tmdbid primary key instead of auto incmremented id
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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<number, typeof allReleases>();
|
||||
// Build releases map using composite key
|
||||
const releasesMap = new Map<string, typeof allReleases>();
|
||||
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,
|
||||
|
||||
@@ -221,7 +221,8 @@ export interface TestEntitiesTable {
|
||||
|
||||
export interface TestReleasesTable {
|
||||
id: Generated<number>;
|
||||
test_entity_id: number;
|
||||
entity_type: 'movie' | 'series';
|
||||
entity_tmdb_id: number;
|
||||
title: string;
|
||||
size_bytes: number | null;
|
||||
languages: string; // JSON array
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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 @@
|
||||
<ReleaseModal
|
||||
bind:open={showReleaseModal}
|
||||
mode={releaseModalMode}
|
||||
entityId={releaseEntityId}
|
||||
entityType={releaseEntityType}
|
||||
entityTmdbId={releaseEntityTmdbId}
|
||||
release={currentRelease}
|
||||
canWriteToBase={data.canWriteToBase}
|
||||
/>
|
||||
|
||||
@@ -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 @@
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="entityId" value={row.id} />
|
||||
<input type="hidden" name="entityType" value={row.type} />
|
||||
<input type="hidden" name="entityTmdbId" value={row.tmdb_id} />
|
||||
<input type="hidden" name="entityTitle" value={row.title} />
|
||||
<input type="hidden" name="layer" value={deleteLayer} />
|
||||
<TableActionButton
|
||||
icon={Trash2}
|
||||
@@ -187,8 +189,8 @@
|
||||
</div>
|
||||
{:else}
|
||||
<ReleaseTable
|
||||
entityId={row.id}
|
||||
entityType={row.type}
|
||||
entityTmdbId={row.tmdb_id}
|
||||
releases={row.releases}
|
||||
{evaluations}
|
||||
{selectedProfileId}
|
||||
|
||||
@@ -318,7 +318,8 @@
|
||||
releases
|
||||
.filter((r) => 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<number, ReleaseEvaluation>;
|
||||
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 })}
|
||||
/>
|
||||
<form
|
||||
id={releaseFormId}
|
||||
@@ -276,7 +276,7 @@
|
||||
<!-- Clickable add row -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => dispatch('add', { entityId })}
|
||||
on:click={() => dispatch('add', { entityType, entityTmdbId })}
|
||||
class="w-full rounded-lg border-2 border-dashed border-neutral-200 py-3 text-sm text-neutral-400 transition-colors hover:border-accent-300 hover:bg-accent-50/50 hover:text-accent-600 dark:border-neutral-700 dark:hover:border-accent-600 dark:hover:bg-accent-900/10 dark:hover:text-accent-400"
|
||||
>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
|
||||
Reference in New Issue
Block a user