From 65fa73efe4d68c0228e89963ff02ca06aa48d897 Mon Sep 17 00:00:00 2001 From: santiagosayshey Date: Fri, 17 Apr 2026 16:39:10 +0930 Subject: [PATCH] feat: add media management onboarding cutscene group (#449) --- docs/frontend/cutscene.md | 46 ++++++++------- src/lib/client/cutscene/definitions/index.ts | 11 ++++ .../stages/media-management/media-settings.ts | 56 +++++++++++++++++++ .../stages/media-management/naming.ts | 47 ++++++++++++++++ .../media-management/quality-definitions.ts | 23 ++++++++ src/lib/client/cutscene/routeResolvers.ts | 22 ++++++++ .../components/MediaSettingsForm.svelte | 6 +- .../media-settings/first/+server.ts | 18 ++++++ .../naming/components/RadarrNamingForm.svelte | 10 +++- .../naming/components/SonarrNamingForm.svelte | 10 +++- .../[databaseId]/naming/first/+server.ts | 18 ++++++ .../quality-definitions/first/+server.ts | 18 ++++++ 12 files changed, 257 insertions(+), 28 deletions(-) create mode 100644 src/lib/client/cutscene/definitions/stages/media-management/media-settings.ts create mode 100644 src/lib/client/cutscene/definitions/stages/media-management/naming.ts create mode 100644 src/lib/client/cutscene/definitions/stages/media-management/quality-definitions.ts create mode 100644 src/routes/media-management/[databaseId]/media-settings/first/+server.ts create mode 100644 src/routes/media-management/[databaseId]/naming/first/+server.ts create mode 100644 src/routes/media-management/[databaseId]/quality-definitions/first/+server.ts diff --git a/docs/frontend/cutscene.md b/docs/frontend/cutscene.md index 3fe2a8a4..9813026e 100644 --- a/docs/frontend/cutscene.md +++ b/docs/frontend/cutscene.md @@ -134,27 +134,30 @@ like Help where a "you're done" modal would be redundant. ### Current Stages -| ID | Name | Steps | Prerequisites | Description | -| ----------------- | ----------- | ----- | --------------------------------- | ----------------------------------------------- | -| `welcome` | Welcome | 3 | | What Profilarr is and how it works | -| `navigation` | Navigation | 5 | | The main sections of the app | -| `personalize` | Personalize | 2 | | Theme toggle and accent color picker | -| `help` | Help | 1 | | Introduces the help button (silent) | -| `database-link` | Link | 7 | | Connect a configuration database | -| `database-manage` | Overview | 6 | `hasDatabase` | Tabs and features of a connected database | -| `arr-link` | Link | 5 | | Connect a Radarr or Sonarr instance | -| `arr-manage` | Overview | 7 | `hasArrInstance` | Tabs and features of a connected Arr instance | -| `arr-sync` | Sync | 7 | `hasArrInstance` | Configure what gets synced and when | -| `arr-upgrades` | Upgrades | 11 | `hasArrInstance` | Automated searching for better quality releases | -| `arr-renames` | Rename | 7 | `hasArrInstance` | Automated file and folder renaming | -| `cf-general` | General | 6 | `hasDatabase` | Identity, rename behavior, and references | -| `cf-conditions` | Conditions | 8 | `hasDatabase` | Define what a custom format matches | -| `cf-testing` | Testing | 7 | `hasDevDatabase`, `parserHealthy` | Verify a format against real release titles | -| `qp-general` | General | 6 | `hasDatabase` | Identity, description, tags, and language | -| `qp-scoring` | Scoring | 7 | `hasDatabase` | Score custom formats and shape upgrade behavior | -| `qp-qualities` | Qualities | 7 | `hasDatabase` | Order, enable, and group qualities | -| `regex-general` | General | 6 | `hasDatabase` | Reusable regex patterns for CF conditions | -| `delay-general` | General | 6 | `hasDatabase` | Name, protocol, delays, and bypass conditions | +| ID | Name | Steps | Prerequisites | Description | +| --------------------------- | ------------------- | ----- | --------------------------------- | -------------------------------------------------------------- | +| `welcome` | Welcome | 3 | | What Profilarr is and how it works | +| `navigation` | Navigation | 5 | | The main sections of the app | +| `personalize` | Personalize | 2 | | Theme toggle and accent color picker | +| `help` | Help | 1 | | Introduces the help button (silent) | +| `database-link` | Link | 7 | | Connect a configuration database | +| `database-manage` | Overview | 6 | `hasDatabase` | Tabs and features of a connected database | +| `arr-link` | Link | 5 | | Connect a Radarr or Sonarr instance | +| `arr-manage` | Overview | 7 | `hasArrInstance` | Tabs and features of a connected Arr instance | +| `arr-sync` | Sync | 7 | `hasArrInstance` | Configure what gets synced and when | +| `arr-upgrades` | Upgrades | 11 | `hasArrInstance` | Automated searching for better quality releases | +| `arr-renames` | Rename | 7 | `hasArrInstance` | Automated file and folder renaming | +| `cf-general` | General | 6 | `hasDatabase` | Identity, rename behavior, and references | +| `cf-conditions` | Conditions | 8 | `hasDatabase` | Define what a custom format matches | +| `cf-testing` | Testing | 7 | `hasDevDatabase`, `parserHealthy` | Verify a format against real release titles | +| `qp-general` | General | 6 | `hasDatabase` | Identity, description, tags, and language | +| `qp-scoring` | Scoring | 7 | `hasDatabase` | Score custom formats and shape upgrade behavior | +| `qp-qualities` | Qualities | 7 | `hasDatabase` | Order, enable, and group qualities | +| `regex-general` | General | 6 | `hasDatabase` | Reusable regex patterns for CF conditions | +| `delay-general` | General | 6 | `hasDatabase` | Name, protocol, delays, and bypass conditions | +| `media-naming` | Naming | 4 | `hasDatabase` | Naming formats and character replacement for files and folders | +| `media-quality-definitions` | Quality Definitions | 1 | `hasDatabase` | File size gates per quality tier | +| `media-settings` | Media Settings | 5 | `hasDatabase` | Propers/repacks handling and file analysis | ## Prerequisites @@ -241,6 +244,7 @@ export const GROUPS: StageGroup[] = [ | Getting Started | Welcome, Navigation, Personalize, Help | | Databases | Connect a Database, Managing a Database | | Arr Instances | Connect an Arr Instance, Managing an Arr Instance | +| Media Management | Naming, Quality Definitions, Media Settings | | Custom Formats | General, Conditions, Testing | | Quality Profiles | General, Scoring, Qualities | | Regular Expressions | General | diff --git a/src/lib/client/cutscene/definitions/index.ts b/src/lib/client/cutscene/definitions/index.ts index 166c05c4..2c21952e 100644 --- a/src/lib/client/cutscene/definitions/index.ts +++ b/src/lib/client/cutscene/definitions/index.ts @@ -18,6 +18,9 @@ import { qpScoringStage } from './stages/quality-profiles/scoring.ts'; import { qpQualitiesStage } from './stages/quality-profiles/qualities.ts'; import { regexGeneralStage } from './stages/regular-expressions/general.ts'; import { delayGeneralStage } from './stages/delay-profiles/general.ts'; +import { mediaNamingStage } from './stages/media-management/naming.ts'; +import { mediaQualityDefinitionsStage } from './stages/media-management/quality-definitions.ts'; +import { mediaSettingsStage } from './stages/media-management/media-settings.ts'; export const STAGES: Record = { welcome: welcomeStage, @@ -38,6 +41,9 @@ export const STAGES: Record = { 'qp-qualities': qpQualitiesStage, 'regex-general': regexGeneralStage, 'delay-general': delayGeneralStage, + 'media-naming': mediaNamingStage, + 'media-quality-definitions': mediaQualityDefinitionsStage, + 'media-settings': mediaSettingsStage, help: helpStage }; @@ -57,6 +63,11 @@ export const GROUPS: StageGroup[] = [ description: 'Connect and manage Radarr/Sonarr instances', stages: ['arr-link', 'arr-manage', 'arr-sync', 'arr-upgrades', 'arr-renames'] }, + { + name: 'Media Management', + description: 'Naming, quality definitions, and media settings', + stages: ['media-naming', 'media-quality-definitions', 'media-settings'] + }, { name: 'Regular Expressions', description: 'Build reusable regex patterns for custom format conditions', diff --git a/src/lib/client/cutscene/definitions/stages/media-management/media-settings.ts b/src/lib/client/cutscene/definitions/stages/media-management/media-settings.ts new file mode 100644 index 00000000..96c5919d --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/media-management/media-settings.ts @@ -0,0 +1,56 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const mediaSettingsStage: Stage = { + id: 'media-settings', + name: 'Media Settings', + description: 'Propers and repacks handling, and file analysis for renames', + prerequisites: [ + { + check: 'hasDatabase', + message: + 'You need at least one connected database to start this stage. Follow the "Link" stage under Databases from the onboarding page to connect one.' + } + ], + steps: [ + { + id: 'media-settings-intro', + route: { resolve: 'firstMediaSettings' }, + title: 'Media Settings', + body: 'Media settings is the miscellaneous bucket: how Radarr or Sonarr treats propers and repacks, and whether it scans files for media information after import. Two decisions, both small on the surface, but both interact with the rest of your setup.', + completion: { type: 'manual' } + }, + { + id: 'media-settings-name', + target: 'media-settings-name', + title: 'Name', + body: 'The name identifies this media settings config when you assign it on the Sync tab of an Arr instance. Keep it descriptive (for example "default" for your baseline or "radarr-strict" for a variant) so the dropdown is easy to read later.', + position: 'below', + freeInteract: true, + completion: { type: 'manual' } + }, + { + id: 'media-settings-propers-repacks', + target: 'media-settings-propers-repacks', + title: 'Propers and Repacks', + body: 'Propers and repacks are re-releases that a group puts out to fix a problem in an earlier upload (scene groups call them propers, p2p groups call them repacks, but they serve the same purpose). The Arr has its own built-in preference system for these, which predates custom formats. The recommended setting here is "Do Not Prefer": a well-built PCD already scores PROPER and REPACK tags through custom formats, so the scoring layer handles the upgrade decision cleanly. Leaving the Arr-level preference on creates a second, opaque ranking system that can quietly override your scores.', + position: 'below', + freeInteract: true, + completion: { type: 'manual' } + }, + { + id: 'media-settings-file-analysis', + target: 'media-settings-file-analysis', + title: 'File Analysis', + body: 'Enable Media Info runs mediainfo against imported files to extract codec, resolution, audio tracks, and similar metadata. This matters downstream: the Rename tab builds filenames from naming tokens like {MediaInfo VideoCodec} and {MediaInfo AudioChannels}, and those tokens resolve to empty strings if media info was never collected. Leave this on if you want renames to produce complete filenames; turn it off only if the scan itself is causing problems on your storage.', + position: 'below', + freeInteract: true, + completion: { type: 'manual' } + }, + { + id: 'media-settings-summary', + title: 'Summary', + body: 'A descriptive name keeps the Sync dropdown readable, "Do Not Prefer" keeps propers and repacks out of your custom formats\' way, and leaving file analysis on lets renames fill in media tokens. Assign the config on the Sync tab of an Arr instance to activate it there.', + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/definitions/stages/media-management/naming.ts b/src/lib/client/cutscene/definitions/stages/media-management/naming.ts new file mode 100644 index 00000000..69e1740c --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/media-management/naming.ts @@ -0,0 +1,47 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const mediaNamingStage: Stage = { + id: 'media-naming', + name: 'Naming', + description: 'Control how Radarr and Sonarr lay out files and folders on disk', + prerequisites: [ + { + check: 'hasDatabase', + message: + 'You need at least one connected database to start this stage. Follow the "Link" stage under Databases from the onboarding page to connect one.' + } + ], + steps: [ + { + id: 'media-naming-intro', + route: { resolve: 'firstNaming' }, + title: 'Naming', + body: "A naming config tells Radarr or Sonarr how to name files and folders on disk. This is the Profilarr-managed equivalent of the Arr's own Settings > Media Management > Episode/Movie Naming screen, and each config lives under a single arr type. Radarr handles a movie file format and a movie folder format, while Sonarr splits things further into standard, daily, and anime episode formats plus series and season folder formats. The default config on your linked database already ships sensible formats; this stage walks the shared sections so you know what to touch if you want to tweak them.", + completion: { type: 'manual' } + }, + { + id: 'media-naming-formats', + target: 'media-naming-formats', + title: 'Naming Formats', + body: 'Format strings use tokens (for example {Movie Title}, {Release Year}, {Quality Full} for Radarr, or {Series Title}, {season:00}, {episode:00}, {Episode Title} for Sonarr) wrapped in curly braces, and a live preview under each field shows how a real release would resolve. Type { in any format field to open the token picker. The fields themselves differ between Radarr and Sonarr; in Radarr you get movie and movie folder formats, in Sonarr you get three episode formats and two folder formats, but the token system and preview behave the same in both places.', + position: 'below', + freeInteract: true, + completion: { type: 'manual' } + }, + { + id: 'media-naming-character-replacement', + target: 'media-naming-character-replacement', + title: 'Character Replacement', + body: 'Filesystems reject characters like : ? * < > | on most operating systems, so Radarr and Sonarr can rewrite them before they hit disk. "Replace Illegal Characters" is the master toggle; when it is on, the colon replacement dropdown decides how colons specifically are handled (delete, replace with space, dash, etc.). Sonarr also lets you pick a custom replacement string. Leave this enabled unless you are on a filesystem that tolerates the original characters and you want names preserved exactly.', + position: 'above', + freeInteract: true, + completion: { type: 'manual' } + }, + { + id: 'media-naming-summary', + title: 'Summary', + body: 'Naming formats shape every file and folder name, and character replacement keeps the result safe for your filesystem. Each config is tied to a single arr type, and nothing takes effect until you assign it on the Sync tab of an Arr instance.', + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/definitions/stages/media-management/quality-definitions.ts b/src/lib/client/cutscene/definitions/stages/media-management/quality-definitions.ts new file mode 100644 index 00000000..44f3cd37 --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/media-management/quality-definitions.ts @@ -0,0 +1,23 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const mediaQualityDefinitionsStage: Stage = { + id: 'media-quality-definitions', + name: 'Quality Definitions', + description: 'File size gates per quality tier, and why they rarely need tuning', + prerequisites: [ + { + check: 'hasDatabase', + message: + 'You need at least one connected database to start this stage. Follow the "Link" stage under Databases from the onboarding page to connect one.' + } + ], + steps: [ + { + id: 'media-quality-definitions-intro', + route: { resolve: 'firstQualityDefinitions' }, + title: 'Quality Definitions', + body: 'Quality definitions set a minimum, preferred, and maximum file size (in MB per minute of runtime) for every quality Radarr and Sonarr know about. Releases outside the min/max range are rejected outright, and the preferred value is the size the grabber aims for when nothing else breaks the tie. The important thing to know: if your quality profiles already lean on well-scored custom formats, the scoring layer is already doing the preference work, and a narrow size window here just starts arguing with it. The usual move is to leave min at 0 and max at the top of the scale for every row, so size becomes a sanity check rather than a second ranking system. This config still has to exist and be saved before the linked database will let quality profiles sync, so it is worth opening once to confirm the defaults look reasonable before you move on.', + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/routeResolvers.ts b/src/lib/client/cutscene/routeResolvers.ts index b1360bdf..32737a1b 100644 --- a/src/lib/client/cutscene/routeResolvers.ts +++ b/src/lib/client/cutscene/routeResolvers.ts @@ -109,5 +109,27 @@ export const routeResolvers: Record Promise> = { if (!dbs.length) return '/delay-profiles'; const { path } = await (await fetch(`/delay-profiles/${dbs[0].id}/first`)).json(); return path; + }, + firstNaming: async () => { + const dbs = await (await fetch('/api/v1/databases')).json(); + if (!dbs.length) return '/media-management'; + const { path } = await (await fetch(`/media-management/${dbs[0].id}/naming/first`)).json(); + return path; + }, + firstQualityDefinitions: async () => { + const dbs = await (await fetch('/api/v1/databases')).json(); + if (!dbs.length) return '/media-management'; + const { path } = await ( + await fetch(`/media-management/${dbs[0].id}/quality-definitions/first`) + ).json(); + return path; + }, + firstMediaSettings: async () => { + const dbs = await (await fetch('/api/v1/databases')).json(); + if (!dbs.length) return '/media-management'; + const { path } = await ( + await fetch(`/media-management/${dbs[0].id}/media-settings/first`) + ).json(); + return path; } }; diff --git a/src/routes/media-management/[databaseId]/media-settings/components/MediaSettingsForm.svelte b/src/routes/media-management/[databaseId]/media-settings/components/MediaSettingsForm.svelte index 1af0371f..69c7511d 100644 --- a/src/routes/media-management/[databaseId]/media-settings/components/MediaSettingsForm.svelte +++ b/src/routes/media-management/[databaseId]/media-settings/components/MediaSettingsForm.svelte @@ -134,7 +134,7 @@ class="space-y-6 rounded-xl border border-neutral-300 bg-white p-6 dark:border-neutral-700/60 dark:bg-neutral-800/50" > -
+

Basic Info

-
+

Propers and Repacks

@@ -169,7 +169,7 @@
-
+

File Analysis

{ + const dbId = parseInt(params.databaseId, 10); + if (isNaN(dbId)) error(400, 'Invalid database ID'); + + const cache = pcdManager.getCache(dbId); + if (!cache) error(404, 'Database cache not available'); + + const items = await mediaSettingsQueries.list(cache); + const path = items.length + ? `/media-management/${dbId}/media-settings/${items[0].arr_type}/${encodeURIComponent(items[0].name)}` + : `/media-management/${dbId}/media-settings`; + return json({ path }); +}; diff --git a/src/routes/media-management/[databaseId]/naming/components/RadarrNamingForm.svelte b/src/routes/media-management/[databaseId]/naming/components/RadarrNamingForm.svelte index d924c096..636cbda5 100644 --- a/src/routes/media-management/[databaseId]/naming/components/RadarrNamingForm.svelte +++ b/src/routes/media-management/[databaseId]/naming/components/RadarrNamingForm.svelte @@ -186,7 +186,10 @@
-

+

Naming Formats

@@ -220,7 +223,10 @@
-

+

Character Replacement

diff --git a/src/routes/media-management/[databaseId]/naming/components/SonarrNamingForm.svelte b/src/routes/media-management/[databaseId]/naming/components/SonarrNamingForm.svelte index 9300a88d..9b887d6c 100644 --- a/src/routes/media-management/[databaseId]/naming/components/SonarrNamingForm.svelte +++ b/src/routes/media-management/[databaseId]/naming/components/SonarrNamingForm.svelte @@ -209,7 +209,10 @@
-

+

Episode Formats

@@ -299,7 +302,10 @@
-

+

Character Replacement

diff --git a/src/routes/media-management/[databaseId]/naming/first/+server.ts b/src/routes/media-management/[databaseId]/naming/first/+server.ts new file mode 100644 index 00000000..72bda71e --- /dev/null +++ b/src/routes/media-management/[databaseId]/naming/first/+server.ts @@ -0,0 +1,18 @@ +import { error, json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { pcdManager } from '$pcd/core/manager.ts'; +import * as namingQueries from '$pcd/entities/mediaManagement/naming/index.ts'; + +export const GET: RequestHandler = async ({ params }) => { + const dbId = parseInt(params.databaseId, 10); + if (isNaN(dbId)) error(400, 'Invalid database ID'); + + const cache = pcdManager.getCache(dbId); + if (!cache) error(404, 'Database cache not available'); + + const items = await namingQueries.list(cache); + const path = items.length + ? `/media-management/${dbId}/naming/${items[0].arr_type}/${encodeURIComponent(items[0].name)}` + : `/media-management/${dbId}/naming`; + return json({ path }); +}; diff --git a/src/routes/media-management/[databaseId]/quality-definitions/first/+server.ts b/src/routes/media-management/[databaseId]/quality-definitions/first/+server.ts new file mode 100644 index 00000000..058d9818 --- /dev/null +++ b/src/routes/media-management/[databaseId]/quality-definitions/first/+server.ts @@ -0,0 +1,18 @@ +import { error, json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { pcdManager } from '$pcd/core/manager.ts'; +import * as qualityDefinitionsQueries from '$pcd/entities/mediaManagement/quality-definitions/index.ts'; + +export const GET: RequestHandler = async ({ params }) => { + const dbId = parseInt(params.databaseId, 10); + if (isNaN(dbId)) error(400, 'Invalid database ID'); + + const cache = pcdManager.getCache(dbId); + if (!cache) error(404, 'Database cache not available'); + + const items = await qualityDefinitionsQueries.list(cache); + const path = items.length + ? `/media-management/${dbId}/quality-definitions/${items[0].arr_type}/${encodeURIComponent(items[0].name)}` + : `/media-management/${dbId}/quality-definitions`; + return json({ path }); +};