Skip to content

Commit 6270827

Browse files
committed
feat: add media_naming compatibility from recyclarr
* adds support for trash guide naming out of the box
1 parent 2305ba6 commit 6270827

File tree

9 files changed

+323
-16
lines changed

9 files changed

+323
-16
lines changed

docs/docs/configuration/config-file.md

+64-1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ sonarr:
138138
#media_management: {}
139139

140140
# experimental available in all *arr
141+
#media_naming_api: {}
142+
143+
# naming from recyclarr: https://recyclarr.dev/wiki/yaml/config-reference/media-naming/
141144
#media_naming: {}
142145

143146
custom_formats: # Custom format assignments
@@ -196,6 +199,66 @@ whisparr: {}
196199
readarr: {}
197200
```
198201
202+
#### Media Naming
203+
204+
You can use the predefined naming configurations from TRaSH-Guide like in recyclarr with the `media_naming` key.
205+
206+
- [TRaSH-Guide Sonarr Naming](https://github.com/TRaSH-Guides/Guides/blob/master/docs/json/sonarr/naming/sonarr-naming.json)
207+
- [TRaSH-Guide Radarr Naming](https://github.com/TRaSH-Guides/Guides/blob/master/docs/json/radarr/naming/radarr-naming.json)
208+
- [Recyclarr Wiki](https://recyclarr.dev/wiki/yaml/config-reference/media-naming/)
209+
210+
The configuration values differs between Radarr and Sonarr.
211+
212+
**Radarr**
213+
214+
```yml
215+
radarr:
216+
instance1:
217+
# Media Naming Configuration
218+
media_naming:
219+
folder: default
220+
movie:
221+
rename: true
222+
standard: default
223+
```
224+
225+
| **Property** | **Description** | **Default** |
226+
| ---------------- | ----------------------------------------------------------------------------- | ----------- |
227+
| `folder` | Key for "Movie Folder Format". Check debug logs or TRaSH-Guide for values. | Not synced |
228+
| `movie.rename` | If set to `true`, this enables the "Rename Movies" checkbox in the Radarr UI. | Not synced |
229+
| `movie.standard` | Key for "Standard Movie Format". Check debug logs or TRaSH-Guide for values. | Not synced |
230+
231+
All configurations above directly affect the "Movie Naming" settings under **Settings > Media Management** in the Radarr UI. If a property is *not specified*, Configarr will not sync that setting, allowing manual configuration.
232+
233+
---
234+
235+
**Sonarr**
236+
237+
```yml
238+
sonarr:
239+
instance1:
240+
# Media Naming Configuration
241+
media_naming:
242+
series: default
243+
season: default
244+
episodes:
245+
rename: true
246+
standard: default
247+
daily: default
248+
anime: default
249+
```
250+
251+
| **Property** | **Description** | **Default** |
252+
| ------------ | ------------------------------------------------------------------------------- | ----------- |
253+
| `series` | Key for "Series Folder Format". Check debug logs or TRaSH-Guide for values. | Not synced |
254+
| `season` | Key for "Season Folder Format". Check debug logs or TRaSH-Guide for values. | Not synced |
255+
| `rename` | If set to `true`, this enables the "Rename Episodes" checkbox in the Sonarr UI. | Not synced |
256+
| `standard` | Key for "Standard Episode Format". Check debug logs or TRaSH-Guide for values. | Not synced |
257+
| `daily` | Key for "Daily Episode Format". Check debug logs or TRaSH-Guide for values. | Not synced |
258+
| `anime` | Key for "Anime Episode Format". Check debug logs or TRaSH-Guide for values. | Not synced |
259+
260+
All configurations above directly affect the "Episode Naming" settings under **Settings > Media Management** in the Sonarr UI. If a property is *not specified*, Configarr will not sync that setting, allowing manual configuration.
261+
199262
### secrets.yml
200263

201264
Store sensitive information like API keys in this file. Never commit this file to version control.
@@ -216,7 +279,7 @@ Configarr will automatically load these configurations on startup and apply them
216279

217280
## Experimental supported fields
218281

219-
- Experimental support for `media_management` and `media_naming` (since v1.5.0)
282+
- Experimental support for `media_management` and `media_naming_api` (since v1.5.0)
220283
With those you can configure different settings in the different tabs available per *arr.
221284
Both fields are under experimental support.
222285
The supports elements in those are dependent on the *arr used.

examples/full/config/config.yml

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ radarr:
6868
recycleBin: "/tmp"
6969

7070
# experimental
71+
media_naming_api: {}
72+
73+
# naming from TRaSH. See docs
7174
media_naming: {}
7275

7376
include:

src/config.ts

+110-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { existsSync, readFileSync } from "node:fs";
22
import yaml from "yaml";
3+
import { NamingConfigResource as RadarrNamingConfigResource } from "./__generated__/radarr/data-contracts";
4+
import { NamingConfigResource as SonarrNamingConfigResource } from "./__generated__/sonarr/data-contracts";
35
import { getHelpers } from "./env";
46
import { loadLocalRecyclarrTemplate } from "./local-importer";
57
import { logger } from "./logger";
68
import { filterInvalidQualityProfiles } from "./quality-profiles";
79
import { loadRecyclarrTemplates } from "./recyclarr-importer";
8-
import { loadQPFromTrash, transformTrashQPCFs, transformTrashQPToTemplate } from "./trash-guide";
9-
import { ArrType, MappedMergedTemplates } from "./types/common.types";
10+
import {
11+
loadNamingFromTrashRadarr,
12+
loadNamingFromTrashSonarr,
13+
loadQPFromTrash,
14+
transformTrashQPCFs,
15+
transformTrashQPToTemplate,
16+
} from "./trash-guide";
17+
import { ArrType, MappedMergedTemplates, MappedTemplates } from "./types/common.types";
1018
import {
1119
ConfigArrInstance,
1220
ConfigCustomFormat,
@@ -17,8 +25,10 @@ import {
1725
InputConfigIncludeItem,
1826
InputConfigInstance,
1927
InputConfigSchema,
28+
MediaNamingType,
2029
MergedConfigInstance,
2130
} from "./types/config.types";
31+
import { TrashQP } from "./types/trashguide.types";
2232

2333
let config: ConfigSchema;
2434
let secrets: any;
@@ -168,9 +178,15 @@ export const mergeConfigsAndTemplates = async (
168178
value: InputConfigArrInstance,
169179
arrType: ArrType,
170180
): Promise<{ config: MergedConfigInstance }> => {
171-
const recyclarrTemplateMap = loadRecyclarrTemplates(arrType);
172181
const localTemplateMap = loadLocalRecyclarrTemplate(arrType);
173-
const trashTemplates = await loadQPFromTrash(arrType);
182+
let recyclarrTemplateMap: Map<string, MappedTemplates> = new Map();
183+
let trashTemplates: Map<string, TrashQP> = new Map();
184+
185+
if (arrType === "RADARR" || arrType === "SONARR") {
186+
// TODO: separation maybe not the best. Maybe time to split up processing for each arrType
187+
recyclarrTemplateMap = loadRecyclarrTemplates(arrType);
188+
trashTemplates = await loadQPFromTrash(arrType);
189+
}
174190

175191
logger.debug(
176192
`Loaded ${recyclarrTemplateMap.size} Recyclarr templates, ${localTemplateMap.size} local templates and ${trashTemplates.size} trash templates.`,
@@ -235,6 +251,10 @@ export const mergeConfigsAndTemplates = async (
235251
mergedTemplates.media_naming = { ...mergedTemplates.media_naming, ...template.media_naming };
236252
}
237253

254+
if (template.media_naming_api) {
255+
mergedTemplates.media_naming_api = { ...mergedTemplates.media_naming_api, ...template.media_naming_api };
256+
}
257+
238258
if (template.customFormatDefinitions) {
239259
if (Array.isArray(template.customFormatDefinitions)) {
240260
mergedTemplates.customFormatDefinitions = [
@@ -280,7 +300,14 @@ export const mergeConfigsAndTemplates = async (
280300
}
281301

282302
if (value.media_naming) {
283-
mergedTemplates.media_naming = { ...mergedTemplates.media_naming, ...value.media_naming };
303+
mergedTemplates.media_naming_api = {
304+
...mergedTemplates.media_naming_api,
305+
...(await mapConfigMediaNamingToApi(arrType, value.media_naming)),
306+
};
307+
}
308+
309+
if (value.media_naming_api) {
310+
mergedTemplates.media_naming_api = { ...mergedTemplates.media_naming_api, ...value.media_naming_api };
284311
}
285312

286313
if (value.quality_definition) {
@@ -366,3 +393,81 @@ export const mergeConfigsAndTemplates = async (
366393

367394
return { config: validatedConfig };
368395
};
396+
397+
const mapConfigMediaNamingToApi = async (arrType: ArrType, mediaNaming: MediaNamingType): Promise<any | null> => {
398+
if (arrType === "RADARR") {
399+
const trashNaming = await loadNamingFromTrashRadarr();
400+
401+
if (trashNaming == null) {
402+
return null;
403+
}
404+
405+
const folderFormat = mediaNamingToApiWithLog("RADARR", mediaNaming.folder, trashNaming.folder, "mediaNaming.folder");
406+
const standardFormat = mediaNamingToApiWithLog("RADARR", mediaNaming.movie?.standard, trashNaming.file, "mediaNaming.movie.standard");
407+
408+
const apiObject: RadarrNamingConfigResource = {
409+
...(folderFormat && { movieFolderFormat: folderFormat }),
410+
...(standardFormat && { standardMovieFormat: standardFormat }),
411+
...(mediaNaming.movie?.rename != null && { renameMovies: mediaNaming.movie?.rename === true }),
412+
};
413+
414+
logger.debug(apiObject, `Mapped mediaNaming to API:`);
415+
return apiObject;
416+
}
417+
418+
if (arrType === "SONARR") {
419+
const trashNaming = await loadNamingFromTrashSonarr();
420+
421+
if (trashNaming == null) {
422+
return null;
423+
}
424+
425+
const seriesFormat = mediaNamingToApiWithLog("SONARR", mediaNaming.series, trashNaming.series, "mediaNaming.series");
426+
const seasonsFormat = mediaNamingToApiWithLog("SONARR", mediaNaming.season, trashNaming.season, "mediaNaming.season");
427+
const standardFormat = mediaNamingToApiWithLog(
428+
"SONARR",
429+
mediaNaming.episodes?.standard,
430+
trashNaming.episodes.standard,
431+
"mediaNaming.episodes.standard",
432+
);
433+
const dailyFormat = mediaNamingToApiWithLog(
434+
"SONARR",
435+
mediaNaming.episodes?.daily,
436+
trashNaming.episodes.daily,
437+
"mediaNaming.episodes.daily",
438+
);
439+
const animeFormat = mediaNamingToApiWithLog(
440+
"SONARR",
441+
mediaNaming.episodes?.anime,
442+
trashNaming.episodes.anime,
443+
"mediaNaming.episodes.anime",
444+
);
445+
446+
const apiObject: SonarrNamingConfigResource = {
447+
...(seriesFormat && { seriesFolderFormat: seriesFormat }),
448+
...(seasonsFormat && { seasonFolderFormat: seasonsFormat }),
449+
...(standardFormat && { standardEpisodeFormat: standardFormat }),
450+
...(dailyFormat && { dailyEpisodeFormat: dailyFormat }),
451+
...(animeFormat && { animeEpisodeFormat: animeFormat }),
452+
...(mediaNaming.episodes?.rename != null && { renameEpisodes: mediaNaming.episodes?.rename === true }),
453+
};
454+
455+
logger.debug(apiObject, `Mapped mediaNaming to API:`);
456+
457+
return apiObject;
458+
}
459+
460+
logger.warn(`MediaNaming not supported for ${arrType}`);
461+
};
462+
463+
const mediaNamingToApiWithLog = (arrType: ArrType, key: string | undefined, trashObject: any, label: string) => {
464+
if (key) {
465+
if (trashObject[key] == null) {
466+
logger.warn(`(${arrType}) Specified ${label} '${key}' could not be found in TRaSH-Guide. Check debug logs for available keys.`);
467+
} else {
468+
return trashObject[key];
469+
}
470+
}
471+
472+
return null;
473+
};

src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ const pipeline = async (value: InputConfigArrInstance, arrType: ArrType) => {
9191
logger.info(`No QualityDefinition configured.`);
9292
}
9393

94-
const namingDiff = await calculateNamingDiff(config.media_naming);
94+
const namingDiff = await calculateNamingDiff(config.media_naming_api);
9595

9696
if (namingDiff) {
9797
if (getEnvs().DRY_RUN) {

src/media-management.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getUnifiedClient } from "./clients/unified-client";
22
import { logger } from "./logger";
3-
import { MediaManagementType, MediaNamingType } from "./types/config.types";
3+
import { MediaManagementType, MediaNamingApiType } from "./types/config.types";
44
import { compareMediamanagement, compareNaming } from "./util";
55

66
const loadNamingFromServer = async () => {
@@ -15,9 +15,9 @@ const loadMediamanagementConfigFromServer = async () => {
1515
return result;
1616
};
1717

18-
export const calculateNamingDiff = async (mediaNaming?: MediaNamingType) => {
18+
export const calculateNamingDiff = async (mediaNaming?: MediaNamingApiType) => {
1919
if (mediaNaming == null) {
20-
logger.debug(`Config 'media_naming' not specified. Ignoring.`);
20+
logger.debug(`Config 'media_naming_api' not specified. Ignoring.`);
2121
return null;
2222
}
2323

@@ -26,12 +26,12 @@ export const calculateNamingDiff = async (mediaNaming?: MediaNamingType) => {
2626
const { changes, equal } = compareNaming(serverData, mediaNaming);
2727

2828
if (equal) {
29-
logger.debug(`Media naming settings are in sync`);
29+
logger.debug(`Media naming API settings are in sync`);
3030
return null;
3131
}
3232

33-
logger.info(`Found ${changes.length} differences for media naming.`);
34-
logger.debug(changes, `Found following changes for media naming`);
33+
logger.info(`Found ${changes.length} differences for media naming api.`);
34+
logger.debug(changes, `Found following changes for media naming api`);
3535

3636
return {
3737
changes,

0 commit comments

Comments
 (0)