diff --git a/src/lib/client/ui/form/Autocomplete.svelte b/src/lib/client/ui/form/Autocomplete.svelte
new file mode 100644
index 0000000..4117aaa
--- /dev/null
+++ b/src/lib/client/ui/form/Autocomplete.svelte
@@ -0,0 +1,177 @@
+
+
+
+
+
+
+ {#each selected as item (item.value)}
+
+ {item.label}
+
+
+ {/each}
+
+
+ {#if selected.length < max}
+
+ {/if}
+
+
+
+ {#if isOpen && filteredOptions.length > 0}
+
+ {#each filteredOptions as option, index (option.value)}
+
+ {/each}
+
+ {/if}
+
+
+ {#if isOpen && inputValue && filteredOptions.length === 0}
+
+ No matches found
+
+ {/if}
+
diff --git a/src/lib/client/ui/form/Select.svelte b/src/lib/client/ui/form/Select.svelte
new file mode 100644
index 0000000..6a17808
--- /dev/null
+++ b/src/lib/client/ui/form/Select.svelte
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+ {#if isOpen}
+
+ {#each options as option, index (option.value)}
+
+ {/each}
+
+ {/if}
+
diff --git a/src/lib/server/pcd/queries/customFormats/index.ts b/src/lib/server/pcd/queries/customFormats/index.ts
index 479c6c4..80e3e2e 100644
--- a/src/lib/server/pcd/queries/customFormats/index.ts
+++ b/src/lib/server/pcd/queries/customFormats/index.ts
@@ -8,6 +8,7 @@ export type { CreateTestInput, CreateTestOptions } from './testCreate.ts';
export type { UpdateTestInput, UpdateTestOptions } from './testUpdate.ts';
export type { DeleteTestOptions } from './testDelete.ts';
export type { ConditionData } from './conditions.ts';
+export type { ConditionListItem } from './listConditions.ts';
export type { ConditionResult, EvaluationResult, ParsedInfo } from './evaluator.ts';
// Export query functions (reads)
@@ -15,6 +16,7 @@ export { list } from './list.ts';
export { general } from './general.ts';
export { getById, listTests, getTestById } from './tests.ts';
export { getConditionsForEvaluation } from './conditions.ts';
+export { listConditions } from './listConditions.ts';
export { evaluateCustomFormat, getParsedInfo } from './evaluator.ts';
// Export mutation functions (writes via PCD operations)
diff --git a/src/lib/server/pcd/queries/customFormats/listConditions.ts b/src/lib/server/pcd/queries/customFormats/listConditions.ts
new file mode 100644
index 0000000..9190473
--- /dev/null
+++ b/src/lib/server/pcd/queries/customFormats/listConditions.ts
@@ -0,0 +1,39 @@
+/**
+ * Custom format condition list query
+ */
+
+import type { PCDCache } from '../../cache.ts';
+
+/** Condition item for list display */
+export interface ConditionListItem {
+ id: number;
+ name: string;
+ type: string;
+ negate: boolean;
+ required: boolean;
+}
+
+/**
+ * Get all conditions for a custom format (basic info for list display)
+ */
+export async function listConditions(
+ cache: PCDCache,
+ formatId: number
+): Promise {
+ const db = cache.kb;
+
+ const conditions = await db
+ .selectFrom('custom_format_conditions')
+ .select(['id', 'name', 'type', 'negate', 'required'])
+ .where('custom_format_id', '=', formatId)
+ .orderBy('id')
+ .execute();
+
+ return conditions.map((c) => ({
+ id: c.id,
+ name: c.name,
+ type: c.type,
+ negate: c.negate === 1,
+ required: c.required === 1
+ }));
+}
diff --git a/src/lib/shared/conditionTypes.ts b/src/lib/shared/conditionTypes.ts
new file mode 100644
index 0000000..c0f7426
--- /dev/null
+++ b/src/lib/shared/conditionTypes.ts
@@ -0,0 +1,84 @@
+/**
+ * Condition types and their valid values for custom formats
+ */
+
+// Arr type for filtering
+export type ArrType = 'all' | 'radarr' | 'sonarr';
+
+// Condition type definitions
+export const CONDITION_TYPES = [
+ { value: 'release_title', label: 'Release Title', arrType: 'all' as ArrType },
+ { value: 'release_group', label: 'Release Group', arrType: 'all' as ArrType },
+ { value: 'edition', label: 'Edition', arrType: 'radarr' as ArrType },
+ { value: 'language', label: 'Language', arrType: 'all' as ArrType },
+ { value: 'source', label: 'Source', arrType: 'all' as ArrType },
+ { value: 'resolution', label: 'Resolution', arrType: 'all' as ArrType },
+ { value: 'quality_modifier', label: 'Quality Modifier', arrType: 'radarr' as ArrType },
+ { value: 'release_type', label: 'Release Type', arrType: 'sonarr' as ArrType },
+ { value: 'indexer_flag', label: 'Indexer Flag', arrType: 'all' as ArrType },
+ { value: 'size', label: 'Size', arrType: 'all' as ArrType },
+ { value: 'year', label: 'Year', arrType: 'all' as ArrType }
+] as const;
+
+// Pattern-based types (use regex patterns as values)
+export const PATTERN_TYPES = ['release_title', 'release_group', 'edition'] as const;
+
+export type ConditionType = (typeof CONDITION_TYPES)[number]['value'];
+
+// Source values
+export const SOURCE_VALUES = [
+ { value: 'unknown', label: 'Unknown', arrType: 'all' as ArrType },
+ { value: 'television', label: 'Television', arrType: 'all' as ArrType },
+ { value: 'television_raw', label: 'Television Raw', arrType: 'sonarr' as ArrType },
+ { value: 'webdl', label: 'WEB-DL', arrType: 'all' as ArrType },
+ { value: 'webrip', label: 'WEBRip', arrType: 'all' as ArrType },
+ { value: 'dvd', label: 'DVD', arrType: 'all' as ArrType },
+ { value: 'bluray', label: 'Bluray', arrType: 'all' as ArrType },
+ { value: 'bluray_raw', label: 'Bluray Raw', arrType: 'sonarr' as ArrType },
+ { value: 'cam', label: 'CAM', arrType: 'all' as ArrType },
+ { value: 'telesync', label: 'Telesync', arrType: 'all' as ArrType },
+ { value: 'telecine', label: 'Telecine', arrType: 'all' as ArrType },
+ { value: 'workprint', label: 'Workprint', arrType: 'all' as ArrType }
+] as const;
+
+// Resolution values
+export const RESOLUTION_VALUES = [
+ { value: '360p', label: '360p', arrType: 'all' as ArrType },
+ { value: '480p', label: '480p', arrType: 'all' as ArrType },
+ { value: '540p', label: '540p', arrType: 'all' as ArrType },
+ { value: '576p', label: '576p', arrType: 'all' as ArrType },
+ { value: '720p', label: '720p', arrType: 'all' as ArrType },
+ { value: '1080p', label: '1080p', arrType: 'all' as ArrType },
+ { value: '2160p', label: '2160p', arrType: 'all' as ArrType }
+] as const;
+
+// Quality modifier values (Radarr only)
+export const QUALITY_MODIFIER_VALUES = [
+ { value: 'none', label: 'None', arrType: 'radarr' as ArrType },
+ { value: 'regional', label: 'Regional', arrType: 'radarr' as ArrType },
+ { value: 'screener', label: 'Screener', arrType: 'radarr' as ArrType },
+ { value: 'rawhd', label: 'RawHD', arrType: 'radarr' as ArrType },
+ { value: 'brdisk', label: 'BRDISK', arrType: 'radarr' as ArrType },
+ { value: 'remux', label: 'REMUX', arrType: 'radarr' as ArrType }
+] as const;
+
+// Release type values (Sonarr only)
+export const RELEASE_TYPE_VALUES = [
+ { value: 'single_episode', label: 'Single Episode', arrType: 'sonarr' as ArrType },
+ { value: 'multi_episode', label: 'Multi Episode', arrType: 'sonarr' as ArrType },
+ { value: 'season_pack', label: 'Season Pack', arrType: 'sonarr' as ArrType }
+] as const;
+
+// Indexer flag values
+export const INDEXER_FLAG_VALUES = [
+ { value: 'freeleech', label: 'Freeleech', arrType: 'all' as ArrType },
+ { value: 'halfleech', label: 'Halfleech', arrType: 'all' as ArrType },
+ { value: 'double_upload', label: 'Double Upload', arrType: 'all' as ArrType },
+ { value: 'internal', label: 'Internal', arrType: 'all' as ArrType },
+ { value: 'scene', label: 'Scene', arrType: 'all' as ArrType },
+ { value: 'freeleech_75', label: 'Freeleech 75%', arrType: 'all' as ArrType },
+ { value: 'freeleech_25', label: 'Freeleech 25%', arrType: 'all' as ArrType },
+ { value: 'nuked', label: 'Nuked', arrType: 'all' as ArrType },
+ { value: 'ptp_golden', label: 'PTP Golden', arrType: 'radarr' as ArrType },
+ { value: 'ptp_approved', label: 'PTP Approved', arrType: 'radarr' as ArrType }
+] as const;
diff --git a/src/routes/custom-formats/[databaseId]/[id]/+layout.svelte b/src/routes/custom-formats/[databaseId]/[id]/+layout.svelte
index 2c9a4a3..7f9231f 100644
--- a/src/routes/custom-formats/[databaseId]/[id]/+layout.svelte
+++ b/src/routes/custom-formats/[databaseId]/[id]/+layout.svelte
@@ -1,7 +1,7 @@
+
+
+ {data.format.name} - Conditions - Profilarr
+
+
+
+
+
+
+
Conditions
+
+ Define the conditions that must be met for this custom format to match a release.
+
+
+
+
+
+
+ {#if draftConditions.length > 0}
+
+
+ Drafts
+ {draftConditions.length}
+
+
+ {#each draftConditions as draft (draft.id)}
+
confirmDraft(draft)}
+ on:discard={() => discardDraft(draft.id)}
+ />
+ {/each}
+
+ {/if}
+
+
+ {#if data.conditions.length === 0 && draftConditions.length === 0}
+
No conditions defined
+ {:else}
+ {#each orderedTypes as group (group.value)}
+
+
+
+
+ {group.label}
+
+ {group.conditions.length}
+
+
+
+ {#each group.conditions as condition (condition.id)}
+
handleRemove(condition.id)}
+ />
+ {/each}
+
+ {/each}
+ {/if}
+
diff --git a/src/routes/custom-formats/[databaseId]/[id]/conditions/components/ConditionCard.svelte b/src/routes/custom-formats/[databaseId]/[id]/conditions/components/ConditionCard.svelte
new file mode 100644
index 0000000..206e4b6
--- /dev/null
+++ b/src/routes/custom-formats/[databaseId]/[id]/conditions/components/ConditionCard.svelte
@@ -0,0 +1,327 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (condition.negate = !condition.negate)}
+ />
+ Negate
+
+
+
+
+ (condition.required = !condition.required)}
+ />
+ Required
+
+
+
+
+
diff --git a/src/routes/custom-formats/[databaseId]/[id]/conditions/components/DraftConditionCard.svelte b/src/routes/custom-formats/[databaseId]/[id]/conditions/components/DraftConditionCard.svelte
new file mode 100644
index 0000000..5ddf121
--- /dev/null
+++ b/src/routes/custom-formats/[databaseId]/[id]/conditions/components/DraftConditionCard.svelte
@@ -0,0 +1,340 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (condition.negate = !condition.negate)}
+ />
+ Negate
+
+
+
+
+ (condition.required = !condition.required)}
+ />
+ Required
+
+
+
+
+
+
+
+