Skip to content

Commit

Permalink
feat: add media_naming compatibility from recyclarr
Browse files Browse the repository at this point in the history
* adds support for trash guide naming out of the box
  • Loading branch information
BlackDark committed Jan 10, 2025
1 parent d142fee commit 3cf73dc
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 17 deletions.
65 changes: 64 additions & 1 deletion docs/docs/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ sonarr:
#media_management: {}

# experimental available in all *arr
#media_naming_api: {}

# naming from recyclarr: https://recyclarr.dev/wiki/yaml/config-reference/media-naming/
#media_naming: {}

custom_formats: # Custom format assignments
Expand Down Expand Up @@ -205,6 +208,66 @@ SONARR_API_KEY: your_sonarr_api_key_here
RADARR_API_KEY: your_radarr_api_key_here
```
## Media Naming
You can use the predefined naming configurations from TRaSH-Guide like in recyclarr with the `media_naming` key.

- [TRaSH-Guide Sonarr Naming](https://github.com/TRaSH-Guides/Guides/blob/master/docs/json/sonarr/naming/sonarr-naming.json)
- [TRaSH-Guide Radarr Naming](https://github.com/TRaSH-Guides/Guides/blob/master/docs/json/radarr/naming/radarr-naming.json)
- [Recyclarr Wiki](https://recyclarr.dev/wiki/yaml/config-reference/media-naming/)

The configuration values differs between Radarr and Sonarr.

**Radarr**

```yml
radarr:
instance1:
# Media Naming Configuration
media_naming:
folder: default
movie:
rename: true
standard: default
```

| **Property** | **Description** | **Default** |
| ---------------- | ----------------------------------------------------------------------------- | ----------- |
| `folder` | Key for "Movie Folder Format". Check debug logs or TRaSH-Guide for values. | Not synced |
| `movie.rename` | If set to `true`, this enables the "Rename Movies" checkbox in the Radarr UI. | Not synced |
| `movie.standard` | Key for "Standard Movie Format". Check debug logs or TRaSH-Guide for values. | Not synced |

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.

---

**Sonarr**

```yml
sonarr:
instance1:
# Media Naming Configuration
media_naming:
series: default
season: default
episodes:
rename: true
standard: default
daily: default
anime: default
```

| **Property** | **Description** | **Default** |
| ------------------- | ------------------------------------------------------------------------------- | ----------- |
| `series` | Key for "Series Folder Format". Check debug logs or TRaSH-Guide for values. | Not synced |
| `season` | Key for "Season Folder Format". Check debug logs or TRaSH-Guide for values. | Not synced |
| `episodes.rename` | If set to `true`, this enables the "Rename Episodes" checkbox in the Sonarr UI. | Not synced |
| `episodes.standard` | Key for "Standard Episode Format". Check debug logs or TRaSH-Guide for values. | Not synced |
| `episodes.daily` | Key for "Daily Episode Format". Check debug logs or TRaSH-Guide for values. | Not synced |
| `episodes.anime` | Key for "Anime Episode Format". Check debug logs or TRaSH-Guide for values. | Not synced |

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.

## Usage

1. Create both `config.yml` and `secrets.yml` files
Expand All @@ -216,7 +279,7 @@ Configarr will automatically load these configurations on startup and apply them

## Experimental supported fields

- Experimental support for `media_management` and `media_naming` (since v1.5.0)
- Experimental support for `media_management` and `media_naming_api` (since v1.5.0)
With those you can configure different settings in the different tabs available per *arr.
Both fields are under experimental support.
The supports elements in those are dependent on the *arr used.
Expand Down
3 changes: 3 additions & 0 deletions examples/full/config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ radarr:
recycleBin: "/tmp"

# experimental
media_naming_api: {}

# naming from TRaSH. See docs
media_naming: {}

include:
Expand Down
115 changes: 110 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { existsSync, readFileSync } from "node:fs";
import yaml from "yaml";
import { NamingConfigResource as RadarrNamingConfigResource } from "./__generated__/radarr/data-contracts";
import { NamingConfigResource as SonarrNamingConfigResource } from "./__generated__/sonarr/data-contracts";
import { getHelpers } from "./env";
import { loadLocalRecyclarrTemplate } from "./local-importer";
import { logger } from "./logger";
import { filterInvalidQualityProfiles } from "./quality-profiles";
import { loadRecyclarrTemplates } from "./recyclarr-importer";
import { loadQPFromTrash, transformTrashQPCFs, transformTrashQPToTemplate } from "./trash-guide";
import { ArrType, MappedMergedTemplates } from "./types/common.types";
import {
loadNamingFromTrashRadarr,
loadNamingFromTrashSonarr,
loadQPFromTrash,
transformTrashQPCFs,
transformTrashQPToTemplate,
} from "./trash-guide";
import { ArrType, MappedMergedTemplates, MappedTemplates } from "./types/common.types";
import {
ConfigArrInstance,
ConfigCustomFormat,
Expand All @@ -17,8 +25,10 @@ import {
InputConfigIncludeItem,
InputConfigInstance,
InputConfigSchema,
MediaNamingType,
MergedConfigInstance,
} from "./types/config.types";
import { TrashQP } from "./types/trashguide.types";

let config: ConfigSchema;
let secrets: any;
Expand Down Expand Up @@ -168,9 +178,15 @@ export const mergeConfigsAndTemplates = async (
value: InputConfigArrInstance,
arrType: ArrType,
): Promise<{ config: MergedConfigInstance }> => {
const recyclarrTemplateMap = loadRecyclarrTemplates(arrType);
const localTemplateMap = loadLocalRecyclarrTemplate(arrType);
const trashTemplates = await loadQPFromTrash(arrType);
let recyclarrTemplateMap: Map<string, MappedTemplates> = new Map();
let trashTemplates: Map<string, TrashQP> = new Map();

if (arrType === "RADARR" || arrType === "SONARR") {
// TODO: separation maybe not the best. Maybe time to split up processing for each arrType
recyclarrTemplateMap = loadRecyclarrTemplates(arrType);
trashTemplates = await loadQPFromTrash(arrType);
}

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

if (template.media_naming_api) {
mergedTemplates.media_naming_api = { ...mergedTemplates.media_naming_api, ...template.media_naming_api };
}

if (template.customFormatDefinitions) {
if (Array.isArray(template.customFormatDefinitions)) {
mergedTemplates.customFormatDefinitions = [
Expand Down Expand Up @@ -280,7 +300,14 @@ export const mergeConfigsAndTemplates = async (
}

if (value.media_naming) {
mergedTemplates.media_naming = { ...mergedTemplates.media_naming, ...value.media_naming };
mergedTemplates.media_naming_api = {
...mergedTemplates.media_naming_api,
...(await mapConfigMediaNamingToApi(arrType, value.media_naming)),
};
}

if (value.media_naming_api) {
mergedTemplates.media_naming_api = { ...mergedTemplates.media_naming_api, ...value.media_naming_api };
}

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

return { config: validatedConfig };
};

const mapConfigMediaNamingToApi = async (arrType: ArrType, mediaNaming: MediaNamingType): Promise<any | null> => {
if (arrType === "RADARR") {
const trashNaming = await loadNamingFromTrashRadarr();

if (trashNaming == null) {
return null;
}

const folderFormat = mediaNamingToApiWithLog("RADARR", mediaNaming.folder, trashNaming.folder, "mediaNaming.folder");
const standardFormat = mediaNamingToApiWithLog("RADARR", mediaNaming.movie?.standard, trashNaming.file, "mediaNaming.movie.standard");

const apiObject: RadarrNamingConfigResource = {
...(folderFormat && { movieFolderFormat: folderFormat }),
...(standardFormat && { standardMovieFormat: standardFormat }),
...(mediaNaming.movie?.rename != null && { renameMovies: mediaNaming.movie?.rename === true }),
};

logger.debug(apiObject, `Mapped mediaNaming to API:`);
return apiObject;
}

if (arrType === "SONARR") {
const trashNaming = await loadNamingFromTrashSonarr();

if (trashNaming == null) {
return null;
}

const seriesFormat = mediaNamingToApiWithLog("SONARR", mediaNaming.series, trashNaming.series, "mediaNaming.series");
const seasonsFormat = mediaNamingToApiWithLog("SONARR", mediaNaming.season, trashNaming.season, "mediaNaming.season");
const standardFormat = mediaNamingToApiWithLog(
"SONARR",
mediaNaming.episodes?.standard,
trashNaming.episodes.standard,
"mediaNaming.episodes.standard",
);
const dailyFormat = mediaNamingToApiWithLog(
"SONARR",
mediaNaming.episodes?.daily,
trashNaming.episodes.daily,
"mediaNaming.episodes.daily",
);
const animeFormat = mediaNamingToApiWithLog(
"SONARR",
mediaNaming.episodes?.anime,
trashNaming.episodes.anime,
"mediaNaming.episodes.anime",
);

const apiObject: SonarrNamingConfigResource = {
...(seriesFormat && { seriesFolderFormat: seriesFormat }),
...(seasonsFormat && { seasonFolderFormat: seasonsFormat }),
...(standardFormat && { standardEpisodeFormat: standardFormat }),
...(dailyFormat && { dailyEpisodeFormat: dailyFormat }),
...(animeFormat && { animeEpisodeFormat: animeFormat }),
...(mediaNaming.episodes?.rename != null && { renameEpisodes: mediaNaming.episodes?.rename === true }),
};

logger.debug(apiObject, `Mapped mediaNaming to API:`);

return apiObject;
}

logger.warn(`MediaNaming not supported for ${arrType}`);
};

const mediaNamingToApiWithLog = (arrType: ArrType, key: string | undefined, trashObject: any, label: string) => {
if (key) {
if (trashObject[key] == null) {
logger.warn(`(${arrType}) Specified ${label} '${key}' could not be found in TRaSH-Guide. Check debug logs for available keys.`);
} else {
return trashObject[key];
}
}

return null;
};
7 changes: 6 additions & 1 deletion src/custom-formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,12 @@ export const mapCustomFormatDefinitions = (customFormatDefinitions: CustomFormat
};

export const loadCustomFormatDefinitions = async (idsToMange: Set<string>, arrType: ArrType, additionalCFDs: CustomFormatDefinitions) => {
const trashCFs = await loadTrashCFs(arrType);
let trashCFs: CFIDToConfigGroup = new Map();

if (arrType === "RADARR" || arrType === "SONARR") {
trashCFs = await loadTrashCFs(arrType);
}

const localFileCFs = await loadLocalCfs();

logger.debug(`Total loaded CF definitions: ${trashCFs.size} TrashCFs, ${localFileCFs.size} LocalCFs, ${additionalCFDs.length} ConfigCFs`);
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const pipeline = async (value: InputConfigArrInstance, arrType: ArrType) => {
logger.info(`No QualityDefinition configured.`);
}

const namingDiff = await calculateNamingDiff(config.media_naming);
const namingDiff = await calculateNamingDiff(config.media_naming_api);

if (namingDiff) {
if (getEnvs().DRY_RUN) {
Expand Down
12 changes: 6 additions & 6 deletions src/media-management.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getUnifiedClient } from "./clients/unified-client";
import { logger } from "./logger";
import { MediaManagementType, MediaNamingType } from "./types/config.types";
import { MediaManagementType, MediaNamingApiType } from "./types/config.types";
import { compareMediamanagement, compareNaming } from "./util";

const loadNamingFromServer = async () => {
Expand All @@ -15,9 +15,9 @@ const loadMediamanagementConfigFromServer = async () => {
return result;
};

export const calculateNamingDiff = async (mediaNaming?: MediaNamingType) => {
export const calculateNamingDiff = async (mediaNaming?: MediaNamingApiType) => {
if (mediaNaming == null) {
logger.debug(`Config 'media_naming' not specified. Ignoring.`);
logger.debug(`Config 'media_naming_api' not specified. Ignoring.`);
return null;
}

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

if (equal) {
logger.debug(`Media naming settings are in sync`);
logger.debug(`Media naming API settings are in sync`);
return null;
}

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

return {
changes,
Expand Down
Loading

0 comments on commit 3cf73dc

Please sign in to comment.