-
+
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}
-
-
- {: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}
-
-
- {:else if arrType === 'radarr' && naming && isRadarrNaming(naming) && isEditing}
-
-
- {: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}
-
-
- {: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'}
+
+ {/if}
+
+
+
+
+
+
+
+
+
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'}
+
+

+
+ {: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'}
+
+ {/if}
+
+
+
+
+
+
+
+
+
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'}
+
+ {/if}
+
+
+
+
+
+
+
+
+
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"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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'}
+
+

+
+ {: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'}
+
+ {/if}
+
+
+
+
+
+
+
+
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'}
+
+

+
+ {/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}
-
-
-