Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add configuration options for media management tab #6

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ Possible ideas:
- Support all CustomFormat specifications
- Maybe this?: https://github.com/recyclarr/recyclarr/issues/225

### Alpha
- Full media management configuration
- Notification handler

## Work TODOs

- [ ] Optimize types. Generated ones work for first step but not very optimal because they do not correctly represent request/response types.
Expand All @@ -65,6 +69,9 @@ Possible ideas:
- [ ] Simple Config validation
- [x] Local recyclarr templates to include
- [ ] Clone existing templates: Lets say you want the same template but with a different name?
- [ ] Media Management Tab configuration with configarr
- [ ] Add notification with simple REST call
- [ ] https://github.com/caronc/apprise-api ?

## Custom formats

Expand Down
45 changes: 44 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { configureRadarrApi, configureSonarrApi, getArrApi, unsetApi } from "./s
import { getConfig } from "./src/config";
import { calculateCFsToManage, loadLocalCfs, loadServerCustomFormats, manageCf, mergeCfSources } from "./src/custom-formats";
import { logHeading, logger } from "./src/logger";
import { calculateManagementDiff, calculateNamingDiff } from "./src/media-management";
import { calculateQualityDefinitionDiff, loadQualityDefinitionFromServer } from "./src/quality-definitions";
import {
calculateQualityProfilesDiff,
Expand Down Expand Up @@ -56,10 +57,28 @@ const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => {
}
}

// TODO Ignore recursive include for now
if (template.media_management) {
recylarrMergedTemplates.media_management = { ...recylarrMergedTemplates.media_management, ...template.media_management };
}

if (template.media_naming) {
recylarrMergedTemplates.media_naming = { ...recylarrMergedTemplates.media_naming, ...template.media_naming };
}

if (template.include) {
logger.warn(`Currently recursive templates includes are not implemented.`);
}
});
}

if (value.media_management) {
recylarrMergedTemplates.media_management = { ...recylarrMergedTemplates.media_management, ...value.media_management };
}

if (value.media_naming) {
recylarrMergedTemplates.media_naming = { ...recylarrMergedTemplates.media_naming, ...value.media_naming };
}

if (value.custom_formats) {
recylarrMergedTemplates.custom_formats.push(...value.custom_formats);
}
Expand Down Expand Up @@ -153,6 +172,30 @@ const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => {
}
}

const namingDiff = await calculateNamingDiff(recylarrMergedTemplates.media_naming);

if (namingDiff) {
if (IS_DRY_RUN) {
logger.info("DryRun: Would update MediaNaming.");
} else {
// TODO this will need a radarr/sonarr separation for sure to have good and correct typings
await api.v3ConfigNamingUpdate(namingDiff.updatedData.id! + "", namingDiff.updatedData as any); // Ignore types
logger.info(`Updated MediaNaming`);
}
}

const managementDiff = await calculateManagementDiff(recylarrMergedTemplates.media_management);

if (managementDiff) {
if (IS_DRY_RUN) {
logger.info("DryRun: Would update MediaManagement.");
} else {
// TODO this will need a radarr/sonarr separation for sure to have good and correct typings
await api.v3ConfigMediamanagementUpdate(managementDiff.updatedData.id! + "", managementDiff.updatedData as any); // Ignore types
logger.info(`Updated MediaManagement`);
}
}

// merge CFs of templates and custom CFs into one mapping of QualityProfile -> CFs + Score
// TODO traversing the merged templates probably to often once should be enough. Loop once and extract a couple of different maps, arrays as needed. Future optimization.
const cfToQualityProfiles = mapQualityProfiles(mergedCFs, recylarrMergedTemplates.custom_formats, recylarrMergedTemplates);
Expand Down
5 changes: 3 additions & 2 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Api as RadarrApi } from "./__generated__/generated-radarr-api";
import { Api as SonarrApi } from "./__generated__/generated-sonarr-api";
import { logger } from "./logger";
import { RadarrApiType, SonarrApiType } from "./types";

let sonarrClient: SonarrApi<unknown>["api"] | undefined;
let radarrClient: RadarrApi<unknown>["api"] | undefined;
let sonarrClient: SonarrApiType | undefined;
let radarrClient: RadarrApiType | undefined;

export const unsetApi = () => {
sonarrClient = undefined;
Expand Down
70 changes: 70 additions & 0 deletions src/media-management.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { getArrApi } from "./api";
import { logger } from "./logger";
import { MediaManagementType, MediaNamingType } from "./types";
import { compareObjectsCarr } from "./util";

export const loadNamingFromServer = async () => {
const api = getArrApi();
const result = await api.v3ConfigNamingList();
return result.data;
};

export const loadManagementConfigFromServer = async () => {
const api = getArrApi();
const result = await api.v3ConfigMediamanagementList();
return result.data;
};

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

const serverData = await loadNamingFromServer();

const { changes, equal } = compareObjectsCarr(serverData, mediaNaming);

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

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

return {
changes,
updatedData: {
...serverData,
...mediaNaming,
},
};
};

export const calculateManagementDiff = async (mediaManagement?: MediaManagementType) => {
if (mediaManagement == null) {
logger.debug(`Config 'media_management' not specified. Ignoring.`);
return null;
}

const serverData = await loadManagementConfigFromServer();

const { changes, equal } = compareObjectsCarr(serverData, mediaManagement);

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

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

return {
changes,
updatedData: {
...serverData,
...mediaManagement,
},
};
};
47 changes: 45 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CustomFormatResource, CustomFormatSpecificationSchema } from "./__generated__/generated-sonarr-api";
import { Api as GeneratedRadarrApi } from "./__generated__/generated-radarr-api";
import { CustomFormatResource, CustomFormatSpecificationSchema, Api as GeneratedSonarrApi } from "./__generated__/generated-sonarr-api";

export type DynamicImportType<T> = { default: T };

Expand Down Expand Up @@ -105,6 +106,8 @@ export type YamlConfigInstance = {
include?: { template: string }[];
custom_formats: YamlList[];
quality_profiles: YamlConfigQualityProfile[];
media_management?: MediaManagementType;
media_naming?: MediaNamingType;
};

export type YamlConfigQualityProfile = {
Expand All @@ -130,7 +133,7 @@ export type YamlConfigQualityProfileItems = {
};

export type RecyclarrTemplates = Partial<
Pick<YamlConfigInstance, "quality_definition" | "custom_formats" | "include" | "quality_profiles">
Pick<YamlConfigInstance, "quality_definition" | "custom_formats" | "include" | "quality_profiles" | "media_management" | "media_naming">
>;

export type RecyclarrMergedTemplates = RecyclarrTemplates & Required<Pick<RecyclarrTemplates, "custom_formats" | "quality_profiles">>;
Expand All @@ -154,3 +157,43 @@ export type ArrType = "SONARR" | "RADARR"; // anime and series exists in trash g

export type QualityDefintionsSonarr = "anime" | "series" | "custom";
export type QualityDefintionsRadarr = "movie" | "custom";

// TODO configure all configs in recyclarr?
export type MediaManagementType = {
autoUnmonitorPreviouslyDownloadedEpisodes?: boolean;
recycleBin?: string;
recycleBinCleanupDays?: number;
downloadPropersAndRepacks?: string;
createEmptySeriesFolders?: boolean;
deleteEmptyFolders?: boolean;
fileDate?: string;
rescanAfterRefresh?: string;
setPermissionsLinux?: boolean;
chmodFolder?: string;
chownGroup?: string;
episodeTitleRequired?: string;
skipFreeSpaceCheckWhenImporting?: boolean;
minimumFreeSpaceWhenImporting?: number;
copyUsingHardlinks?: boolean;
useScriptImport?: boolean;
scriptImportPath?: string;
importExtraFiles?: boolean;
extraFileExtensions?: string;
enableMediaInfo?: boolean;
};

export type MediaNamingType = {
renameEpisodes?: boolean;
replaceIllegalCharacters?: boolean;
colonReplacementFormat?: number;
multiEpisodeStyle?: number;
standardEpisodeFormat?: string;
dailyEpisodeFormat?: string;
animeEpisodeFormat?: string;
seriesFolderFormat?: string;
seasonFolderFormat?: string;
specialsFolderFormat?: string;
};

export type SonarrApiType = GeneratedSonarrApi<unknown>["api"];
export type RadarrApiType = GeneratedRadarrApi<unknown>["api"];