From 2e36df30e58dbe3d264b44499c7dffdf27b50900 Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Tue, 27 Jan 2026 07:47:31 +1030 Subject: [PATCH] refactor: media management can now contain multiple configs for each setting --- .../ui/navigation/pageNav/groupItem.svelte | 21 +- .../ui/navigation/pageNav/pageNav.svelte | 9 +- src/lib/server/db/migrations.ts | 4 +- .../038_add_media_management_config_names.ts | 60 ++ src/lib/server/db/queries/arrSync.ts | 28 +- src/lib/server/db/schema.sql | 5 +- .../pcd/queries/mediaManagement/combined.ts | 118 +++ .../server/pcd/queries/mediaManagement/get.ts | 229 ----- .../pcd/queries/mediaManagement/index.ts | 45 - .../mediaManagement/media-settings/create.ts | 94 ++ .../mediaManagement/media-settings/index.ts | 9 + .../mediaManagement/media-settings/read.ts | 82 ++ .../mediaManagement/media-settings/remove.ts | 57 ++ .../mediaManagement/media-settings/types.ts | 13 + .../mediaManagement/media-settings/update.ts | 101 +++ .../queries/mediaManagement/naming/create.ts | 130 +++ .../queries/mediaManagement/naming/index.ts | 9 + .../queries/mediaManagement/naming/read.ts | 115 +++ .../queries/mediaManagement/naming/remove.ts | 64 ++ .../queries/mediaManagement/naming/types.ts | 12 + .../queries/mediaManagement/naming/update.ts | 138 +++ .../quality-definitions/create.ts | 101 +++ .../quality-definitions/index.ts | 9 + .../quality-definitions/read.ts | 124 +++ .../quality-definitions/remove.ts | 57 ++ .../quality-definitions/types.ts | 24 + .../quality-definitions/update.ts | 120 +++ .../pcd/queries/mediaManagement/types.ts | 97 --- .../pcd/queries/mediaManagement/update.ts | 503 ----------- src/lib/server/pcd/schema.ts | 10 +- src/lib/server/sync/mediaManagement/syncer.ts | 122 +-- src/lib/shared/mediaManagement.ts | 6 +- src/routes/arr/[id]/sync/+page.server.ts | 40 +- src/routes/arr/[id]/sync/+page.svelte | 10 +- .../sync/components/MediaManagement.svelte | 177 +++- src/routes/arr/components/InstanceForm.svelte | 8 +- .../databases/components/InstanceForm.svelte | 8 +- src/routes/media-management/+page.server.ts | 9 +- .../[databaseId]/+layout.server.ts | 15 +- .../[databaseId]/+layout.svelte | 44 +- .../[databaseId]/+page.server.ts | 8 +- .../components/MediaSettingsSection.svelte | 265 ------ .../components/NamingSection.svelte | 813 ------------------ .../QualityDefinitionsSection.svelte | 547 ------------ .../media-settings/+page.server.ts | 28 + .../[databaseId]/media-settings/+page.svelte | 51 ++ .../components/MediaSettingsForm.svelte | 291 +++++++ .../media-settings/new/+page.server.ts | 74 ++ .../media-settings/new/+page.svelte | 61 ++ .../radarr/[name]/+page.server.ts | 142 +++ .../media-settings/radarr/[name]/+page.svelte | 18 + .../sonarr/[name]/+page.server.ts | 142 +++ .../media-settings/sonarr/[name]/+page.svelte | 18 + .../media-settings/views/TableView.svelte | 79 ++ .../[databaseId]/naming/+page.server.ts | 28 + .../[databaseId]/naming/+page.svelte | 51 ++ .../naming/components/RadarrNamingForm.svelte | 352 ++++++++ .../naming/components/SonarrNamingForm.svelte | 477 ++++++++++ .../[databaseId]/naming/new/+page.server.ts | 118 +++ .../[databaseId]/naming/new/+page.svelte | 68 ++ .../naming/radarr/[name]/+page.server.ts | 150 ++++ .../naming/radarr/[name]/+page.svelte | 17 + .../naming/sonarr/[name]/+page.server.ts | 158 ++++ .../naming/sonarr/[name]/+page.svelte | 17 + .../naming/views/TableView.svelte | 64 ++ .../quality-definitions/+page.server.ts | 28 + .../quality-definitions/+page.svelte | 51 ++ .../components/QualityDefinitionsForm.svelte | 513 +++++++++++ .../quality-definitions/new/+page.server.ts | 105 +++ .../quality-definitions/new/+page.svelte | 64 ++ .../radarr/[name]/+page.server.ts | 149 ++++ .../radarr/[name]/+page.svelte | 19 + .../sonarr/[name]/+page.server.ts | 148 ++++ .../sonarr/[name]/+page.svelte | 19 + .../views/TableView.svelte | 52 ++ .../[databaseId]/radarr/+page.server.ts | 260 ------ .../[databaseId]/radarr/+page.svelte | 105 --- .../[databaseId]/sonarr/+page.server.ts | 288 ------- .../[databaseId]/sonarr/+page.svelte | 105 --- 79 files changed, 5351 insertions(+), 3419 deletions(-) create mode 100644 src/lib/server/db/migrations/038_add_media_management_config_names.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/combined.ts delete mode 100644 src/lib/server/pcd/queries/mediaManagement/get.ts delete mode 100644 src/lib/server/pcd/queries/mediaManagement/index.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/media-settings/create.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/media-settings/index.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/media-settings/read.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/media-settings/remove.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/media-settings/types.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/media-settings/update.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/naming/create.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/naming/index.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/naming/read.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/naming/remove.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/naming/types.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/naming/update.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/quality-definitions/create.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/quality-definitions/index.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/quality-definitions/read.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/quality-definitions/remove.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/quality-definitions/types.ts create mode 100644 src/lib/server/pcd/queries/mediaManagement/quality-definitions/update.ts delete mode 100644 src/lib/server/pcd/queries/mediaManagement/types.ts delete mode 100644 src/lib/server/pcd/queries/mediaManagement/update.ts delete mode 100644 src/routes/media-management/[databaseId]/components/MediaSettingsSection.svelte delete mode 100644 src/routes/media-management/[databaseId]/components/NamingSection.svelte delete mode 100644 src/routes/media-management/[databaseId]/components/QualityDefinitionsSection.svelte create mode 100644 src/routes/media-management/[databaseId]/media-settings/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/media-settings/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/media-settings/components/MediaSettingsForm.svelte create mode 100644 src/routes/media-management/[databaseId]/media-settings/new/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/media-settings/new/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/media-settings/radarr/[name]/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/media-settings/radarr/[name]/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/media-settings/sonarr/[name]/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/media-settings/sonarr/[name]/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/media-settings/views/TableView.svelte create mode 100644 src/routes/media-management/[databaseId]/naming/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/naming/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/naming/components/RadarrNamingForm.svelte create mode 100644 src/routes/media-management/[databaseId]/naming/components/SonarrNamingForm.svelte create mode 100644 src/routes/media-management/[databaseId]/naming/new/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/naming/new/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/naming/radarr/[name]/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/naming/radarr/[name]/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/naming/sonarr/[name]/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/naming/sonarr/[name]/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/naming/views/TableView.svelte create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/components/QualityDefinitionsForm.svelte create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/new/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/new/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/radarr/[name]/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/radarr/[name]/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/sonarr/[name]/+page.server.ts create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/sonarr/[name]/+page.svelte create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/views/TableView.svelte delete mode 100644 src/routes/media-management/[databaseId]/radarr/+page.server.ts delete mode 100644 src/routes/media-management/[databaseId]/radarr/+page.svelte delete mode 100644 src/routes/media-management/[databaseId]/sonarr/+page.server.ts delete mode 100644 src/routes/media-management/[databaseId]/sonarr/+page.svelte diff --git a/src/lib/client/ui/navigation/pageNav/groupItem.svelte b/src/lib/client/ui/navigation/pageNav/groupItem.svelte index 996d50e..11a7953 100644 --- a/src/lib/client/ui/navigation/pageNav/groupItem.svelte +++ b/src/lib/client/ui/navigation/pageNav/groupItem.svelte @@ -4,13 +4,26 @@ interface Props { label: string; href: string; + /** Optional pattern to match against pathname for active state (supports string includes or regex) */ + activePattern?: string | RegExp; } - let { label, href }: Props = $props(); + let { label, href, activePattern }: Props = $props(); - const isActive = $derived( - $page.url.pathname === href || $page.url.pathname.startsWith(href + '/') - ); + const isActive = $derived.by(() => { + const pathname = $page.url.pathname; + + // Use custom pattern if provided + if (activePattern) { + if (typeof activePattern === 'string') { + return pathname.includes(activePattern); + } + return activePattern.test(pathname); + } + + // Default behavior + return pathname === href || pathname.startsWith(href + '/'); + }); + initialOpen={true} + hasItems={true} + > + + + + { + // Get the first available media settings config + const mediaSettingsList = await listMediaSettings(cache); + const radarrMediaSettings = mediaSettingsList.find(c => c.arr_type === 'radarr'); + const mediaSettings = radarrMediaSettings + ? await getRadarrMediaSettings(cache, radarrMediaSettings.name) + : null; + + // Get the first available naming config + const namingList = await listNaming(cache); + const radarrNaming = namingList.find(c => c.arr_type === 'radarr'); + const naming = radarrNaming + ? await getRadarrNaming(cache, radarrNaming.name) + : null; + + // Get quality definitions (not yet refactored to multi-config) + const qualityDefinitions = await getQualityDefinitions(cache, 'radarr'); + + return { + mediaSettings, + naming, + qualityDefinitions + }; +} + +/** + * Get all Sonarr media management configs (returns first available of each type) + */ +export async function getSonarr(cache: PCDCache): Promise { + // Get the first available media settings config + const mediaSettingsList = await listMediaSettings(cache); + const sonarrMediaSettings = mediaSettingsList.find(c => c.arr_type === 'sonarr'); + const mediaSettings = sonarrMediaSettings + ? await getSonarrMediaSettings(cache, sonarrMediaSettings.name) + : null; + + // Get the first available naming config + const namingList = await listNaming(cache); + const sonarrNaming = namingList.find(c => c.arr_type === 'sonarr'); + const naming = sonarrNaming + ? await getSonarrNaming(cache, sonarrNaming.name) + : null; + + // Get quality definitions (not yet refactored to multi-config) + const qualityDefinitions = await getQualityDefinitions(cache, 'sonarr'); + + return { + mediaSettings, + naming, + qualityDefinitions + }; +} + +/** + * Get quality definitions for an arr type (returns first available config) + */ +async function getQualityDefinitions(cache: PCDCache, arrType: 'radarr' | 'sonarr'): Promise { + const qualityDefsList = await listQualityDefs(cache); + const config = qualityDefsList.find(c => c.arr_type === arrType); + + if (!config) { + return []; + } + + const getByName = arrType === 'radarr' ? getRadarrQualityDefs : getSonarrQualityDefs; + const fullConfig = await getByName(cache, config.name); + + if (!fullConfig) { + return []; + } + + return fullConfig.entries.map((entry: QualityDefinitionEntry) => ({ + quality_name: entry.quality_name, + min_size: entry.min_size, + max_size: entry.max_size, + preferred_size: entry.preferred_size + })); +} diff --git a/src/lib/server/pcd/queries/mediaManagement/get.ts b/src/lib/server/pcd/queries/mediaManagement/get.ts deleted file mode 100644 index 5978a02..0000000 --- a/src/lib/server/pcd/queries/mediaManagement/get.ts +++ /dev/null @@ -1,229 +0,0 @@ -/** - * Media Management get queries - */ - -import type { PCDCache } from '../../cache.ts'; -import type { - MediaManagementData, - RadarrMediaManagementData, - SonarrMediaManagementData, - QualityDefinition, - RadarrNaming, - SonarrNaming, - MediaSettings, - PropersRepacks -} from './types.ts'; -import { - colonReplacementFromDb, - multiEpisodeStyleFromDb, - radarrColonReplacementFromDb -} from './types.ts'; - -/** - * Get Radarr media management data - */ -export async function getRadarr(cache: PCDCache): Promise { - const db = cache.kb; - - const [qualityDefinitions, naming, mediaSettings] = await Promise.all([ - getRadarrQualityDefinitions(db), - getRadarrNaming(db), - getRadarrMediaSettings(db) - ]); - - return { qualityDefinitions, naming, mediaSettings }; -} - -/** - * Get Sonarr media management data - */ -export async function getSonarr(cache: PCDCache): Promise { - const db = cache.kb; - - const [qualityDefinitions, naming, mediaSettings] = await Promise.all([ - getSonarrQualityDefinitions(db), - getSonarrNaming(db), - getSonarrMediaSettings(db) - ]); - - return { qualityDefinitions, naming, mediaSettings }; -} - -/** - * Get all media management data for a PCD database - */ -export async function get(cache: PCDCache): Promise { - const db = cache.kb; - - // Fetch all data in parallel - const [ - radarrQualityDefs, - sonarrQualityDefs, - radarrNaming, - sonarrNaming, - radarrMediaSettings, - sonarrMediaSettings - ] = await Promise.all([ - getRadarrQualityDefinitions(db), - getSonarrQualityDefinitions(db), - getRadarrNaming(db), - getSonarrNaming(db), - getRadarrMediaSettings(db), - getSonarrMediaSettings(db) - ]); - - return { - qualityDefinitions: { - radarr: radarrQualityDefs, - sonarr: sonarrQualityDefs - }, - naming: { - radarr: radarrNaming, - sonarr: sonarrNaming - }, - mediaSettings: { - radarr: radarrMediaSettings, - sonarr: sonarrMediaSettings - } - }; -} - -/** - * Get Radarr quality definitions with quality names - */ -async function getRadarrQualityDefinitions(db: PCDCache['kb']): Promise { - const rows = await db - .selectFrom('radarr_quality_definitions as rqd') - .innerJoin('qualities as q', 'q.name', 'rqd.quality_name') - .select(['rqd.quality_name', 'rqd.min_size', 'rqd.max_size', 'rqd.preferred_size']) - .orderBy('rqd.quality_name') - .execute(); - - return rows.map((row) => ({ - quality_name: row.quality_name, - min_size: row.min_size, - max_size: row.max_size, - preferred_size: row.preferred_size - })); -} - -/** - * Get Sonarr quality definitions with quality names - */ -async function getSonarrQualityDefinitions(db: PCDCache['kb']): Promise { - const rows = await db - .selectFrom('sonarr_quality_definitions as sqd') - .innerJoin('qualities as q', 'q.name', 'sqd.quality_name') - .select(['sqd.quality_name', 'sqd.min_size', 'sqd.max_size', 'sqd.preferred_size']) - .orderBy('sqd.quality_name') - .execute(); - - return rows.map((row) => ({ - quality_name: row.quality_name, - min_size: row.min_size, - max_size: row.max_size, - preferred_size: row.preferred_size - })); -} - -/** - * Get Radarr naming settings - */ -async function getRadarrNaming(db: PCDCache['kb']): Promise { - const row = await db - .selectFrom('radarr_naming') - .select([ - 'id', - 'rename', - 'movie_format', - 'movie_folder_format', - 'replace_illegal_characters', - 'colon_replacement_format' - ]) - .executeTakeFirst(); - - if (!row) return null; - - return { - id: row.id, - rename: row.rename === 1, - movie_format: row.movie_format, - movie_folder_format: row.movie_folder_format, - replace_illegal_characters: row.replace_illegal_characters === 1, - colon_replacement_format: radarrColonReplacementFromDb(row.colon_replacement_format as number) - }; -} - -/** - * Get Sonarr naming settings - */ -async function getSonarrNaming(db: PCDCache['kb']): Promise { - const row = await db - .selectFrom('sonarr_naming') - .select([ - 'id', - 'rename', - 'standard_episode_format', - 'daily_episode_format', - 'anime_episode_format', - 'series_folder_format', - 'season_folder_format', - 'replace_illegal_characters', - 'colon_replacement_format', - 'custom_colon_replacement_format', - 'multi_episode_style' - ]) - .executeTakeFirst(); - - if (!row) return null; - - return { - id: row.id, - rename: row.rename === 1, - standard_episode_format: row.standard_episode_format, - daily_episode_format: row.daily_episode_format, - anime_episode_format: row.anime_episode_format, - series_folder_format: row.series_folder_format, - season_folder_format: row.season_folder_format, - replace_illegal_characters: row.replace_illegal_characters === 1, - colon_replacement_format: colonReplacementFromDb(row.colon_replacement_format as number), - custom_colon_replacement_format: row.custom_colon_replacement_format, - multi_episode_style: multiEpisodeStyleFromDb(row.multi_episode_style as number) - }; -} - -/** - * Get Radarr media settings - */ -async function getRadarrMediaSettings(db: PCDCache['kb']): Promise { - const row = await db - .selectFrom('radarr_media_settings') - .select(['id', 'propers_repacks', 'enable_media_info']) - .executeTakeFirst(); - - if (!row) return null; - - return { - id: row.id, - propers_repacks: row.propers_repacks as PropersRepacks, - enable_media_info: row.enable_media_info === 1 - }; -} - -/** - * Get Sonarr media settings - */ -async function getSonarrMediaSettings(db: PCDCache['kb']): Promise { - const row = await db - .selectFrom('sonarr_media_settings') - .select(['id', 'propers_repacks', 'enable_media_info']) - .executeTakeFirst(); - - if (!row) return null; - - return { - id: row.id, - propers_repacks: row.propers_repacks as PropersRepacks, - enable_media_info: row.enable_media_info === 1 - }; -} diff --git a/src/lib/server/pcd/queries/mediaManagement/index.ts b/src/lib/server/pcd/queries/mediaManagement/index.ts deleted file mode 100644 index d3734cd..0000000 --- a/src/lib/server/pcd/queries/mediaManagement/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Media Management queries - */ - -// Export all types -export type { - ArrType, - QualityDefinition, - QualityDefinitionsData, - RadarrNaming, - SonarrNaming, - NamingData, - MediaSettings, - MediaSettingsData, - MediaManagementData, - RadarrMediaManagementData, - SonarrMediaManagementData, - PropersRepacks -} from './types.ts'; - -// Export constants and helpers -export { PROPERS_REPACKS_OPTIONS, getPropersRepacksLabel } from './types.ts'; - -// Export query functions -export { get, getRadarr, getSonarr } from './get.ts'; - -// Export update functions -export type { - UpdateMediaSettingsInput, - UpdateMediaSettingsOptions, - UpdateSonarrNamingInput, - UpdateSonarrNamingOptions, - UpdateRadarrNamingInput, - UpdateRadarrNamingOptions, - UpdateQualityDefinitionInput, - UpdateQualityDefinitionsOptions -} from './update.ts'; -export { - updateRadarrMediaSettings, - updateSonarrMediaSettings, - updateSonarrNaming, - updateRadarrNaming, - updateRadarrQualityDefinitions, - updateSonarrQualityDefinitions -} from './update.ts'; diff --git a/src/lib/server/pcd/queries/mediaManagement/media-settings/create.ts b/src/lib/server/pcd/queries/mediaManagement/media-settings/create.ts new file mode 100644 index 0000000..877c8fd --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/media-settings/create.ts @@ -0,0 +1,94 @@ +/** + * Create media settings config operations + */ + +import type { PCDCache } from '../../../cache.ts'; +import { writeOperation, type OperationLayer } from '../../../writer.ts'; +import type { PropersRepacks } from '$lib/shared/mediaManagement.ts'; + +export interface CreateMediaSettingsInput { + name: string; + propersRepacks: PropersRepacks; + enableMediaInfo: boolean; +} + +export interface CreateMediaSettingsOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + input: CreateMediaSettingsInput; +} + +export async function createRadarrMediaSettings(options: CreateMediaSettingsOptions) { + const { databaseId, cache, layer, input } = options; + const db = cache.kb; + + // Check if name already exists + const existing = await db + .selectFrom('radarr_media_settings') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A radarr media settings config with name "${input.name}" already exists`); + } + + const insertQuery = db + .insertInto('radarr_media_settings') + .values({ + name: input.name, + propers_repacks: input.propersRepacks, + enable_media_info: input.enableMediaInfo ? 1 : 0 + }) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `create-radarr-media-settings-${input.name}`, + queries: [insertQuery], + metadata: { + operation: 'create', + entity: 'radarr_media_settings', + name: input.name + } + }); +} + +export async function createSonarrMediaSettings(options: CreateMediaSettingsOptions) { + const { databaseId, cache, layer, input } = options; + const db = cache.kb; + + // Check if name already exists + const existing = await db + .selectFrom('sonarr_media_settings') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A sonarr media settings config with name "${input.name}" already exists`); + } + + const insertQuery = db + .insertInto('sonarr_media_settings') + .values({ + name: input.name, + propers_repacks: input.propersRepacks, + enable_media_info: input.enableMediaInfo ? 1 : 0 + }) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `create-sonarr-media-settings-${input.name}`, + queries: [insertQuery], + metadata: { + operation: 'create', + entity: 'sonarr_media_settings', + name: input.name + } + }); +} diff --git a/src/lib/server/pcd/queries/mediaManagement/media-settings/index.ts b/src/lib/server/pcd/queries/mediaManagement/media-settings/index.ts new file mode 100644 index 0000000..56194d8 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/media-settings/index.ts @@ -0,0 +1,9 @@ +/** + * Media settings queries index + */ + +export * from './types.ts'; +export * from './read.ts'; +export * from './create.ts'; +export * from './update.ts'; +export * from './remove.ts'; diff --git a/src/lib/server/pcd/queries/mediaManagement/media-settings/read.ts b/src/lib/server/pcd/queries/mediaManagement/media-settings/read.ts new file mode 100644 index 0000000..5146c61 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/media-settings/read.ts @@ -0,0 +1,82 @@ +/** + * Media settings read operations (list and get) + */ + +import type { PCDCache } from '../../../cache.ts'; +import type { MediaSettings, PropersRepacks } from '$lib/shared/mediaManagement.ts'; +import type { MediaSettingsListItem } from './types.ts'; + +export async function list(cache: PCDCache): Promise { + const db = cache.kb; + + const [radarrRows, sonarrRows] = await Promise.all([ + db.selectFrom('radarr_media_settings').select(['name', 'propers_repacks', 'enable_media_info', 'updated_at']).execute(), + db.selectFrom('sonarr_media_settings').select(['name', 'propers_repacks', 'enable_media_info', 'updated_at']).execute() + ]); + + const items: MediaSettingsListItem[] = []; + + for (const row of radarrRows) { + items.push({ + name: row.name, + arr_type: 'radarr', + propers_repacks: row.propers_repacks, + enable_media_info: row.enable_media_info === 1, + updated_at: row.updated_at + }); + } + + for (const row of sonarrRows) { + items.push({ + name: row.name, + arr_type: 'sonarr', + propers_repacks: row.propers_repacks, + enable_media_info: row.enable_media_info === 1, + updated_at: row.updated_at + }); + } + + return items; +} + +export async function getRadarrByName( + cache: PCDCache, + name: string +): Promise { + const db = cache.kb; + + const row = await db + .selectFrom('radarr_media_settings') + .select(['name', 'propers_repacks', 'enable_media_info']) + .where('name', '=', name) + .executeTakeFirst(); + + if (!row) return null; + + return { + name: row.name, + propers_repacks: row.propers_repacks as PropersRepacks, + enable_media_info: row.enable_media_info === 1 + }; +} + +export async function getSonarrByName( + cache: PCDCache, + name: string +): Promise { + const db = cache.kb; + + const row = await db + .selectFrom('sonarr_media_settings') + .select(['name', 'propers_repacks', 'enable_media_info']) + .where('name', '=', name) + .executeTakeFirst(); + + if (!row) return null; + + return { + name: row.name, + propers_repacks: row.propers_repacks as PropersRepacks, + enable_media_info: row.enable_media_info === 1 + }; +} diff --git a/src/lib/server/pcd/queries/mediaManagement/media-settings/remove.ts b/src/lib/server/pcd/queries/mediaManagement/media-settings/remove.ts new file mode 100644 index 0000000..8fd3181 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/media-settings/remove.ts @@ -0,0 +1,57 @@ +/** + * Remove media settings config operations + */ + +import type { PCDCache } from '../../../cache.ts'; +import { writeOperation, type OperationLayer } from '../../../writer.ts'; + +export interface RemoveMediaSettingsOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + name: string; +} + +export async function removeRadarrMediaSettings(options: RemoveMediaSettingsOptions) { + const { databaseId, cache, layer, name } = options; + const db = cache.kb; + + const deleteQuery = db + .deleteFrom('radarr_media_settings') + .where('name', '=', name) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `delete-radarr-media-settings-${name}`, + queries: [deleteQuery], + metadata: { + operation: 'delete', + entity: 'radarr_media_settings', + name + } + }); +} + +export async function removeSonarrMediaSettings(options: RemoveMediaSettingsOptions) { + const { databaseId, cache, layer, name } = options; + const db = cache.kb; + + const deleteQuery = db + .deleteFrom('sonarr_media_settings') + .where('name', '=', name) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `delete-sonarr-media-settings-${name}`, + queries: [deleteQuery], + metadata: { + operation: 'delete', + entity: 'sonarr_media_settings', + name + } + }); +} diff --git a/src/lib/server/pcd/queries/mediaManagement/media-settings/types.ts b/src/lib/server/pcd/queries/mediaManagement/media-settings/types.ts new file mode 100644 index 0000000..5c1dd7e --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/media-settings/types.ts @@ -0,0 +1,13 @@ +/** + * Media settings query-specific types + */ + +export type ArrType = 'radarr' | 'sonarr'; + +export interface MediaSettingsListItem { + name: string; + arr_type: ArrType; + propers_repacks: string; + enable_media_info: boolean; + updated_at: string; +} diff --git a/src/lib/server/pcd/queries/mediaManagement/media-settings/update.ts b/src/lib/server/pcd/queries/mediaManagement/media-settings/update.ts new file mode 100644 index 0000000..4944db9 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/media-settings/update.ts @@ -0,0 +1,101 @@ +/** + * Update media settings config operations + */ + +import type { PCDCache } from '../../../cache.ts'; +import { writeOperation, type OperationLayer } from '../../../writer.ts'; +import type { PropersRepacks } from '$lib/shared/mediaManagement.ts'; + +export interface UpdateMediaSettingsInput { + name: string; + propersRepacks: PropersRepacks; + enableMediaInfo: boolean; +} + +export interface UpdateMediaSettingsOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + currentName: string; + input: UpdateMediaSettingsInput; +} + +export async function updateRadarrMediaSettings(options: UpdateMediaSettingsOptions) { + const { databaseId, cache, layer, currentName, input } = options; + const db = cache.kb; + + // If renaming, check if new name already exists + if (input.name !== currentName) { + const existing = await db + .selectFrom('radarr_media_settings') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A radarr media settings config with name "${input.name}" already exists`); + } + } + + const updateQuery = db + .updateTable('radarr_media_settings') + .set({ + name: input.name, + propers_repacks: input.propersRepacks, + enable_media_info: input.enableMediaInfo ? 1 : 0 + }) + .where('name', '=', currentName) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `update-radarr-media-settings-${input.name}`, + queries: [updateQuery], + metadata: { + operation: 'update', + entity: 'radarr_media_settings', + name: input.name + } + }); +} + +export async function updateSonarrMediaSettings(options: UpdateMediaSettingsOptions) { + const { databaseId, cache, layer, currentName, input } = options; + const db = cache.kb; + + // If renaming, check if new name already exists + if (input.name !== currentName) { + const existing = await db + .selectFrom('sonarr_media_settings') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A sonarr media settings config with name "${input.name}" already exists`); + } + } + + const updateQuery = db + .updateTable('sonarr_media_settings') + .set({ + name: input.name, + propers_repacks: input.propersRepacks, + enable_media_info: input.enableMediaInfo ? 1 : 0 + }) + .where('name', '=', currentName) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `update-sonarr-media-settings-${input.name}`, + queries: [updateQuery], + metadata: { + operation: 'update', + entity: 'sonarr_media_settings', + name: input.name + } + }); +} diff --git a/src/lib/server/pcd/queries/mediaManagement/naming/create.ts b/src/lib/server/pcd/queries/mediaManagement/naming/create.ts new file mode 100644 index 0000000..c72841f --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/naming/create.ts @@ -0,0 +1,130 @@ +/** + * Create naming config operations + */ + +import type { PCDCache } from '../../../cache.ts'; +import { writeOperation, type OperationLayer } from '../../../writer.ts'; +import type { RadarrColonReplacementFormat, ColonReplacementFormat, MultiEpisodeStyle } from '$lib/shared/mediaManagement.ts'; +import { radarrColonReplacementToDb, colonReplacementToDb, multiEpisodeStyleToDb } from '$lib/shared/mediaManagement.ts'; + +export interface CreateRadarrNamingInput { + name: string; + rename: boolean; + movieFormat: string; + movieFolderFormat: string; + replaceIllegalCharacters: boolean; + colonReplacementFormat: RadarrColonReplacementFormat; +} + +export interface CreateRadarrNamingOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + input: CreateRadarrNamingInput; +} + +export async function createRadarrNaming(options: CreateRadarrNamingOptions) { + const { databaseId, cache, layer, input } = options; + const db = cache.kb; + + // Check if name already exists + const existing = await db + .selectFrom('radarr_naming') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A radarr naming config with name "${input.name}" already exists`); + } + + const insertQuery = db + .insertInto('radarr_naming') + .values({ + name: input.name, + rename: input.rename ? 1 : 0, + movie_format: input.movieFormat, + movie_folder_format: input.movieFolderFormat, + replace_illegal_characters: input.replaceIllegalCharacters ? 1 : 0, + colon_replacement_format: radarrColonReplacementToDb(input.colonReplacementFormat) + }) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `create-radarr-naming-${input.name}`, + queries: [insertQuery], + metadata: { + operation: 'create', + entity: 'radarr_naming', + name: input.name + } + }); +} + +export interface CreateSonarrNamingInput { + name: string; + rename: boolean; + standardEpisodeFormat: string; + dailyEpisodeFormat: string; + animeEpisodeFormat: string; + seriesFolderFormat: string; + seasonFolderFormat: string; + replaceIllegalCharacters: boolean; + colonReplacementFormat: ColonReplacementFormat; + customColonReplacementFormat: string | null; + multiEpisodeStyle: MultiEpisodeStyle; +} + +export interface CreateSonarrNamingOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + input: CreateSonarrNamingInput; +} + +export async function createSonarrNaming(options: CreateSonarrNamingOptions) { + const { databaseId, cache, layer, input } = options; + const db = cache.kb; + + // Check if name already exists + const existing = await db + .selectFrom('sonarr_naming') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A sonarr naming config with name "${input.name}" already exists`); + } + + const insertQuery = db + .insertInto('sonarr_naming') + .values({ + name: input.name, + rename: input.rename ? 1 : 0, + standard_episode_format: input.standardEpisodeFormat, + daily_episode_format: input.dailyEpisodeFormat, + anime_episode_format: input.animeEpisodeFormat, + series_folder_format: input.seriesFolderFormat, + season_folder_format: input.seasonFolderFormat, + replace_illegal_characters: input.replaceIllegalCharacters ? 1 : 0, + colon_replacement_format: colonReplacementToDb(input.colonReplacementFormat), + custom_colon_replacement_format: input.customColonReplacementFormat, + multi_episode_style: multiEpisodeStyleToDb(input.multiEpisodeStyle) + }) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `create-sonarr-naming-${input.name}`, + queries: [insertQuery], + metadata: { + operation: 'create', + entity: 'sonarr_naming', + name: input.name + } + }); +} diff --git a/src/lib/server/pcd/queries/mediaManagement/naming/index.ts b/src/lib/server/pcd/queries/mediaManagement/naming/index.ts new file mode 100644 index 0000000..9565a4a --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/naming/index.ts @@ -0,0 +1,9 @@ +/** + * Naming queries index + */ + +export * from './types.ts'; +export * from './read.ts'; +export * from './create.ts'; +export * from './update.ts'; +export * from './remove.ts'; diff --git a/src/lib/server/pcd/queries/mediaManagement/naming/read.ts b/src/lib/server/pcd/queries/mediaManagement/naming/read.ts new file mode 100644 index 0000000..d7c5b62 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/naming/read.ts @@ -0,0 +1,115 @@ +/** + * Naming read operations (list and get) + */ + +import type { PCDCache } from '../../../cache.ts'; +import type { RadarrNaming, SonarrNaming } from '$lib/shared/mediaManagement.ts'; +import { + radarrColonReplacementFromDb, + colonReplacementFromDb, + multiEpisodeStyleFromDb +} from '$lib/shared/mediaManagement.ts'; +import type { NamingListItem } from './types.ts'; + +export async function list(cache: PCDCache): Promise { + const db = cache.kb; + + const [radarrRows, sonarrRows] = await Promise.all([ + db.selectFrom('radarr_naming').select(['name', 'rename', 'updated_at']).execute(), + db.selectFrom('sonarr_naming').select(['name', 'rename', 'updated_at']).execute() + ]); + + const items: NamingListItem[] = []; + + for (const row of radarrRows) { + items.push({ + name: row.name, + arr_type: 'radarr', + rename: row.rename === 1, + updated_at: row.updated_at + }); + } + + for (const row of sonarrRows) { + items.push({ + name: row.name, + arr_type: 'sonarr', + rename: row.rename === 1, + updated_at: row.updated_at + }); + } + + return items; +} + +export async function getRadarrByName( + cache: PCDCache, + name: string +): Promise { + const db = cache.kb; + + const row = await db + .selectFrom('radarr_naming') + .select([ + 'name', + 'rename', + 'movie_format', + 'movie_folder_format', + 'replace_illegal_characters', + 'colon_replacement_format' + ]) + .where('name', '=', name) + .executeTakeFirst(); + + if (!row) return null; + + return { + name: row.name, + rename: row.rename === 1, + movie_format: row.movie_format, + movie_folder_format: row.movie_folder_format, + replace_illegal_characters: row.replace_illegal_characters === 1, + colon_replacement_format: radarrColonReplacementFromDb(row.colon_replacement_format as number) + }; +} + +export async function getSonarrByName( + cache: PCDCache, + name: string +): Promise { + const db = cache.kb; + + const row = await db + .selectFrom('sonarr_naming') + .select([ + 'name', + 'rename', + 'standard_episode_format', + 'daily_episode_format', + 'anime_episode_format', + 'series_folder_format', + 'season_folder_format', + 'replace_illegal_characters', + 'colon_replacement_format', + 'custom_colon_replacement_format', + 'multi_episode_style' + ]) + .where('name', '=', name) + .executeTakeFirst(); + + if (!row) return null; + + return { + name: row.name, + rename: row.rename === 1, + standard_episode_format: row.standard_episode_format, + daily_episode_format: row.daily_episode_format, + anime_episode_format: row.anime_episode_format, + series_folder_format: row.series_folder_format, + season_folder_format: row.season_folder_format, + replace_illegal_characters: row.replace_illegal_characters === 1, + colon_replacement_format: colonReplacementFromDb(row.colon_replacement_format as number), + custom_colon_replacement_format: row.custom_colon_replacement_format, + multi_episode_style: multiEpisodeStyleFromDb(row.multi_episode_style as number) + }; +} diff --git a/src/lib/server/pcd/queries/mediaManagement/naming/remove.ts b/src/lib/server/pcd/queries/mediaManagement/naming/remove.ts new file mode 100644 index 0000000..be618d6 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/naming/remove.ts @@ -0,0 +1,64 @@ +/** + * Remove naming config operations + */ + +import type { PCDCache } from '../../../cache.ts'; +import { writeOperation, type OperationLayer } from '../../../writer.ts'; + +export interface RemoveRadarrNamingOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + name: string; +} + +export async function removeRadarrNaming(options: RemoveRadarrNamingOptions) { + const { databaseId, cache, layer, name } = options; + const db = cache.kb; + + const deleteQuery = db + .deleteFrom('radarr_naming') + .where('name', '=', name) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `delete-radarr-naming-${name}`, + queries: [deleteQuery], + metadata: { + operation: 'delete', + entity: 'radarr_naming', + name + } + }); +} + +export interface RemoveSonarrNamingOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + name: string; +} + +export async function removeSonarrNaming(options: RemoveSonarrNamingOptions) { + const { databaseId, cache, layer, name } = options; + const db = cache.kb; + + const deleteQuery = db + .deleteFrom('sonarr_naming') + .where('name', '=', name) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `delete-sonarr-naming-${name}`, + queries: [deleteQuery], + metadata: { + operation: 'delete', + entity: 'sonarr_naming', + name + } + }); +} diff --git a/src/lib/server/pcd/queries/mediaManagement/naming/types.ts b/src/lib/server/pcd/queries/mediaManagement/naming/types.ts new file mode 100644 index 0000000..2b6e86b --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/naming/types.ts @@ -0,0 +1,12 @@ +/** + * Naming query-specific types + */ + +export type ArrType = 'radarr' | 'sonarr'; + +export interface NamingListItem { + name: string; + arr_type: ArrType; + rename: boolean; + updated_at: string; +} diff --git a/src/lib/server/pcd/queries/mediaManagement/naming/update.ts b/src/lib/server/pcd/queries/mediaManagement/naming/update.ts new file mode 100644 index 0000000..e4fee39 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/naming/update.ts @@ -0,0 +1,138 @@ +/** + * Update naming config operations + */ + +import type { PCDCache } from '../../../cache.ts'; +import { writeOperation, type OperationLayer } from '../../../writer.ts'; +import type { RadarrColonReplacementFormat, ColonReplacementFormat, MultiEpisodeStyle } from '$lib/shared/mediaManagement.ts'; +import { radarrColonReplacementToDb, colonReplacementToDb, multiEpisodeStyleToDb } from '$lib/shared/mediaManagement.ts'; + +export interface UpdateRadarrNamingInput { + name: string; + rename: boolean; + movieFormat: string; + movieFolderFormat: string; + replaceIllegalCharacters: boolean; + colonReplacementFormat: RadarrColonReplacementFormat; +} + +export interface UpdateRadarrNamingOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + currentName: string; + input: UpdateRadarrNamingInput; +} + +export async function updateRadarrNaming(options: UpdateRadarrNamingOptions) { + const { databaseId, cache, layer, currentName, input } = options; + const db = cache.kb; + + // If renaming, check if new name already exists + if (input.name !== currentName) { + const existing = await db + .selectFrom('radarr_naming') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A radarr naming config with name "${input.name}" already exists`); + } + } + + const updateQuery = db + .updateTable('radarr_naming') + .set({ + name: input.name, + rename: input.rename ? 1 : 0, + movie_format: input.movieFormat, + movie_folder_format: input.movieFolderFormat, + replace_illegal_characters: input.replaceIllegalCharacters ? 1 : 0, + colon_replacement_format: radarrColonReplacementToDb(input.colonReplacementFormat) + }) + .where('name', '=', currentName) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `update-radarr-naming-${input.name}`, + queries: [updateQuery], + metadata: { + operation: 'update', + entity: 'radarr_naming', + name: input.name + } + }); +} + +export interface UpdateSonarrNamingInput { + name: string; + rename: boolean; + standardEpisodeFormat: string; + dailyEpisodeFormat: string; + animeEpisodeFormat: string; + seriesFolderFormat: string; + seasonFolderFormat: string; + replaceIllegalCharacters: boolean; + colonReplacementFormat: ColonReplacementFormat; + customColonReplacementFormat: string | null; + multiEpisodeStyle: MultiEpisodeStyle; +} + +export interface UpdateSonarrNamingOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + currentName: string; + input: UpdateSonarrNamingInput; +} + +export async function updateSonarrNaming(options: UpdateSonarrNamingOptions) { + const { databaseId, cache, layer, currentName, input } = options; + const db = cache.kb; + + // If renaming, check if new name already exists + if (input.name !== currentName) { + const existing = await db + .selectFrom('sonarr_naming') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A sonarr naming config with name "${input.name}" already exists`); + } + } + + const updateQuery = db + .updateTable('sonarr_naming') + .set({ + name: input.name, + rename: input.rename ? 1 : 0, + standard_episode_format: input.standardEpisodeFormat, + daily_episode_format: input.dailyEpisodeFormat, + anime_episode_format: input.animeEpisodeFormat, + series_folder_format: input.seriesFolderFormat, + season_folder_format: input.seasonFolderFormat, + replace_illegal_characters: input.replaceIllegalCharacters ? 1 : 0, + colon_replacement_format: colonReplacementToDb(input.colonReplacementFormat), + custom_colon_replacement_format: input.customColonReplacementFormat, + multi_episode_style: multiEpisodeStyleToDb(input.multiEpisodeStyle) + }) + .where('name', '=', currentName) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `update-sonarr-naming-${input.name}`, + queries: [updateQuery], + metadata: { + operation: 'update', + entity: 'sonarr_naming', + name: input.name + } + }); +} diff --git a/src/lib/server/pcd/queries/mediaManagement/quality-definitions/create.ts b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/create.ts new file mode 100644 index 0000000..1c628d2 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/create.ts @@ -0,0 +1,101 @@ +/** + * Quality definitions create operations + */ + +import type { PCDCache } from '../../../cache.ts'; +import { writeOperation, type OperationLayer } from '../../../writer.ts'; +import type { QualityDefinitionEntry } from './types.ts'; + +export interface CreateQualityDefinitionsInput { + name: string; + entries: QualityDefinitionEntry[]; +} + +export interface CreateQualityDefinitionsOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + input: CreateQualityDefinitionsInput; +} + +export async function createRadarrQualityDefinitions(options: CreateQualityDefinitionsOptions) { + const { databaseId, cache, layer, input } = options; + const db = cache.kb; + + // Check if name already exists + const existing = await db + .selectFrom('radarr_quality_definitions') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A radarr quality definitions config with name "${input.name}" already exists`); + } + + const queries = input.entries.map(entry => + db + .insertInto('radarr_quality_definitions') + .values({ + name: input.name, + quality_name: entry.quality_name, + min_size: entry.min_size, + max_size: entry.max_size, + preferred_size: entry.preferred_size + }) + .compile() + ); + + return writeOperation({ + databaseId, + layer, + description: `create-radarr-quality-definitions-${input.name}`, + queries, + metadata: { + operation: 'create', + entity: 'radarr_quality_definitions', + name: input.name + } + }); +} + +export async function createSonarrQualityDefinitions(options: CreateQualityDefinitionsOptions) { + const { databaseId, cache, layer, input } = options; + const db = cache.kb; + + // Check if name already exists + const existing = await db + .selectFrom('sonarr_quality_definitions') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A sonarr quality definitions config with name "${input.name}" already exists`); + } + + const queries = input.entries.map(entry => + db + .insertInto('sonarr_quality_definitions') + .values({ + name: input.name, + quality_name: entry.quality_name, + min_size: entry.min_size, + max_size: entry.max_size, + preferred_size: entry.preferred_size + }) + .compile() + ); + + return writeOperation({ + databaseId, + layer, + description: `create-sonarr-quality-definitions-${input.name}`, + queries, + metadata: { + operation: 'create', + entity: 'sonarr_quality_definitions', + name: input.name + } + }); +} diff --git a/src/lib/server/pcd/queries/mediaManagement/quality-definitions/index.ts b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/index.ts new file mode 100644 index 0000000..2a082b8 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/index.ts @@ -0,0 +1,9 @@ +/** + * Quality definitions queries + */ + +export * from './types.ts'; +export * from './read.ts'; +export * from './create.ts'; +export * from './update.ts'; +export * from './remove.ts'; diff --git a/src/lib/server/pcd/queries/mediaManagement/quality-definitions/read.ts b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/read.ts new file mode 100644 index 0000000..6e4a906 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/read.ts @@ -0,0 +1,124 @@ +/** + * Quality definitions read operations + */ + +import type { PCDCache } from '../../../cache.ts'; +import type { QualityDefinitionListItem, QualityDefinitionsConfig, QualityDefinitionEntry, ArrType } from './types.ts'; + +/** + * Get available qualities for an arr type from quality_api_mappings + * Returns quality names that can be used for that arr type + */ +export async function getAvailableQualities(cache: PCDCache, arrType: ArrType): Promise { + const rows = await cache.kb + .selectFrom('quality_api_mappings') + .where('arr_type', '=', arrType) + .select(['quality_name']) + .orderBy('quality_name') + .execute(); + + return rows.map(row => row.quality_name); +} + +/** + * List all quality definitions configs + * Returns distinct config names with quality counts + */ +export async function list(cache: PCDCache): Promise { + // Get radarr configs + const radarrRows = await cache.kb + .selectFrom('radarr_quality_definitions') + .select(['name']) + .select((eb) => eb.fn.count('quality_name').as('quality_count')) + .select((eb) => eb.fn.max('updated_at').as('updated_at')) + .groupBy('name') + .execute(); + + // Get sonarr configs + const sonarrRows = await cache.kb + .selectFrom('sonarr_quality_definitions') + .select(['name']) + .select((eb) => eb.fn.count('quality_name').as('quality_count')) + .select((eb) => eb.fn.max('updated_at').as('updated_at')) + .groupBy('name') + .execute(); + + const result: QualityDefinitionListItem[] = []; + + for (const row of radarrRows) { + result.push({ + name: row.name, + arr_type: 'radarr', + quality_count: Number(row.quality_count), + updated_at: row.updated_at ?? '' + }); + } + + for (const row of sonarrRows) { + result.push({ + name: row.name, + arr_type: 'sonarr', + quality_count: Number(row.quality_count), + updated_at: row.updated_at ?? '' + }); + } + + // Sort by updated_at desc + result.sort((a, b) => b.updated_at.localeCompare(a.updated_at)); + + return result; +} + +/** + * Get a Radarr quality definitions config by name + */ +export async function getRadarrByName(cache: PCDCache, name: string): Promise { + const rows = await cache.kb + .selectFrom('radarr_quality_definitions') + .where('name', '=', name) + .select(['quality_name', 'min_size', 'max_size', 'preferred_size']) + .execute(); + + if (rows.length === 0) { + return null; + } + + const entries: QualityDefinitionEntry[] = rows.map(row => ({ + quality_name: row.quality_name, + min_size: row.min_size, + max_size: row.max_size, + preferred_size: row.preferred_size + })); + + return { + name, + entries + }; +} + +/** + * Get a Sonarr quality definitions config by name + */ +export async function getSonarrByName(cache: PCDCache, name: string): Promise { + const rows = await cache.kb + .selectFrom('sonarr_quality_definitions') + .where('name', '=', name) + .select(['quality_name', 'min_size', 'max_size', 'preferred_size']) + .execute(); + + if (rows.length === 0) { + return null; + } + + const entries: QualityDefinitionEntry[] = rows.map(row => ({ + quality_name: row.quality_name, + min_size: row.min_size, + max_size: row.max_size, + preferred_size: row.preferred_size + })); + + return { + name, + entries + }; +} diff --git a/src/lib/server/pcd/queries/mediaManagement/quality-definitions/remove.ts b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/remove.ts new file mode 100644 index 0000000..a8d4e08 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/remove.ts @@ -0,0 +1,57 @@ +/** + * Quality definitions remove operations + */ + +import type { PCDCache } from '../../../cache.ts'; +import { writeOperation, type OperationLayer } from '../../../writer.ts'; + +export interface RemoveQualityDefinitionsOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + name: string; +} + +export async function removeRadarrQualityDefinitions(options: RemoveQualityDefinitionsOptions) { + const { databaseId, cache, layer, name } = options; + const db = cache.kb; + + const deleteQuery = db + .deleteFrom('radarr_quality_definitions') + .where('name', '=', name) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `remove-radarr-quality-definitions-${name}`, + queries: [deleteQuery], + metadata: { + operation: 'delete', + entity: 'radarr_quality_definitions', + name + } + }); +} + +export async function removeSonarrQualityDefinitions(options: RemoveQualityDefinitionsOptions) { + const { databaseId, cache, layer, name } = options; + const db = cache.kb; + + const deleteQuery = db + .deleteFrom('sonarr_quality_definitions') + .where('name', '=', name) + .compile(); + + return writeOperation({ + databaseId, + layer, + description: `remove-sonarr-quality-definitions-${name}`, + queries: [deleteQuery], + metadata: { + operation: 'delete', + entity: 'sonarr_quality_definitions', + name + } + }); +} diff --git a/src/lib/server/pcd/queries/mediaManagement/quality-definitions/types.ts b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/types.ts new file mode 100644 index 0000000..e8543d0 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/types.ts @@ -0,0 +1,24 @@ +/** + * Quality definitions types + */ + +export type ArrType = 'radarr' | 'sonarr'; + +export interface QualityDefinitionListItem { + name: string; + arr_type: ArrType; + quality_count: number; + updated_at: string; +} + +export interface QualityDefinitionEntry { + quality_name: string; + min_size: number; + max_size: number; + preferred_size: number; +} + +export interface QualityDefinitionsConfig { + name: string; + entries: QualityDefinitionEntry[]; +} diff --git a/src/lib/server/pcd/queries/mediaManagement/quality-definitions/update.ts b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/update.ts new file mode 100644 index 0000000..9a51fc6 --- /dev/null +++ b/src/lib/server/pcd/queries/mediaManagement/quality-definitions/update.ts @@ -0,0 +1,120 @@ +/** + * Quality definitions update operations + */ + +import type { PCDCache } from '../../../cache.ts'; +import { writeOperation, type OperationLayer } from '../../../writer.ts'; +import type { QualityDefinitionEntry } from './types.ts'; + +export interface UpdateQualityDefinitionsInput { + name: string; + entries: QualityDefinitionEntry[]; +} + +export interface UpdateQualityDefinitionsOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + currentName: string; + input: UpdateQualityDefinitionsInput; +} + +export async function updateRadarrQualityDefinitions(options: UpdateQualityDefinitionsOptions) { + const { databaseId, cache, layer, currentName, input } = options; + const db = cache.kb; + + // If renaming, check if new name already exists + if (input.name !== currentName) { + const existing = await db + .selectFrom('radarr_quality_definitions') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A radarr quality definitions config with name "${input.name}" already exists`); + } + } + + // Delete all existing entries for this config + const deleteQuery = db + .deleteFrom('radarr_quality_definitions') + .where('name', '=', currentName) + .compile(); + + // Insert all new entries + const insertQueries = input.entries.map(entry => + db + .insertInto('radarr_quality_definitions') + .values({ + name: input.name, + quality_name: entry.quality_name, + min_size: entry.min_size, + max_size: entry.max_size, + preferred_size: entry.preferred_size + }) + .compile() + ); + + return writeOperation({ + databaseId, + layer, + description: `update-radarr-quality-definitions-${input.name}`, + queries: [deleteQuery, ...insertQueries], + metadata: { + operation: 'update', + entity: 'radarr_quality_definitions', + name: input.name + } + }); +} + +export async function updateSonarrQualityDefinitions(options: UpdateQualityDefinitionsOptions) { + const { databaseId, cache, layer, currentName, input } = options; + const db = cache.kb; + + // If renaming, check if new name already exists + if (input.name !== currentName) { + const existing = await db + .selectFrom('sonarr_quality_definitions') + .where('name', '=', input.name) + .select('name') + .executeTakeFirst(); + + if (existing) { + throw new Error(`A sonarr quality definitions config with name "${input.name}" already exists`); + } + } + + // Delete all existing entries for this config + const deleteQuery = db + .deleteFrom('sonarr_quality_definitions') + .where('name', '=', currentName) + .compile(); + + // Insert all new entries + const insertQueries = input.entries.map(entry => + db + .insertInto('sonarr_quality_definitions') + .values({ + name: input.name, + quality_name: entry.quality_name, + min_size: entry.min_size, + max_size: entry.max_size, + preferred_size: entry.preferred_size + }) + .compile() + ); + + return writeOperation({ + databaseId, + layer, + description: `update-sonarr-quality-definitions-${input.name}`, + queries: [deleteQuery, ...insertQueries], + metadata: { + operation: 'update', + entity: 'sonarr_quality_definitions', + name: input.name + } + }); +} diff --git a/src/lib/server/pcd/queries/mediaManagement/types.ts b/src/lib/server/pcd/queries/mediaManagement/types.ts deleted file mode 100644 index cfd4c85..0000000 --- a/src/lib/server/pcd/queries/mediaManagement/types.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Media Management query-specific types - */ - -// ============================================================================ -// QUALITY DEFINITIONS -// ============================================================================ - -export interface QualityDefinition { - quality_name: string; - min_size: number; - max_size: number; - preferred_size: number; -} - -export interface QualityDefinitionsData { - radarr: QualityDefinition[]; - sonarr: QualityDefinition[]; -} - -// ============================================================================ -// NAMING SETTINGS -// ============================================================================ - -// Re-export naming types from shared -export type { - RadarrNaming, - SonarrNaming, - ColonReplacementFormat, - MultiEpisodeStyle, - RadarrColonReplacementFormat -} from '$lib/shared/mediaManagement.ts'; -export { - COLON_REPLACEMENT_OPTIONS, - getColonReplacementLabel, - colonReplacementFromDb, - colonReplacementToDb, - RADARR_COLON_REPLACEMENT_OPTIONS, - getRadarrColonReplacementLabel, - radarrColonReplacementFromDb, - radarrColonReplacementToDb, - MULTI_EPISODE_STYLE_OPTIONS, - getMultiEpisodeStyleLabel, - multiEpisodeStyleFromDb, - multiEpisodeStyleToDb -} from '$lib/shared/mediaManagement.ts'; - -// Import types for local use in interfaces -import type { RadarrNaming, SonarrNaming } from '$lib/shared/mediaManagement.ts'; - -export interface NamingData { - radarr: RadarrNaming | null; - sonarr: SonarrNaming | null; -} - -// ============================================================================ -// MEDIA SETTINGS -// ============================================================================ - -// Re-export from shared for convenience -export type { PropersRepacks, MediaSettings } from '$lib/shared/mediaManagement.ts'; -export { PROPERS_REPACKS_OPTIONS, getPropersRepacksLabel } from '$lib/shared/mediaManagement.ts'; - -import type { MediaSettings } from '$lib/shared/mediaManagement.ts'; - -export interface MediaSettingsData { - radarr: MediaSettings | null; - sonarr: MediaSettings | null; -} - -// ============================================================================ -// COMBINED DATA -// ============================================================================ - -export interface MediaManagementData { - qualityDefinitions: QualityDefinitionsData; - naming: NamingData; - mediaSettings: MediaSettingsData; -} - -// ============================================================================ -// ARR-TYPE SPECIFIC DATA -// ============================================================================ - -export type ArrType = 'radarr' | 'sonarr'; - -export interface RadarrMediaManagementData { - qualityDefinitions: QualityDefinition[]; - naming: RadarrNaming | null; - mediaSettings: MediaSettings | null; -} - -export interface SonarrMediaManagementData { - qualityDefinitions: QualityDefinition[]; - naming: SonarrNaming | null; - mediaSettings: MediaSettings | null; -} diff --git a/src/lib/server/pcd/queries/mediaManagement/update.ts b/src/lib/server/pcd/queries/mediaManagement/update.ts deleted file mode 100644 index 1aeef00..0000000 --- a/src/lib/server/pcd/queries/mediaManagement/update.ts +++ /dev/null @@ -1,503 +0,0 @@ -/** - * Media Management update operations - * Uses writeOperation() to append SQL operations to PCD layers - */ - -import type { PCDCache } from '../../cache.ts'; -import { writeOperation, type OperationLayer } from '../../writer.ts'; -import { logger } from '$logger/logger.ts'; -import type { - PropersRepacks, - ColonReplacementFormat, - MultiEpisodeStyle, - MediaSettings, - SonarrNaming, - RadarrNaming, - RadarrColonReplacementFormat -} from '$lib/shared/mediaManagement.ts'; -import { - colonReplacementToDb, - multiEpisodeStyleToDb, - radarrColonReplacementToDb -} from '$lib/shared/mediaManagement.ts'; - -// ============================================================================ -// MEDIA SETTINGS -// ============================================================================ - -export interface UpdateMediaSettingsInput { - propers_repacks: PropersRepacks; - enable_media_info: boolean; -} - -export interface UpdateMediaSettingsOptions { - databaseId: number; - cache: PCDCache; - layer: OperationLayer; - current: MediaSettings; - input: UpdateMediaSettingsInput; -} - -/** - * Update Radarr media settings - */ -export async function updateRadarrMediaSettings(options: UpdateMediaSettingsOptions) { - const { databaseId, cache, layer, current, input } = options; - const db = cache.kb; - - const query = db - .updateTable('radarr_media_settings') - .set({ - propers_repacks: input.propers_repacks, - enable_media_info: input.enable_media_info ? 1 : 0 - }) - .where('id', '=', current.id) - // Value guards - .where('propers_repacks', '=', current.propers_repacks) - .where('enable_media_info', '=', current.enable_media_info ? 1 : 0) - .compile(); - - // Track changes - const changes: Record = {}; - if (current.propers_repacks !== input.propers_repacks) { - changes.propers_repacks = { from: current.propers_repacks, to: input.propers_repacks }; - } - if (current.enable_media_info !== input.enable_media_info) { - changes.enable_media_info = { from: current.enable_media_info, to: input.enable_media_info }; - } - - await logger.info('Save radarr media settings', { - source: 'MediaManagement', - meta: { id: current.id, changes } - }); - - return await writeOperation({ - databaseId, - layer, - description: 'update-radarr-media-settings', - queries: [query], - metadata: { - operation: 'update', - entity: 'radarr_media_settings', - name: 'media-settings' - } - }); -} - -/** - * Update Sonarr media settings - */ -export async function updateSonarrMediaSettings(options: UpdateMediaSettingsOptions) { - const { databaseId, cache, layer, current, input } = options; - const db = cache.kb; - - const query = db - .updateTable('sonarr_media_settings') - .set({ - propers_repacks: input.propers_repacks, - enable_media_info: input.enable_media_info ? 1 : 0 - }) - .where('id', '=', current.id) - // Value guards - .where('propers_repacks', '=', current.propers_repacks) - .where('enable_media_info', '=', current.enable_media_info ? 1 : 0) - .compile(); - - // Track changes - const changes: Record = {}; - if (current.propers_repacks !== input.propers_repacks) { - changes.propers_repacks = { from: current.propers_repacks, to: input.propers_repacks }; - } - if (current.enable_media_info !== input.enable_media_info) { - changes.enable_media_info = { from: current.enable_media_info, to: input.enable_media_info }; - } - - await logger.info('Save sonarr media settings', { - source: 'MediaManagement', - meta: { id: current.id, changes } - }); - - return await writeOperation({ - databaseId, - layer, - description: 'update-sonarr-media-settings', - queries: [query], - metadata: { - operation: 'update', - entity: 'sonarr_media_settings', - name: 'media-settings' - } - }); -} - -// ============================================================================ -// SONARR NAMING -// ============================================================================ - -export interface UpdateSonarrNamingInput { - rename: boolean; - replace_illegal_characters: boolean; - colon_replacement_format: ColonReplacementFormat; - custom_colon_replacement_format: string | null; - standard_episode_format: string; - daily_episode_format: string; - anime_episode_format: string; - series_folder_format: string; - season_folder_format: string; - multi_episode_style: MultiEpisodeStyle; -} - -export interface UpdateSonarrNamingOptions { - databaseId: number; - cache: PCDCache; - layer: OperationLayer; - current: SonarrNaming; - input: UpdateSonarrNamingInput; -} - -/** - * Update Sonarr naming settings - */ -export async function updateSonarrNaming(options: UpdateSonarrNamingOptions) { - const { databaseId, cache, layer, current, input } = options; - const db = cache.kb; - - const query = db - .updateTable('sonarr_naming') - .set({ - rename: input.rename ? 1 : 0, - replace_illegal_characters: input.replace_illegal_characters ? 1 : 0, - colon_replacement_format: colonReplacementToDb(input.colon_replacement_format), - custom_colon_replacement_format: input.custom_colon_replacement_format, - standard_episode_format: input.standard_episode_format, - daily_episode_format: input.daily_episode_format, - anime_episode_format: input.anime_episode_format, - series_folder_format: input.series_folder_format, - season_folder_format: input.season_folder_format, - multi_episode_style: multiEpisodeStyleToDb(input.multi_episode_style) - }) - .where('id', '=', current.id) - // Value guards - check key fields match expected values - .where('rename', '=', current.rename ? 1 : 0) - .where('replace_illegal_characters', '=', current.replace_illegal_characters ? 1 : 0) - .compile(); - - // Track changes - const changes: Record = {}; - if (current.rename !== input.rename) { - changes.rename = { from: current.rename, to: input.rename }; - } - if (current.replace_illegal_characters !== input.replace_illegal_characters) { - changes.replace_illegal_characters = { - from: current.replace_illegal_characters, - to: input.replace_illegal_characters - }; - } - if (current.colon_replacement_format !== input.colon_replacement_format) { - changes.colon_replacement_format = { - from: current.colon_replacement_format, - to: input.colon_replacement_format - }; - } - if (current.custom_colon_replacement_format !== input.custom_colon_replacement_format) { - changes.custom_colon_replacement_format = { - from: current.custom_colon_replacement_format, - to: input.custom_colon_replacement_format - }; - } - if (current.standard_episode_format !== input.standard_episode_format) { - changes.standard_episode_format = { - from: current.standard_episode_format, - to: input.standard_episode_format - }; - } - if (current.daily_episode_format !== input.daily_episode_format) { - changes.daily_episode_format = { - from: current.daily_episode_format, - to: input.daily_episode_format - }; - } - if (current.anime_episode_format !== input.anime_episode_format) { - changes.anime_episode_format = { - from: current.anime_episode_format, - to: input.anime_episode_format - }; - } - if (current.series_folder_format !== input.series_folder_format) { - changes.series_folder_format = { - from: current.series_folder_format, - to: input.series_folder_format - }; - } - if (current.season_folder_format !== input.season_folder_format) { - changes.season_folder_format = { - from: current.season_folder_format, - to: input.season_folder_format - }; - } - if (current.multi_episode_style !== input.multi_episode_style) { - changes.multi_episode_style = { - from: current.multi_episode_style, - to: input.multi_episode_style - }; - } - - await logger.info('Save sonarr naming settings', { - source: 'MediaManagement', - meta: { id: current.id, changes } - }); - - return await writeOperation({ - databaseId, - layer, - description: 'update-sonarr-naming', - queries: [query], - metadata: { - operation: 'update', - entity: 'sonarr_naming', - name: 'naming-settings' - } - }); -} - -// ============================================================================ -// RADARR NAMING -// ============================================================================ - -export interface UpdateRadarrNamingInput { - rename: boolean; - replace_illegal_characters: boolean; - colon_replacement_format: RadarrColonReplacementFormat; - movie_format: string; - movie_folder_format: string; -} - -export interface UpdateRadarrNamingOptions { - databaseId: number; - cache: PCDCache; - layer: OperationLayer; - current: RadarrNaming; - input: UpdateRadarrNamingInput; -} - -/** - * Update Radarr naming settings - */ -export async function updateRadarrNaming(options: UpdateRadarrNamingOptions) { - const { databaseId, cache, layer, current, input } = options; - const db = cache.kb; - - const query = db - .updateTable('radarr_naming') - .set({ - rename: input.rename ? 1 : 0, - replace_illegal_characters: input.replace_illegal_characters ? 1 : 0, - colon_replacement_format: radarrColonReplacementToDb(input.colon_replacement_format), - movie_format: input.movie_format, - movie_folder_format: input.movie_folder_format - }) - .where('id', '=', current.id) - // Value guards - check key fields match expected values - .where('rename', '=', current.rename ? 1 : 0) - .where('replace_illegal_characters', '=', current.replace_illegal_characters ? 1 : 0) - .compile(); - - // Track changes - const changes: Record = {}; - if (current.rename !== input.rename) { - changes.rename = { from: current.rename, to: input.rename }; - } - if (current.replace_illegal_characters !== input.replace_illegal_characters) { - changes.replace_illegal_characters = { - from: current.replace_illegal_characters, - to: input.replace_illegal_characters - }; - } - if (current.colon_replacement_format !== input.colon_replacement_format) { - changes.colon_replacement_format = { - from: current.colon_replacement_format, - to: input.colon_replacement_format - }; - } - if (current.movie_format !== input.movie_format) { - changes.movie_format = { from: current.movie_format, to: input.movie_format }; - } - if (current.movie_folder_format !== input.movie_folder_format) { - changes.movie_folder_format = { - from: current.movie_folder_format, - to: input.movie_folder_format - }; - } - - await logger.info('Save radarr naming settings', { - source: 'MediaManagement', - meta: { id: current.id, changes } - }); - - return await writeOperation({ - databaseId, - layer, - description: 'update-radarr-naming', - queries: [query], - metadata: { - operation: 'update', - entity: 'radarr_naming', - name: 'naming-settings' - } - }); -} - -// ============================================================================ -// QUALITY DEFINITIONS -// ============================================================================ - -import type { QualityDefinition } from './types.ts'; - -export interface UpdateQualityDefinitionInput { - quality_name: string; - min_size: number; - max_size: number; - preferred_size: number; -} - -export interface UpdateQualityDefinitionsOptions { - databaseId: number; - cache: PCDCache; - layer: OperationLayer; - current: QualityDefinition[]; - input: UpdateQualityDefinitionInput[]; -} - -/** - * Update Radarr quality definitions - */ -export async function updateRadarrQualityDefinitions(options: UpdateQualityDefinitionsOptions) { - const { databaseId, cache, layer, current, input } = options; - const db = cache.kb; - - // Track changes per quality definition - const changes: Record = {}; - - // Build queries for each changed definition - const queries = input.map((def) => { - const currentDef = current.find((c) => c.quality_name === def.quality_name); - if (!currentDef) { - throw new Error(`Quality definition not found for quality_name: ${def.quality_name}`); - } - - // Track changes for this definition - const defChanges: Record = {}; - if (currentDef.min_size !== def.min_size) { - defChanges.min_size = { from: currentDef.min_size, to: def.min_size }; - } - if (currentDef.max_size !== def.max_size) { - defChanges.max_size = { from: currentDef.max_size, to: def.max_size }; - } - if (currentDef.preferred_size !== def.preferred_size) { - defChanges.preferred_size = { from: currentDef.preferred_size, to: def.preferred_size }; - } - if (Object.keys(defChanges).length > 0) { - changes[currentDef.quality_name] = defChanges as { from: unknown; to: unknown }; - } - - return ( - db - .updateTable('radarr_quality_definitions') - .set({ - min_size: def.min_size, - max_size: def.max_size, - preferred_size: def.preferred_size - }) - .where('quality_name', '=', def.quality_name) - // Value guards - .where('min_size', '=', currentDef.min_size) - .where('max_size', '=', currentDef.max_size) - .where('preferred_size', '=', currentDef.preferred_size) - .compile() - ); - }); - - await logger.info('Save radarr quality definitions', { - source: 'MediaManagement', - meta: { changes } - }); - - return await writeOperation({ - databaseId, - layer, - description: 'update-radarr-quality-definitions', - queries, - metadata: { - operation: 'update', - entity: 'radarr_quality_definitions', - name: 'quality-definitions' - } - }); -} - -/** - * Update Sonarr quality definitions - */ -export async function updateSonarrQualityDefinitions(options: UpdateQualityDefinitionsOptions) { - const { databaseId, cache, layer, current, input } = options; - const db = cache.kb; - - // Track changes per quality definition - const changes: Record = {}; - - // Build queries for each changed definition - const queries = input.map((def) => { - const currentDef = current.find((c) => c.quality_name === def.quality_name); - if (!currentDef) { - throw new Error(`Quality definition not found for quality_name: ${def.quality_name}`); - } - - // Track changes for this definition - const defChanges: Record = {}; - if (currentDef.min_size !== def.min_size) { - defChanges.min_size = { from: currentDef.min_size, to: def.min_size }; - } - if (currentDef.max_size !== def.max_size) { - defChanges.max_size = { from: currentDef.max_size, to: def.max_size }; - } - if (currentDef.preferred_size !== def.preferred_size) { - defChanges.preferred_size = { from: currentDef.preferred_size, to: def.preferred_size }; - } - if (Object.keys(defChanges).length > 0) { - changes[currentDef.quality_name] = defChanges as { from: unknown; to: unknown }; - } - - return ( - db - .updateTable('sonarr_quality_definitions') - .set({ - min_size: def.min_size, - max_size: def.max_size, - preferred_size: def.preferred_size - }) - .where('quality_name', '=', def.quality_name) - // Value guards - .where('min_size', '=', currentDef.min_size) - .where('max_size', '=', currentDef.max_size) - .where('preferred_size', '=', currentDef.preferred_size) - .compile() - ); - }); - - await logger.info('Save sonarr quality definitions', { - source: 'MediaManagement', - meta: { changes } - }); - - return await writeOperation({ - databaseId, - layer, - description: 'update-sonarr-quality-definitions', - queries, - metadata: { - operation: 'update', - entity: 'sonarr_quality_definitions', - name: 'quality-definitions' - } - }); -} diff --git a/src/lib/server/pcd/schema.ts b/src/lib/server/pcd/schema.ts index 0fef741..dc48f9d 100644 --- a/src/lib/server/pcd/schema.ts +++ b/src/lib/server/pcd/schema.ts @@ -260,6 +260,7 @@ export interface DelayProfilesTable { // ============================================================================ export interface RadarrQualityDefinitionsTable { + name: string; quality_name: string; min_size: number; max_size: number; @@ -269,6 +270,7 @@ export interface RadarrQualityDefinitionsTable { } export interface SonarrQualityDefinitionsTable { + name: string; quality_name: string; min_size: number; max_size: number; @@ -278,7 +280,7 @@ export interface SonarrQualityDefinitionsTable { } export interface RadarrNamingTable { - id: number; + name: string; rename: number; movie_format: string; movie_folder_format: string; @@ -289,7 +291,7 @@ export interface RadarrNamingTable { } export interface SonarrNamingTable { - id: number; + name: string; rename: number; standard_episode_format: string; daily_episode_format: string; @@ -305,7 +307,7 @@ export interface SonarrNamingTable { } export interface RadarrMediaSettingsTable { - id: number; + name: string; propers_repacks: string; enable_media_info: number; created_at: Generated; @@ -313,7 +315,7 @@ export interface RadarrMediaSettingsTable { } export interface SonarrMediaSettingsTable { - id: number; + name: string; propers_repacks: string; enable_media_info: number; created_at: Generated; diff --git a/src/lib/server/sync/mediaManagement/syncer.ts b/src/lib/server/sync/mediaManagement/syncer.ts index d387065..716511a 100644 --- a/src/lib/server/sync/mediaManagement/syncer.ts +++ b/src/lib/server/sync/mediaManagement/syncer.ts @@ -17,9 +17,11 @@ import { BaseSyncer, type SyncResult } from '../base.ts'; import { arrSyncQueries } from '$db/queries/arrSync.ts'; import { getCache, type PCDCache } from '$pcd/cache.ts'; -import { getRadarr, getSonarr } from '$pcd/queries/mediaManagement/get.ts'; +import type { QualityDefinition } from '$pcd/queries/mediaManagement/combined.ts'; +import { getRadarrByName as getRadarrMediaSettings, getSonarrByName as getSonarrMediaSettings } from '$pcd/queries/mediaManagement/media-settings/read.ts'; +import { getRadarrByName as getRadarrNaming, getSonarrByName as getSonarrNaming } from '$pcd/queries/mediaManagement/naming/read.ts'; +import { getRadarrByName as getRadarrQualityDefs, getSonarrByName as getSonarrQualityDefs } from '$pcd/queries/mediaManagement/quality-definitions/read.ts'; import type { MediaSettings, RadarrNaming, SonarrNaming } from '$lib/shared/mediaManagement.ts'; -import type { QualityDefinition } from '$pcd/queries/mediaManagement/types.ts'; import { colonReplacementToDb, multiEpisodeStyleToDb } from '$lib/shared/mediaManagement.ts'; import type { ArrType, @@ -58,16 +60,19 @@ export class MediaManagementSyncer extends BaseSyncer { source: 'Sync:MediaManagement', meta: { instanceId: this.instanceId, - hasMediaSettings: !!syncConfig.mediaSettingsDatabaseId, - hasNaming: !!syncConfig.namingDatabaseId, - hasQualityDefs: !!syncConfig.qualityDefinitionsDatabaseId + hasMediaSettings: !!syncConfig.mediaSettingsDatabaseId && !!syncConfig.mediaSettingsConfigName, + hasNaming: !!syncConfig.namingDatabaseId && !!syncConfig.namingConfigName, + hasQualityDefs: !!syncConfig.qualityDefinitionsDatabaseId && !!syncConfig.qualityDefinitionsConfigName } }); - // Sync media settings if configured - if (syncConfig.mediaSettingsDatabaseId) { + // Sync media settings if configured (both database and config name required) + if (syncConfig.mediaSettingsDatabaseId && syncConfig.mediaSettingsConfigName) { try { - const synced = await this.syncMediaSettings(syncConfig.mediaSettingsDatabaseId); + const synced = await this.syncMediaSettings( + syncConfig.mediaSettingsDatabaseId, + syncConfig.mediaSettingsConfigName + ); if (synced) totalSynced++; } catch (error) { const msg = error instanceof Error ? error.message : 'Unknown error'; @@ -79,10 +84,13 @@ export class MediaManagementSyncer extends BaseSyncer { } } - // Sync naming if configured - if (syncConfig.namingDatabaseId) { + // Sync naming if configured (both database and config name required) + if (syncConfig.namingDatabaseId && syncConfig.namingConfigName) { try { - const synced = await this.syncNaming(syncConfig.namingDatabaseId); + const synced = await this.syncNaming( + syncConfig.namingDatabaseId, + syncConfig.namingConfigName + ); if (synced) totalSynced++; } catch (error) { const msg = error instanceof Error ? error.message : 'Unknown error'; @@ -94,10 +102,13 @@ export class MediaManagementSyncer extends BaseSyncer { } } - // Sync quality definitions if configured - if (syncConfig.qualityDefinitionsDatabaseId) { + // Sync quality definitions if configured (both database and config name required) + if (syncConfig.qualityDefinitionsDatabaseId && syncConfig.qualityDefinitionsConfigName) { try { - const synced = await this.syncQualityDefinitions(syncConfig.qualityDefinitionsDatabaseId); + const synced = await this.syncQualityDefinitions( + syncConfig.qualityDefinitionsDatabaseId, + syncConfig.qualityDefinitionsConfigName + ); if (synced) totalSynced++; } catch (error) { const msg = error instanceof Error ? error.message : 'Unknown error'; @@ -128,7 +139,7 @@ export class MediaManagementSyncer extends BaseSyncer { // Media Settings // ========================================================================= - private async syncMediaSettings(databaseId: number): Promise { + private async syncMediaSettings(databaseId: number, configName: string): Promise { const cache = getCache(databaseId); if (!cache) { await logger.warn(`PCD cache not found for database ${databaseId}`, { @@ -138,18 +149,18 @@ export class MediaManagementSyncer extends BaseSyncer { return false; } - // Fetch from PCD + // Fetch from PCD by config name let mediaSettings: MediaSettings | null = null; if (this.instanceType === 'radarr') { - mediaSettings = (await getRadarr(cache)).mediaSettings; + mediaSettings = await getRadarrMediaSettings(cache, configName); } else if (this.instanceType === 'sonarr') { - mediaSettings = (await getSonarr(cache)).mediaSettings; + mediaSettings = await getSonarrMediaSettings(cache, configName); } if (!mediaSettings) { - await logger.debug('No media settings found in PCD', { + await logger.debug(`Media settings config "${configName}" not found in PCD`, { source: 'Sync:MediaSettings', - meta: { instanceId: this.instanceId } + meta: { instanceId: this.instanceId, configName } }); return false; } @@ -168,6 +179,7 @@ export class MediaManagementSyncer extends BaseSyncer { source: 'Sync:MediaSettings', meta: { instanceId: this.instanceId, + configName, propersRepacks: updatedConfig.downloadPropersAndRepacks, enableMediaInfo: updatedConfig.enableMediaInfo } @@ -190,7 +202,7 @@ export class MediaManagementSyncer extends BaseSyncer { // Naming // ========================================================================= - private async syncNaming(databaseId: number): Promise { + private async syncNaming(databaseId: number, configName: string): Promise { const cache = getCache(databaseId); if (!cache) { await logger.warn(`PCD cache not found for database ${databaseId}`, { @@ -201,9 +213,9 @@ export class MediaManagementSyncer extends BaseSyncer { } if (this.instanceType === 'radarr') { - return this.syncRadarrNaming(cache, databaseId); + return this.syncRadarrNaming(cache, configName); } else if (this.instanceType === 'sonarr') { - return this.syncSonarrNaming(cache, databaseId); + return this.syncSonarrNaming(cache, configName); } await logger.warn(`Unsupported instance type for naming sync: ${this.instanceType}`, { @@ -214,16 +226,14 @@ export class MediaManagementSyncer extends BaseSyncer { } private async syncRadarrNaming( - cache: ReturnType, - databaseId: number + cache: PCDCache, + configName: string ): Promise { - if (!cache) return false; - - const naming = (await getRadarr(cache)).naming; + const naming = await getRadarrNaming(cache, configName); if (!naming) { - await logger.debug('No Radarr naming found in PCD', { + await logger.debug(`Radarr naming config "${configName}" not found in PCD`, { source: 'Sync:Naming', - meta: { instanceId: this.instanceId, databaseId } + meta: { instanceId: this.instanceId, configName } }); return false; } @@ -245,6 +255,7 @@ export class MediaManagementSyncer extends BaseSyncer { source: 'Sync:Naming', meta: { instanceId: this.instanceId, + configName, renameMovies: updatedConfig.renameMovies, colonReplacementFormat: updatedConfig.colonReplacementFormat } @@ -255,16 +266,14 @@ export class MediaManagementSyncer extends BaseSyncer { } private async syncSonarrNaming( - cache: ReturnType, - databaseId: number + cache: PCDCache, + configName: string ): Promise { - if (!cache) return false; - - const naming = (await getSonarr(cache)).naming; + const naming = await getSonarrNaming(cache, configName); if (!naming) { - await logger.debug('No Sonarr naming found in PCD', { + await logger.debug(`Sonarr naming config "${configName}" not found in PCD`, { source: 'Sync:Naming', - meta: { instanceId: this.instanceId, databaseId } + meta: { instanceId: this.instanceId, configName } }); return false; } @@ -291,6 +300,7 @@ export class MediaManagementSyncer extends BaseSyncer { source: 'Sync:Naming', meta: { instanceId: this.instanceId, + configName, renameEpisodes: updatedConfig.renameEpisodes, colonReplacementFormat: updatedConfig.colonReplacementFormat, multiEpisodeStyle: updatedConfig.multiEpisodeStyle @@ -305,7 +315,7 @@ export class MediaManagementSyncer extends BaseSyncer { // Quality Definitions // ========================================================================= - private async syncQualityDefinitions(databaseId: number): Promise { + private async syncQualityDefinitions(databaseId: number, configName: string): Promise { const cache = getCache(databaseId); if (!cache) { await logger.warn(`PCD cache not found for database ${databaseId}`, { @@ -315,18 +325,29 @@ export class MediaManagementSyncer extends BaseSyncer { return false; } - // Fetch quality definitions from PCD - let pcdDefinitions: QualityDefinition[] = []; - if (this.instanceType === 'radarr') { - pcdDefinitions = (await getRadarr(cache)).qualityDefinitions; - } else if (this.instanceType === 'sonarr') { - pcdDefinitions = (await getSonarr(cache)).qualityDefinitions; + // Fetch quality definitions from PCD by config name + const getByName = this.instanceType === 'radarr' ? getRadarrQualityDefs : getSonarrQualityDefs; + const qualityDefsConfig = await getByName(cache, configName); + + if (!qualityDefsConfig) { + await logger.debug(`Quality definitions config "${configName}" not found in PCD`, { + source: 'Sync:QualityDefinitions', + meta: { instanceId: this.instanceId, configName } + }); + return false; } + const pcdDefinitions: QualityDefinition[] = qualityDefsConfig.entries.map(entry => ({ + quality_name: entry.quality_name, + min_size: entry.min_size, + max_size: entry.max_size, + preferred_size: entry.preferred_size + })); + if (pcdDefinitions.length === 0) { - await logger.debug('No quality definitions found in PCD', { + await logger.debug(`Quality definitions config "${configName}" has no entries`, { source: 'Sync:QualityDefinitions', - meta: { instanceId: this.instanceId } + meta: { instanceId: this.instanceId, configName } }); return false; } @@ -369,23 +390,24 @@ export class MediaManagementSyncer extends BaseSyncer { } // Update the definition + // PCD uses 0 for "unlimited", Radarr API uses null arrDef.minSize = pcdDef.min_size; - arrDef.maxSize = pcdDef.max_size; - arrDef.preferredSize = pcdDef.preferred_size; + arrDef.maxSize = pcdDef.max_size === 0 ? null : pcdDef.max_size; + arrDef.preferredSize = pcdDef.preferred_size === 0 ? null : pcdDef.preferred_size; updatedCount++; } if (updatedCount === 0) { await logger.debug('No quality definitions matched for update', { source: 'Sync:QualityDefinitions', - meta: { instanceId: this.instanceId } + meta: { instanceId: this.instanceId, configName } }); return false; } await logger.debug(`Updating ${updatedCount} quality definitions`, { source: 'Sync:QualityDefinitions', - meta: { instanceId: this.instanceId, updatedCount } + meta: { instanceId: this.instanceId, configName, updatedCount } }); // PUT the full array back diff --git a/src/lib/shared/mediaManagement.ts b/src/lib/shared/mediaManagement.ts index e1e68eb..9c86740 100644 --- a/src/lib/shared/mediaManagement.ts +++ b/src/lib/shared/mediaManagement.ts @@ -14,7 +14,7 @@ export type PropersRepacks = 'doNotPrefer' | 'preferAndUpgrade' | 'doNotUpgradeA // ============================================================================ export interface MediaSettings { - id: number; + name: string; propers_repacks: PropersRepacks; enable_media_info: boolean; } @@ -204,7 +204,7 @@ export function radarrColonReplacementToDb(value: RadarrColonReplacementFormat): } export interface RadarrNaming { - id: number; + name: string; rename: boolean; movie_format: string; movie_folder_format: string; @@ -213,7 +213,7 @@ export interface RadarrNaming { } export interface SonarrNaming { - id: number; + name: string; rename: boolean; replace_illegal_characters: boolean; colon_replacement_format: ColonReplacementFormat; diff --git a/src/routes/arr/[id]/sync/+page.server.ts b/src/routes/arr/[id]/sync/+page.server.ts index f8ce2be..40fb36b 100644 --- a/src/routes/arr/[id]/sync/+page.server.ts +++ b/src/routes/arr/[id]/sync/+page.server.ts @@ -6,6 +6,9 @@ import { pcdManager } from '$pcd/pcd.ts'; import { logger } from '$logger/logger.ts'; import * as qualityProfileQueries from '$pcd/queries/qualityProfiles/index.ts'; import * as delayProfileQueries from '$pcd/queries/delayProfiles/index.ts'; +import * as namingQueries from '$pcd/queries/mediaManagement/naming/index.ts'; +import * as qualityDefinitionsQueries from '$pcd/queries/mediaManagement/quality-definitions/index.ts'; +import * as mediaSettingsQueries from '$pcd/queries/mediaManagement/media-settings/index.ts'; import { calculateNextRun } from '$lib/server/sync/utils.ts'; import { updateSyncArrJobEnabled } from '$lib/server/jobs/init.ts'; @@ -24,8 +27,9 @@ export const load: ServerLoad = async ({ params }) => { // Get all databases const databases = pcdManager.getAll(); + const arrType = instance.type as 'radarr' | 'sonarr'; - // Fetch profiles from each database + // Fetch profiles and configs from each database const databasesWithProfiles = await Promise.all( databases.map(async (db) => { const cache = pcdManager.getCache(db.id); @@ -34,20 +38,40 @@ export const load: ServerLoad = async ({ params }) => { id: db.id, name: db.name, qualityProfiles: [], - delayProfiles: [] + delayProfiles: [], + namingConfigs: [], + qualityDefinitionsConfigs: [], + mediaSettingsConfigs: [] }; } - const [qualityProfiles, delayProfiles] = await Promise.all([ + const [qualityProfiles, delayProfiles, allNamingConfigs, allQualityDefinitionsConfigs, allMediaSettingsConfigs] = await Promise.all([ qualityProfileQueries.list(cache), - delayProfileQueries.list(cache) + delayProfileQueries.list(cache), + namingQueries.list(cache), + qualityDefinitionsQueries.list(cache), + mediaSettingsQueries.list(cache) ]); + // Filter configs by arr type - only show configs for the instance's arr type + const namingConfigs = allNamingConfigs + .filter(c => c.arr_type === arrType) + .map(c => ({ name: c.name })); + const qualityDefinitionsConfigs = allQualityDefinitionsConfigs + .filter(c => c.arr_type === arrType) + .map(c => ({ name: c.name })); + const mediaSettingsConfigs = allMediaSettingsConfigs + .filter(c => c.arr_type === arrType) + .map(c => ({ name: c.name })); + return { id: db.id, name: db.name, qualityProfiles, - delayProfiles + delayProfiles, + namingConfigs, + qualityDefinitionsConfigs, + mediaSettingsConfigs }; }) ); @@ -154,10 +178,13 @@ export const actions: Actions = { const instance = arrInstancesQueries.getById(id); const formData = await request.formData(); const namingDatabaseId = formData.get('namingDatabaseId') as string | null; + const namingConfigName = formData.get('namingConfigName') as string | null; const qualityDefinitionsDatabaseId = formData.get('qualityDefinitionsDatabaseId') as | string | null; + const qualityDefinitionsConfigName = formData.get('qualityDefinitionsConfigName') as string | null; const mediaSettingsDatabaseId = formData.get('mediaSettingsDatabaseId') as string | null; + const mediaSettingsConfigName = formData.get('mediaSettingsConfigName') as string | null; const trigger = formData.get('trigger') as SyncTrigger; const cron = formData.get('cron') as string | null; @@ -166,12 +193,15 @@ export const actions: Actions = { const effectiveCron = cron || null; arrSyncQueries.saveMediaManagementSync(id, { namingDatabaseId: namingDatabaseId ? parseInt(namingDatabaseId, 10) : null, + namingConfigName: namingConfigName || null, qualityDefinitionsDatabaseId: qualityDefinitionsDatabaseId ? parseInt(qualityDefinitionsDatabaseId, 10) : null, + qualityDefinitionsConfigName: qualityDefinitionsConfigName || null, mediaSettingsDatabaseId: mediaSettingsDatabaseId ? parseInt(mediaSettingsDatabaseId, 10) : null, + mediaSettingsConfigName: mediaSettingsConfigName || null, trigger: effectiveTrigger, cron: effectiveCron, nextRunAt: effectiveTrigger === 'schedule' ? calculateNextRun(effectiveCron) : null diff --git a/src/routes/arr/[id]/sync/+page.svelte b/src/routes/arr/[id]/sync/+page.svelte index 2a4fee0..7beb2a5 100644 --- a/src/routes/arr/[id]/sync/+page.svelte +++ b/src/routes/arr/[id]/sync/+page.svelte @@ -41,8 +41,11 @@ let mediaManagementState = { namingDatabaseId: data.syncData.mediaManagement.namingDatabaseId, + namingConfigName: data.syncData.mediaManagement.namingConfigName, qualityDefinitionsDatabaseId: data.syncData.mediaManagement.qualityDefinitionsDatabaseId, - mediaSettingsDatabaseId: data.syncData.mediaManagement.mediaSettingsDatabaseId + qualityDefinitionsConfigName: data.syncData.mediaManagement.qualityDefinitionsConfigName, + mediaSettingsDatabaseId: data.syncData.mediaManagement.mediaSettingsDatabaseId, + mediaSettingsConfigName: data.syncData.mediaManagement.mediaSettingsConfigName }; let mediaManagementTrigger: SyncTrigger = data.syncData.mediaManagement.trigger; let mediaManagementCron: string = data.syncData.mediaManagement.cron || '0 * * * *'; @@ -69,8 +72,11 @@ $: hasMediaManagement = typeof mediaManagementState.namingDatabaseId === 'number' && + typeof mediaManagementState.namingConfigName === 'string' && typeof mediaManagementState.qualityDefinitionsDatabaseId === 'number' && - typeof mediaManagementState.mediaSettingsDatabaseId === 'number'; + typeof mediaManagementState.qualityDefinitionsConfigName === 'string' && + typeof mediaManagementState.mediaSettingsDatabaseId === 'number' && + typeof mediaManagementState.mediaSettingsConfigName === 'string'; $: hasDelayProfile = typeof delayProfileState.databaseId === 'number' && diff --git a/src/routes/arr/[id]/sync/components/MediaManagement.svelte b/src/routes/arr/[id]/sync/components/MediaManagement.svelte index 92b92e2..de05de4 100644 --- a/src/routes/arr/[id]/sync/components/MediaManagement.svelte +++ b/src/routes/arr/[id]/sync/components/MediaManagement.svelte @@ -3,46 +3,129 @@ import SyncFooter from './SyncFooter.svelte'; import { alertStore } from '$lib/client/alerts/store.ts'; + interface ConfigOption { + name: string; + } + interface Database { id: number; name: string; + namingConfigs: ConfigOption[]; + qualityDefinitionsConfigs: ConfigOption[]; + mediaSettingsConfigs: ConfigOption[]; } export let databases: Database[]; export let state: { namingDatabaseId: number | null; + namingConfigName: string | null; qualityDefinitionsDatabaseId: number | null; + qualityDefinitionsConfigName: string | null; mediaSettingsDatabaseId: number | null; + mediaSettingsConfigName: string | null; } = { namingDatabaseId: null, + namingConfigName: null, qualityDefinitionsDatabaseId: null, - mediaSettingsDatabaseId: null + qualityDefinitionsConfigName: null, + mediaSettingsDatabaseId: null, + mediaSettingsConfigName: null }; let showNamingDropdown = false; let showQualityDropdown = false; let showMediaDropdown = false; - function getSelectedName(id: number | null): string { - if (id === null) return 'None'; - return databases.find((db) => db.id === id)?.name ?? 'None'; + // Build flat list of options: { databaseId, databaseName, configName } + type SelectionOption = { + databaseId: number; + databaseName: string; + configName: string; + }; + + function getNamingOptions(): SelectionOption[] { + const options: SelectionOption[] = []; + for (const db of databases) { + for (const config of db.namingConfigs) { + options.push({ + databaseId: db.id, + databaseName: db.name, + configName: config.name + }); + } + } + return options; } - function selectNaming(id: number | null) { - state.namingDatabaseId = id; + function getQualityDefinitionsOptions(): SelectionOption[] { + const options: SelectionOption[] = []; + for (const db of databases) { + for (const config of db.qualityDefinitionsConfigs) { + options.push({ + databaseId: db.id, + databaseName: db.name, + configName: config.name + }); + } + } + return options; + } + + function getMediaSettingsOptions(): SelectionOption[] { + const options: SelectionOption[] = []; + for (const db of databases) { + for (const config of db.mediaSettingsConfigs) { + options.push({ + databaseId: db.id, + databaseName: db.name, + configName: config.name + }); + } + } + return options; + } + + $: namingOptions = getNamingOptions(); + $: qualityDefinitionsOptions = getQualityDefinitionsOptions(); + $: mediaSettingsOptions = getMediaSettingsOptions(); + + function getSelectedLabel(databaseId: number | null, configName: string | null): string { + if (databaseId === null || configName === null) return 'None'; + const db = databases.find((d) => d.id === databaseId); + if (!db) return 'None'; + return `${db.name} / ${configName}`; + } + + function selectNaming(databaseId: number | null, configName: string | null) { + state.namingDatabaseId = databaseId; + state.namingConfigName = configName; showNamingDropdown = false; } - function selectQuality(id: number | null) { - state.qualityDefinitionsDatabaseId = id; + function selectQuality(databaseId: number | null, configName: string | null) { + state.qualityDefinitionsDatabaseId = databaseId; + state.qualityDefinitionsConfigName = configName; showQualityDropdown = false; } - function selectMedia(id: number | null) { - state.mediaSettingsDatabaseId = id; + function selectMedia(databaseId: number | null, configName: string | null) { + state.mediaSettingsDatabaseId = databaseId; + state.mediaSettingsConfigName = configName; showMediaDropdown = false; } + function isNamingSelected(databaseId: number, configName: string): boolean { + return state.namingDatabaseId === databaseId && state.namingConfigName === configName; + } + + function isQualitySelected(databaseId: number, configName: string): boolean { + return state.qualityDefinitionsDatabaseId === databaseId && state.qualityDefinitionsConfigName === configName; + } + + function isMediaSelected(databaseId: number, configName: string): boolean { + return state.mediaSettingsDatabaseId === databaseId && state.mediaSettingsConfigName === configName; + } + export let syncTrigger: 'manual' | 'on_pull' | 'on_change' | 'schedule' = 'manual'; export let cronExpression: string = '0 * * * *'; @@ -60,11 +143,14 @@ try { const formData = new FormData(); formData.set('namingDatabaseId', state.namingDatabaseId?.toString() ?? ''); + formData.set('namingConfigName', state.namingConfigName ?? ''); formData.set( 'qualityDefinitionsDatabaseId', state.qualityDefinitionsDatabaseId?.toString() ?? '' ); + formData.set('qualityDefinitionsConfigName', state.qualityDefinitionsConfigName ?? ''); formData.set('mediaSettingsDatabaseId', state.mediaSettingsDatabaseId?.toString() ?? ''); + formData.set('mediaSettingsConfigName', state.mediaSettingsConfigName ?? ''); formData.set('trigger', syncTrigger); formData.set('cron', cronExpression); @@ -115,7 +201,7 @@

Media Management

- Select which database to use for each media management setting + Select which database config to use for each media management setting

@@ -134,17 +220,17 @@ on:blur={() => setTimeout(() => (showNamingDropdown = false), 200)} class="flex w-full items-center justify-between rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700" > - {getSelectedName(state.namingDatabaseId)} - + {getSelectedLabel(state.namingDatabaseId, state.namingConfigName)} + {#if showNamingDropdown}
- {#each databases as database} + {#each namingOptions as option} {/each} + {#if namingOptions.length === 0} +
+ No naming configs available +
+ {/if}
{/if} @@ -181,17 +272,17 @@ on:blur={() => setTimeout(() => (showQualityDropdown = false), 200)} class="flex w-full items-center justify-between rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700" > - {getSelectedName(state.qualityDefinitionsDatabaseId)} - + {getSelectedLabel(state.qualityDefinitionsDatabaseId, state.qualityDefinitionsConfigName)} + {#if showQualityDropdown}
- {#each databases as database} + {#each qualityDefinitionsOptions as option} {/each} + {#if qualityDefinitionsOptions.length === 0} +
+ No quality definitions configs available +
+ {/if}
{/if} @@ -228,17 +324,17 @@ on:blur={() => setTimeout(() => (showMediaDropdown = false), 200)} class="flex w-full items-center justify-between rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700" > - {getSelectedName(state.mediaSettingsDatabaseId)} - + {getSelectedLabel(state.mediaSettingsDatabaseId, state.mediaSettingsConfigName)} + {#if showMediaDropdown}
- {#each databases as database} + {#each mediaSettingsOptions as option} {/each} + {#if mediaSettingsOptions.length === 0} +
+ No media settings configs available +
+ {/if}
{/if} diff --git a/src/routes/arr/components/InstanceForm.svelte b/src/routes/arr/components/InstanceForm.svelte index af0040e..560de8f 100644 --- a/src/routes/arr/components/InstanceForm.svelte +++ b/src/routes/arr/components/InstanceForm.svelte @@ -200,9 +200,9 @@ >
- + {#if mode === 'edit'}

Type cannot be changed after creation @@ -277,9 +277,9 @@

- +

Press Enter to add a tag, Backspace to remove

diff --git a/src/routes/databases/components/InstanceForm.svelte b/src/routes/databases/components/InstanceForm.svelte index f9689f1..3814f2f 100644 --- a/src/routes/databases/components/InstanceForm.svelte +++ b/src/routes/databases/components/InstanceForm.svelte @@ -203,9 +203,9 @@
- +

How often to check for updates from the remote repository

@@ -219,9 +219,9 @@
- +

Automatically pull updates when available, or just receive notifications

diff --git a/src/routes/media-management/+page.server.ts b/src/routes/media-management/+page.server.ts index 7011906..d02941a 100644 --- a/src/routes/media-management/+page.server.ts +++ b/src/routes/media-management/+page.server.ts @@ -2,13 +2,16 @@ import { redirect } from '@sveltejs/kit'; import type { ServerLoad } from '@sveltejs/kit'; import { pcdManager } from '$pcd/pcd.ts'; -export const load: ServerLoad = () => { +export const load: ServerLoad = ({ url }) => { // Get all databases const databases = pcdManager.getAll(); - // If there are databases, redirect to the first one's radarr page + // Check for section query param (naming, media-settings, quality-definitions) + const section = url.searchParams.get('section') || 'naming'; + + // If there are databases, redirect to the first one's requested section if (databases.length > 0) { - throw redirect(303, `/media-management/${databases[0].id}/radarr`); + throw redirect(303, `/media-management/${databases[0].id}/${section}`); } // If no databases, return empty array (page will show empty state) diff --git a/src/routes/media-management/[databaseId]/+layout.server.ts b/src/routes/media-management/[databaseId]/+layout.server.ts index 2c32f0b..b6e4eff 100644 --- a/src/routes/media-management/[databaseId]/+layout.server.ts +++ b/src/routes/media-management/[databaseId]/+layout.server.ts @@ -1,33 +1,30 @@ import { error } from '@sveltejs/kit'; -import type { ServerLoad } from '@sveltejs/kit'; +import type { LayoutServerLoad } from './$types'; import { pcdManager } from '$pcd/pcd.ts'; +import { canWriteToBase } from '$pcd/writer.ts'; -export const load: ServerLoad = async ({ params }) => { +export const load: LayoutServerLoad = 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'); } return { databases, - currentDatabase + currentDatabase, + canWriteToBase: canWriteToBase(currentDatabaseId) }; }; diff --git a/src/routes/media-management/[databaseId]/+layout.svelte b/src/routes/media-management/[databaseId]/+layout.svelte index 3e53b47..47138e8 100644 --- a/src/routes/media-management/[databaseId]/+layout.svelte +++ b/src/routes/media-management/[databaseId]/+layout.svelte @@ -5,30 +5,31 @@ export let data: LayoutData; - // Map databases to tabs + // Determine current config type from URL for proper database tab hrefs + $: currentPath = $page.url.pathname; + $: currentConfigType = currentPath.includes('/quality-definitions') + ? 'quality-definitions' + : currentPath.includes('/media-settings') + ? 'media-settings' + : 'naming'; + + // Check if we're on a nested page (new/edit) + $: isNestedPage = currentPath.includes('/new') || currentPath.includes('/radarr/') || currentPath.includes('/sonarr/'); + + // Map databases to tabs - preserve current config type when switching databases $: databaseTabs = data.databases.map((db) => ({ label: db.name, - href: `/media-management/${db.id}/radarr`, + href: `/media-management/${db.id}/${currentConfigType}`, active: db.id === data.currentDatabase.id })); - // Determine current arr type from URL - $: currentPath = $page.url.pathname; - $: currentArrType = currentPath.endsWith('/sonarr') ? 'sonarr' : 'radarr'; - - // Arr type tabs - $: arrTypeTabs = [ - { - label: 'Radarr', - href: `/media-management/${data.currentDatabase.id}/radarr`, - active: currentArrType === 'radarr' - }, - { - label: 'Sonarr', - href: `/media-management/${data.currentDatabase.id}/sonarr`, - active: currentArrType === 'sonarr' - } - ]; + // Back button for nested pages + $: backButton = isNestedPage + ? { + label: 'Back', + href: `/media-management/${data.currentDatabase.id}/${currentConfigType}` + } + : undefined; @@ -37,10 +38,7 @@
- - - - + diff --git a/src/routes/media-management/[databaseId]/+page.server.ts b/src/routes/media-management/[databaseId]/+page.server.ts index 2fe8060..f6b62a0 100644 --- a/src/routes/media-management/[databaseId]/+page.server.ts +++ b/src/routes/media-management/[databaseId]/+page.server.ts @@ -1,7 +1,7 @@ import { redirect } from '@sveltejs/kit'; -import type { ServerLoad } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; -export const load: ServerLoad = async ({ params }) => { - // Redirect to radarr by default - throw redirect(303, `/media-management/${params.databaseId}/radarr`); +export const load: PageServerLoad = async ({ params }) => { + // Redirect to naming settings by default + throw redirect(302, `/media-management/${params.databaseId}/naming`); }; diff --git a/src/routes/media-management/[databaseId]/components/MediaSettingsSection.svelte b/src/routes/media-management/[databaseId]/components/MediaSettingsSection.svelte deleted file mode 100644 index 01033e6..0000000 --- a/src/routes/media-management/[databaseId]/components/MediaSettingsSection.svelte +++ /dev/null @@ -1,265 +0,0 @@ - - -
-
-

Media Settings

- {#if settings && !isEditing} - - {/if} -
- - {#if !settings} -
-

- No media settings configured for {arrType === 'radarr' ? 'Radarr' : 'Sonarr'} -

-
- {:else if isEditing} - -
{ - isSaving = true; - return async ({ result, update }) => { - if (result.type === 'failure' && result.data) { - alertStore.add('error', (result.data as { error?: string }).error || 'Failed to save'); - } else if (result.type === 'success') { - alertStore.add('success', 'Media settings updated'); - isEditing = false; - } - await update(); - isSaving = false; - }; - }} - > - - - -
-
- -
-

- Propers and Repacks -

-
- {#each PROPERS_REPACKS_OPTIONS as option} - - {/each} -
- -
- - -
- - -
-
- - -
- - -
-
-
- {:else} - -
-
- -
-
-
- Propers and Repacks -
-
- How to handle proper and repack releases -
-
- - {getPropersRepacksLabel(settings.propers_repacks)} - -
- - -
-
-
- Analyse video files -
-
- Extract media information from video files -
-
- - {settings.enable_media_info ? 'Enabled' : 'Disabled'} - -
-
-
- {/if} -
- - -{#if canWriteToBase} - (showSaveTargetModal = false)} - /> -{/if} diff --git a/src/routes/media-management/[databaseId]/components/NamingSection.svelte b/src/routes/media-management/[databaseId]/components/NamingSection.svelte deleted file mode 100644 index a6fbe17..0000000 --- a/src/routes/media-management/[databaseId]/components/NamingSection.svelte +++ /dev/null @@ -1,813 +0,0 @@ - - -
-
-

Naming

- {#if naming && !isEditing} - - {/if} -
- - {#if !naming} -
-

- No naming settings configured for {arrType === 'radarr' ? 'Radarr' : 'Sonarr'} -

-
- {:else if arrType === 'sonarr' && isSonarrNaming(naming) && isEditing} - -
{ - isSaving = true; - return async ({ result, update }) => { - if (result.type === 'failure' && result.data) { - alertStore.add('error', (result.data as { error?: string }).error || 'Failed to save'); - } else if (result.type === 'success') { - alertStore.add('success', 'Naming settings updated'); - isEditing = false; - } - await update(); - isSaving = false; - }; - }} - > - - -
-
- -
- - -
- - -
- - -
- - - {#if formReplaceIllegalCharacters} -
- - Colon Replacement - -
- {#each COLON_REPLACEMENT_OPTIONS as option} - - {/each} -
- -
- - - {#if formColonReplacement === 'custom'} -
- - -
- {/if} - {/if} - - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - Multi-Episode Style - -
- {#each MULTI_EPISODE_STYLE_OPTIONS as option} - - {/each} -
- -
-
- - -
- - -
-
-
- {:else if arrType === 'radarr' && naming && isRadarrNaming(naming) && isEditing} - -
{ - isSaving = true; - return async ({ result, update }) => { - if (result.type === 'failure' && result.data) { - alertStore.add('error', (result.data as { error?: string }).error || 'Failed to save'); - } else if (result.type === 'success') { - alertStore.add('success', 'Naming settings updated'); - isEditing = false; - } - await update(); - isSaving = false; - }; - }} - > - - -
-
- -
- - -
- - -
- - -
- - - {#if formReplaceIllegalCharacters} -
- - Colon Replacement - -
- {#each RADARR_COLON_REPLACEMENT_OPTIONS as option} - - {/each} -
- -
- {/if} - - -
- - -
- - -
- - -
-
- - -
- - -
-
-
- {:else} - -
-
- -
-
-
- Rename {arrType === 'radarr' ? 'Movies' : 'Episodes'} -
-
- Rename files when importing -
-
- - {naming.rename ? 'Enabled' : 'Disabled'} - -
- - -
-
-
- Replace Illegal Characters -
-
- Replace characters not allowed in file names -
-
- - {naming.replace_illegal_characters ? 'Enabled' : 'Disabled'} - -
- - {#if arrType === 'sonarr' && isSonarrNaming(naming)} - - {#if naming.replace_illegal_characters} -
-
-
- Colon Replacement -
-
- How to replace colons in file names -
-
- - {getColonReplacementLabel(naming.colon_replacement_format)} - {#if naming.colon_replacement_format === 'custom' && naming.custom_colon_replacement_format} - ({naming.custom_colon_replacement_format}) - {/if} - -
- {/if} - - -
-
- Standard Episode Format -
- - {naming.standard_episode_format} - -
- - -
-
- Daily Episode Format -
- - {naming.daily_episode_format} - -
- - -
-
- Anime Episode Format -
- - {naming.anime_episode_format} - -
- - -
-
- Series Folder Format -
- - {naming.series_folder_format} - -
- - -
-
- Season Folder Format -
- - {naming.season_folder_format} - -
- - -
-
-
- Multi-Episode Style -
-
- How to format multi-episode files -
-
- - {getMultiEpisodeStyleLabel(naming.multi_episode_style)} - -
- {:else if arrType === 'radarr' && isRadarrNaming(naming)} - - - {#if naming.replace_illegal_characters} -
-
-
- Colon Replacement -
-
- How to replace colons in file names -
-
- - {getRadarrColonReplacementLabel(naming.colon_replacement_format)} - -
- {/if} - -
-
- Movie Format -
- - {naming.movie_format} - -
- -
-
- Movie Folder Format -
- - {naming.movie_folder_format} - -
- {/if} -
-
- {/if} -
- - -{#if canWriteToBase} - (showSaveTargetModal = false)} - /> -{/if} diff --git a/src/routes/media-management/[databaseId]/components/QualityDefinitionsSection.svelte b/src/routes/media-management/[databaseId]/components/QualityDefinitionsSection.svelte deleted file mode 100644 index 8304d85..0000000 --- a/src/routes/media-management/[databaseId]/components/QualityDefinitionsSection.svelte +++ /dev/null @@ -1,547 +0,0 @@ - - -
-
-

- Quality Definitions -

- -
- -
- - - {#if showUnitDropdown} - - {#each unitOptions as unit} - { - selectedUnitId = unit.id; - showUnitDropdown = false; - }} - /> - {/each} - - {/if} -
- - - {#if definitions.length > 0 && !isEditing} - - {/if} -
-
- - {#if definitions.length === 0} -
-

- No quality definitions configured for {arrType === 'radarr' ? 'Radarr' : 'Sonarr'} -

-
- {:else if isEditing} - -
{ - isSaving = true; - return async ({ result, update }) => { - if (result.type === 'failure' && result.data) { - alertStore.add('error', (result.data as { error?: string }).error || 'Failed to save'); - } else if (result.type === 'success') { - alertStore.add('success', 'Quality definitions updated'); - isEditing = false; - } - await update(); - if (result.type === 'success') { - markersMap = {}; - } - isSaving = false; - }; - }} - > - - - - group.resolution} - emptyMessage="No quality definitions" - flushExpanded - flushBottom - bind:expandedRows - > - - {#if column.key === 'label'} - {row.label} - {:else if column.key === 'count'} - - {row.definitions.length} - - {/if} - - - -
- {#each row.definitions as def (def.quality_name)} - {@const markers = markersMap[def.quality_name] || createMarkers(def)} -
- -
- {def.quality_name} -
- - -
- syncToDefinition(def.quality_name)} - /> -
- - -
-
- Min (MB/m) -
- syncToDefinition(def.quality_name)} - /> -
- -
-
- Pref (MB/m) -
- syncToDefinition(def.quality_name)} - /> -
- -
-
- Max (MB/m) -
- syncToDefinition(def.quality_name)} - /> -
-
- {/each} -
-
-
- - -
- - -
-
- {:else} - - group.resolution} - emptyMessage="No quality definitions" - flushExpanded - bind:expandedRows - > - - {#if column.key === 'label'} - {row.label} - {:else if column.key === 'count'} - - {row.definitions.length} - - {/if} - - - -
- {#each row.definitions as def (def.quality_name)} - {@const markers = markersMap[def.quality_name] || createMarkers(def)} -
- -
- {def.quality_name} -
- - -
- -
- - -
-
- Min (MB/m) -
-
- {def.min_size} -
-
- -
-
- Pref (MB/m) -
-
- {def.preferred_size === 0 || def.preferred_size >= baseScaleMax ? 'Unlimited' : def.preferred_size} -
-
- -
-
- Max (MB/m) -
-
- {def.max_size === 0 || def.max_size >= baseScaleMax ? 'Unlimited' : def.max_size} -
-
-
- {/each} -
-
-
- {/if} -
- - -{#if canWriteToBase} - (showSaveTargetModal = false)} - /> -{/if} diff --git a/src/routes/media-management/[databaseId]/media-settings/+page.server.ts b/src/routes/media-management/[databaseId]/media-settings/+page.server.ts new file mode 100644 index 0000000..ab20f9f --- /dev/null +++ b/src/routes/media-management/[databaseId]/media-settings/+page.server.ts @@ -0,0 +1,28 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { list } from '$pcd/queries/mediaManagement/media-settings/read.ts'; + +export const load: PageServerLoad = async ({ params }) => { + const { databaseId } = params; + + if (!databaseId) { + throw error(400, 'Missing database ID'); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + const mediaSettingsConfigs = await list(cache); + + return { + mediaSettingsConfigs + }; +}; diff --git a/src/routes/media-management/[databaseId]/media-settings/+page.svelte b/src/routes/media-management/[databaseId]/media-settings/+page.svelte new file mode 100644 index 0000000..bfdac0c --- /dev/null +++ b/src/routes/media-management/[databaseId]/media-settings/+page.svelte @@ -0,0 +1,51 @@ + + + + + + goto(`/media-management/${data.currentDatabase.id}/media-settings/new`)} + /> + + + +
+ {#if data.mediaSettingsConfigs.length === 0} +
+

+ No media settings configs found for {data.currentDatabase.name} +

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

No media settings configs match your search

+
+ {:else} + + {/if} +
diff --git a/src/routes/media-management/[databaseId]/media-settings/components/MediaSettingsForm.svelte b/src/routes/media-management/[databaseId]/media-settings/components/MediaSettingsForm.svelte new file mode 100644 index 0000000..0f82636 --- /dev/null +++ b/src/routes/media-management/[databaseId]/media-settings/components/MediaSettingsForm.svelte @@ -0,0 +1,291 @@ + + + +
+

{title}

+

{description}

+
+
+ {#if mode === 'edit'} +
+
+ +
+
+ +
+

Basic Info

+
+ + update('name', e.currentTarget.value)} + placeholder="e.g., default" + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+
+ +
+ + +
+

Propers and Repacks

+
+ {#each PROPERS_REPACKS_OPTIONS as option} + + {/each} +
+
+ +
+ + +
+

File Analysis

+ + +
+
+
+ + + + + +{#if mode === 'edit'} + +{/if} + +{#if canWriteToBase} + (showSaveTargetModal = false)} + /> + + (showDeleteTargetModal = false)} + /> +{/if} diff --git a/src/routes/media-management/[databaseId]/media-settings/new/+page.server.ts b/src/routes/media-management/[databaseId]/media-settings/new/+page.server.ts new file mode 100644 index 0000000..66cd33f --- /dev/null +++ b/src/routes/media-management/[databaseId]/media-settings/new/+page.server.ts @@ -0,0 +1,74 @@ +import { error, redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { canWriteToBase } from '$pcd/writer.ts'; +import type { OperationLayer } from '$pcd/writer.ts'; +import type { ArrType } from '$pcd/queries/mediaManagement/media-settings/types.ts'; +import type { PropersRepacks } from '$lib/shared/mediaManagement.ts'; +import { createRadarrMediaSettings, createSonarrMediaSettings } from '$pcd/queries/mediaManagement/media-settings/index.ts'; + +export const load: PageServerLoad = async ({ parent }) => { + const parentData = await parent(); + return { + canWriteToBase: parentData.canWriteToBase + }; +}; + +export const actions: Actions = { + default: async ({ request, params }) => { + const { databaseId } = params; + + if (!databaseId) { + return fail(400, { error: 'Missing database ID' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const formData = await request.formData(); + const arrType = formData.get('arrType') as ArrType; + const name = formData.get('name') as string; + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (!name?.trim()) { + return fail(400, { error: 'Name is required' }); + } + + if (!arrType || (arrType !== 'radarr' && arrType !== 'sonarr')) { + return fail(400, { error: 'Invalid arr type' }); + } + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const propersRepacks = formData.get('propersRepacks') as PropersRepacks; + const enableMediaInfo = formData.get('enableMediaInfo') === 'true'; + + const createFn = arrType === 'radarr' ? createRadarrMediaSettings : createSonarrMediaSettings; + + const result = await createFn({ + databaseId: currentDatabaseId, + cache, + layer, + input: { + name: name.trim(), + propersRepacks: propersRepacks || 'doNotPrefer', + enableMediaInfo + } + }); + + if (!result.success) { + return fail(500, { error: result.error || `Failed to create ${arrType} media settings` }); + } + + throw redirect(303, `/media-management/${databaseId}/media-settings`); + } +}; diff --git a/src/routes/media-management/[databaseId]/media-settings/new/+page.svelte b/src/routes/media-management/[databaseId]/media-settings/new/+page.svelte new file mode 100644 index 0000000..40d02aa --- /dev/null +++ b/src/routes/media-management/[databaseId]/media-settings/new/+page.svelte @@ -0,0 +1,61 @@ + + +{#if !selectedArrType} +
+ {#each arrTypeOptions as option} + + {/each} +
+{:else} + +{/if} + + diff --git a/src/routes/media-management/[databaseId]/media-settings/radarr/[name]/+page.server.ts b/src/routes/media-management/[databaseId]/media-settings/radarr/[name]/+page.server.ts new file mode 100644 index 0000000..bd6b8aa --- /dev/null +++ b/src/routes/media-management/[databaseId]/media-settings/radarr/[name]/+page.server.ts @@ -0,0 +1,142 @@ +import { error, redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { canWriteToBase } from '$pcd/writer.ts'; +import type { OperationLayer } from '$pcd/writer.ts'; +import { getRadarrByName, updateRadarrMediaSettings, removeRadarrMediaSettings } from '$pcd/queries/mediaManagement/media-settings/index.ts'; +import type { PropersRepacks } from '$lib/shared/mediaManagement.ts'; + +export const load: PageServerLoad = async ({ params, parent }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + throw error(400, 'Missing parameters'); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + const decodedName = decodeURIComponent(name); + const mediaSettingsConfig = await getRadarrByName(cache, decodedName); + + if (!mediaSettingsConfig) { + throw error(404, 'Media settings config not found'); + } + + const parentData = await parent(); + + return { + mediaSettingsConfig, + canWriteToBase: parentData.canWriteToBase + }; +}; + +export const actions: Actions = { + update: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getRadarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Media settings config not found' }); + } + + const formData = await request.formData(); + const newName = formData.get('name') as string; + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (!newName?.trim()) { + return fail(400, { error: 'Name is required' }); + } + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const propersRepacks = formData.get('propersRepacks') as PropersRepacks; + const enableMediaInfo = formData.get('enableMediaInfo') === 'true'; + + const result = await updateRadarrMediaSettings({ + databaseId: currentDatabaseId, + cache, + layer, + currentName: decodedName, + input: { + name: newName.trim(), + propersRepacks: propersRepacks || 'doNotPrefer', + enableMediaInfo + } + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to update media settings config' }); + } + + throw redirect(303, `/media-management/${databaseId}/media-settings`); + }, + + delete: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getRadarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Media settings config not found' }); + } + + const formData = await request.formData(); + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const result = await removeRadarrMediaSettings({ + databaseId: currentDatabaseId, + cache, + layer, + name: decodedName + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to delete media settings config' }); + } + + throw redirect(303, `/media-management/${databaseId}/media-settings`); + } +}; diff --git a/src/routes/media-management/[databaseId]/media-settings/radarr/[name]/+page.svelte b/src/routes/media-management/[databaseId]/media-settings/radarr/[name]/+page.svelte new file mode 100644 index 0000000..0b7d8cb --- /dev/null +++ b/src/routes/media-management/[databaseId]/media-settings/radarr/[name]/+page.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/src/routes/media-management/[databaseId]/media-settings/sonarr/[name]/+page.server.ts b/src/routes/media-management/[databaseId]/media-settings/sonarr/[name]/+page.server.ts new file mode 100644 index 0000000..1911e3e --- /dev/null +++ b/src/routes/media-management/[databaseId]/media-settings/sonarr/[name]/+page.server.ts @@ -0,0 +1,142 @@ +import { error, redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { canWriteToBase } from '$pcd/writer.ts'; +import type { OperationLayer } from '$pcd/writer.ts'; +import { getSonarrByName, updateSonarrMediaSettings, removeSonarrMediaSettings } from '$pcd/queries/mediaManagement/media-settings/index.ts'; +import type { PropersRepacks } from '$lib/shared/mediaManagement.ts'; + +export const load: PageServerLoad = async ({ params, parent }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + throw error(400, 'Missing parameters'); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + const decodedName = decodeURIComponent(name); + const mediaSettingsConfig = await getSonarrByName(cache, decodedName); + + if (!mediaSettingsConfig) { + throw error(404, 'Media settings config not found'); + } + + const parentData = await parent(); + + return { + mediaSettingsConfig, + canWriteToBase: parentData.canWriteToBase + }; +}; + +export const actions: Actions = { + update: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getSonarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Media settings config not found' }); + } + + const formData = await request.formData(); + const newName = formData.get('name') as string; + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (!newName?.trim()) { + return fail(400, { error: 'Name is required' }); + } + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const propersRepacks = formData.get('propersRepacks') as PropersRepacks; + const enableMediaInfo = formData.get('enableMediaInfo') === 'true'; + + const result = await updateSonarrMediaSettings({ + databaseId: currentDatabaseId, + cache, + layer, + currentName: decodedName, + input: { + name: newName.trim(), + propersRepacks: propersRepacks || 'doNotPrefer', + enableMediaInfo + } + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to update media settings config' }); + } + + throw redirect(303, `/media-management/${databaseId}/media-settings`); + }, + + delete: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getSonarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Media settings config not found' }); + } + + const formData = await request.formData(); + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const result = await removeSonarrMediaSettings({ + databaseId: currentDatabaseId, + cache, + layer, + name: decodedName + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to delete media settings config' }); + } + + throw redirect(303, `/media-management/${databaseId}/media-settings`); + } +}; diff --git a/src/routes/media-management/[databaseId]/media-settings/sonarr/[name]/+page.svelte b/src/routes/media-management/[databaseId]/media-settings/sonarr/[name]/+page.svelte new file mode 100644 index 0000000..36bc927 --- /dev/null +++ b/src/routes/media-management/[databaseId]/media-settings/sonarr/[name]/+page.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/src/routes/media-management/[databaseId]/media-settings/views/TableView.svelte b/src/routes/media-management/[databaseId]/media-settings/views/TableView.svelte new file mode 100644 index 0000000..82b6df2 --- /dev/null +++ b/src/routes/media-management/[databaseId]/media-settings/views/TableView.svelte @@ -0,0 +1,79 @@ + + + + + {#if column.key === 'name'} + {row.name} + {:else if column.key === 'arr_type'} +
+ {row.arr_type} +
+ {:else if column.key === 'propers_repacks'} + {@const config = propersRepacksConfig[row.propers_repacks] || { variant: 'neutral', label: row.propers_repacks }} + {config.label} + {:else if column.key === 'enable_media_info'} + {#if row.enable_media_info} + Enabled + {:else} + Disabled + {/if} + {/if} +
+
diff --git a/src/routes/media-management/[databaseId]/naming/+page.server.ts b/src/routes/media-management/[databaseId]/naming/+page.server.ts new file mode 100644 index 0000000..b719642 --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/+page.server.ts @@ -0,0 +1,28 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { list } from '$pcd/queries/mediaManagement/naming/read.ts'; + +export const load: PageServerLoad = async ({ params }) => { + const { databaseId } = params; + + if (!databaseId) { + throw error(400, 'Missing database ID'); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + const namingConfigs = await list(cache); + + return { + namingConfigs + }; +}; diff --git a/src/routes/media-management/[databaseId]/naming/+page.svelte b/src/routes/media-management/[databaseId]/naming/+page.svelte new file mode 100644 index 0000000..fd1f654 --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/+page.svelte @@ -0,0 +1,51 @@ + + + + + + goto(`/media-management/${data.currentDatabase.id}/naming/new`)} + /> + + + +
+ {#if data.namingConfigs.length === 0} +
+

+ No naming configs found for {data.currentDatabase.name} +

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

No naming configs match your search

+
+ {:else} + + {/if} +
diff --git a/src/routes/media-management/[databaseId]/naming/components/RadarrNamingForm.svelte b/src/routes/media-management/[databaseId]/naming/components/RadarrNamingForm.svelte new file mode 100644 index 0000000..ea95e7b --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/components/RadarrNamingForm.svelte @@ -0,0 +1,352 @@ + + + +
+

{title}

+

{description}

+
+
+ {#if mode === 'edit'} +
+
+ +
+
+ +
+

Basic Info

+
+ + update('name', e.currentTarget.value)} + placeholder="e.g., default" + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+ + +
+ +
+ + +
+

Naming Formats

+
+ + update('movieFormat', e.currentTarget.value)} + placeholder="e.g., Movie Title (Year) Quality" + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 font-mono text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+ +
+ + update('movieFolderFormat', e.currentTarget.value)} + placeholder="e.g., Movie Title (Year)" + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 font-mono text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+
+ +
+ + +
+

Character Replacement

+ + + + {#if formData.replaceIllegalCharacters} +
+ + Colon Replacement + +
+ {#each RADARR_COLON_REPLACEMENT_OPTIONS as option} + + {/each} +
+
+ {/if} +
+
+
+ + + + + +{#if mode === 'edit'} + +{/if} + +{#if canWriteToBase} + (showSaveTargetModal = false)} + /> + + (showDeleteTargetModal = false)} + /> +{/if} diff --git a/src/routes/media-management/[databaseId]/naming/components/SonarrNamingForm.svelte b/src/routes/media-management/[databaseId]/naming/components/SonarrNamingForm.svelte new file mode 100644 index 0000000..a08c82c --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/components/SonarrNamingForm.svelte @@ -0,0 +1,477 @@ + + + +
+

{title}

+

{description}

+
+
+ {#if mode === 'edit'} +
+
+ +
+
+ +
+

Basic Info

+
+ + update('name', e.currentTarget.value)} + placeholder="e.g., default" + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+ + +
+ +
+ + +
+

Episode Formats

+
+ + update('standardEpisodeFormat', e.currentTarget.value)} + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 font-mono text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+ +
+ + update('dailyEpisodeFormat', e.currentTarget.value)} + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 font-mono text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+ +
+ + update('animeEpisodeFormat', e.currentTarget.value)} + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 font-mono text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+
+ +
+ + +
+

Folder Formats

+
+ + update('seriesFolderFormat', e.currentTarget.value)} + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 font-mono text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+ +
+ + update('seasonFolderFormat', e.currentTarget.value)} + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 font-mono text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+
+ +
+ + +
+

Multi-Episode Style

+
+ {#each MULTI_EPISODE_STYLE_OPTIONS as option} + + {/each} +
+
+ +
+ + +
+

Character Replacement

+ + + + {#if formData.replaceIllegalCharacters} +
+ + Colon Replacement + +
+ {#each COLON_REPLACEMENT_OPTIONS as option} + + {/each} +
+
+ + {#if showCustomColonInput} +
+ + update('customColonReplacementFormat', e.currentTarget.value)} + placeholder="Enter custom replacement character(s)" + class="mt-1 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:ring-1 focus:ring-accent-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" + /> +
+ {/if} + {/if} +
+
+
+ + + + + +{#if mode === 'edit'} + +{/if} + +{#if canWriteToBase} + (showSaveTargetModal = false)} + /> + + (showDeleteTargetModal = false)} + /> +{/if} diff --git a/src/routes/media-management/[databaseId]/naming/new/+page.server.ts b/src/routes/media-management/[databaseId]/naming/new/+page.server.ts new file mode 100644 index 0000000..3ce9e03 --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/new/+page.server.ts @@ -0,0 +1,118 @@ +import { error, redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { canWriteToBase } from '$pcd/writer.ts'; +import type { OperationLayer } from '$pcd/writer.ts'; +import type { ArrType } from '$pcd/queries/mediaManagement/naming/types.ts'; +import type { RadarrColonReplacementFormat, ColonReplacementFormat, MultiEpisodeStyle } from '$lib/shared/mediaManagement.ts'; +import { createRadarrNaming, createSonarrNaming } from '$pcd/queries/mediaManagement/naming/index.ts'; + +export const load: PageServerLoad = async ({ parent }) => { + const parentData = await parent(); + return { + canWriteToBase: parentData.canWriteToBase + }; +}; + +export const actions: Actions = { + default: async ({ request, params }) => { + const { databaseId } = params; + + if (!databaseId) { + return fail(400, { error: 'Missing database ID' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const formData = await request.formData(); + const arrType = formData.get('arrType') as ArrType; + const name = formData.get('name') as string; + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (!name?.trim()) { + return fail(400, { error: 'Name is required' }); + } + + if (!arrType || (arrType !== 'radarr' && arrType !== 'sonarr')) { + return fail(400, { error: 'Invalid arr type' }); + } + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + if (arrType === 'radarr') { + const rename = formData.get('rename') === 'true'; + const movieFormat = formData.get('movieFormat') as string; + const movieFolderFormat = formData.get('movieFolderFormat') as string; + const replaceIllegalCharacters = formData.get('replaceIllegalCharacters') === 'true'; + const colonReplacementFormat = formData.get( + 'colonReplacementFormat' + ) as RadarrColonReplacementFormat; + + const result = await createRadarrNaming({ + databaseId: currentDatabaseId, + cache, + layer, + input: { + name: name.trim(), + rename, + movieFormat: movieFormat || '', + movieFolderFormat: movieFolderFormat || '', + replaceIllegalCharacters, + colonReplacementFormat: colonReplacementFormat || 'delete' + } + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to create radarr naming config' }); + } + } else { + const rename = formData.get('rename') === 'true'; + const standardEpisodeFormat = formData.get('standardEpisodeFormat') as string; + const dailyEpisodeFormat = formData.get('dailyEpisodeFormat') as string; + const animeEpisodeFormat = formData.get('animeEpisodeFormat') as string; + const seriesFolderFormat = formData.get('seriesFolderFormat') as string; + const seasonFolderFormat = formData.get('seasonFolderFormat') as string; + const replaceIllegalCharacters = formData.get('replaceIllegalCharacters') === 'true'; + const colonReplacementFormat = formData.get( + 'colonReplacementFormat' + ) as ColonReplacementFormat; + const customColonReplacementFormat = formData.get('customColonReplacementFormat') as string; + const multiEpisodeStyle = formData.get('multiEpisodeStyle') as MultiEpisodeStyle; + + const result = await createSonarrNaming({ + databaseId: currentDatabaseId, + cache, + layer, + input: { + name: name.trim(), + rename, + standardEpisodeFormat: standardEpisodeFormat || '', + dailyEpisodeFormat: dailyEpisodeFormat || '', + animeEpisodeFormat: animeEpisodeFormat || '', + seriesFolderFormat: seriesFolderFormat || '', + seasonFolderFormat: seasonFolderFormat || '', + replaceIllegalCharacters, + colonReplacementFormat: colonReplacementFormat || 'delete', + customColonReplacementFormat: customColonReplacementFormat || null, + multiEpisodeStyle: multiEpisodeStyle || 'extend' + } + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to create sonarr naming config' }); + } + } + + throw redirect(303, `/media-management/${databaseId}/naming`); + } +}; diff --git a/src/routes/media-management/[databaseId]/naming/new/+page.svelte b/src/routes/media-management/[databaseId]/naming/new/+page.svelte new file mode 100644 index 0000000..663fce3 --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/new/+page.svelte @@ -0,0 +1,68 @@ + + +{#if !selectedArrType} +
+ {#each arrTypeOptions as option} + + {/each} +
+{:else if selectedArrType === 'radarr'} + +{:else} + +{/if} + + diff --git a/src/routes/media-management/[databaseId]/naming/radarr/[name]/+page.server.ts b/src/routes/media-management/[databaseId]/naming/radarr/[name]/+page.server.ts new file mode 100644 index 0000000..6f6ddfa --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/radarr/[name]/+page.server.ts @@ -0,0 +1,150 @@ +import { error, redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { canWriteToBase } from '$pcd/writer.ts'; +import type { OperationLayer } from '$pcd/writer.ts'; +import { getRadarrByName, updateRadarrNaming, removeRadarrNaming } from '$pcd/queries/mediaManagement/naming/index.ts'; +import type { RadarrColonReplacementFormat } from '$lib/shared/mediaManagement.ts'; + +export const load: PageServerLoad = async ({ params, parent }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + throw error(400, 'Missing parameters'); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + const decodedName = decodeURIComponent(name); + const namingConfig = await getRadarrByName(cache, decodedName); + + if (!namingConfig) { + throw error(404, 'Naming config not found'); + } + + const parentData = await parent(); + + return { + namingConfig, + canWriteToBase: parentData.canWriteToBase + }; +}; + +export const actions: Actions = { + update: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getRadarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Naming config not found' }); + } + + const formData = await request.formData(); + const newName = formData.get('name') as string; + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (!newName?.trim()) { + return fail(400, { error: 'Name is required' }); + } + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const rename = formData.get('rename') === 'true'; + const movieFormat = formData.get('movieFormat') as string; + const movieFolderFormat = formData.get('movieFolderFormat') as string; + const replaceIllegalCharacters = formData.get('replaceIllegalCharacters') === 'true'; + const colonReplacementFormat = formData.get( + 'colonReplacementFormat' + ) as RadarrColonReplacementFormat; + + const result = await updateRadarrNaming({ + databaseId: currentDatabaseId, + cache, + layer, + currentName: decodedName, + input: { + name: newName.trim(), + rename, + movieFormat: movieFormat || '', + movieFolderFormat: movieFolderFormat || '', + replaceIllegalCharacters, + colonReplacementFormat: colonReplacementFormat || 'delete' + } + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to update naming config' }); + } + + throw redirect(303, `/media-management/${databaseId}/naming`); + }, + + delete: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getRadarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Naming config not found' }); + } + + const formData = await request.formData(); + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const result = await removeRadarrNaming({ + databaseId: currentDatabaseId, + cache, + layer, + name: decodedName + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to delete naming config' }); + } + + throw redirect(303, `/media-management/${databaseId}/naming`); + } +}; diff --git a/src/routes/media-management/[databaseId]/naming/radarr/[name]/+page.svelte b/src/routes/media-management/[databaseId]/naming/radarr/[name]/+page.svelte new file mode 100644 index 0000000..6f674de --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/radarr/[name]/+page.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/src/routes/media-management/[databaseId]/naming/sonarr/[name]/+page.server.ts b/src/routes/media-management/[databaseId]/naming/sonarr/[name]/+page.server.ts new file mode 100644 index 0000000..b464ac8 --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/sonarr/[name]/+page.server.ts @@ -0,0 +1,158 @@ +import { error, redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { canWriteToBase } from '$pcd/writer.ts'; +import type { OperationLayer } from '$pcd/writer.ts'; +import { getSonarrByName, updateSonarrNaming, removeSonarrNaming } from '$pcd/queries/mediaManagement/naming/index.ts'; +import type { ColonReplacementFormat, MultiEpisodeStyle } from '$lib/shared/mediaManagement.ts'; + +export const load: PageServerLoad = async ({ params, parent }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + throw error(400, 'Missing parameters'); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + const decodedName = decodeURIComponent(name); + const namingConfig = await getSonarrByName(cache, decodedName); + + if (!namingConfig) { + throw error(404, 'Naming config not found'); + } + + const parentData = await parent(); + + return { + namingConfig, + canWriteToBase: parentData.canWriteToBase + }; +}; + +export const actions: Actions = { + update: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getSonarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Naming config not found' }); + } + + const formData = await request.formData(); + const newName = formData.get('name') as string; + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (!newName?.trim()) { + return fail(400, { error: 'Name is required' }); + } + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const rename = formData.get('rename') === 'true'; + const standardEpisodeFormat = formData.get('standardEpisodeFormat') as string; + const dailyEpisodeFormat = formData.get('dailyEpisodeFormat') as string; + const animeEpisodeFormat = formData.get('animeEpisodeFormat') as string; + const seriesFolderFormat = formData.get('seriesFolderFormat') as string; + const seasonFolderFormat = formData.get('seasonFolderFormat') as string; + const replaceIllegalCharacters = formData.get('replaceIllegalCharacters') === 'true'; + const colonReplacementFormat = formData.get('colonReplacementFormat') as ColonReplacementFormat; + const customColonReplacementFormat = formData.get('customColonReplacementFormat') as string; + const multiEpisodeStyle = formData.get('multiEpisodeStyle') as MultiEpisodeStyle; + + const result = await updateSonarrNaming({ + databaseId: currentDatabaseId, + cache, + layer, + currentName: decodedName, + input: { + name: newName.trim(), + rename, + standardEpisodeFormat: standardEpisodeFormat || '', + dailyEpisodeFormat: dailyEpisodeFormat || '', + animeEpisodeFormat: animeEpisodeFormat || '', + seriesFolderFormat: seriesFolderFormat || '', + seasonFolderFormat: seasonFolderFormat || '', + replaceIllegalCharacters, + colonReplacementFormat: colonReplacementFormat || 'delete', + customColonReplacementFormat: customColonReplacementFormat || null, + multiEpisodeStyle: multiEpisodeStyle || 'extend' + } + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to update naming config' }); + } + + throw redirect(303, `/media-management/${databaseId}/naming`); + }, + + delete: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getSonarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Naming config not found' }); + } + + const formData = await request.formData(); + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const result = await removeSonarrNaming({ + databaseId: currentDatabaseId, + cache, + layer, + name: decodedName + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to delete naming config' }); + } + + throw redirect(303, `/media-management/${databaseId}/naming`); + } +}; diff --git a/src/routes/media-management/[databaseId]/naming/sonarr/[name]/+page.svelte b/src/routes/media-management/[databaseId]/naming/sonarr/[name]/+page.svelte new file mode 100644 index 0000000..c455e1b --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/sonarr/[name]/+page.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/src/routes/media-management/[databaseId]/naming/views/TableView.svelte b/src/routes/media-management/[databaseId]/naming/views/TableView.svelte new file mode 100644 index 0000000..d805d8d --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/views/TableView.svelte @@ -0,0 +1,64 @@ + + + + + {#if column.key === 'name'} + {row.name} + {:else if column.key === 'arr_type'} +
+ {row.arr_type} +
+ {:else if column.key === 'rename'} + {#if row.rename} + Enabled + {:else} + Disabled + {/if} + {/if} +
+
diff --git a/src/routes/media-management/[databaseId]/quality-definitions/+page.server.ts b/src/routes/media-management/[databaseId]/quality-definitions/+page.server.ts new file mode 100644 index 0000000..e2d10ed --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/+page.server.ts @@ -0,0 +1,28 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { list } from '$pcd/queries/mediaManagement/quality-definitions/read.ts'; + +export const load: PageServerLoad = async ({ params }) => { + const { databaseId } = params; + + if (!databaseId) { + throw error(400, 'Missing database ID'); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + const qualityDefinitionsConfigs = await list(cache); + + return { + qualityDefinitionsConfigs + }; +}; diff --git a/src/routes/media-management/[databaseId]/quality-definitions/+page.svelte b/src/routes/media-management/[databaseId]/quality-definitions/+page.svelte new file mode 100644 index 0000000..970b70a --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/+page.svelte @@ -0,0 +1,51 @@ + + + + + + goto(`/media-management/${data.currentDatabase.id}/quality-definitions/new`)} + /> + + + +
+ {#if data.qualityDefinitionsConfigs.length === 0} +
+

+ No quality definitions configs found for {data.currentDatabase.name} +

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

No quality definitions configs match your search

+
+ {:else} + + {/if} +
diff --git a/src/routes/media-management/[databaseId]/quality-definitions/components/QualityDefinitionsForm.svelte b/src/routes/media-management/[databaseId]/quality-definitions/components/QualityDefinitionsForm.svelte new file mode 100644 index 0000000..7185987 --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/components/QualityDefinitionsForm.svelte @@ -0,0 +1,513 @@ + + + +
+

{title}

+

{description}

+
+
+ +
+ + + {#if showUnitDropdown} + + {#each unitOptions as unit} + { + selectedUnitId = unit.id; + showUnitDropdown = false; + }} + /> + {/each} + + {/if} +
+ + {#if mode === 'edit'} +
+
+ +
+ +
+

Basic Info

+
+ + +
+
+ + + {#if entries.length === 0} +
+

+ No qualities available for {arrLabel} +

+
+ {:else} + group.resolution} + emptyMessage="No quality definitions" + flushExpanded + bind:expandedRows + > + + {#if column.key === 'label'} + {row.label} + {:else if column.key === 'count'} + + {row.entries.length} + + {/if} + + + +
+ {#each row.entries as entry (entry.quality_name)} + {@const markers = markersMap[entry.quality_name] || createMarkers(entry)} +
+ +
+ {entry.quality_name} +
+ + +
+ syncToEntry(entry.quality_name)} + /> +
+ + +
+
+ Min (MB/m) +
+ syncToEntry(entry.quality_name)} + /> +
+ +
+
+ Pref (MB/m) +
+ syncToEntry(entry.quality_name)} + /> +
+ +
+
+ Max (MB/m) +
+ syncToEntry(entry.quality_name)} + /> +
+
+ {/each} +
+
+
+ {/if} +
+ + + + + +{#if mode === 'edit'} + +{/if} + +{#if canWriteToBase} + (showSaveTargetModal = false)} + /> + + (showDeleteTargetModal = false)} + /> +{/if} diff --git a/src/routes/media-management/[databaseId]/quality-definitions/new/+page.server.ts b/src/routes/media-management/[databaseId]/quality-definitions/new/+page.server.ts new file mode 100644 index 0000000..4f999c1 --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/new/+page.server.ts @@ -0,0 +1,105 @@ +import { error, redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { canWriteToBase } from '$pcd/writer.ts'; +import type { OperationLayer } from '$pcd/writer.ts'; +import type { ArrType } from '$pcd/queries/mediaManagement/quality-definitions/types.ts'; +import { getAvailableQualities } from '$pcd/queries/mediaManagement/quality-definitions/read.ts'; +import { createRadarrQualityDefinitions, createSonarrQualityDefinitions } from '$pcd/queries/mediaManagement/quality-definitions/index.ts'; + +export const load: PageServerLoad = async ({ params, parent }) => { + const { databaseId } = params; + + if (!databaseId) { + throw error(400, 'Missing database ID'); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + // Get available qualities for both arr types + const radarrQualities = await getAvailableQualities(cache, 'radarr'); + const sonarrQualities = await getAvailableQualities(cache, 'sonarr'); + + const parentData = await parent(); + + return { + canWriteToBase: parentData.canWriteToBase, + radarrQualities, + sonarrQualities + }; +}; + +export const actions: Actions = { + default: async ({ request, params }) => { + const { databaseId } = params; + + if (!databaseId) { + return fail(400, { error: 'Missing database ID' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const formData = await request.formData(); + const arrType = formData.get('arrType') as ArrType; + const name = formData.get('name') as string; + const layer = (formData.get('layer') as OperationLayer) || 'user'; + const entriesJson = formData.get('entries') as string; + + if (!name?.trim()) { + return fail(400, { error: 'Name is required' }); + } + + if (!arrType || (arrType !== 'radarr' && arrType !== 'sonarr')) { + return fail(400, { error: 'Invalid arr type' }); + } + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + let entries; + try { + entries = JSON.parse(entriesJson || '[]'); + } catch { + return fail(400, { error: 'Invalid entries data' }); + } + + if (!Array.isArray(entries) || entries.length === 0) { + return fail(400, { error: 'At least one quality definition is required' }); + } + + const createFn = arrType === 'radarr' ? createRadarrQualityDefinitions : createSonarrQualityDefinitions; + + const result = await createFn({ + databaseId: currentDatabaseId, + cache, + layer, + input: { + name: name.trim(), + entries + } + }); + + if (!result.success) { + return fail(500, { error: result.error || `Failed to create ${arrType} quality definitions` }); + } + + throw redirect(303, `/media-management/${databaseId}/quality-definitions`); + } +}; diff --git a/src/routes/media-management/[databaseId]/quality-definitions/new/+page.svelte b/src/routes/media-management/[databaseId]/quality-definitions/new/+page.svelte new file mode 100644 index 0000000..00a7328 --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/new/+page.svelte @@ -0,0 +1,64 @@ + + +{#if !selectedArrType} +
+ {#each arrTypeOptions as option} + + {/each} +
+{:else} + +{/if} + + diff --git a/src/routes/media-management/[databaseId]/quality-definitions/radarr/[name]/+page.server.ts b/src/routes/media-management/[databaseId]/quality-definitions/radarr/[name]/+page.server.ts new file mode 100644 index 0000000..e109c71 --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/radarr/[name]/+page.server.ts @@ -0,0 +1,149 @@ +import { error, redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { canWriteToBase } from '$pcd/writer.ts'; +import type { OperationLayer } from '$pcd/writer.ts'; +import { getRadarrByName, getAvailableQualities } from '$pcd/queries/mediaManagement/quality-definitions/read.ts'; +import { updateRadarrQualityDefinitions, removeRadarrQualityDefinitions } from '$pcd/queries/mediaManagement/quality-definitions/index.ts'; + +export const load: PageServerLoad = async ({ params, parent }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + throw error(400, 'Missing parameters'); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + const decodedName = decodeURIComponent(name); + const qualityDefinitionsConfig = await getRadarrByName(cache, decodedName); + + if (!qualityDefinitionsConfig) { + throw error(404, 'Quality definitions config not found'); + } + + const availableQualities = await getAvailableQualities(cache, 'radarr'); + const parentData = await parent(); + + return { + qualityDefinitionsConfig, + availableQualities, + canWriteToBase: parentData.canWriteToBase + }; +}; + +export const actions: Actions = { + update: async ({ request, params }) => { + console.log('[QD-ACTION] update action called at', Date.now()); + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getRadarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Quality definitions config not found' }); + } + + const formData = await request.formData(); + const newName = formData.get('name') as string; + const layer = (formData.get('layer') as OperationLayer) || 'user'; + const entriesJson = formData.get('entries') as string; + + if (!newName?.trim()) { + return fail(400, { error: 'Name is required' }); + } + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + let entries; + try { + entries = JSON.parse(entriesJson || '[]'); + } catch { + return fail(400, { error: 'Invalid entries data' }); + } + + const result = await updateRadarrQualityDefinitions({ + databaseId: currentDatabaseId, + cache, + layer, + currentName: decodedName, + input: { + name: newName.trim(), + entries + } + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to update quality definitions config' }); + } + + throw redirect(303, `/media-management/${databaseId}/quality-definitions`); + }, + + delete: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getRadarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Quality definitions config not found' }); + } + + const formData = await request.formData(); + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const result = await removeRadarrQualityDefinitions({ + databaseId: currentDatabaseId, + cache, + layer, + name: decodedName + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to delete quality definitions config' }); + } + + throw redirect(303, `/media-management/${databaseId}/quality-definitions`); + } +}; diff --git a/src/routes/media-management/[databaseId]/quality-definitions/radarr/[name]/+page.svelte b/src/routes/media-management/[databaseId]/quality-definitions/radarr/[name]/+page.svelte new file mode 100644 index 0000000..0161e8a --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/radarr/[name]/+page.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/routes/media-management/[databaseId]/quality-definitions/sonarr/[name]/+page.server.ts b/src/routes/media-management/[databaseId]/quality-definitions/sonarr/[name]/+page.server.ts new file mode 100644 index 0000000..ea41de7 --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/sonarr/[name]/+page.server.ts @@ -0,0 +1,148 @@ +import { error, redirect, fail } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { pcdManager } from '$pcd/pcd.ts'; +import { canWriteToBase } from '$pcd/writer.ts'; +import type { OperationLayer } from '$pcd/writer.ts'; +import { getSonarrByName, getAvailableQualities } from '$pcd/queries/mediaManagement/quality-definitions/read.ts'; +import { updateSonarrQualityDefinitions, removeSonarrQualityDefinitions } from '$pcd/queries/mediaManagement/quality-definitions/index.ts'; + +export const load: PageServerLoad = async ({ params, parent }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + throw error(400, 'Missing parameters'); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + throw error(400, 'Invalid database ID'); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + throw error(500, 'Database cache not available'); + } + + const decodedName = decodeURIComponent(name); + const qualityDefinitionsConfig = await getSonarrByName(cache, decodedName); + + if (!qualityDefinitionsConfig) { + throw error(404, 'Quality definitions config not found'); + } + + const availableQualities = await getAvailableQualities(cache, 'sonarr'); + const parentData = await parent(); + + return { + qualityDefinitionsConfig, + availableQualities, + canWriteToBase: parentData.canWriteToBase + }; +}; + +export const actions: Actions = { + update: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getSonarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Quality definitions config not found' }); + } + + const formData = await request.formData(); + const newName = formData.get('name') as string; + const layer = (formData.get('layer') as OperationLayer) || 'user'; + const entriesJson = formData.get('entries') as string; + + if (!newName?.trim()) { + return fail(400, { error: 'Name is required' }); + } + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + let entries; + try { + entries = JSON.parse(entriesJson || '[]'); + } catch { + return fail(400, { error: 'Invalid entries data' }); + } + + const result = await updateSonarrQualityDefinitions({ + databaseId: currentDatabaseId, + cache, + layer, + currentName: decodedName, + input: { + name: newName.trim(), + entries + } + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to update quality definitions config' }); + } + + throw redirect(303, `/media-management/${databaseId}/quality-definitions`); + }, + + delete: async ({ request, params }) => { + const { databaseId, name } = params; + + if (!databaseId || !name) { + return fail(400, { error: 'Missing parameters' }); + } + + const currentDatabaseId = parseInt(databaseId, 10); + if (isNaN(currentDatabaseId)) { + return fail(400, { error: 'Invalid database ID' }); + } + + const cache = pcdManager.getCache(currentDatabaseId); + if (!cache) { + return fail(500, { error: 'Database cache not available' }); + } + + const decodedName = decodeURIComponent(name); + const current = await getSonarrByName(cache, decodedName); + if (!current) { + return fail(404, { error: 'Quality definitions config not found' }); + } + + const formData = await request.formData(); + const layer = (formData.get('layer') as OperationLayer) || 'user'; + + if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { + return fail(403, { error: 'Cannot write to base layer without personal access token' }); + } + + const result = await removeSonarrQualityDefinitions({ + databaseId: currentDatabaseId, + cache, + layer, + name: decodedName + }); + + if (!result.success) { + return fail(500, { error: result.error || 'Failed to delete quality definitions config' }); + } + + throw redirect(303, `/media-management/${databaseId}/quality-definitions`); + } +}; diff --git a/src/routes/media-management/[databaseId]/quality-definitions/sonarr/[name]/+page.svelte b/src/routes/media-management/[databaseId]/quality-definitions/sonarr/[name]/+page.svelte new file mode 100644 index 0000000..619f1fb --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/sonarr/[name]/+page.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/routes/media-management/[databaseId]/quality-definitions/views/TableView.svelte b/src/routes/media-management/[databaseId]/quality-definitions/views/TableView.svelte new file mode 100644 index 0000000..29fdf8e --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/views/TableView.svelte @@ -0,0 +1,52 @@ + + + + + {#if column.key === 'name'} + {row.name} + {:else if column.key === 'arr_type'} +
+ {row.arr_type} +
+ {/if} +
+
diff --git a/src/routes/media-management/[databaseId]/radarr/+page.server.ts b/src/routes/media-management/[databaseId]/radarr/+page.server.ts deleted file mode 100644 index 2ecd9a1..0000000 --- a/src/routes/media-management/[databaseId]/radarr/+page.server.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { error, fail } from '@sveltejs/kit'; -import type { ServerLoad, Actions } from '@sveltejs/kit'; -import { pcdManager } from '$pcd/pcd.ts'; -import { canWriteToBase } from '$pcd/writer.ts'; -import type { OperationLayer } from '$pcd/writer.ts'; -import * as mediaManagementQueries from '$pcd/queries/mediaManagement/index.ts'; -import type { PropersRepacks, RadarrColonReplacementFormat } from '$lib/shared/mediaManagement.ts'; -import { RADARR_COLON_REPLACEMENT_OPTIONS } from '$lib/shared/mediaManagement.ts'; -import { logger } from '$logger/logger.ts'; - -export const load: ServerLoad = async ({ params }) => { - const { databaseId } = params; - - // Parse the database ID - const currentDatabaseId = parseInt(databaseId as string, 10); - - // Get the cache for the database - const cache = pcdManager.getCache(currentDatabaseId); - if (!cache) { - throw error(500, 'Database cache not available'); - } - - // Load Radarr media management data - const mediaManagement = await mediaManagementQueries.getRadarr(cache); - - return { - mediaManagement, - canWriteToBase: canWriteToBase(currentDatabaseId) - }; -}; - -export const actions: Actions = { - updateMediaSettings: async ({ request, params }) => { - const { databaseId } = params; - - if (!databaseId) { - return fail(400, { error: 'Missing database ID' }); - } - - const currentDatabaseId = parseInt(databaseId, 10); - if (isNaN(currentDatabaseId)) { - return fail(400, { error: 'Invalid database ID' }); - } - - const cache = pcdManager.getCache(currentDatabaseId); - if (!cache) { - return fail(500, { error: 'Database cache not available' }); - } - - const formData = await request.formData(); - - // Get layer - const layer = (formData.get('layer') as OperationLayer) || 'user'; - - // Check layer permission - if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { - return fail(403, { error: 'Cannot write to base layer without personal access token' }); - } - - // Get current data for value guards - const currentData = await mediaManagementQueries.getRadarr(cache); - if (!currentData.mediaSettings) { - return fail(404, { error: 'Media settings not found' }); - } - - // Parse form data - const propersRepacks = formData.get('propersRepacks') as PropersRepacks; - const enableMediaInfo = formData.get('enableMediaInfo') === 'on'; - - // Validate propers_repacks - const validOptions: PropersRepacks[] = [ - 'doNotPrefer', - 'preferAndUpgrade', - 'doNotUpgradeAutomatically' - ]; - if (!validOptions.includes(propersRepacks)) { - await logger.warn('Invalid propers and repacks option', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, propersRepacks } - }); - return fail(400, { error: 'Invalid propers and repacks option' }); - } - - const result = await mediaManagementQueries.updateRadarrMediaSettings({ - databaseId: currentDatabaseId, - cache, - layer, - current: currentData.mediaSettings, - input: { - propers_repacks: propersRepacks, - enable_media_info: enableMediaInfo - } - }); - - if (!result.success) { - await logger.error('Failed to update Radarr media settings', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, error: result.error } - }); - return fail(500, { error: result.error || 'Failed to update media settings' }); - } - - return { success: true }; - }, - - updateNaming: async ({ request, params }) => { - const { databaseId } = params; - - if (!databaseId) { - return fail(400, { error: 'Missing database ID' }); - } - - const currentDatabaseId = parseInt(databaseId, 10); - if (isNaN(currentDatabaseId)) { - return fail(400, { error: 'Invalid database ID' }); - } - - const cache = pcdManager.getCache(currentDatabaseId); - if (!cache) { - return fail(500, { error: 'Database cache not available' }); - } - - const formData = await request.formData(); - - // Get layer - const layer = (formData.get('layer') as OperationLayer) || 'user'; - - // Check layer permission - if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { - return fail(403, { error: 'Cannot write to base layer without personal access token' }); - } - - // Get current data for value guards - const currentData = await mediaManagementQueries.getRadarr(cache); - if (!currentData.naming) { - return fail(404, { error: 'Naming settings not found' }); - } - - // Parse form data - const rename = formData.get('rename') === 'on'; - const replaceIllegalCharacters = formData.get('replaceIllegalCharacters') === 'on'; - const colonReplacement = formData.get('colonReplacement') as RadarrColonReplacementFormat; - const movieFormat = formData.get('movieFormat') as string; - const movieFolderFormat = formData.get('movieFolderFormat') as string; - - // Validate colon replacement (only if replace illegal characters is on) - if (replaceIllegalCharacters) { - const validColonOptions = RADARR_COLON_REPLACEMENT_OPTIONS.map((o) => o.value); - if (!validColonOptions.includes(colonReplacement)) { - await logger.warn('Invalid colon replacement option', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, colonReplacement } - }); - return fail(400, { error: 'Invalid colon replacement option' }); - } - } - - // Default colon replacement when not replacing illegal characters - const effectiveColonReplacement = replaceIllegalCharacters ? colonReplacement : 'delete'; - - const result = await mediaManagementQueries.updateRadarrNaming({ - databaseId: currentDatabaseId, - cache, - layer, - current: currentData.naming, - input: { - rename, - replace_illegal_characters: replaceIllegalCharacters, - colon_replacement_format: effectiveColonReplacement, - movie_format: movieFormat, - movie_folder_format: movieFolderFormat - } - }); - - if (!result.success) { - await logger.error('Failed to update Radarr naming settings', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, error: result.error } - }); - return fail(500, { error: result.error || 'Failed to update naming settings' }); - } - - return { success: true }; - }, - - updateQualityDefinitions: async ({ request, params }) => { - const { databaseId } = params; - - if (!databaseId) { - return fail(400, { error: 'Missing database ID' }); - } - - const currentDatabaseId = parseInt(databaseId, 10); - if (isNaN(currentDatabaseId)) { - return fail(400, { error: 'Invalid database ID' }); - } - - const cache = pcdManager.getCache(currentDatabaseId); - if (!cache) { - return fail(500, { error: 'Database cache not available' }); - } - - const formData = await request.formData(); - - // Get layer - const layer = (formData.get('layer') as OperationLayer) || 'user'; - - // Check layer permission - if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { - return fail(403, { error: 'Cannot write to base layer without personal access token' }); - } - - // Get current data for value guards - const currentData = await mediaManagementQueries.getRadarr(cache); - if (!currentData.qualityDefinitions || currentData.qualityDefinitions.length === 0) { - return fail(404, { error: 'Quality definitions not found' }); - } - - // Parse the definitions from form data (JSON string) - const definitionsJson = formData.get('definitions') as string; - if (!definitionsJson) { - return fail(400, { error: 'Missing definitions data' }); - } - - let definitions: { - quality_name: string; - min_size: number; - max_size: number; - preferred_size: number; - }[]; - try { - definitions = JSON.parse(definitionsJson); - } catch { - return fail(400, { error: 'Invalid definitions JSON' }); - } - - // Validate definitions - if (!Array.isArray(definitions) || definitions.length === 0) { - return fail(400, { error: 'Invalid definitions format' }); - } - - const result = await mediaManagementQueries.updateRadarrQualityDefinitions({ - databaseId: currentDatabaseId, - cache, - layer, - current: currentData.qualityDefinitions, - input: definitions - }); - - if (!result.success) { - await logger.error('Failed to update Radarr quality definitions', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, error: result.error } - }); - return fail(500, { error: result.error || 'Failed to update quality definitions' }); - } - - return { success: true }; - } -}; diff --git a/src/routes/media-management/[databaseId]/radarr/+page.svelte b/src/routes/media-management/[databaseId]/radarr/+page.svelte deleted file mode 100644 index d458d40..0000000 --- a/src/routes/media-management/[databaseId]/radarr/+page.svelte +++ /dev/null @@ -1,105 +0,0 @@ - - -
- {#if !hasAnyData} -
-

- No Radarr media management settings configured -

-
- {:else} - - - - - - {/if} -
- - diff --git a/src/routes/media-management/[databaseId]/sonarr/+page.server.ts b/src/routes/media-management/[databaseId]/sonarr/+page.server.ts deleted file mode 100644 index 7bd2ae2..0000000 --- a/src/routes/media-management/[databaseId]/sonarr/+page.server.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { error, fail } from '@sveltejs/kit'; -import type { ServerLoad, Actions } from '@sveltejs/kit'; -import { pcdManager } from '$pcd/pcd.ts'; -import { canWriteToBase } from '$pcd/writer.ts'; -import type { OperationLayer } from '$pcd/writer.ts'; -import * as mediaManagementQueries from '$pcd/queries/mediaManagement/index.ts'; -import type { - PropersRepacks, - ColonReplacementFormat, - MultiEpisodeStyle -} from '$lib/shared/mediaManagement.ts'; -import { - COLON_REPLACEMENT_OPTIONS, - MULTI_EPISODE_STYLE_OPTIONS -} from '$lib/shared/mediaManagement.ts'; -import { logger } from '$logger/logger.ts'; - -export const load: ServerLoad = async ({ params }) => { - const { databaseId } = params; - - // Parse the database ID - const currentDatabaseId = parseInt(databaseId as string, 10); - - // Get the cache for the database - const cache = pcdManager.getCache(currentDatabaseId); - if (!cache) { - throw error(500, 'Database cache not available'); - } - - // Load Sonarr media management data - const mediaManagement = await mediaManagementQueries.getSonarr(cache); - - return { - mediaManagement, - canWriteToBase: canWriteToBase(currentDatabaseId) - }; -}; - -export const actions: Actions = { - updateMediaSettings: async ({ request, params }) => { - const { databaseId } = params; - - if (!databaseId) { - return fail(400, { error: 'Missing database ID' }); - } - - const currentDatabaseId = parseInt(databaseId, 10); - if (isNaN(currentDatabaseId)) { - return fail(400, { error: 'Invalid database ID' }); - } - - const cache = pcdManager.getCache(currentDatabaseId); - if (!cache) { - return fail(500, { error: 'Database cache not available' }); - } - - const formData = await request.formData(); - - // Get layer - const layer = (formData.get('layer') as OperationLayer) || 'user'; - - // Check layer permission - if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { - return fail(403, { error: 'Cannot write to base layer without personal access token' }); - } - - // Get current data for value guards - const currentData = await mediaManagementQueries.getSonarr(cache); - if (!currentData.mediaSettings) { - return fail(404, { error: 'Media settings not found' }); - } - - // Parse form data - const propersRepacks = formData.get('propersRepacks') as PropersRepacks; - const enableMediaInfo = formData.get('enableMediaInfo') === 'on'; - - // Validate propers_repacks - const validOptions: PropersRepacks[] = [ - 'doNotPrefer', - 'preferAndUpgrade', - 'doNotUpgradeAutomatically' - ]; - if (!validOptions.includes(propersRepacks)) { - await logger.warn('Invalid propers and repacks option', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, propersRepacks } - }); - return fail(400, { error: 'Invalid propers and repacks option' }); - } - - const result = await mediaManagementQueries.updateSonarrMediaSettings({ - databaseId: currentDatabaseId, - cache, - layer, - current: currentData.mediaSettings, - input: { - propers_repacks: propersRepacks, - enable_media_info: enableMediaInfo - } - }); - - if (!result.success) { - await logger.error('Failed to update Sonarr media settings', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, error: result.error } - }); - return fail(500, { error: result.error || 'Failed to update media settings' }); - } - - return { success: true }; - }, - - updateNaming: async ({ request, params }) => { - const { databaseId } = params; - - if (!databaseId) { - return fail(400, { error: 'Missing database ID' }); - } - - const currentDatabaseId = parseInt(databaseId, 10); - if (isNaN(currentDatabaseId)) { - return fail(400, { error: 'Invalid database ID' }); - } - - const cache = pcdManager.getCache(currentDatabaseId); - if (!cache) { - return fail(500, { error: 'Database cache not available' }); - } - - const formData = await request.formData(); - - // Get layer - const layer = (formData.get('layer') as OperationLayer) || 'user'; - - // Check layer permission - if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { - return fail(403, { error: 'Cannot write to base layer without personal access token' }); - } - - // Get current data for value guards - const currentData = await mediaManagementQueries.getSonarr(cache); - if (!currentData.naming) { - return fail(404, { error: 'Naming settings not found' }); - } - - // Parse form data - const rename = formData.get('rename') === 'on'; - const replaceIllegalCharacters = formData.get('replaceIllegalCharacters') === 'on'; - const colonReplacement = formData.get('colonReplacement') as ColonReplacementFormat; - const customColonReplacement = formData.get('customColonReplacement') as string | null; - const standardEpisodeFormat = formData.get('standardEpisodeFormat') as string; - const dailyEpisodeFormat = formData.get('dailyEpisodeFormat') as string; - const animeEpisodeFormat = formData.get('animeEpisodeFormat') as string; - const seriesFolderFormat = formData.get('seriesFolderFormat') as string; - const seasonFolderFormat = formData.get('seasonFolderFormat') as string; - const multiEpisodeStyle = formData.get('multiEpisodeStyle') as MultiEpisodeStyle; - - // Validate colon replacement (only if replace illegal characters is on) - if (replaceIllegalCharacters) { - const validColonOptions = COLON_REPLACEMENT_OPTIONS.map((o) => o.value); - if (!validColonOptions.includes(colonReplacement)) { - await logger.warn('Invalid colon replacement option', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, colonReplacement } - }); - return fail(400, { error: 'Invalid colon replacement option' }); - } - } - - // Validate multi-episode style - const validMultiEpisodeOptions = MULTI_EPISODE_STYLE_OPTIONS.map((o) => o.value); - if (!validMultiEpisodeOptions.includes(multiEpisodeStyle)) { - await logger.warn('Invalid multi-episode style option', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, multiEpisodeStyle } - }); - return fail(400, { error: 'Invalid multi-episode style option' }); - } - - // Default colon replacement when not replacing illegal characters - const effectiveColonReplacement = replaceIllegalCharacters ? colonReplacement : 'delete'; - - const result = await mediaManagementQueries.updateSonarrNaming({ - databaseId: currentDatabaseId, - cache, - layer, - current: currentData.naming, - input: { - rename, - replace_illegal_characters: replaceIllegalCharacters, - colon_replacement_format: effectiveColonReplacement, - custom_colon_replacement_format: - effectiveColonReplacement === 'custom' ? customColonReplacement : null, - standard_episode_format: standardEpisodeFormat, - daily_episode_format: dailyEpisodeFormat, - anime_episode_format: animeEpisodeFormat, - series_folder_format: seriesFolderFormat, - season_folder_format: seasonFolderFormat, - multi_episode_style: multiEpisodeStyle - } - }); - - if (!result.success) { - await logger.error('Failed to update Sonarr naming settings', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, error: result.error } - }); - return fail(500, { error: result.error || 'Failed to update naming settings' }); - } - - return { success: true }; - }, - - updateQualityDefinitions: async ({ request, params }) => { - const { databaseId } = params; - - if (!databaseId) { - return fail(400, { error: 'Missing database ID' }); - } - - const currentDatabaseId = parseInt(databaseId, 10); - if (isNaN(currentDatabaseId)) { - return fail(400, { error: 'Invalid database ID' }); - } - - const cache = pcdManager.getCache(currentDatabaseId); - if (!cache) { - return fail(500, { error: 'Database cache not available' }); - } - - const formData = await request.formData(); - - // Get layer - const layer = (formData.get('layer') as OperationLayer) || 'user'; - - // Check layer permission - if (layer === 'base' && !canWriteToBase(currentDatabaseId)) { - return fail(403, { error: 'Cannot write to base layer without personal access token' }); - } - - // Get current data for value guards - const currentData = await mediaManagementQueries.getSonarr(cache); - if (!currentData.qualityDefinitions || currentData.qualityDefinitions.length === 0) { - return fail(404, { error: 'Quality definitions not found' }); - } - - // Parse the definitions from form data (JSON string) - const definitionsJson = formData.get('definitions') as string; - if (!definitionsJson) { - return fail(400, { error: 'Missing definitions data' }); - } - - let definitions: { - quality_name: string; - min_size: number; - max_size: number; - preferred_size: number; - }[]; - try { - definitions = JSON.parse(definitionsJson); - } catch { - return fail(400, { error: 'Invalid definitions JSON' }); - } - - // Validate definitions - if (!Array.isArray(definitions) || definitions.length === 0) { - return fail(400, { error: 'Invalid definitions format' }); - } - - const result = await mediaManagementQueries.updateSonarrQualityDefinitions({ - databaseId: currentDatabaseId, - cache, - layer, - current: currentData.qualityDefinitions, - input: definitions - }); - - if (!result.success) { - await logger.error('Failed to update Sonarr quality definitions', { - source: 'MediaManagement', - meta: { databaseId: currentDatabaseId, error: result.error } - }); - return fail(500, { error: result.error || 'Failed to update quality definitions' }); - } - - return { success: true }; - } -}; diff --git a/src/routes/media-management/[databaseId]/sonarr/+page.svelte b/src/routes/media-management/[databaseId]/sonarr/+page.svelte deleted file mode 100644 index 311ad11..0000000 --- a/src/routes/media-management/[databaseId]/sonarr/+page.svelte +++ /dev/null @@ -1,105 +0,0 @@ - - -
- {#if !hasAnyData} -
-

- No Sonarr media management settings configured -

-
- {:else} - - - - - - {/if} -
- -