refactor: media management can now contain multiple configs for each setting

This commit is contained in:
Sam Chau
2026-01-27 07:47:31 +10:30
parent 60049737b3
commit 2e36df30e5
79 changed files with 5351 additions and 3419 deletions

View File

@@ -39,6 +39,7 @@ import { migration as migration034 } from './migrations/034_add_sync_status.ts';
import { migration as migration035 } from './migrations/035_add_job_skipped_status.ts';
import { migration as migration036 } from './migrations/036_create_auth_tables.ts';
import { migration as migration037 } from './migrations/037_add_session_metadata.ts';
import { migration as migration038 } from './migrations/038_add_media_management_config_names.ts';
export interface Migration {
version: number;
@@ -296,7 +297,8 @@ export function loadMigrations(): Migration[] {
migration034,
migration035,
migration036,
migration037
migration037,
migration038
];
// Sort by version number

View File

@@ -0,0 +1,60 @@
import type { Migration } from '../migrations.ts';
/**
* Migration 038: Add config name columns to arr_sync_media_management
*
* With multi-config support, each database can have multiple naming, quality definitions,
* and media settings configs. We need to store which specific config to use, not just
* which database.
*
* Adds:
* - naming_config_name: Name of the naming config to sync
* - quality_definitions_config_name: Name of the quality definitions config to sync
* - media_settings_config_name: Name of the media settings config to sync
*/
export const migration: Migration = {
version: 38,
name: 'Add media management config names',
up: `
ALTER TABLE arr_sync_media_management ADD COLUMN naming_config_name TEXT;
ALTER TABLE arr_sync_media_management ADD COLUMN quality_definitions_config_name TEXT;
ALTER TABLE arr_sync_media_management ADD COLUMN media_settings_config_name TEXT;
`,
down: `
-- SQLite doesn't support DROP COLUMN directly, so we recreate the table
CREATE TABLE arr_sync_media_management_new (
instance_id INTEGER PRIMARY KEY,
naming_database_id INTEGER,
quality_definitions_database_id INTEGER,
media_settings_database_id INTEGER,
trigger TEXT NOT NULL DEFAULT 'none',
cron TEXT,
should_sync INTEGER NOT NULL DEFAULT 0,
next_run_at TEXT,
sync_status TEXT NOT NULL DEFAULT 'idle',
last_error TEXT,
last_synced_at TEXT,
FOREIGN KEY (instance_id) REFERENCES arr_instances(id) ON DELETE CASCADE,
FOREIGN KEY (naming_database_id) REFERENCES database_instances(id) ON DELETE SET NULL,
FOREIGN KEY (quality_definitions_database_id) REFERENCES database_instances(id) ON DELETE SET NULL,
FOREIGN KEY (media_settings_database_id) REFERENCES database_instances(id) ON DELETE SET NULL
);
INSERT INTO arr_sync_media_management_new (
instance_id, naming_database_id, quality_definitions_database_id,
media_settings_database_id, trigger, cron, should_sync, next_run_at,
sync_status, last_error, last_synced_at
)
SELECT
instance_id, naming_database_id, quality_definitions_database_id,
media_settings_database_id, trigger, cron, should_sync, next_run_at,
sync_status, last_error, last_synced_at
FROM arr_sync_media_management;
DROP TABLE arr_sync_media_management;
ALTER TABLE arr_sync_media_management_new RENAME TO arr_sync_media_management;
`
};

View File

@@ -29,8 +29,11 @@ export interface DelayProfilesSyncData {
export interface MediaManagementSyncData {
namingDatabaseId: number | null;
namingConfigName: string | null;
qualityDefinitionsDatabaseId: number | null;
qualityDefinitionsConfigName: string | null;
mediaSettingsDatabaseId: number | null;
mediaSettingsConfigName: string | null;
trigger: SyncTrigger;
cron: string | null;
nextRunAt?: string | null;
@@ -60,8 +63,11 @@ interface DelayProfileConfigRow {
interface MediaManagementRow {
instance_id: number;
naming_database_id: number | null;
naming_config_name: string | null;
quality_definitions_database_id: number | null;
quality_definitions_config_name: string | null;
media_settings_database_id: number | null;
media_settings_config_name: string | null;
trigger: string;
cron: string | null;
}
@@ -176,8 +182,11 @@ export const arrSyncQueries = {
return {
namingDatabaseId: row?.naming_database_id ?? null,
namingConfigName: row?.naming_config_name ?? null,
qualityDefinitionsDatabaseId: row?.quality_definitions_database_id ?? null,
qualityDefinitionsConfigName: row?.quality_definitions_config_name ?? null,
mediaSettingsDatabaseId: row?.media_settings_database_id ?? null,
mediaSettingsConfigName: row?.media_settings_config_name ?? null,
trigger: (row?.trigger as SyncTrigger) ?? 'manual',
cron: row?.cron ?? null
};
@@ -186,25 +195,34 @@ export const arrSyncQueries = {
saveMediaManagementSync(instanceId: number, data: MediaManagementSyncData): void {
db.execute(
`INSERT INTO arr_sync_media_management
(instance_id, naming_database_id, quality_definitions_database_id, media_settings_database_id, trigger, cron, next_run_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
(instance_id, naming_database_id, naming_config_name, quality_definitions_database_id, quality_definitions_config_name, media_settings_database_id, media_settings_config_name, trigger, cron, next_run_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(instance_id) DO UPDATE SET
naming_database_id = ?,
naming_config_name = ?,
quality_definitions_database_id = ?,
quality_definitions_config_name = ?,
media_settings_database_id = ?,
media_settings_config_name = ?,
trigger = ?,
cron = ?,
next_run_at = ?`,
instanceId,
data.namingDatabaseId,
data.namingConfigName,
data.qualityDefinitionsDatabaseId,
data.qualityDefinitionsConfigName,
data.mediaSettingsDatabaseId,
data.mediaSettingsConfigName,
data.trigger,
data.cron,
data.nextRunAt ?? null,
data.namingDatabaseId,
data.namingConfigName,
data.qualityDefinitionsDatabaseId,
data.qualityDefinitionsConfigName,
data.mediaSettingsDatabaseId,
data.mediaSettingsConfigName,
data.trigger,
data.cron,
data.nextRunAt ?? null
@@ -252,15 +270,15 @@ export const arrSyncQueries = {
databaseId
);
db.execute(
'UPDATE arr_sync_media_management SET naming_database_id = NULL WHERE naming_database_id = ?',
'UPDATE arr_sync_media_management SET naming_database_id = NULL, naming_config_name = NULL WHERE naming_database_id = ?',
databaseId
);
db.execute(
'UPDATE arr_sync_media_management SET quality_definitions_database_id = NULL WHERE quality_definitions_database_id = ?',
'UPDATE arr_sync_media_management SET quality_definitions_database_id = NULL, quality_definitions_config_name = NULL WHERE quality_definitions_database_id = ?',
databaseId
);
db.execute(
'UPDATE arr_sync_media_management SET media_settings_database_id = NULL WHERE media_settings_database_id = ?',
'UPDATE arr_sync_media_management SET media_settings_database_id = NULL, media_settings_config_name = NULL WHERE media_settings_database_id = ?',
databaseId
);
},

View File

@@ -336,14 +336,17 @@ CREATE TABLE arr_sync_delay_profiles_config (
-- ==============================================================================
-- TABLE: arr_sync_media_management
-- Purpose: Store media management sync configuration (one per instance)
-- Migration: 015_create_arr_sync_tables.ts, 016_add_should_sync_flags.ts, 029_add_database_id_foreign_keys.ts, 034_add_sync_status.ts
-- Migration: 015_create_arr_sync_tables.ts, 016_add_should_sync_flags.ts, 029_add_database_id_foreign_keys.ts, 034_add_sync_status.ts, 038_add_media_management_config_names.ts
-- ==============================================================================
CREATE TABLE arr_sync_media_management (
instance_id INTEGER PRIMARY KEY,
naming_database_id INTEGER, -- Database to use for naming settings
naming_config_name TEXT, -- Name of the naming config to sync (Migration 038)
quality_definitions_database_id INTEGER, -- Database to use for quality definitions
quality_definitions_config_name TEXT, -- Name of the quality definitions config to sync (Migration 038)
media_settings_database_id INTEGER, -- Database to use for media settings
media_settings_config_name TEXT, -- Name of the media settings config to sync (Migration 038)
trigger TEXT NOT NULL DEFAULT 'none', -- 'none', 'manual', 'on_pull', 'on_change', 'schedule'
cron TEXT, -- Cron expression for schedule trigger
should_sync INTEGER NOT NULL DEFAULT 0, -- Flag for pending sync (Migration 016) - deprecated

View File

@@ -0,0 +1,118 @@
/**
* Combined getters for media management configs
* Provides backward compatibility for the syncer which expects combined objects
*
* NOTE: This returns the first available config for each type.
* The syncer will need to be updated later to support selecting specific named configs.
*/
import type { PCDCache } from '../../cache.ts';
import type { MediaSettings, RadarrNaming, SonarrNaming } from '$lib/shared/mediaManagement.ts';
import { getRadarrByName as getRadarrMediaSettings, getSonarrByName as getSonarrMediaSettings } from './media-settings/read.ts';
import { getRadarrByName as getRadarrNaming, getSonarrByName as getSonarrNaming } from './naming/read.ts';
import { getRadarrByName as getRadarrQualityDefs, getSonarrByName as getSonarrQualityDefs } from './quality-definitions/read.ts';
import { list as listMediaSettings } from './media-settings/read.ts';
import { list as listNaming } from './naming/read.ts';
import { list as listQualityDefs } from './quality-definitions/read.ts';
import type { QualityDefinitionEntry } from './quality-definitions/types.ts';
export interface QualityDefinition {
quality_name: string;
min_size: number;
max_size: number | null;
preferred_size: number | null;
}
export interface RadarrCombined {
mediaSettings: MediaSettings | null;
naming: RadarrNaming | null;
qualityDefinitions: QualityDefinition[];
}
export interface SonarrCombined {
mediaSettings: MediaSettings | null;
naming: SonarrNaming | null;
qualityDefinitions: QualityDefinition[];
}
/**
* Get all Radarr media management configs (returns first available of each type)
*/
export async function getRadarr(cache: PCDCache): Promise<RadarrCombined> {
// 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<SonarrCombined> {
// 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<QualityDefinition[]> {
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
}));
}

View File

@@ -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<RadarrMediaManagementData> {
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<SonarrMediaManagementData> {
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<MediaManagementData> {
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<QualityDefinition[]> {
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<QualityDefinition[]> {
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<RadarrNaming | null> {
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<SonarrNaming | null> {
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<MediaSettings | null> {
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<MediaSettings | null> {
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
};
}

View File

@@ -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';

View File

@@ -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
}
});
}

View File

@@ -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';

View File

@@ -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<MediaSettingsListItem[]> {
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<MediaSettings | null> {
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<MediaSettings | null> {
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
};
}

View File

@@ -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
}
});
}

View File

@@ -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;
}

View File

@@ -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
}
});
}

View File

@@ -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
}
});
}

View File

@@ -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';

View File

@@ -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<NamingListItem[]> {
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<RadarrNaming | null> {
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<SonarrNaming | null> {
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)
};
}

View File

@@ -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
}
});
}

View File

@@ -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;
}

View File

@@ -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
}
});
}

View File

@@ -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
}
});
}

View File

@@ -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';

View File

@@ -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<string[]> {
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<QualityDefinitionListItem[]> {
// 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<QualityDefinitionsConfig | null> {
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<QualityDefinitionsConfig | null> {
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
};
}

View File

@@ -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
}
});
}

View File

@@ -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[];
}

View File

@@ -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
}
});
}

View File

@@ -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;
}

View File

@@ -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<string, { from: unknown; to: unknown }> = {};
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<string, { from: unknown; to: unknown }> = {};
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<string, { from: unknown; to: unknown }> = {};
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<string, { from: unknown; to: unknown }> = {};
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<string, { from: unknown; to: unknown }> = {};
// 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<string, { from: unknown; to: unknown }> = {};
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<string, { from: unknown; to: unknown }> = {};
// 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<string, { from: unknown; to: unknown }> = {};
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'
}
});
}

View File

@@ -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<string>;
@@ -313,7 +315,7 @@ export interface RadarrMediaSettingsTable {
}
export interface SonarrMediaSettingsTable {
id: number;
name: string;
propers_repacks: string;
enable_media_info: number;
created_at: Generated<string>;

View File

@@ -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<boolean> {
private async syncMediaSettings(databaseId: number, configName: string): Promise<boolean> {
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<boolean> {
private async syncNaming(databaseId: number, configName: string): Promise<boolean> {
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<typeof getCache>,
databaseId: number
cache: PCDCache,
configName: string
): Promise<boolean> {
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<typeof getCache>,
databaseId: number
cache: PCDCache,
configName: string
): Promise<boolean> {
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<boolean> {
private async syncQualityDefinitions(databaseId: number, configName: string): Promise<boolean> {
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