diff --git a/.env.template b/.env.template index d3d076d..3aa057e 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,6 @@ #CONFIG_LOCATION=/app/config/config.yml #SECRETS_LOCATION=/app/config/secrets.yml -DRY_RUN=true +DRY_RUN=true # not fully supported yet LOAD_LOCAL_SAMPLES=false DEBUG_CREATE_FILES=false +LOG_LEVEL=info diff --git a/README.md b/README.md index c913eaa..48e87a9 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ Possible ideas: - [ ] Plain docker - [ ] Kubernetes - [ ] Simple Config validation -- [ ] Custom recyclarr templates? - - [ ] Lets say you want the same template but with a different name +- [x] Local recyclarr templates to include +- [ ] Clone existing templates: Lets say you want the same template but with a different name? ## Development @@ -110,6 +110,7 @@ services: - ./config:/app/config # Contains the config.yml and secrets.yml - ./dockerrepos:/app/repos # Cache repositories - ./custom/cfs:/app/cfs # Optional if custom formats locally provided + - ./custom/templates:/app/templates # Optional if custom templates ``` ### Kubernetes diff --git a/config.yml.template b/config.yml.template index d8a8d98..8f1003d 100644 --- a/config.yml.template +++ b/config.yml.template @@ -5,6 +5,7 @@ recyclarrConfigUrl: https://github.com/recyclarr/config-templates # Optional if you want to add custom formats locally #localCustomFormatsPath: ./custom/cfs +#localConfigTemplatesPath: /app/templates sonarr: series: diff --git a/docker-compose.yml b/docker-compose.yml index 158d7b0..249ddaa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - ./config:/app/config - ./dockerrepos:/app/repos - ./custom/cfs:/app/cfs + - ./custom/templates:/app/templates sonarr: image: lscr.io/linuxserver/sonarr:4.0.2 diff --git a/index.ts b/index.ts index de5b8e0..1f91efc 100644 --- a/index.ts +++ b/index.ts @@ -5,6 +5,7 @@ import { CustomFormatResource } from "./src/__generated__/generated-sonarr-api"; import { configureRadarrApi, configureSonarrApi, getArrApi, unsetApi } from "./src/api"; import { getConfig } from "./src/config"; import { calculateCFsToManage, loadLocalCfs, loadServerCustomFormats, manageCf, mergeCfSources } from "./src/custom-formats"; +import { logHeading, logger } from "./src/logger"; import { calculateQualityDefinitionDiff, loadQualityDefinitionFromServer } from "./src/quality-definitions"; import { calculateQualityProfilesDiff, @@ -27,12 +28,17 @@ const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => { }; if (value.include) { - console.log(`Recyclarr Includes:\n${value.include.map((e) => e.template).join("\n")}`); + logger.info(`Recyclarr includes ${value.include.length} templates`); + logger.debug( + value.include.map((e) => e.template), + "Included templates", + ); + value.include.forEach((e) => { const template = recyclarrTemplateMap.get(e.template); if (!template) { - console.log(`Unknown recyclarr template requested: ${e.template}`); + logger.info(`Unknown recyclarr template requested: ${e.template}`); return; } @@ -72,10 +78,10 @@ const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => { const idsToManage = calculateCFsToManage(recylarrMergedTemplates); - console.log(`Stuff to manage: ${Array.from(idsToManage)}`); + logger.debug(Array.from(idsToManage), `CustomFormats to manage`); const serverCFs = await loadServerCustomFormats(); - console.log(`CFs on server: ${serverCFs.length}`); + logger.info(`CustomFormats on server: ${serverCFs.length}`); const serverCFMapping = serverCFs.reduce((p, c) => { p.set(c.name!, c); @@ -83,7 +89,7 @@ const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => { }, new Map()); await manageCf(mergedCFs, serverCFMapping, idsToManage); - console.log(`CustomFormats should be in sync`); + logger.info(`CustomFormats synchronized`); const qualityDefinition = recylarrMergedTemplates.quality_definition?.type; @@ -109,18 +115,18 @@ const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => { if (changeMap.size > 0) { if (IS_DRY_RUN) { - console.log("DryRun: Would update QualityDefinitions."); + logger.info("DryRun: Would update QualityDefinitions."); } else { - console.log(`Diffs in quality definitions found`, changeMap.values()); + logger.info(`Diffs in quality definitions found`, changeMap.values()); await api.v3QualitydefinitionUpdateUpdate(restData as any); // Ignore types - console.log(`Updated QualityDefinitions`); + logger.info(`Updated QualityDefinitions`); } } else { - console.log(`QualityDefinitions do not need update!`); + logger.info(`QualityDefinitions do not need update!`); } if (create.length > 0) { - console.log(`Currently not implemented this case for quality definitions.`); + logger.info(`Currently not implemented this case for quality definitions.`); } } @@ -169,18 +175,18 @@ const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => { }); } - console.log(`QPs: Create: ${create.length}, Update: ${changedQPs.length}, Unchanged: ${noChanges.length}`); + logger.info(`QualityProfiles: Create: ${create.length}, Update: ${changedQPs.length}, Unchanged: ${noChanges.length}`); if (!IS_DRY_RUN) { for (const element of create) { try { const newProfile = await api.v3QualityprofileCreate(element as any); // Ignore types - console.log(`Created QualityProfile: ${newProfile.data.name}`); + logger.info(`Created QualityProfile: ${newProfile.data.name}`); } catch (error: any) { let message; if (error.response) { - console.log(error.response); + logger.info(error.response); // The request was made and the server responded with a status code // that falls out of the range of 2xx message = `Failed creating QualityProfile (${element.name}): Data ${JSON.stringify(error.response.data)}`; @@ -188,10 +194,10 @@ const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - console.log(error.request); + logger.info(error.request); } else { // Something happened in setting up the request that triggered an Error - console.log("Error", error.message); + logger.info("Error", error.message); } throw new Error(message); @@ -201,7 +207,7 @@ const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => { for (const element of changedQPs) { try { const newProfile = await api.v3QualityprofileUpdate("" + element.id, element as any); // Ignore types - console.log(`Updated QualityProfile: ${newProfile.data.name}`); + logger.info(`Updated QualityProfile: ${newProfile.data.name}`); } catch (error: any) { let message; @@ -213,10 +219,10 @@ const pipeline = async (value: YamlConfigInstance, arrType: ArrType) => { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - console.log(error.request); + logger.info(error.request); } else { // Something happened in setting up the request that triggered an Error - console.log("Error", error.message); + logger.info("Error", error.message); } throw new Error(message); @@ -242,9 +248,11 @@ const run = async () => { // TODO currently this has to be run sequentially because of the centrally configured api + logHeading(`Processing Sonarr ...`); + for (const instanceName in applicationConfig.sonarr) { const instance = applicationConfig.sonarr[instanceName]; - console.log(`Processing Sonarr Instance: ${instanceName}`); + logger.info(`Processing Sonarr Instance: ${instanceName}`); await configureSonarrApi(instance.base_url, instance.api_key); await pipeline(instance, "SONARR"); unsetApi(); @@ -256,11 +264,13 @@ const run = async () => { applicationConfig.sonarr !== null && Object.keys(applicationConfig.sonarr).length <= 0 ) { - console.log(`No sonarr instances defined.`); + logger.info(`No sonarr instances defined.`); } + logHeading(`Processing Radarr ...`); + for (const instanceName in applicationConfig.radarr) { - console.log(`Processing Radarr instance: ${instanceName}`); + logger.info(`Processing Radarr instance: ${instanceName}`); const instance = applicationConfig.radarr[instanceName]; await configureRadarrApi(instance.base_url, instance.api_key); await pipeline(instance, "RADARR"); @@ -273,7 +283,7 @@ const run = async () => { applicationConfig.radarr !== null && Object.keys(applicationConfig.radarr).length <= 0 ) { - console.log(`No radarr instances defined.`); + logger.info(`No radarr instances defined.`); } }; diff --git a/package.json b/package.json index a013658..8740f1e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "axios": "^1.6.8", "dotenv": "^16.4.5", "node-ts": "^6.0.1", + "pino": "^8.19.0", + "pino-pretty": "^11.0.0", "simple-git": "^3.23.0", "swagger-typescript-api": "^13.0.3", "tsx": "^4.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3a89b8..ad1d40f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ dependencies: node-ts: specifier: ^6.0.1 version: 6.0.1 + pino: + specifier: ^8.19.0 + version: 8.19.0 + pino-pretty: + specifier: ^11.0.0 + version: 11.0.0 simple-git: specifier: ^3.23.0 version: 3.23.0 @@ -1098,6 +1104,13 @@ packages: pretty-format: 29.7.0 dev: true + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1169,6 +1182,11 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false + /atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + dev: false + /axios@1.6.8: resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} dependencies: @@ -1182,6 +1200,10 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1195,6 +1217,13 @@ packages: fill-range: 7.0.1 dev: false + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1280,6 +1309,10 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: false + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: false + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1329,6 +1362,10 @@ packages: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} dev: false + /dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dev: false + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -1397,6 +1434,12 @@ packages: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} dev: false + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + /ensure-posix-path@1.1.1: resolution: {integrity: sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==} dev: false @@ -1605,6 +1648,16 @@ packages: engines: {node: '>=6.0.0'} dev: false + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: false + /execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -1620,6 +1673,10 @@ packages: strip-final-newline: 3.0.0 dev: true + /fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + dev: false + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: false @@ -1643,6 +1700,11 @@ packages: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} dev: false + /fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + dev: false + /fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} dev: false @@ -1829,6 +1891,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + /help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + dev: false + /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true @@ -1842,6 +1908,10 @@ packages: engines: {node: '>=16.17.0'} dev: true + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + /ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -1941,6 +2011,11 @@ packages: istanbul-lib-report: 3.0.1 dev: true + /joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: false @@ -2109,6 +2184,10 @@ packages: dependencies: brace-expansion: 1.1.11 + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + /mlly@1.6.1: resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} dependencies: @@ -2246,6 +2325,11 @@ packages: yaml: 1.10.2 dev: false + /on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -2347,6 +2431,54 @@ packages: engines: {node: '>=8.6'} dev: false + /pino-abstract-transport@1.1.0: + resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==} + dependencies: + readable-stream: 4.5.2 + split2: 4.2.0 + dev: false + + /pino-pretty@11.0.0: + resolution: {integrity: sha512-YFJZqw59mHIY72wBnBs7XhLGG6qpJMa4pEQTRgEPEbjIYbng2LXEZZF1DoyDg9CfejEy8uZCyzpcBXXG0oOCwQ==} + hasBin: true + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.1.0 + pump: 3.0.0 + readable-stream: 4.5.2 + secure-json-parse: 2.7.0 + sonic-boom: 3.8.0 + strip-json-comments: 3.1.1 + dev: false + + /pino-std-serializers@6.2.2: + resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} + dev: false + + /pino@8.19.0: + resolution: {integrity: sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.1.0 + pino-std-serializers: 6.2.2 + process-warning: 3.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 3.8.0 + thread-stream: 2.4.1 + dev: false + /pkg-types@1.0.3: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} dependencies: @@ -2417,10 +2549,26 @@ packages: react-is: 18.2.0 dev: true + /process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + dev: false + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: false + /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + /punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2430,10 +2578,30 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: false + /quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + dev: false + /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true + /readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + dev: false + + /real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + dev: false + /reftools@1.1.9: resolution: {integrity: sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==} dev: false @@ -2494,6 +2662,19 @@ packages: queue-microtask: 1.2.3 dev: false + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + dev: false + + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: false + /semver@7.6.0: resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} engines: {node: '>=10'} @@ -2580,11 +2761,22 @@ packages: engines: {node: '>=8'} dev: false + /sonic-boom@3.8.0: + resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==} + dependencies: + atomic-sleep: 1.0.0 + dev: false + /source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} dev: true + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + dev: false + /stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true @@ -2602,6 +2794,12 @@ packages: strip-ansi: 6.0.1 dev: false + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2696,6 +2894,12 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: false + /thread-stream@2.4.1: + resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==} + dependencies: + real-require: 0.2.0 + dev: false + /tinybench@2.6.0: resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} dev: true diff --git a/src/api.ts b/src/api.ts index 80e8249..e2f7895 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,6 @@ import { Api as RadarrApi } from "./__generated__/generated-radarr-api"; import { Api as SonarrApi } from "./__generated__/generated-sonarr-api"; +import { logger } from "./logger"; let sonarrClient: SonarrApi["api"] | undefined; let radarrClient: RadarrApi["api"] | undefined; @@ -60,10 +61,10 @@ export const configureSonarrApi = async (url: string, apiKey: string) => { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - console.log(error.request); + logger.error(error.request); } else { // Something happened in setting up the request that triggered an Error - console.log("Error", error.message); + logger.error("Error", error.message); } throw new Error(message); @@ -113,10 +114,10 @@ export const configureRadarrApi = async (url: string, apiKey: string) => { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - console.log(error.request); + logger.error(error.request); } else { // Something happened in setting up the request that triggered an Error - console.log("Error", error.message); + logger.error("Error", error.message); } throw new Error(message); diff --git a/src/config.ts b/src/config.ts index b11cf3f..a417c39 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,6 +5,7 @@ import { ROOT_PATH } from "./util"; const CONFIG_LOCATION = process.env.CONFIG_LOCATION ?? `${ROOT_PATH}/config.yml`; const SECRETS_LOCATION = process.env.SECRETS_LOCATION ?? `${ROOT_PATH}/secrets.yml`; +export const LOG_LEVEL = process.env.LOG_LEVEL ?? `info`; let config: YamlConfig; let secrets: any; diff --git a/src/custom-formats.ts b/src/custom-formats.ts index 4cd26f2..60f09ed 100644 --- a/src/custom-formats.ts +++ b/src/custom-formats.ts @@ -3,6 +3,7 @@ import path from "path"; import { CustomFormatResource } from "./__generated__/generated-sonarr-api"; import { getArrApi } from "./api"; import { getConfig } from "./config"; +import { logger } from "./logger"; import { CFProcessing, ConfigarrCF, DynamicImportType, TrashCF, YamlInput } from "./types"; import { IS_DRY_RUN, IS_LOCAL_SAMPLE_MODE, compareObjectsCarr, mapImportCfToRequestCf, toCarrCF } from "./util"; @@ -12,7 +13,7 @@ export const deleteAllCustomFormats = async () => { for (const cf of cfOnServer.data) { await api.v3CustomformatDelete(cf.id!); - console.log(`Deleted CF: '${cf.name}'`); + logger.info(`Deleted CF: '${cf.name}'`); } }; @@ -27,14 +28,19 @@ export const loadServerCustomFormats = async (): Promise export const manageCf = async (cfProcessing: CFProcessing, serverCfs: Map, cfsToManage: Set) => { const { carrIdMapping: trashIdToObject } = cfProcessing; - const api = getArrApi(); + let updatedCFs = 0; + let errorCFs = 0; + let validCFs = 0; + let createCFs = 0; + const manageSingle = async (carrId: string) => { const tr = trashIdToObject.get(carrId); if (!tr) { - console.log(`TrashID to manage ${carrId} does not exists`); + logger.info(`TrashID to manage ${carrId} does not exists`); + errorCFs++; return; } @@ -45,34 +51,40 @@ export const manageCf = async (cfProcessing: CFProcessing, serverCfs: Map => { const config = getConfig(); if (config.localCustomFormatsPath == null) { - console.log(`No local custom formats specified. Skipping.`); + logger.debug(`No local custom formats specified. Skipping.`); return null; } const cfPath = path.resolve(config.localCustomFormatsPath); if (!fs.existsSync(cfPath)) { - console.log(`Provided local custom formats path '${config.localCustomFormatsPath}' does not exist.`); + logger.info(`Provided local custom formats path '${config.localCustomFormatsPath}' does not exist.`); return null; } @@ -142,14 +156,14 @@ export const mergeCfSources = (listOfCfs: (CFProcessing | null)[]): CFProcessing for (const [key, value] of c.carrIdMapping.entries()) { if (p.carrIdMapping.has(key)) { - console.log(`Overwriting ${key} during CF merge`); + logger.info(`Overwriting ${key} during CF merge`); } p.carrIdMapping.set(key, value); } for (const [key, value] of c.cfNameToCarrConfig.entries()) { if (p.cfNameToCarrConfig.has(key)) { - console.log(`Overwriting ${key} during CF merge`); + logger.info(`Overwriting ${key} during CF merge`); } p.cfNameToCarrConfig.set(key, value); } diff --git a/src/example.test.ts b/src/example.test.ts index 4cba084..4add609 100644 --- a/src/example.test.ts +++ b/src/example.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; import { CustomFormatResource, PrivacyLevel, QualityDefinitionResource, QualitySource } from "./__generated__/generated-sonarr-api"; -import { calculateQualityDefinitionDiff, loadQualityDefinitionFromServer } from "./quality-definitions"; +import { calculateQualityDefinitionDiff } from "./quality-definitions"; import { TrashCF, TrashCFSpF, TrashQualityDefintion } from "./types"; import { compareObjectsCarr, mapImportCfToRequestCf, toCarrCF } from "./util"; @@ -215,11 +215,6 @@ describe("QualityDefinitions", async () => { }, ], }; - test("test import", async ({}) => { - const result = await loadQualityDefinitionFromServer(); - - console.log(result); - }); test("calculateQualityDefinitionDiff - no diff", async ({}) => { const result = calculateQualityDefinitionDiff(server, client); diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..967b7c9 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,24 @@ +import pino from "pino"; +import { LOG_LEVEL } from "./config"; + +export const logger = pino({ + level: LOG_LEVEL, + transport: { + target: "pino-pretty", + options: { + colorize: true, + }, + }, +}); + +export const logSeparator = () => { + logger.info(`#############################################`); +}; + +export const logHeading = (title: string) => { + logger.info(""); + logSeparator(); + logger.info(`### ${title}`); + logSeparator(); + logger.info(""); +}; diff --git a/src/quality-definitions.ts b/src/quality-definitions.ts index 3ecb8d6..8d70714 100644 --- a/src/quality-definitions.ts +++ b/src/quality-definitions.ts @@ -1,6 +1,7 @@ import path from "path"; import { QualityDefinitionResource } from "./__generated__/generated-sonarr-api"; import { getArrApi } from "./api"; +import { logger } from "./logger"; import { TrashQualityDefintion, TrashQualityDefintionQuality } from "./types"; import { IS_LOCAL_SAMPLE_MODE } from "./util"; @@ -29,7 +30,7 @@ export const calculateQualityDefinitionDiff = (serverQDs: QualityDefinitionResou const changes: string[] = []; if (!element.maxSize) { - console.log(`No maxSize defined: ${element.title}`); + logger.info(`No maxSize defined: ${element.title}`); } if (element.minSize !== tq.min) { diff --git a/src/quality-profiles.ts b/src/quality-profiles.ts index a721e67..9ed534e 100644 --- a/src/quality-profiles.ts +++ b/src/quality-profiles.ts @@ -8,6 +8,7 @@ import { } from "./__generated__/generated-sonarr-api"; import { getArrApi } from "./api"; import { loadServerCustomFormats } from "./custom-formats"; +import { logger } from "./logger"; import { loadQualityDefinitionFromServer } from "./quality-definitions"; import { CFProcessing, RecyclarrMergedTemplates, YamlConfigQualityProfile, YamlConfigQualityProfileItems, YamlList } from "./types"; import { IS_LOCAL_SAMPLE_MODE, notEmpty } from "./util"; @@ -28,7 +29,7 @@ export const mapQualityProfiles = ({ carrIdMapping }: CFProcessing, customFormat const carr = carrIdMapping.get(trashId); if (!carr) { - console.log(`Unknown ID for CF. ${trashId}`); + logger.info(`Unknown ID for CF. ${trashId}`); continue; } @@ -215,7 +216,7 @@ export const calculateQualityProfilesDiff = async ( }, new Map()) ?? new Map(); if (!serverMatch) { - console.log(`QualityProfile not found in server. Ignoring: ${name}`); + logger.info(`QualityProfile not found in server. Ignoring: ${name}`); const mappedQ = mapQualities(qd, value); const qualityToId = mappedQ.reduce>((p, c) => { @@ -304,7 +305,7 @@ export const calculateQualityProfilesDiff = async ( // TODO do we want to enforce the whole structure or only match those which are enabled by us? if (!compareQualities(value.qualities, serverQualitiesMapped)) { - console.log(`QualityProfile Items mismatch will update whole array`); + logger.info(`QualityProfile items mismatch will update whole array`); diffExist = true; changeList.push(`QualityProfile items do not match`); @@ -419,10 +420,10 @@ export const calculateQualityProfilesDiff = async ( updatedServerObject.formatItems = newCFFormats; } else { - console.log(`No scoring for QualityProfile ${serverMatch.name!} found`); + logger.info(`No scoring for QualityProfile ${serverMatch.name!} found`); } - console.log(`QualityProfile (${value.name}) - CF Changes: ${scoringDiff}, Some other diff: ${diffExist}`); + logger.debug(`QualityProfile (${value.name}) - CF Changes: ${scoringDiff}, Some other diff: ${diffExist}`); if (scoringDiff || diffExist) { changedQPs.push(updatedServerObject); @@ -431,9 +432,10 @@ export const calculateQualityProfilesDiff = async ( } if (changeList.length > 0) { - console.log(`ChangeList for QualityProfile:\n`, changeList); + logger.debug(`QualityProfile '${value.name}' is not in sync. Will be updated.`); + logger.debug(changeList, `ChangeList for QualityProfile`); } else { - console.log(`QualityProfile has no changes.`); + logger.debug(`QualityProfile '${value.name}' is in sync.`); } } @@ -443,15 +445,15 @@ export const calculateQualityProfilesDiff = async ( export const filterInvalidQualityProfiles = (profiles: YamlConfigQualityProfile[]): YamlConfigQualityProfile[] => { return profiles.filter((p) => { if (p.name == null) { - console.log(`QP filtered because no name provided`); + logger.info(p, `QualityProfile filtered because no name provided`); return false; } if (p.qualities == null) { - console.log(`QP ${p.name} filtered because no qualities provided`); + logger.info(`QualityProfile: '${p.name}' filtered because no qualities provided`); return false; } if (p.upgrade == null) { - console.log(`QP ${p.name} filtered because no upgrade definition provided`); + logger.info(`QualityProfile: '${p.name}' filtered because no upgrade definition provided`); return false; } diff --git a/src/recyclarr-importer.ts b/src/recyclarr-importer.ts index 7579959..fedbbea 100644 --- a/src/recyclarr-importer.ts +++ b/src/recyclarr-importer.ts @@ -1,7 +1,9 @@ import { default as fs } from "fs"; +import path from "path"; import simpleGit, { CheckRepoActions } from "simple-git"; import yaml from "yaml"; import { getConfig } from "./config"; +import { logger } from "./logger"; import { ArrType, RecyclarrTemplates } from "./types"; import { recyclarrRepoPaths } from "./util"; @@ -25,14 +27,32 @@ export const cloneRecyclarrTemplateRepo = async () => { await gitClient.checkout(applicationConfig.trashRevision ?? "master"); - console.log(`Updating Recyclarr repo`); + logger.info(`Updating Recyclarr repo`); +}; + +export const getLocalTemplatePath = () => { + const config = getConfig(); + + if (config.localConfigTemplatesPath == null) { + logger.debug(`No local templates specified. Skipping.`); + return null; + } + + const customPath = path.resolve(config.localConfigTemplatesPath); + + if (!fs.existsSync(customPath)) { + logger.info(`Provided local templates path '${config.localCustomFormatsPath}' does not exist.`); + return null; + } + + return customPath; }; export const loadRecyclarrTemplates = (arrType: ArrType) => { const map = new Map(); const fillMap = (path: string) => { - const files = fs.readdirSync(`${path}`).filter((fn) => fn.endsWith("yml")); + const files = fs.readdirSync(`${path}`).filter((fn) => fn.endsWith("yaml") || fn.endsWith("yml")); files.forEach((f) => map.set(f.substring(0, f.lastIndexOf(".")), yaml.parse(fs.readFileSync(`${path}/${f}`, "utf8")))); }; @@ -47,5 +67,11 @@ export const loadRecyclarrTemplates = (arrType: ArrType) => { fillMap(recyclarrRepoPaths.sonarrQP); } + const localPath = getLocalTemplatePath(); + + if (localPath) { + fillMap(localPath); + } + return map; }; diff --git a/src/trash-guide.ts b/src/trash-guide.ts index 37be9d5..7878c53 100644 --- a/src/trash-guide.ts +++ b/src/trash-guide.ts @@ -3,6 +3,7 @@ import path from "path"; import simpleGit, { CheckRepoActions } from "simple-git"; import { CustomFormatResource } from "./__generated__/generated-sonarr-api"; import { getConfig } from "./config"; +import { logger } from "./logger"; import { ArrType, CFProcessing, @@ -35,7 +36,7 @@ export const cloneTrashRepo = async () => { await gitClient.checkout(applicationConfig.trashRevision ?? "master"); - console.log(`Updating TrashGuide repo`); + logger.info(`Updating TrashGuide repo`); }; export const loadSonarrTrashCFs = async (arrType: ArrType): Promise => { @@ -47,7 +48,6 @@ export const loadSonarrTrashCFs = async (arrType: ArrType): Promise(); const carrIdToObject = new Map(); const cfNameToCarrObject = new Map(); @@ -78,7 +78,7 @@ export const loadSonarrTrashCFs = async (arrType: ArrType): Promise