diff --git a/.env.example b/.env.example index 90fb092..4c5accf 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,8 @@ BLOCK_REMOVED_NO_FILES_RELEASES=false REMOVE_NOT_AN_UPGRADE=false REMOVE_SERIES_ID_MISMATCH=false BLOCK_REMOVED_SERIES_ID_MISMATCH_RELEASES=false +REMOVE_EPISODE_COUNT_MISMATCH=false +BLOCK_REMOVED_EPISODE_COUNT_MISMATCH_RELEASES=false REMOVE_UNDETERMINED_SAMPLE=false BLOCK_REMOVED_UNDETERMIND_SAMPLE=false diff --git a/README.md b/README.md index 2b1cebc..e31aaab 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Automated queue cleaner for Sonarr that removes stuck downloads based on configu | `REMOVE_NOT_AN_UPGRADE` | `false` | Remove items flagged as "Not an upgrade" | | `REMOVE_SERIES_ID_MISMATCH` | `false` | Remove items with series ID matching conflicts | | `BLOCK_REMOVED_SERIES_ID_MISMATCH_RELEASES` | `false` | Add series ID mismatch items to blocklist | +| `REMOVE_EPISODE_COUNT_MISMATCH` | `false` | Remove items where the on-disk file spans more episodes than the release | +| `BLOCK_REMOVED_EPISODE_COUNT_MISMATCH_RELEASES` | `false` | Add episode-count mismatch items to blocklist | | `REMOVE_UNDETERMINED_SAMPLE` | `false` | Remove items unable to determine if file is a sample | | `BLOCK_REMOVED_UNDETERMINED_SAMPLE` | `false` | Add undetermined sample items to blocklist | | `DRY_RUN` | `false` | Log actions without actually removing/blocking items | @@ -100,6 +102,8 @@ services: REMOVE_NOT_AN_UPGRADE: 'true' REMOVE_SERIES_ID_MISMATCH: 'true' BLOCK_REMOVED_SERIES_ID_MISMATCH_RELEASES: 'false' + REMOVE_EPISODE_COUNT_MISMATCH: 'false' + BLOCK_REMOVED_EPISODE_COUNT_MISMATCH_RELEASES: 'false' REMOVE_UNDETERMINED_SAMPLE: 'false' BLOCK_REMOVED_UNDETERMINED_SAMPLE: 'false' DRY_RUN: 'false' diff --git a/package.json b/package.json index 8e1614f..57b81e4 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "axios": "^1.13.1", - "cron": "^4.3.3", + "axios": "^1.13.2", + "cron": "^4.3.4", "dotenv": "^17.2.3", "yaml": "^2.8.1" }, @@ -22,14 +22,14 @@ "@eslint/js": "^9.39.1", "@types/jest": "^30.0.0", "@types/node": "^24.10.0", - "@typescript-eslint/eslint-plugin": "^8.46.3", - "@typescript-eslint/parser": "^8.46.3", + "@typescript-eslint/eslint-plugin": "^8.46.4", + "@typescript-eslint/parser": "^8.46.4", "eslint": "^9.39.1", "jest": "^30.2.0", "ts-jest": "^29.4.5", "tsx": "^4.20.6", "typescript": "^5.9.3", - "typescript-eslint": "^8.46.3" + "typescript-eslint": "^8.46.4" }, "engines": { "node": ">=22" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 865e493..a6e0dff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: dependencies: axios: - specifier: ^1.13.1 - version: 1.13.1 + specifier: ^1.13.2 + version: 1.13.2 cron: - specifier: ^4.3.3 - version: 4.3.3 + specifier: ^4.3.4 + version: 4.3.4 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -31,11 +31,11 @@ importers: specifier: ^24.10.0 version: 24.10.0 '@typescript-eslint/eslint-plugin': - specifier: ^8.46.3 - version: 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + specifier: ^8.46.4 + version: 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^8.46.3 - version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + specifier: ^8.46.4 + version: 8.46.4(eslint@9.39.1)(typescript@5.9.3) eslint: specifier: ^9.39.1 version: 9.39.1 @@ -52,8 +52,8 @@ importers: specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.46.3 - version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) + specifier: ^8.46.4 + version: 8.46.4(eslint@9.39.1)(typescript@5.9.3) packages: @@ -631,63 +631,63 @@ packages: '@types/yargs@17.0.34': resolution: {integrity: sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==} - '@typescript-eslint/eslint-plugin@8.46.3': - resolution: {integrity: sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==} + '@typescript-eslint/eslint-plugin@8.46.4': + resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.46.3 + '@typescript-eslint/parser': ^8.46.4 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.46.3': - resolution: {integrity: sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==} + '@typescript-eslint/parser@8.46.4': + resolution: {integrity: sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.46.3': - resolution: {integrity: sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==} + '@typescript-eslint/project-service@8.46.4': + resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.46.3': - resolution: {integrity: sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==} + '@typescript-eslint/scope-manager@8.46.4': + resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.3': - resolution: {integrity: sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==} + '@typescript-eslint/tsconfig-utils@8.46.4': + resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.46.3': - resolution: {integrity: sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==} + '@typescript-eslint/type-utils@8.46.4': + resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.46.3': - resolution: {integrity: sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==} + '@typescript-eslint/types@8.46.4': + resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.46.3': - resolution: {integrity: sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==} + '@typescript-eslint/typescript-estree@8.46.4': + resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.46.3': - resolution: {integrity: sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==} + '@typescript-eslint/utils@8.46.4': + resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.46.3': - resolution: {integrity: sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==} + '@typescript-eslint/visitor-keys@8.46.4': + resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -838,8 +838,8 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axios@1.13.1: - resolution: {integrity: sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==} + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} babel-jest@30.2.0: resolution: {integrity: sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==} @@ -869,8 +869,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.24: - resolution: {integrity: sha512-uUhTRDPXamakPyghwrUcjaGvvBqGrWvBHReoiULMIpOJVM9IYzQh83Xk2Onx5HlGI2o10NNCzcs9TG/S3TkwrQ==} + baseline-browser-mapping@2.8.25: + resolution: {integrity: sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==} hasBin: true brace-expansion@1.1.12: @@ -883,8 +883,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.27.0: - resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -914,8 +914,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001753: - resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==} + caniuse-lite@1.0.30001754: + resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -929,8 +929,8 @@ packages: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} - cjs-module-lexer@2.1.0: - resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} + cjs-module-lexer@2.1.1: + resolution: {integrity: sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==} cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} @@ -960,8 +960,8 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cron@4.3.3: - resolution: {integrity: sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==} + cron@4.3.4: + resolution: {integrity: sha512-OiO0l73MGhQOZQCjYZ0v7r8yFWpBOWteemwR1RIxiHtfVsIOwiTJZDvg7GmKzggkwC0RO8tI3P1QYBUCIZNYRQ==} engines: {node: '>=18.x'} cross-spawn@7.0.6: @@ -1011,8 +1011,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.244: - resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==} + electron-to-chromium@1.5.249: + resolution: {integrity: sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -1914,8 +1914,8 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - typescript-eslint@8.46.3: - resolution: {integrity: sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==} + typescript-eslint@8.46.4: + resolution: {integrity: sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2046,7 +2046,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.27.0 + browserslist: 4.28.0 lru-cache: 5.1.1 semver: 6.3.1 @@ -2656,14 +2656,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 eslint: 9.39.1 graphemer: 1.4.0 ignore: 7.0.5 @@ -2673,41 +2673,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.3 eslint: 9.39.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.46.3(typescript@5.9.3)': + '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.46.3': + '@typescript-eslint/scope-manager@8.46.4': dependencies: - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 - '@typescript-eslint/tsconfig-utils@8.46.3(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.46.3(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.46.4(eslint@9.39.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.1 ts-api-utils: 2.1.0(typescript@5.9.3) @@ -2715,14 +2715,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.46.3': {} + '@typescript-eslint/types@8.46.4': {} - '@typescript-eslint/typescript-estree@8.46.3(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.46.3(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3) - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/visitor-keys': 8.46.3 + '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -2733,20 +2733,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.46.3(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.4(eslint@9.39.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) - '@typescript-eslint/scope-manager': 8.46.3 - '@typescript-eslint/types': 8.46.3 - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) eslint: 9.39.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.46.3': + '@typescript-eslint/visitor-keys@8.46.4': dependencies: - '@typescript-eslint/types': 8.46.3 + '@typescript-eslint/types': 8.46.4 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} @@ -2852,7 +2852,7 @@ snapshots: asynckit@0.4.0: {} - axios@1.13.1: + axios@1.13.2: dependencies: follow-redirects: 1.15.11 form-data: 4.0.4 @@ -2914,7 +2914,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.24: {} + baseline-browser-mapping@2.8.25: {} brace-expansion@1.1.12: dependencies: @@ -2929,13 +2929,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.27.0: + browserslist@4.28.0: dependencies: - baseline-browser-mapping: 2.8.24 - caniuse-lite: 1.0.30001753 - electron-to-chromium: 1.5.244 + baseline-browser-mapping: 2.8.25 + caniuse-lite: 1.0.30001754 + electron-to-chromium: 1.5.249 node-releases: 2.0.27 - update-browserslist-db: 1.1.4(browserslist@4.27.0) + update-browserslist-db: 1.1.4(browserslist@4.28.0) bs-logger@0.2.6: dependencies: @@ -2958,7 +2958,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001753: {} + caniuse-lite@1.0.30001754: {} chalk@4.1.2: dependencies: @@ -2969,7 +2969,7 @@ snapshots: ci-info@4.3.1: {} - cjs-module-lexer@2.1.0: {} + cjs-module-lexer@2.1.1: {} cliui@8.0.1: dependencies: @@ -2995,7 +2995,7 @@ snapshots: convert-source-map@2.0.0: {} - cron@4.3.3: + cron@4.3.4: dependencies: '@types/luxon': 3.7.1 luxon: 3.7.2 @@ -3030,7 +3030,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.244: {} + electron-to-chromium@1.5.249: {} emittery@0.13.1: {} @@ -3638,7 +3638,7 @@ snapshots: '@jest/types': 30.2.0 '@types/node': 24.10.0 chalk: 4.1.2 - cjs-module-lexer: 2.1.0 + cjs-module-lexer: 2.1.1 collect-v8-coverage: 1.0.3 glob: 10.4.5 graceful-fs: 4.2.11 @@ -4080,12 +4080,12 @@ snapshots: type-fest@4.41.0: {} - typescript-eslint@8.46.3(eslint@9.39.1)(typescript@5.9.3): + typescript-eslint@8.46.4(eslint@9.39.1)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/parser': 8.46.3(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.46.3(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.3(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1)(typescript@5.9.3) eslint: 9.39.1 typescript: 5.9.3 transitivePeerDependencies: @@ -4122,9 +4122,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - update-browserslist-db@1.1.4(browserslist@4.27.0): + update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: - browserslist: 4.27.0 + browserslist: 4.28.0 escalade: 3.2.0 picocolors: 1.1.1 diff --git a/src/cleaner.ts b/src/cleaner.ts index f6b9ba4..9ff345f 100644 --- a/src/cleaner.ts +++ b/src/cleaner.ts @@ -117,6 +117,11 @@ export class QueueCleaner { return { type: 'seriesIdMismatch', shouldBlock: this.rules.blockRemovedSeriesIdMismatchReleases }; } + if (this.rules.removeEpisodeCountMismatch && message.includes('Episode file on disk contains more episodes than this file contains')) { + this.log('debug', 'Item matched episode count mismatch rule', item.title); + return { type: 'episodeCountMismatch', shouldBlock: this.rules.blockRemovedEpisodeCountMismatchReleases }; + } + if (this.rules.removeUndeterminedSample && message.includes('Unable to determine if file is a sample')) { this.log('debug', 'Item matched undetermined sample rule', item.title); return { type: 'undeterminedSample', shouldBlock: this.rules.blockRemovedUndeterminedSampleReleases }; diff --git a/src/config.ts b/src/config.ts index e84b57e..95a226b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,6 +26,8 @@ const rulesFromEnv: RuleConfig = { removeNotAnUpgrade: parseBooleanEnv('REMOVE_NOT_AN_UPGRADE'), removeSeriesIdMismatch: parseBooleanEnv('REMOVE_SERIES_ID_MISMATCH'), blockRemovedSeriesIdMismatchReleases: parseBooleanEnv('BLOCK_REMOVED_SERIES_ID_MISMATCH_RELEASES'), + removeEpisodeCountMismatch: parseBooleanEnv('REMOVE_EPISODE_COUNT_MISMATCH'), + blockRemovedEpisodeCountMismatchReleases: parseBooleanEnv('BLOCK_REMOVED_EPISODE_COUNT_MISMATCH_RELEASES'), removeUndeterminedSample: parseBooleanEnv('REMOVE_UNDETERMINED_SAMPLE'), // Keep legacy misspelling for backward compatibility with existing deployments. blockRemovedUndeterminedSampleReleases: getNormalizedEnvBoolean([ diff --git a/src/types.ts b/src/types.ts index 6a6eeb5..790ff45 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,8 @@ export interface RuleConfig { removeNotAnUpgrade: boolean; removeSeriesIdMismatch: boolean; blockRemovedSeriesIdMismatchReleases: boolean; + removeEpisodeCountMismatch: boolean; + blockRemovedEpisodeCountMismatchReleases: boolean; removeUndeterminedSample: boolean; blockRemovedUndeterminedSampleReleases: boolean; } @@ -43,7 +45,7 @@ export interface StatusMessage { messages?: string[]; } -export type RuleType = 'quality' | 'archive' | 'noFiles' | 'notAnUpgrade' | 'seriesIdMismatch' | 'undeterminedSample'; +export type RuleType = 'quality' | 'archive' | 'noFiles' | 'notAnUpgrade' | 'seriesIdMismatch' | 'episodeCountMismatch' | 'undeterminedSample'; export interface RuleMatch { type: RuleType; diff --git a/tests/cleaner.test.ts b/tests/cleaner.test.ts index a7988c0..086303b 100644 --- a/tests/cleaner.test.ts +++ b/tests/cleaner.test.ts @@ -1,6 +1,6 @@ import { QueueCleaner, QueueCleanerOptions } from '../src/cleaner'; import { SonarrClient } from '../src/sonarr'; -import { createMockInstance, createRuleConfig, createMockQueueItem, createQualityBlockedItem, createArchiveBlockedItem, createNoFilesBlockedItem, createNotAnUpgradeItem, createSeriesIdMismatchItem, createUndeterminedSampleItem } from './test-utils'; +import { createMockInstance, createRuleConfig, createMockQueueItem, createQualityBlockedItem, createArchiveBlockedItem, createNoFilesBlockedItem, createNotAnUpgradeItem, createSeriesIdMismatchItem, createEpisodeCountMismatchItem, createUndeterminedSampleItem } from './test-utils'; jest.mock('../src/sonarr'); const MockedSonarrClient = SonarrClient as jest.MockedClass; @@ -270,6 +270,53 @@ describe('QueueCleaner', () => { }); }); + describe('episode count mismatch items', () => { + it('should remove episode count mismatch items when enabled', async () => { + const cleaner = createCleaner({ + rules: createRuleConfig({ removeEpisodeCountMismatch: true }) + }); + const items = [createEpisodeCountMismatchItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).toHaveBeenCalledWith(123); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); + }); + + it('should block episode count mismatch items when blocking enabled', async () => { + const cleaner = createCleaner({ + rules: createRuleConfig({ + removeEpisodeCountMismatch: true, + blockRemovedEpisodeCountMismatchReleases: true + }) + }); + const items = [createEpisodeCountMismatchItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.blockRelease).toHaveBeenCalledWith(123); + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + }); + + it('should skip episode count mismatch items when disabled', async () => { + const cleaner = createCleaner({ + rules: createRuleConfig({ removeEpisodeCountMismatch: false }) + }); + const items = [createEpisodeCountMismatchItem()]; + + mockSonarrClient.getQueue.mockResolvedValue(items); + + await cleaner.cleanQueue(); + + expect(mockSonarrClient.removeFromQueue).not.toHaveBeenCalled(); + expect(mockSonarrClient.blockRelease).not.toHaveBeenCalled(); + }); + }); + it('should process multiple matching items', async () => { const cleaner = createCleaner({ rules: createRuleConfig({ diff --git a/tests/test-utils.ts b/tests/test-utils.ts index a45ad75..ba7fe97 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -14,6 +14,8 @@ export const createRuleConfig = (overrides: Partial = {}): RuleConfi removeNotAnUpgrade: false, removeSeriesIdMismatch: false, blockRemovedSeriesIdMismatchReleases: false, + removeEpisodeCountMismatch: false, + blockRemovedEpisodeCountMismatchReleases: false, removeUndeterminedSample: false, blockRemovedUndeterminedSampleReleases: false, ...overrides @@ -75,6 +77,14 @@ export const createSeriesIdMismatchItem = (overrides: Partial = {}): ...overrides }); +export const createEpisodeCountMismatchItem = (overrides: Partial = {}): QueueItem => + createMockQueueItem({ + statusMessages: [{ + messages: ['Episode file on disk contains more episodes than this file contains'] + }], + ...overrides + }); + export const createUndeterminedSampleItem = (overrides: Partial = {}): QueueItem => createMockQueueItem({ statusMessages: [{