diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cebb743..c26810d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,13 +63,13 @@ jobs: fetch-depth: '0' - name: Bump version and push tag - if: ${{ github.ref_name == 'stable' }} + if: ${{ github.ref_name == 'latest' }} id: setversion uses: anothrNick/github-tag-action@1.36.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} WITH_V: true - RELEASE_BRANCHES: stable + RELEASE_BRANCHES: latest - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -96,7 +96,7 @@ jobs: BRANCH_NAME="${{ github.ref_name }}" if [[ "$BRANCH_NAME" == "dev" ]]; then IMAGE_TAG="dev" - elif [[ "$BRANCH_NAME" == "stable" ]]; then + elif [[ "$BRANCH_NAME" == "latest" ]]; then IMAGE_TAG=${{ steps.setversion.outputs.new_tag }} else IMAGE_TAG=$BRANCH_NAME @@ -116,7 +116,7 @@ jobs: run: | BRANCH_NAME="${{ github.ref_name }}" TAG_LATEST="" - if [[ "$BRANCH_NAME" == "stable" ]]; then + if [[ "$BRANCH_NAME" == "latest" ]]; then TAG_LATEST="-t $IMAGE_NAME:latest" fi diff --git a/README.md b/README.md index 6ce9821..40e914f 100644 --- a/README.md +++ b/README.md @@ -1,247 +1,275 @@ -*Like this app? Thanks for giving it a* ⭐️ +_Like this app? Thanks for giving it a_ ⭐️ # **Decluttarr** ## Table of contents -- [Overview](#overview) -- [Dependencies & Hints & FAQ](#dependencies--hints--faq) -- [Getting started](#getting-started) -- [Explanation of the settings](#explanation-of-the-settings) -- [Credits](#credits) -- [Disclaimer](#disclaimer) + +- [Overview](#overview) +- [Dependencies & Hints & FAQ](#dependencies--hints--faq) +- [Getting started](#getting-started) +- [Explanation of the settings](#explanation-of-the-settings) +- [Credits](#credits) +- [Disclaimer](#disclaimer) ## Overview + Decluttarr keeps the radarr & sonarr & lidarr & readarr & whisparr queue free of stalled / redundant downloads Feature overview: -- Automatically delete downloads that are stuck downloading metadata (& trigger download from another source) -- Automatically delete failed downloads (& trigger download from another source) -- Automatically delete downloads belonging to radarr/sonarr/etc. items that have been deleted in the meantime ('Orphan downloads') -- Automatically delete stalled downloads, after they have been found to be stalled multiple times in a row (& trigger download from another source) -- Automatically delete slow downloads, after they have been found to be slow multiple times in a row (& trigger download from another source) -- Automatically delete downloads belonging to radarr/sonarr/etc. items that are unmonitored -- Automatically delete downloads that failed importing since they are not a format upgrade (i.e. a better version is already present) + +- Automatically delete downloads that are stuck downloading metadata (& trigger download from another source) +- Automatically delete failed downloads (& trigger download from another source) +- Automatically delete downloads belonging to radarr/sonarr/etc. items that have been deleted in the meantime ('Orphan downloads') +- Automatically delete stalled downloads, after they have been found to be stalled multiple times in a row (& trigger download from another source) +- Automatically delete slow downloads, after they have been found to be slow multiple times in a row (& trigger download from another source) +- Automatically delete downloads belonging to radarr/sonarr/etc. items that are unmonitored +- Automatically delete downloads that failed importing since they are not a format upgrade (i.e. a better version is already present) You may run this locally by launching main.py, or by pulling the docker image. -You can find a sample docker-compose.yml in the docker folder. +You can find a sample docker-compose.yml [here](#method-1-docker). ## Dependencies & Hints & FAQ -- Use Sonarr v4 & Radarr v5 (currently 'nightly' tag instead of 'latest'), else certain features may not work correctly -- qBittorrent is recommended but not required. If you don't use qBittorrent, you will experience the following limitations: - - When detecting slow downloads, the speeds provided by the *arr apps will be used, which is less accurate than what qBittorrent returns when queried directly - - The feature that allows to protect downloads from removal (NO_STALLED_REMOVAL_QBIT_TAG) does not work - - The feature that ignores private trackers does not work -- If you see strange errors such as "found 10 / 3 times", consider turning on the setting "Reject Blocklisted Torrent Hashes While Grabbing". On nightly Radarr/Sonarr/Readarr/Lidarr/Whisparr, the option is located under settings/indexers in the advanced options of each indexer, on Prowlarr it is under settings/apps and then the advanced settings of the respective app -- When broken torrents are removed the files belonging to them are deleted -- Across all removal types: A new download from another source is automatically added by radarr/sonarr/lidarr/readarr/whisparr (if available) -- If you use qBittorrent and none of your torrents get removed and the verbose logs tell that all torrents are protected by the NO_STALLED_REMOVAL_QBIT_TAG even if they are not, you may be using a qBittorrent version that has problems with API calls and you may want to consider switching to a different qBit image (see https://github.com/ManiMatter/decluttarr/issues/56) -- Currently, “*Arr” apps are only supported in English. Refer to issue https://github.com/ManiMatter/decluttarr/issues/132 for more details + +- Use Sonarr v4 & Radarr v5 (currently 'nightly' tag instead of 'latest'), else certain features may not work correctly +- qBittorrent is recommended but not required. If you don't use qBittorrent, you will experience the following limitations: + - When detecting slow downloads, the speeds provided by the \*arr apps will be used, which is less accurate than what qBittorrent returns when queried directly + - The feature that allows to protect downloads from removal (NO_STALLED_REMOVAL_QBIT_TAG) does not work + - The feature that ignores private trackers does not work +- If you see strange errors such as "found 10 / 3 times", consider turning on the setting "Reject Blocklisted Torrent Hashes While Grabbing". On nightly Radarr/Sonarr/Readarr/Lidarr/Whisparr, the option is located under settings/indexers in the advanced options of each indexer, on Prowlarr it is under settings/apps and then the advanced settings of the respective app +- When broken torrents are removed the files belonging to them are deleted +- Across all removal types: A new download from another source is automatically added by radarr/sonarr/lidarr/readarr/whisparr (if available) +- If you use qBittorrent and none of your torrents get removed and the verbose logs tell that all torrents are protected by the NO_STALLED_REMOVAL_QBIT_TAG even if they are not, you may be using a qBittorrent version that has problems with API calls and you may want to consider switching to a different qBit image (see https://github.com/ManiMatter/decluttarr/issues/56) +- Currently, “\*Arr” apps are only supported in English. Refer to issue https://github.com/ManiMatter/decluttarr/issues/132 for more details +- If you experience yaml issues, please check the closed issues. There are different notations, and it may very well be that the issue you found has already been solved in one of the issues. Once you figured your problem, feel free to post your yaml to help others here: https://github.com/ManiMatter/decluttarr/issues/173 +- declutarr only supports single radarr / sonarr instances. If you have multiple instances of those \*arrs, solution is to run multiple decluclutarrs as well ## Getting started + There's two ways to run this: -- As a docker container with docker-compose -- By cloning the repository and running the script manually + +- As a docker container with docker-compose +- By cloning the repository and running the script manually Both ways are explained below and there's an explanation for the different settings below that ### Method 1: Docker -1) Make a `docker-compose.yml` file -2) Use the following as a base for that and tweak the settings to your needs -``` + +1. Make a `docker-compose.yml` file +2. Use the following as a base for that and tweak the settings to your needs + +```yaml version: "3.3" services: - decluttarr: - image: ghcr.io/manimatter/decluttarr:latest - container_name: decluttarr - restart: always - environment: - TZ=Europe/Zurich - PUID=1000 - PGID=1000 - - ## General - # TEST_RUN=True - # SSL_VERIFICATION=False - LOG_LEVEL: INFO - - ## Features - REMOVE_TIMER: 10 - REMOVE_FAILED: True - REMOVE_FAILED_IMPORTS: True - REMOVE_METADATA_MISSING: True - REMOVE_MISSING_FILES: True - REMOVE_ORPHANS: True - REMOVE_SLOW: True - REMOVE_STALLED: True - REMOVE_UNMONITORED: True - RUN_PERIODIC_RESCANS: ' - { - "SONARR": {"MISSING": true, "CUTOFF_UNMET": true, "MAX_CONCURRENT_SCANS": 3, "MIN_DAYS_BEFORE_RESCAN": 7}, - "RADARR": {"MISSING": true, "CUTOFF_UNMET": true, "MAX_CONCURRENT_SCANS": 3, "MIN_DAYS_BEFORE_RESCAN": 7} - }' - - # Feature Settings - PERMITTED_ATTEMPTS: 3 - NO_STALLED_REMOVAL_QBIT_TAG: Don't Kill - REMOVE_SLOW: True - MIN_DOWNLOAD_SPEED: 100 - FAILED_IMPORT_MESSAGE_PATTERNS: ' - [ - "Not a Custom Format upgrade for existing", - "Not an upgrade for existing" - ]' - - ## Radarr - RADARR_URL: http://radarr:7878 - RADARR_KEY: $RADARR_API_KEY - - ## Sonarr - SONARR_URL: http://sonarr:8989 - SONARR_KEY: $SONARR_API_KEY - - ## Lidarr - LIDARR_URL=http://lidarr:8686 - LIDARR_KEY=$LIDARR_API_KEY - - ## Readarr - READARR_URL=http://readarr:8787 - READARR_KEY=$READARR_API_KEY - - ## Whisparr - WHISPARR_URL=http://whisparr:6969 - WHISPARR_KEY=$WHISPARR_API_KEY - - ## qBitorrent - QBITTORRENT_URL: http://qbittorrent:8080 - # QBITTORRENT_USERNAME=Your name - # QBITTORRENT_PASSWORD=Your password + decluttarr: + image: ghcr.io/manimatter/decluttarr:latest + container_name: decluttarr + restart: always + environment: + TZ: Europe/Zurich + PUID: 1000 + PGID: 1000 + + ## General + # TEST_RUN: True + # SSL_VERIFICATION: False + LOG_LEVEL: INFO + + ## Features + REMOVE_TIMER: 10 + REMOVE_FAILED: True + REMOVE_FAILED_IMPORTS: True + REMOVE_METADATA_MISSING: True + REMOVE_MISSING_FILES: True + REMOVE_ORPHANS: True + REMOVE_SLOW: True + REMOVE_STALLED: True + REMOVE_UNMONITORED: True + RUN_PERIODIC_RESCANS: ' + { + "SONARR": {"MISSING": true, "CUTOFF_UNMET": true, "MAX_CONCURRENT_SCANS": 3, "MIN_DAYS_BEFORE_RESCAN": 7}, + "RADARR": {"MISSING": true, "CUTOFF_UNMET": true, "MAX_CONCURRENT_SCANS": 3, "MIN_DAYS_BEFORE_RESCAN": 7} + }' + + # Feature Settings + PERMITTED_ATTEMPTS: 3 + NO_STALLED_REMOVAL_QBIT_TAG: Don't Kill + MIN_DOWNLOAD_SPEED: 100 + FAILED_IMPORT_MESSAGE_PATTERNS: ' + [ + "Not a Custom Format upgrade for existing", + "Not an upgrade for existing" + ]' + IGNORED_DOWNLOAD_CLIENTS: ["emulerr"] + + ## Radarr + RADARR_URL: http://radarr:7878 + RADARR_KEY: $RADARR_API_KEY + + ## Sonarr + SONARR_URL: http://sonarr:8989 + SONARR_KEY: $SONARR_API_KEY + + ## Lidarr + LIDARR_URL: http://lidarr:8686 + LIDARR_KEY: $LIDARR_API_KEY + + ## Readarr + READARR_URL: http://readarr:8787 + READARR_KEY: $READARR_API_KEY + + ## Whisparr + WHISPARR_URL: http://whisparr:6969 + WHISPARR_KEY: $WHISPARR_API_KEY + + ## qBitorrent + QBITTORRENT_URL: http://qbittorrent:8080 + # QBITTORRENT_USERNAME: Your name + # QBITTORRENT_PASSWORD: Your password ``` -3) Run `docker-compose up -d` in the directory where the file is located to create the docker container -Note: Always pull the "**latest**" version. The "dev" version is for testing only, and should only be pulled when contributing code or supporting with bug fixes + +3. Run `docker-compose up -d` in the directory where the file is located to create the docker container + Note: Always pull the "**latest**" version. The "dev" version is for testing only, and should only be pulled when contributing code or supporting with bug fixes ### Method 2: Running manually -1) Clone the repository with `git clone -b main https://github.com/ManiMatter/decluttarr.git` -2) Rename the `config.conf-Example` inside the config folder to `config.conf` -3) Tweak `config.conf` to your needs -4) Install the libraries listed in the docker/requirements.txt (pip install -r requirements.txt) -5) Run the script with `python3 main.py` -Note: The `config.conf` is disregarded when running via docker-compose.yml + +1. Clone the repository with `git clone -b latest https://github.com/ManiMatter/decluttarr.git` +Note: Do provide the `-b latest` in the clone command, else you will be pulling the dev branch which is not what you are after. +2. Rename the `config.conf-Example` inside the config folder to `config.conf` +3. Tweak `config.conf` to your needs +4. Install the libraries listed in the docker/requirements.txt (pip install -r requirements.txt) +5. Run the script with `python3 main.py` + Note: The `config.conf` is disregarded when running via docker-compose.yml ## Explanation of the settings ### **General settings** + Configures the general behavior of the application (across all features) **LOG_LEVEL** -- Sets the level at which logging will take place -- `INFO` will only show changes applied to radarr/sonarr/lidarr/readarr/whisparr -- `VERBOSE` shows each check being performed even if no change is applied -- `DEBUG` shows very granular information, only required for debugging -- Type: String -- Permissible Values: CRITICAL, ERROR, WARNING, INFO, VERBOSE, DEBUG -- Is Mandatory: No (Defaults to INFO) + +- Sets the level at which logging will take place +- `INFO` will only show changes applied to radarr/sonarr/lidarr/readarr/whisparr +- `VERBOSE` shows each check being performed even if no change is applied +- `DEBUG` shows very granular information, only required for debugging +- Type: String +- Permissible Values: CRITICAL, ERROR, WARNING, INFO, VERBOSE, DEBUG +- Is Mandatory: No (Defaults to INFO) **TEST_RUN** -- Allows you to safely try out this tool. If active, downloads will not be removed -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to False) + +- Allows you to safely try out this tool. If active, downloads will not be removed +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to False) **SSL_VERIFICATION** -- Turns SSL certificate verification on or off for all API calls -- `True` means that the SSL certificate verification is on -- Warning: It's important to note that disabling SSL verification can have security implications, as it makes the system vulnerable to man-in-the-middle attacks. It should only be done in a controlled and secure environment where the risks are well understood and mitigated -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to True) + +- Turns SSL certificate verification on or off for all API calls +- `True` means that the SSL certificate verification is on +- Warning: It's important to note that disabling SSL verification can have security implications, as it makes the system vulnerable to man-in-the-middle attacks. It should only be done in a controlled and secure environment where the risks are well understood and mitigated +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to True) --- ### **Features settings** + Steers which type of cleaning is applied to the downloads queue **REMOVE_TIMER** -- Sets the frequency of how often the queue is checked for orphan and stalled downloads -- Type: Integer -- Unit: Minutes -- Is Mandatory: No (Defaults to 10) + +- Sets the frequency of how often the queue is checked for orphan and stalled downloads +- Type: Integer +- Unit: Minutes +- Is Mandatory: No (Defaults to 10) **REMOVE_FAILED** -- Steers whether failed downloads with no connections are removed from the queue -- These downloads are not added to the blocklist -- A new download from another source is automatically added by radarr/sonarr/lidarr/readarr/whisparr (if available) -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to False) + +- Steers whether failed downloads with no connections are removed from the queue +- These downloads are not added to the blocklist + - A new download from another source is automatically added by radarr/sonarr/lidarr/readarr/whisparr (if available) +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to False) **REMOVE_FAILED_IMPORTS** -- Steers whether downloads that failed importing are removed from the queue -- This can happen, for example, when a better version is already present -- Note: Only considers an import failed if the import message contains a warning that is listed on FAILED_IMPORT_MESSAGE_PATTERNS (see below) -- These downloads are added to the blocklist -- If the setting IGNORE_PRIVATE_TRACKERS is true, and the affected torrent is a private tracker, the queue item will be removed, but the torrent files will be kept -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to False) + +- Steers whether downloads that failed importing are removed from the queue +- This can happen, for example, when a better version is already present +- Note: Only considers an import failed if the import message contains a warning that is listed on FAILED_IMPORT_MESSAGE_PATTERNS (see below) +- These downloads are added to the blocklist +- If the setting IGNORE_PRIVATE_TRACKERS is true, and the affected torrent is a private tracker, the queue item will be removed, but the torrent files will be kept +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to False) **REMOVE_METADATA_MISSING** -- Steers whether downloads stuck obtaining metadata are removed from the queue -- These downloads are added to the blocklist, so that they are not re-requested -- A new download from another source is automatically added by radarr/sonarr/lidarr/readarr/whisparr (if available) -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to False) + +- Steers whether downloads stuck obtaining metadata are removed from the queue +- These downloads are added to the blocklist, so that they are not re-requested +- A new download from another source is automatically added by radarr/sonarr/lidarr/readarr/whisparr (if available) +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to False) **REMOVE_MISSING_FILES** -- Steers whether downloads that have the warning "Files Missing" are removed from the queue -- These downloads are not added to the blocklist -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to False) + +- Steers whether downloads that have the warning "Files Missing" are removed from the queue +- These downloads are not added to the blocklist +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to False) **REMOVE_ORPHANS** -- Steers whether orphan downloads are removed from the queue -- Orphan downloads are those that do not belong to any requested media anymore (Since the media was removed from radarr/sonarr/lidarr/readarr/whisparr after the download started) -- These downloads are not added to the blocklist -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to False) + +- Steers whether orphan downloads are removed from the queue +- Orphan downloads are those that do not belong to any requested media anymore (Since the media was removed from radarr/sonarr/lidarr/readarr/whisparr after the download started) +- These downloads are not added to the blocklist +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to False) **REMOVE_SLOW** -- Steers whether slow downloads are removed from the queue -- Slow downloads are added to the blocklist, so that they are not re-requested in the future -- Note: Does not apply to usenet downloads (since there users pay for certain speed, slowness should not occurr) -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to False) + +- Steers whether slow downloads are removed from the queue +- Slow downloads are added to the blocklist, so that they are not re-requested in the future +- Note: Does not apply to usenet downloads (since there users pay for certain speed, slowness should not occurr) +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to False) **REMOVE_STALLED** -- Steers whether stalled downloads with no connections are removed from the queue -- These downloads are added to the blocklist, so that they are not re-requested in the future -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to False) + +- Steers whether stalled downloads with no connections are removed from the queue +- These downloads are added to the blocklist, so that they are not re-requested in the future +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to False) **REMOVE_UNMONITORED** -- Steers whether downloads belonging to unmonitored media are removed from the queue -- Note: Will only remove from queue if all TV shows depending on the same download are unmonitored -- These downloads are not added to the blocklist -- Note: Since sonarr does not support multi-season packs, if you download one you should protect it with `NO_STALLED_REMOVAL_QBIT_TAG` that is explained further down -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to False) + +- Steers whether downloads belonging to unmonitored media are removed from the queue +- Note: Will only remove from queue if all TV shows depending on the same download are unmonitored +- These downloads are not added to the blocklist +- Note: Since sonarr does not support multi-season packs, if you download one you should protect it with `NO_STALLED_REMOVAL_QBIT_TAG` that is explained further down +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to False) **RUN_PERIODIC_RESCANS** -- Steers whether searches are automatically triggered for items that are missing or have not yet met the cutoff -- Note: Only supports Radarr/Sonarr currently (Lidarr depending on: https://github.com/Lidarr/Lidarr/pull/5084 / Readarr Depending on: https://github.com/Readarr/Readarr/pull/3724) -- Type: Dictionaire -- Is Mandatory: No (Defaults to no searches being triggered automatically) -- "SONARR"/"RADARR" turns on the automatic searches for the respective instances -- "MISSING"/"CUTOFF_UNMET" turns on the automatic search for those wanted items (defaults to True) -- "MAX_CONCURRENT_SCANS" specifies the maximum number of items to be searched in each scan. This value dictates how many items are processed per search operation, which occurs according to the interval set by the REMOVE_TIMER. -- Note: The limit is per wanted list. Thus if both Radarr & Sonarr are set up for automatic searches, both for missing and cutoff unmet items, the actual count may be four times the MAX_CONCURRENT_SCANS -- "MIN_DAYS_BEFORE_RESCAN" steers the days that need to pass before an item is considered again for a scan -- Note: RUN_PERIODIC_RESCANS will always search those items that haven been searched for longest + +- Steers whether searches are automatically triggered for items that are missing or have not yet met the cutoff +- Note: Only supports Radarr/Sonarr currently (Lidarr depending on: https://github.com/Lidarr/Lidarr/pull/5084 / Readarr Depending on: https://github.com/Readarr/Readarr/pull/3724) +- Type: Dictionaire +- Is Mandatory: No (Defaults to no searches being triggered automatically) +- "SONARR"/"RADARR" turns on the automatic searches for the respective instances +- "MISSING"/"CUTOFF_UNMET" turns on the automatic search for those wanted items (defaults to True) +- "MAX_CONCURRENT_SCANS" specifies the maximum number of items to be searched in each scan. This value dictates how many items are processed per search operation, which occurs according to the interval set by the REMOVE_TIMER. +- Note: The limit is per wanted list. Thus if both Radarr & Sonarr are set up for automatic searches, both for missing and cutoff unmet items, the actual count may be four times the MAX_CONCURRENT_SCANS +- "MIN_DAYS_BEFORE_RESCAN" steers the days that need to pass before an item is considered again for a scan +- Note: RUN_PERIODIC_RESCANS will always search those items that haven been searched for longest ``` RUN_PERIODIC_RESCANS: ' @@ -251,132 +279,176 @@ Steers which type of cleaning is applied to the downloads queue }' ``` +There are different yaml notations, any some users suggested the below alternative notation. +If it you face issues, please first check the closed issues before opening a new one (e.g., https://github.com/ManiMatter/decluttarr/issues/173) + +``` +- RUN_PERIODIC_RESCANS=[ +{ +"SONARR":[{"MISSING":true, "CUTOFF_UNMET":true, "MAX_CONCURRENT_SCANS":3, "MIN_DAYS_BEFORE_RESCAN":7}], +"RADARR":[{"MISSING":true, "CUTOFF_UNMET":true, "MAX_CONCURRENT_SCANS":3, "MIN_DAYS_BEFORE_RESCAN":7}] +} +``` + **MIN_DOWNLOAD_SPEED** -- Sets the minimum download speed for active downloads -- If the increase in the downloaded file size of a download is less than this value between two consecutive checks, the download is considered slow and is removed if happening more ofthen than the permitted attempts -- Type: Integer -- Unit: KBytes per second -- Is Mandatory: No (Defaults to 100, but is only enforced when "REMOVE_SLOW" is true) + +- Sets the minimum download speed for active downloads +- If the increase in the downloaded file size of a download is less than this value between two consecutive checks, the download is considered slow and is removed if happening more ofthen than the permitted attempts +- Type: Integer +- Unit: KBytes per second +- Is Mandatory: No (Defaults to 100, but is only enforced when "REMOVE_SLOW" is true) **PERMITTED_ATTEMPTS** -- Defines how many times a download has to be caught as stalled, slow or stuck downloading metadata before it is removed -- Type: Integer -- Unit: Number of scans -- Is Mandatory: No (Defaults to 3) + +- Defines how many times a download has to be caught as stalled, slow or stuck downloading metadata before it is removed +- Type: Integer +- Unit: Number of scans +- Is Mandatory: No (Defaults to 3) **NO_STALLED_REMOVAL_QBIT_TAG** -- Downloads in qBittorrent tagged with this tag will not be removed -- Feature is not available when not using qBittorrent as torrent manager -- Applies to all types of removal (ie. nothing will be removed automatically by decluttarr) -- Note: You may want to try "force recheck" to get your stuck torrents manually back up and running -- Tag is automatically created in qBittorrent (required qBittorrent is reachable on `QBITTORRENT_URL`) -- Important: Also protects unmonitored downloads from being removed (relevant for multi-season packs) -- Type: String -- Is Mandatory: No (Defaults to `Don't Kill`) + +- Downloads in qBittorrent tagged with this tag will not be removed +- Feature is not available when not using qBittorrent as torrent manager +- Applies to all types of removal (ie. nothing will be removed automatically by decluttarr) +- Note: You may want to try "force recheck" to get your stuck torrents manually back up and running +- Tag is automatically created in qBittorrent (required qBittorrent is reachable on `QBITTORRENT_URL`) +- Important: Also protects unmonitored downloads from being removed (relevant for multi-season packs) +- Type: String +- Is Mandatory: No (Defaults to `Don't Kill`) **IGNORE_PRIVATE_TRACKERS** -- Private torrents in qBittorrent will not be removed from the queue if this is set to true -- Only works if qBittorrent is used (does not work with transmission etc.) -- Applies to all types of removal (ie. nothing will be removed automatically by decluttarr); only exception to this is REMOVE_NO_FORMAT_UPGRADE, where for private trackers the queue item is removed (but the torrent files are kept) -- Note: You may want to try "force recheck" to get your stuck torrents manually back up and running -- Type: Boolean -- Permissible Values: True, False -- Is Mandatory: No (Defaults to True) + +- Private torrents in qBittorrent will not be removed from the queue if this is set to true +- Only works if qBittorrent is used (does not work with transmission etc.) +- Applies to all types of removal (ie. nothing will be removed automatically by decluttarr); only exception to this is REMOVE_NO_FORMAT_UPGRADE, where for private trackers the queue item is removed (but the torrent files are kept) +- Note: You may want to try "force recheck" to get your stuck torrents manually back up and running +- Type: Boolean +- Permissible Values: True, False +- Is Mandatory: No (Defaults to True) **FAILED_IMPORT_MESSAGE_PATTERNS** -- Works in together with REMOVE_FAILED_IMPORTS (only relevant if this setting is true) -- Defines the patterns based on which the tool decides if a completed download that has warnings on import should be considered failed -- Queue items are considered failed if any of the specified patterns is contained in one of the messages of the queue item -- Note: If left empty (or not specified), any such pending import with warning is considered failed + +- Works in together with REMOVE_FAILED_IMPORTS (only relevant if this setting is true) +- Defines the patterns based on which the tool decides if a completed download that has warnings on import should be considered failed +- Queue items are considered failed if any of the specified patterns is contained in one of the messages of the queue item +- Note: If left empty (or not specified), any such pending import with warning is considered failed +- Type: List +- Recommended values: ["Not a Custom Format upgrade for existing", "Not an upgrade for existing"] +- Is Mandatory: No (Defaults to [], which means all messages are failures) + +**IGNORED_DOWNLOAD_CLIENTS** + +- If specified, downloads of the listed download clients are not removed / skipped entirely +- Is useful if multiple download clients are used and some of them are known to have slow downloads that recover (and thus should not be subject to slowness check), while other download clients should be monitored - Type: List -- Recommended values: ["Not a Custom Format upgrade for existing", "Not an upgrade for existing"] -- Is Mandatory: No (Defaults to [], which means all messages are failures) +- Is Mandatory: No (Defaults to [], which means no download clients are skipped) --- ### **Radarr section** + Defines radarr instance on which download queue should be decluttered **RADARR_URL** -- URL under which the instance can be reached -- If not defined, this instance will not be monitored + +- URL under which the instance can be reached +- If not defined, this instance will not be monitored **RADARR_KEY** -- Your API key for radarr + +- Your API key for radarr --- ### **Sonarr section** + Defines sonarr instance on which download queue should be decluttered **SONARR_URL** -- URL under which the instance can be reached -- If not defined, this instance will not be monitored + +- URL under which the instance can be reached +- If not defined, this instance will not be monitored **SONARR_KEY** -- Your API key for sonarr + +- Your API key for sonarr --- ### **Lidarr section** + Defines lidarr instance on which download queue should be decluttered **LIDARR_URL** -- URL under which the instance can be reached -- If not defined, this instance will not be monitored + +- URL under which the instance can be reached +- If not defined, this instance will not be monitored **LIDARR_KEY** -- Your API key for lidarr + +- Your API key for lidarr --- ### **Readarr section** + Defines readarr instance on which download queue should be decluttered **READARR_URL** -- URL under which the instance can be reached -- If not defined, this instance will not be monitored + +- URL under which the instance can be reached +- If not defined, this instance will not be monitored **READARR_KEY** -- Your API key for readarr + +- Your API key for readarr --- ### **Whisparr section** + Defines whisparr instance on which download queue should be decluttered **WHISPARR_URL** -- URL under which the instance can be reached -- If not defined, this instance will not be monitored + +- URL under which the instance can be reached +- If not defined, this instance will not be monitored **WHISPARR_KEY** -- Your API key for whisparr + +- Your API key for whisparr --- ### **qBittorrent section** + Defines settings to connect with qBittorrent If a different torrent manager is used, comment out this section (see above the limitations in functionality that arises from this) **QBITTORRENT_URL** -- URL under which the instance can be reached -- If not defined, the NO_STALLED_REMOVAL_QBIT_TAG takes no effect + +- URL under which the instance can be reached +- If not defined, the NO_STALLED_REMOVAL_QBIT_TAG takes no effect **QBITTORRENT_USERNAME** -- Username used to log in to qBittorrent -- Optional; not needed if authentication bypassing on qBittorrent is enabled (for instance for local connections) + +- Username used to log in to qBittorrent +- Optional; not needed if authentication bypassing on qBittorrent is enabled (for instance for local connections) **QBITTORRENT_PASSWORD** -- Password used to log in to qBittorrent -- Optional; not needed if authentication bypassing on qBittorrent is enabled (for instance for local connections) + +- Password used to log in to qBittorrent +- Optional; not needed if authentication bypassing on qBittorrent is enabled (for instance for local connections) ## Credits -- Script for detecting stalled downloads expanded on code by MattDGTL/sonarr-radarr-queue-cleaner -- Script to read out config expanded on code by syncarr/syncarr -- SONARR/RADARR team & contributors for their great product, API documenation, and guidance in their Discord channel -- Particular thanks to them for adding an additional flag to their API that allowed this script detect downloads stuck finding metadata -- craggles17 for arm compatibility -- Fxsch for improved documentation / ReadMe + +- Script for detecting stalled downloads expanded on code by MattDGTL/sonarr-radarr-queue-cleaner +- Script to read out config expanded on code by syncarr/syncarr +- SONARR/RADARR team & contributors for their great product, API documenation, and guidance in their Discord channel +- Particular thanks to them for adding an additional flag to their API that allowed this script detect downloads stuck finding metadata +- craggles17 for arm compatibility +- Fxsch for improved documentation / ReadMe ## Disclaimer + This script comes free of any warranty, and you are using it at your own risk diff --git a/config/config.conf-Example b/config/config.conf-Example index a34995a..b3b75f9 100644 --- a/config/config.conf-Example +++ b/config/config.conf-Example @@ -20,6 +20,7 @@ PERMITTED_ATTEMPTS = 3 NO_STALLED_REMOVAL_QBIT_TAG = Don't Kill IGNORE_PRIVATE_TRACKERS = FALSE FAILED_IMPORT_MESSAGE_PATTERNS = ["Not a Custom Format upgrade for existing", "Not an upgrade for existing"] +IGNORED_DOWNLOAD_CLIENTS = ["emulerr"] [radarr] RADARR_URL = http://radarr:7878 diff --git a/config/definitions.py b/config/definitions.py index 79d5642..f483e23 100644 --- a/config/definitions.py +++ b/config/definitions.py @@ -27,6 +27,7 @@ NO_STALLED_REMOVAL_QBIT_TAG = get_config_value('NO_STALLED_REMOVAL_QBIT_TAG', 'feature_settings', False, str, 'Don\'t Kill') IGNORE_PRIVATE_TRACKERS = get_config_value('IGNORE_PRIVATE_TRACKERS', 'feature_settings', False, bool, True) FAILED_IMPORT_MESSAGE_PATTERNS = get_config_value('FAILED_IMPORT_MESSAGE_PATTERNS','feature_settings', False, list, []) +IGNORED_DOWNLOAD_CLIENTS = get_config_value('IGNORED_DOWNLOAD_CLIENTS', 'feature_settings', False, list, []) # Radarr RADARR_URL = get_config_value('RADARR_URL', 'radarr', False, str) diff --git a/main.py b/main.py index c89255e..34d66e9 100644 --- a/main.py +++ b/main.py @@ -62,6 +62,14 @@ async def main(settingsDict): # Start Cleaning while True: logger.verbose("-" * 50) + + # Refresh qBit Cookie + if settingsDict["QBITTORRENT_URL"]: + await qBitRefreshCookie(settingsDict) + if not settingsDict["QBIT_COOKIE"]: + logger.error("Cookie Refresh failed - exiting decluttarr") + exit() + # Cache protected (via Tag) and private torrents protectedDownloadIDs, privateDowloadIDs = await getProtectedAndPrivateFromQbit( settingsDict diff --git a/src/decluttarr.py b/src/decluttarr.py index aaea68b..3fab741 100644 --- a/src/decluttarr.py +++ b/src/decluttarr.py @@ -58,7 +58,7 @@ async def queueCleaner( logger.verbose("Cleaning queue on %s:", NAME) # Refresh queue: try: - full_queue = await get_queue(BASE_URL, API_KEY, params={full_queue_param: True}) + full_queue = await get_queue(BASE_URL, API_KEY, settingsDict, params={full_queue_param: True}) if full_queue: logger.debug("queueCleaner/full_queue at start:") logger.debug(full_queue) diff --git a/src/jobs/remove_failed.py b/src/jobs/remove_failed.py index ecfdfe8..1b58c90 100644 --- a/src/jobs/remove_failed.py +++ b/src/jobs/remove_failed.py @@ -28,7 +28,7 @@ async def remove_failed( # Detects failed and triggers delete. Does not add to blocklist try: failType = "failed" - queue = await get_queue(BASE_URL, API_KEY) + queue = await get_queue(BASE_URL, API_KEY, settingsDict) logger.debug("remove_failed/queue IN: %s", formattedQueueInfo(queue)) if not queue: diff --git a/src/jobs/remove_failed_imports.py b/src/jobs/remove_failed_imports.py index 6f9f056..858dd0a 100644 --- a/src/jobs/remove_failed_imports.py +++ b/src/jobs/remove_failed_imports.py @@ -18,7 +18,7 @@ async def remove_failed_imports( # Detects downloads stuck downloading meta data and triggers repeat check and subsequent delete. Adds to blocklist try: failType = "failed import" - queue = await get_queue(BASE_URL, API_KEY) + queue = await get_queue(BASE_URL, API_KEY, settingsDict) logger.debug("remove_failed_imports/queue IN: %s", formattedQueueInfo(queue)) if not queue: return 0 diff --git a/src/jobs/remove_metadata_missing.py b/src/jobs/remove_metadata_missing.py index 8e08ef8..848f917 100644 --- a/src/jobs/remove_metadata_missing.py +++ b/src/jobs/remove_metadata_missing.py @@ -28,7 +28,7 @@ async def remove_metadata_missing( # Detects downloads stuck downloading meta data and triggers repeat check and subsequent delete. Adds to blocklist try: failType = "missing metadata" - queue = await get_queue(BASE_URL, API_KEY) + queue = await get_queue(BASE_URL, API_KEY, settingsDict) logger.debug("remove_metadata_missing/queue IN: %s", formattedQueueInfo(queue)) if not queue: return 0 diff --git a/src/jobs/remove_missing_files.py b/src/jobs/remove_missing_files.py index 8f09a29..ecd913d 100644 --- a/src/jobs/remove_missing_files.py +++ b/src/jobs/remove_missing_files.py @@ -28,7 +28,7 @@ async def remove_missing_files( # Detects downloads broken because of missing files. Does not add to blocklist try: failType = "missing files" - queue = await get_queue(BASE_URL, API_KEY) + queue = await get_queue(BASE_URL, API_KEY, settingsDict) logger.debug("remove_missing_files/queue IN: %s", formattedQueueInfo(queue)) if not queue: return 0 diff --git a/src/jobs/remove_orphans.py b/src/jobs/remove_orphans.py index a152a42..6cee734 100644 --- a/src/jobs/remove_orphans.py +++ b/src/jobs/remove_orphans.py @@ -28,8 +28,10 @@ async def remove_orphans( # Removes downloads belonging to movies/tv shows that have been deleted in the meantime. Does not add to blocklist try: failType = "orphan" - full_queue = await get_queue(BASE_URL, API_KEY, params={full_queue_param: True}) - queue = await get_queue(BASE_URL, API_KEY) + full_queue = await get_queue( + BASE_URL, API_KEY, settingsDict, params={full_queue_param: True} + ) + queue = await get_queue(BASE_URL, API_KEY, settingsDict) logger.debug("remove_orphans/full queue IN: %s", formattedQueueInfo(full_queue)) if not full_queue: return 0 # By now the queue may be empty @@ -63,7 +65,9 @@ async def remove_orphans( logger.debug( "remove_orphans/full queue OUT: %s", formattedQueueInfo( - await get_queue(BASE_URL, API_KEY, params={full_queue_param: True}) + await get_queue( + BASE_URL, API_KEY, settingsDict, params={full_queue_param: True} + ) ), ) return len(affectedItems) diff --git a/src/jobs/remove_slow.py b/src/jobs/remove_slow.py index 608d3d8..ad1475b 100644 --- a/src/jobs/remove_slow.py +++ b/src/jobs/remove_slow.py @@ -30,7 +30,7 @@ async def remove_slow( # Detects slow downloads and triggers delete. Adds to blocklist try: failType = "slow" - queue = await get_queue(BASE_URL, API_KEY) + queue = await get_queue(BASE_URL, API_KEY, settingsDict) logger.debug("remove_slow/queue IN: %s", formattedQueueInfo(queue)) if not queue: return 0 @@ -56,7 +56,7 @@ async def remove_slow( continue if queueItem["status"] == "downloading": if ( - queueItem["sizeleft"] == 0 + queueItem["size"] > 0 and queueItem["sizeleft"] == 0 ): # Skip items that are finished downloading but are still marked as downloading. May be the case when files are moving logger.info( ">>> Detected %s download that has completed downloading - skipping check (torrent files likely in process of being moved): %s", diff --git a/src/jobs/remove_stalled.py b/src/jobs/remove_stalled.py index 00e9ac3..dd90427 100644 --- a/src/jobs/remove_stalled.py +++ b/src/jobs/remove_stalled.py @@ -28,7 +28,7 @@ async def remove_stalled( # Detects stalled and triggers repeat check and subsequent delete. Adds to blocklist try: failType = "stalled" - queue = await get_queue(BASE_URL, API_KEY) + queue = await get_queue(BASE_URL, API_KEY, settingsDict) logger.debug("remove_stalled/queue IN: %s", formattedQueueInfo(queue)) if not queue: return 0 diff --git a/src/jobs/remove_unmonitored.py b/src/jobs/remove_unmonitored.py index cf7cc72..c3c8bba 100644 --- a/src/jobs/remove_unmonitored.py +++ b/src/jobs/remove_unmonitored.py @@ -29,7 +29,7 @@ async def remove_unmonitored( # Removes downloads belonging to movies/tv shows that are not monitored. Does not add to blocklist try: failType = "unmonitored" - queue = await get_queue(BASE_URL, API_KEY) + queue = await get_queue(BASE_URL, API_KEY, settingsDict) logger.debug("remove_unmonitored/queue IN: %s", formattedQueueInfo(queue)) if not queue: return 0 diff --git a/src/jobs/run_periodic_rescans.py b/src/jobs/run_periodic_rescans.py index a5e0880..cfd04d3 100644 --- a/src/jobs/run_periodic_rescans.py +++ b/src/jobs/run_periodic_rescans.py @@ -23,7 +23,7 @@ async def run_periodic_rescans( if not arr_type in settingsDict["RUN_PERIODIC_RESCANS"]: return try: - queue = await get_queue(BASE_URL, API_KEY) + queue = await get_queue(BASE_URL, API_KEY, settingsDict) check_on_endpoint = [] RESCAN_SETTINGS = settingsDict["RUN_PERIODIC_RESCANS"][arr_type] if RESCAN_SETTINGS["MISSING"]: diff --git a/src/utils/loadScripts.py b/src/utils/loadScripts.py index 4599a0d..2bcb240 100644 --- a/src/utils/loadScripts.py +++ b/src/utils/loadScripts.py @@ -6,6 +6,7 @@ from dateutil.relativedelta import relativedelta as rd import requests from src.utils.rest import rest_get, rest_post # +from src.utils.shared import qBitRefreshCookie import asyncio from packaging import version @@ -28,6 +29,7 @@ async def getArrInstanceName(settingsDict, arrApp): settingsDict[arrApp + '_NAME'] = arrApp.title() return settingsDict + async def getProtectedAndPrivateFromQbit(settingsDict): # Returns two lists containing the hashes of Qbit that are either protected by tag, or are private trackers (if IGNORE_PRIVATE_TRACKERS is true) protectedDownloadIDs = [] @@ -101,7 +103,8 @@ def showSettings(settingsDict): if settingsDict['QBITTORRENT_URL']: logger.info('Downloads with this tag will be skipped: \"%s\"', settingsDict['NO_STALLED_REMOVAL_QBIT_TAG']) logger.info('Private Trackers will be skipped: %s', settingsDict['IGNORE_PRIVATE_TRACKERS']) - + if settingsDict['IGNORED_DOWNLOAD_CLIENTS']: + logger.info('Download clients skipped: %s',", ".join(settingsDict['IGNORED_DOWNLOAD_CLIENTS'])) logger.info('') logger.info('*** Configured Instances ***') @@ -187,18 +190,9 @@ async def instanceChecks(settingsDict): # Check Bittorrent if settingsDict['QBITTORRENT_URL']: # Checking if qbit can be reached, and checking if version is OK - try: - response = await asyncio.get_event_loop().run_in_executor(None, lambda: requests.post(settingsDict['QBITTORRENT_URL']+'/auth/login', data={'username': settingsDict['QBITTORRENT_USERNAME'], 'password': settingsDict['QBITTORRENT_PASSWORD']}, headers={'content-type': 'application/x-www-form-urlencoded'}, verify=settingsDict['SSL_VERIFICATION'])) - if response.text == 'Fails.': - raise ConnectionError('Login failed.') - response.raise_for_status() - settingsDict['QBIT_COOKIE'] = {'SID': response.cookies['SID']} - except Exception as error: + await qBitRefreshCookie(settingsDict) + if not settingsDict['QBIT_COOKIE']: error_occured = True - logger.error('!! %s Error: !!', 'qBittorrent') - logger.error('> %s', error) - logger.error('> Details:') - logger.error(response.text) if not error_occured: qbit_version = await rest_get(settingsDict['QBITTORRENT_URL']+'/app/version',cookies=settingsDict['QBIT_COOKIE']) diff --git a/src/utils/main.py b/src/utils/main.py deleted file mode 100644 index c89255e..0000000 --- a/src/utils/main.py +++ /dev/null @@ -1,89 +0,0 @@ -# Import Libraries -import asyncio -import logging, verboselogs - -logger = verboselogs.VerboseLogger(__name__) -import json - -# Import Functions -from config.definitions import settingsDict -from src.utils.loadScripts import * -from src.decluttarr import queueCleaner -from src.utils.rest import rest_get, rest_post -from src.utils.trackers import Defective_Tracker, Download_Sizes_Tracker - -# Hide SSL Verification Warnings -if settingsDict["SSL_VERIFICATION"] == False: - import warnings - - warnings.filterwarnings("ignore", message="Unverified HTTPS request") - -# Set up logging -setLoggingFormat(settingsDict) - - -# Main function -async def main(settingsDict): - # Adds to settings Dict the instances that are actually configures - settingsDict["INSTANCES"] = [] - for arrApplication in settingsDict["SUPPORTED_ARR_APPS"]: - if settingsDict[arrApplication + "_URL"]: - settingsDict["INSTANCES"].append(arrApplication) - - # Pre-populates the dictionaries (in classes) that track the items that were already caught as having problems or removed - defectiveTrackingInstances = {} - for instance in settingsDict["INSTANCES"]: - defectiveTrackingInstances[instance] = {} - defective_tracker = Defective_Tracker(defectiveTrackingInstances) - download_sizes_tracker = Download_Sizes_Tracker({}) - - # Get name of arr-instances - for instance in settingsDict["INSTANCES"]: - settingsDict = await getArrInstanceName(settingsDict, instance) - - # Check outdated - upgradeChecks(settingsDict) - - # Welcome Message - showWelcome() - - # Current Settings - showSettings(settingsDict) - - # Check Minimum Version and if instances are reachable and retrieve qbit cookie - settingsDict = await instanceChecks(settingsDict) - - # Create qBit protection tag if not existing - await createQbitProtectionTag(settingsDict) - - # Show Logger Level - showLoggerLevel(settingsDict) - - # Start Cleaning - while True: - logger.verbose("-" * 50) - # Cache protected (via Tag) and private torrents - protectedDownloadIDs, privateDowloadIDs = await getProtectedAndPrivateFromQbit( - settingsDict - ) - - # Run script for each instance - for instance in settingsDict["INSTANCES"]: - await queueCleaner( - settingsDict, - instance, - defective_tracker, - download_sizes_tracker, - protectedDownloadIDs, - privateDowloadIDs, - ) - logger.verbose("") - logger.verbose("Queue clean-up complete!") - - # Wait for the next run - await asyncio.sleep(settingsDict["REMOVE_TIMER"] * 60) - return - - -if __name__ == "__main__": - asyncio.run(main(settingsDict)) diff --git a/src/utils/shared.py b/src/utils/shared.py index 4b050e4..2dba683 100644 --- a/src/utils/shared.py +++ b/src/utils/shared.py @@ -1,6 +1,7 @@ # Shared Functions import logging, verboselogs - +import asyncio +import requests logger = verboselogs.VerboseLogger(__name__) from src.utils.rest import rest_get, rest_delete, rest_post from src.utils.nest_functions import add_keys_nested_dict, nested_get @@ -22,7 +23,7 @@ async def get_arr_records(BASE_URL, API_KEY, params={}, end_point=""): return records["records"] -async def get_queue(BASE_URL, API_KEY, params={}): +async def get_queue(BASE_URL, API_KEY, settingsDict, params={}): # Refreshes and retrieves the current queue await rest_post( url=BASE_URL + "/command", @@ -31,6 +32,7 @@ async def get_queue(BASE_URL, API_KEY, params={}): ) queue = await get_arr_records(BASE_URL, API_KEY, params=params, end_point="queue") queue = filterOutDelayedQueueItems(queue) + queue = filterOutIgnoredDownloadClients(queue, settingsDict) return queue @@ -59,6 +61,28 @@ def filterOutDelayedQueueItems(queue): return filtered_queue +def filterOutIgnoredDownloadClients(queue, settingsDict): + """ + Filters out queue items whose download client is listed in IGNORED_DOWNLOAD_CLIENTS. + """ + if queue is None: + return queue + filtered_queue = [] + + for queue_item in queue: + download_client = queue_item.get("downloadClient", "Unknown client") + if download_client in settingsDict["IGNORED_DOWNLOAD_CLIENTS"]: + logger.debug( + ">>> Queue item ignored due to ignored download client: %s (Download Client: %s)", + queue_item["title"], + download_client, + ) + else: + filtered_queue.append(queue_item) + + return filtered_queue + + def privateTrackerCheck(settingsDict, affectedItems, failType, privateDowloadIDs): # Ignores private tracker items (if setting is turned on) for affectedItem in reversed(affectedItems): @@ -154,7 +178,7 @@ async def execute_checks( ) # Exit Logs if settingsDict["LOG_LEVEL"] == "DEBUG": - queue = await get_queue(BASE_URL, API_KEY) + queue = await get_queue(BASE_URL, API_KEY, settingsDict) logger.debug( "execute_checks/queue OUT (failType: %s): %s", failType, @@ -314,14 +338,12 @@ def errorDetails(NAME, error): exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] logger.warning( - ">>> Queue cleaning failed on %s. (File: %s / Line: %s / Error Message: %s / Error Type: %s)", + ">>> Queue cleaning failed on %s. (File: %s / Line: %s / %s)", NAME, fname, exc_tb.tb_lineno, - error, - exc_type, + traceback.format_exc(), ) - logger.debug(traceback.format_exc()) return @@ -353,6 +375,11 @@ def formattedQueueInfo(queue): except Exception as error: errorDetails("formattedQueueInfo", error) logger.debug("formattedQueueInfo/queue for debug: %s", str(queue)) + if isinstance(error, KeyError): + logger.debug( + "formattedQueueInfo/queue_item with error for debug: %s", queue_item + ) + return "error" @@ -372,3 +399,18 @@ async def qBitOffline(settingsDict, failType, NAME): ) return True return False + +async def qBitRefreshCookie(settingsDict): + try: + response = await asyncio.get_event_loop().run_in_executor(None, lambda: requests.post(settingsDict['QBITTORRENT_URL']+'/auth/login', data={'username': settingsDict['QBITTORRENT_USERNAME'], 'password': settingsDict['QBITTORRENT_PASSWORD']}, headers={'content-type': 'application/x-www-form-urlencoded'}, verify=settingsDict['SSL_VERIFICATION'])) + if response.text == 'Fails.': + raise ConnectionError('Login failed.') + response.raise_for_status() + settingsDict['QBIT_COOKIE'] = {'SID': response.cookies['SID']} + logger.debug('qBit cookie refreshed!') + except Exception as error: + logger.error('!! %s Error: !!', 'qBittorrent') + logger.error('> %s', error) + logger.error('> Details:') + logger.error(response.text) + settingsDict['QBIT_COOKIE'] = {} \ No newline at end of file