Compare commits

...

12 Commits

Author SHA1 Message Date
Marius Nechifor
64bb9fc513 Remove stalled downloads (#21) 2024-12-17 00:40:35 +02:00
Marius Nechifor
0a6ec21c95 simplified how the download client selection works (#22) 2024-12-17 00:40:35 +02:00
Marius Nechifor
74c49f041d upgraded to .NET 9 (#19) 2024-12-05 11:03:25 +02:00
Marius Nechifor
f35abdefe5 Add sonarr search option (#18)
* added Sonarr search type option

* updated test data

* fixed duplicated Sonarr search items when using search type Season

* added enhanced logging option along with Sonarr and Radarr enhanced logs

* switched to ghcr.io
2024-12-04 22:38:32 +02:00
Marius Nechifor
43a11f0e4c added Serilog and file logging (#17) 2024-11-28 23:12:08 +02:00
Marius Nechifor
a5a54e324d fixed faulty regex detection and concurrent data accessing (#16) 2024-11-28 23:05:29 +02:00
Marius Nechifor
53adb6c1c1 fixed queue cleaner being triggered perpetually after each run (#15) 2024-11-25 21:53:17 +02:00
Marius Nechifor
a0c8ff72fb Trigger queue cleaner sequentially (#14)
* added option to run queue cleaner after content blocker

* updated readme to clearly state what the jobs do
2024-11-25 21:33:06 +02:00
Marius Nechifor
599242aa2a added startup job trigger (#12) 2024-11-24 01:47:01 +02:00
Marius Nechifor
3e0913b437 Fix empty torrents (#11)
* fixed unwanted deletion of torrents in downloading metadata state

* refactored jobs code

* updated arr test data

* updated gitignore

* updated test configuration and removed dispensable files
2024-11-24 01:01:20 +02:00
Marius Nechifor
54cabd98b4 remove empty creds restriction (#10)
* removed empty checks on qbit and deluge credentials

* updated configuration readme
2024-11-19 23:54:16 +02:00
Marius Nechifor
cbc5c571b3 fixed missing torrent check on content blocker (#9) 2024-11-19 23:23:22 +02:00
115 changed files with 1546 additions and 1874 deletions

5
.gitignore vendored
View File

@@ -168,4 +168,7 @@ src/.idea/
**/logs/
**/MediaCover/
**/archive/
**/archive/
**/Backups/
*.fastresume
*.bak

169
README.md
View File

@@ -8,77 +8,90 @@ The tool supports both qBittorrent's built-in exclusion features and its own blo
Refer to the [Environment variables](#Environment-variables) section for detailed configuration instructions and the [Setup](#Setup) section for an in-depth explanation of the cleanup process.
## Key features
- Marks unwanted files as skip/unwanted in the download client.
- Automatically strikes stalled or stuck downloads.
- Removes and blocks downloads that reached the maximum number of strikes or are marked as unwanted by the download client or by cleanuperr and triggers a search for removed downloads.
## Important note
Only the <b>latest versions</b> of qBittorrent, Deluge, Sonarr etc. are supported, or earlier versions that have the same API as the latest version.
Only the **latest versions** of the following apps are supported, or earlier versions that have the same API as the latest version:
- qBittorrent
- Deluge
- Transmission
- Sonarr
- Radarr
This tool is actively developed and still a work in progress. Join the Discord server if you want to reach out to me quickly (or just stay updated on new releases) so we can squash those pesky bugs together: https://discord.gg/cJYPs9Bt
This tool is actively developed and still a work in progress. Join the Discord server if you want to reach out to me quickly (or just stay updated on new releases) so we can squash those pesky bugs together:
> https://discord.gg/cJYPs9Bt
# How it works
1. **Content blocker** will:
- Run every 5 minutes (or configured cron).
- Process all items in the *arr queue.
- Find the corresponding item from the download client for each queue item.
- Mark the files that were found in the queue as **unwanted/skipped** if:
- They **are listed in the blacklist**, or
- They **are not included in the whitelist**.
2. **Queue cleaner** will:
- Run every 5 minutes (or configured cron).
- Process all items in the *arr queue.
- Check each queue item if it is **stalled (download speed is 0)**, **stuck in matadata downloading** or **failed to be imported**.
- If it is, the item receives a **strike** and will continue to accumulate strikes every time it meets any of these conditions.
- Check each queue item if it meets one of the following condition in the download client:
- **Marked as completed, but 0 bytes have been downloaded** (due to files being blocked by qBittorrent or the **content blocker**).
- All associated files of are marked as **unwanted/skipped**.
- If the item **DOES NOT** match the above criteria, it will be skipped.
- If the item **DOES** match the criteria or has received the **maximum number of strikes**:
- It will be removed from the *arr's queue and blocked.
- It will be deleted from the download client.
- A new search will be triggered for the *arr item.
# Setup
## Using qBittorrent's built-in feature (works only with qBittorrent)
1. Go to qBittorrent -> Options -> Downloads -> make sure `Excluded file names` is checked -> Set an exclusion list.
- [blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist)
- [permissive blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist_permissive)
1. Go to qBittorrent -> Options -> Downloads -> make sure `Excluded file names` is checked -> Paste an exclusion list that you have copied.
- [blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist), or
- [permissive blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist_permissive), or
- create your own
2. Start cleanuperr with `QUEUECLEANER__ENABLED` set to `true`.
3. cleanuperr will execute a queue cleaner cron job at every 5 minutes that will:
1. go through all items from Sonarr/Radarr's queue.
2. each a queue item is checked:
- if it has been <b>marked as completed and 0 bytes have been downloaded</b> (because qBittorrent blocked the files).
- if all its files are skipped.
3. if the item <b>IS NOT</b> as described, it is skipped.
4. if the item <b>IS</b> as described, it is removed from Sonarr/Radarr's queue, removed from qBittorrent and a search is triggered for the show/movie.
2. qBittorrent will block files from being downloaded. In the case of malicious content, **nothing is downloaded and the torrent is marked as complete**.
3. Start **cleanuperr** with `QUEUECLEANER__ENABLED` set to `true`.
4. The **queue cleaner** will perform a cleanup process as described in the [How it works](#how-it-works) section.
## Using cleanuperr's blocklist (works with all supported download clients)
1. Start cleanuperr with both `QUEUECLEANER_ENABLED` and `CONTENTBLOCKER_ENABLED` set to `true`.
2. Be sure to set and enable a blacklist or a whitelist as described in the [Environment variables](#Environment-variables) section.
3. cleanuperr with execute the following jobs:
- the same queue cleaner as described [here](#Using-qBittorrents-built-in-feature)
- a content blocker cron job at every 5 minutes that will mark files as unwanted/skipped if:
- they are in the blacklist.
- they are not in the whitelist.
1. Set both `QUEUECLEANER_ENABLED` and `CONTENTBLOCKER_ENABLED` to `true` in your environment variables.
2. Configure and enable either a **blacklist** or a **whitelist** as described in the [Environment variables](#Environment-variables) section.
3. Once configured, cleanuperr will perform the following tasks:
- Execute the **content blocker** job, as explained in the [How it works](#how-it-works) section.
- Execute the **queue cleaner** job, as explained in the [How it works](#how-it-works) section.
## Usage
### Docker
```
docker run -d \
-e TRIGGERS__QUEUECLEANER="0 0/5 * * * ?" \
-e QBITTORRENT__ENABLED=true \
-e QBITTORRENT__URL="http://localhost:8080" \
-e QBITTORRENT__USERNAME="user" \
-e QBITTORRENT__PASSWORD="pass" \
-e SONARR__ENABLED=true \
-e SONARR__INSTANCES__0__URL="http://localhost:8989" \
-e SONARR__INSTANCES__0__APIKEY="secret1" \
-e SONARR__INSTANCES__1__URL="http://localhost:8990" \
-e SONARR__INSTANCES__1__APIKEY="secret2" \
-e RADARR__ENABLED=true \
-e RADARR__INSTANCES__0__URL="http://localhost:7878" \
-e RADARR__INSTANCES__0__APIKEY="secret3" \
-e RADARR__INSTANCES__1__URL="http://localhost:7879" \
-e RADARR__INSTANCES__1__APIKEY="secret4" \
...
flaminel/cleanuperr:latest
```
### Docker compose yaml
```
version: "3.3"
services:
cleanuperr:
volumes:
- ./cleanuperr/logs:/var/logs
environment:
- LOGGING__LOGLEVEL__DEFAULT=Information
- LOGGING__LOGLEVEL=Information
- LOGGING__FILE__ENABLED=false
- LOGGING__FILE__PATH=/var/logs/
- LOGGING__ENHANCED=true
- TRIGGERS__QUEUECLEANER=0 0/5 * * * ?
- TRIGGERS__CONTENTBLOCKER=0 0/5 * * * ?
- QUEUECLEANER__ENABLED=true
- QUEUECLEANER__RUNSEQUENTIALLY=true
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
- QUEUECLEANER__STALLED_MAX_STRIKES=5
- CONTENTBLOCKER__ENABLED=true
- CONTENTBLOCKER__BLACKLIST__ENABLED=true
@@ -87,32 +100,35 @@ services:
# - CONTENTBLOCKER__WHITELIST__ENABLED=true
# - CONTENTBLOCKER__BLACKLIST__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist
- QBITTORRENT__ENABLED=true
- DOWNLOAD_CLIENT=qBittorrent
- QBITTORRENT__URL=http://localhost:8080
- QBITTORRENT__USERNAME=user
- QBITTORRENT__PASSWORD=pass
# OR
# - DELUGE__ENABLED=true
# - DOWNLOAD_CLIENT=deluge
# - DELUGE__URL=http://localhost:8112
# - DELUGE__PASSWORD=testing
# OR
# - TRANSMISSION__ENABLED=true
# - DOWNLOAD_CLIENT=transmission
# - TRANSMISSION__URL=http://localhost:9091
# - TRANSMISSION__USERNAME=test
# - TRANSMISSION__PASSWORD=testing
- SONARR__ENABLED=true
- SONARR__SEARCHTYPE=Episode
- SONARR__STALLED_MAX_STRIKES=5
- SONARR__INSTANCES__0__URL=http://localhost:8989
- SONARR__INSTANCES__0__APIKEY=secret1
- SONARR__INSTANCES__1__URL=http://localhost:8990
- SONARR__INSTANCES__1__APIKEY=secret2
- RADARR__ENABLED=true
- RADARR__STALLED_MAX_STRIKES=5
- RADARR__INSTANCES__0__URL=http://localhost:7878
- RADARR__INSTANCES__0__APIKEY=secret3
- RADARR__INSTANCES__1__URL=http://localhost:7879
- RADARR__INSTANCES__1__APIKEY=secret4
image: flaminel/cleanuperr:latest
image: ghcr.io/flmorg/cleanuperr:latest
restart: unless-stopped
```
@@ -120,40 +136,45 @@ services:
| Variable | Required | Description | Default value |
|---|---|---|---|
| LOGGING__LOGLEVEL__DEFAULT | No | Can be `Debug`, `Information`, `Warning` or `Error` | Information |
| LOGGING__LOGLEVEL | No | Can be `Verbose`, `Debug`, `Information`, `Warning`, `Error` or `Fatal` | `Information` |
| LOGGING__FILE__ENABLED | No | Enable or disable logging to file | false |
| LOGGING__FILE__PATH | No | Directory where to save the log files | empty |
| LOGGING__ENHANCED | No | Enhance logs whenever possible<br>A more detailed description is provided [here](variables.md#LOGGING__ENHANCED) | true |
|||||
| TRIGGERS__QUEUECLEANER | Yes if queue cleaner is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) | 0 0/5 * * * ? |
| TRIGGERS__CONTENTBLOCKER | Yes if content blocker is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html) | 0 0/5 * * * ? |
| TRIGGERS__QUEUECLEANER | Yes if queue cleaner is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)<br>Can be a max of 1h interval | 0 0/5 * * * ? |
| TRIGGERS__CONTENTBLOCKER | Yes if content blocker is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)<br>Can be a max of 1h interval | 0 0/5 * * * ? |
|||||
| QUEUECLEANER__ENABLED | No | Enable or disable the queue cleaner | true |
| QUEUECLEANER__RUNSEQUENTIALLY | No | If set to true, the queue cleaner will run after the content blocker instead of running in parallel, streamlining the cleaning process | true |
| QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES | No | After how many strikes should a failed import be removed<br>0 means never | 0 |
| QUEUECLEANER__STALLED_MAX_STRIKES | No | After how many strikes should a stalled download be removed<br>0 means never | 0 |
|||||
| CONTENTBLOCKER__ENABLED | No | Enable or disable the content blocker | false |
| CONTENTBLOCKER__BLACKLIST__ENABLED | Yes if content blocker is enabled and whitelist is not enabled | Enable or disable the blacklist | false |
| CONTENTBLOCKER__BLACKLIST__PATH | Yes if blacklist is enabled | Path to the blacklist (local file or url); Needs to be json compatible | empty |
| CONTENTBLOCKER__BLACKLIST__PATH | Yes if blacklist is enabled | Path to the blacklist (local file or url)<br>Needs to be json compatible | empty |
| CONTENTBLOCKER__WHITELIST__ENABLED | Yes if content blocker is enabled and blacklist is not enabled | Enable or disable the whitelist | false |
| CONTENTBLOCKER__BLACKLIST__PATH | Yes if whitelist is enabled | Path to the whitelist (local file or url); Needs to be json compatible | empty |
| CONTENTBLOCKER__BLACKLIST__PATH | Yes if whitelist is enabled | Path to the whitelist (local file or url)<br>Needs to be json compatible | empty |
|||||
| QBITTORRENT__ENABLED | No | Enable or disable qBittorrent | true |
| QBITTORRENT__URL | Yes if qBittorrent is enabled | qBittorrent instance url | http://localhost:8112 |
| QBITTORRENT__USERNAME | Yes if qBittorrent is enabled | qBittorrent user | empty |
| QBITTORRENT__PASSWORD | Yes if qBittorrent is enabled | qBittorrent password | empty |
| DOWNLOAD_CLIENT | No | Download client that is used by *arrs<br>Can be `qbittorrent`, `deluge` or `transmission` | `qbittorrent` |
| QBITTORRENT__URL | No | qBittorrent instance url | http://localhost:8112 |
| QBITTORRENT__USERNAME | No | qBittorrent user | empty |
| QBITTORRENT__PASSWORD | No | qBittorrent password | empty |
|||||
| DELUGE__ENABLED | No | Enable or disable Deluge | false |
| DELUGE__URL | Yes if Deluge is enabled | Deluge instance url | http://localhost:8080 |
| DELUGE__PASSWORD | Yes if Deluge is enabled | Deluge password | empty |
| DELUGE__URL | No | Deluge instance url | http://localhost:8080 |
| DELUGE__PASSWORD | No | Deluge password | empty |
|||||
| TRANSMISSION__ENABLED | No | Enable or disable Transmission | true |
| TRANSMISSION__URL | Yes if Transmission is enabled | Transmission instance url | http://localhost:9091 |
| TRANSMISSION__URL | No | Transmission instance url | http://localhost:9091 |
| TRANSMISSION__USERNAME | No | Transmission user | empty |
| TRANSMISSION__PASSWORD | No | Transmission password | empty |
|||||
| SONARR__ENABLED | No | Whether Sonarr cleanup is enabled or not | true |
| SONARR__INSTANCES__0__URL | Yes | First Sonarr instance url | http://localhost:8989 |
| SONARR__INSTANCES__0__APIKEY | Yes | First Sonarr instance API key | empty |
| SONARR__ENABLED | No | Enable or disable Sonarr cleanup | true |
| SONARR__SEARCHTYPE | No | What to search for after removing a queue item<br>Can be `Episode`, `Season` or `Series` | `Episode` |
| SONARR__INSTANCES__0__URL | No | First Sonarr instance url | http://localhost:8989 |
| SONARR__INSTANCES__0__APIKEY | No | First Sonarr instance API key | empty |
|||||
| RADARR__ENABLED | No | Whether Radarr cleanup is enabled or not | false |
| RADARR__INSTANCES__0__URL | Yes | First Radarr instance url | http://localhost:8989 |
| RADARR__INSTANCES__0__APIKEY | Yes | First Radarr instance API key | empty |
| RADARR__ENABLED | No | Enable or disable Radarr cleanup | false |
| RADARR__INSTANCES__0__URL | No | First Radarr instance url | http://localhost:8989 |
| RADARR__INSTANCES__0__APIKEY | No | First Radarr instance API key | empty |
#
### To be noted
@@ -167,7 +188,7 @@ services:
example* // file name starts with "example"
*example* // file name has "example" in the name
example // file name is exactly the word "example"
<ANY_REGEX> // regex
regex:<ANY_REGEX> // regex that needs to be marked at the start of the line with "regex:"
```
5. Multiple Sonarr/Radarr instances can be specified using this format, where `<NUMBER>` starts from 0:
```
@@ -181,8 +202,14 @@ SONARR__INSTANCES__<NUMBER>__APIKEY
1. Download the binaries from [releases](https://github.com/flmorg/cleanuperr/releases).
2. Extract them from the zip file.
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [above](/README.md#environment-variables).
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [above](#environment-variables).
### Run as a Windows Service
Check out this stackoverflow answer on how to do it: https://stackoverflow.com/a/15719678
Check out this stackoverflow answer on how to do it: https://stackoverflow.com/a/15719678
## Credits
Special thanks for inspiration go to:
- [ThijmenGThN/swaparr](https://github.com/ThijmenGThN/swaparr)
- [ManiMatter/decluttarr](https://github.com/ManiMatter/decluttarr)
- [PaeyMoopy/sonarr-radarr-queue-cleaner](https://github.com/PaeyMoopy/sonarr-radarr-queue-cleaner)

View File

@@ -1,9 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Serilog" Version="4.2.0" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
namespace Common.Configuration;
namespace Common.Configuration.Arr;
public abstract record ArrConfig
{

View File

@@ -1,4 +1,4 @@
namespace Common.Configuration;
namespace Common.Configuration.Arr;
public sealed class ArrInstance
{

View File

@@ -1,4 +1,4 @@
namespace Common.Configuration;
namespace Common.Configuration.Arr;
public sealed record RadarrConfig : ArrConfig
{

View File

@@ -0,0 +1,8 @@
namespace Common.Configuration.Arr;
public sealed record SonarrConfig : ArrConfig
{
public const string SectionName = "Sonarr";
public SonarrSearchType SearchType { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace Common.Configuration.Arr;
public enum SonarrSearchType
{
Episode,
Season,
Series
}

View File

@@ -1,6 +1,6 @@
namespace Common.Configuration.ContentBlocker;
public sealed record ContentBlockerConfig : IConfig
public sealed record ContentBlockerConfig : IJobConfig
{
public const string SectionName = "ContentBlocker";

View File

@@ -1,32 +1,18 @@
using System.Security;
namespace Common.Configuration;
namespace Common.Configuration.DownloadClient;
public sealed record DelugeConfig : IConfig
{
public const string SectionName = "Deluge";
public required bool Enabled { get; init; }
public Uri? Url { get; init; }
public string? Password { get; init; }
public void Validate()
{
if (!Enabled)
{
return;
}
if (Url is null)
{
throw new ArgumentNullException(nameof(Url));
}
if (string.IsNullOrEmpty(Password))
{
throw new ArgumentNullException(nameof(Password));
}
}
}

View File

@@ -0,0 +1,20 @@
namespace Common.Configuration.DownloadClient;
public sealed class QBitConfig : IConfig
{
public const string SectionName = "qBittorrent";
public Uri? Url { get; init; }
public string? Username { get; init; }
public string? Password { get; init; }
public void Validate()
{
if (Url is null)
{
throw new ArgumentNullException(nameof(Url));
}
}
}

View File

@@ -1,11 +1,9 @@
namespace Common.Configuration;
namespace Common.Configuration.DownloadClient;
public record TransmissionConfig
public record TransmissionConfig : IConfig
{
public const string SectionName = "Transmission";
public required bool Enabled { get; init; }
public Uri? Url { get; init; }
public string? Username { get; init; }
@@ -14,11 +12,6 @@ public record TransmissionConfig
public void Validate()
{
if (!Enabled)
{
return;
}
if (Url is null)
{
throw new ArgumentNullException(nameof(Url));

View File

@@ -0,0 +1,6 @@
namespace Common.Configuration;
public static class EnvironmentVariables
{
public const string DownloadClient = "DOWNLOAD_CLIENT";
}

View File

@@ -0,0 +1,6 @@
namespace Common.Configuration;
public interface IJobConfig : IConfig
{
bool Enabled { get; init; }
}

View File

@@ -0,0 +1,12 @@
namespace Common.Configuration.Logging;
public class FileLogConfig : IConfig
{
public bool Enabled { get; set; }
public string Path { get; set; } = string.Empty;
public void Validate()
{
}
}

View File

@@ -0,0 +1,18 @@
using Serilog.Events;
namespace Common.Configuration.Logging;
public class LoggingConfig : IConfig
{
public const string SectionName = "Logging";
public LogEventLevel LogLevel { get; set; }
public bool Enhanced { get; set; }
public FileLogConfig? File { get; set; }
public void Validate()
{
}
}

View File

@@ -1,39 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Common.Configuration;
public sealed class QBitConfig : IConfig
{
public const string SectionName = "qBittorrent";
public required bool Enabled { get; init; }
public Uri? Url { get; init; }
public string? Username { get; init; }
public string? Password { get; init; }
public void Validate()
{
if (!Enabled)
{
return;
}
if (Url is null)
{
throw new ArgumentNullException(nameof(Url));
}
if (string.IsNullOrEmpty(Username))
{
throw new ArgumentNullException(nameof(Username));
}
if (string.IsNullOrEmpty(Password))
{
throw new ArgumentNullException(nameof(Password));
}
}
}

View File

@@ -1,8 +1,22 @@
namespace Common.Configuration.QueueCleaner;
using Microsoft.Extensions.Configuration;
public sealed record QueueCleanerConfig
namespace Common.Configuration.QueueCleaner;
public sealed record QueueCleanerConfig : IJobConfig
{
public const string SectionName = "QueueCleaner";
public required bool Enabled { get; init; }
public required bool RunSequentially { get; init; }
[ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")]
public ushort ImportFailedMaxStrikes { get; init; }
[ConfigurationKeyName("STALLED_MAX_STRIKES")]
public ushort StalledMaxStrikes { get; init; }
public void Validate()
{
}
}

View File

@@ -1,6 +0,0 @@
namespace Common.Configuration;
public sealed record SonarrConfig : ArrConfig
{
public const string SectionName = "Sonarr";
}

View File

@@ -1,13 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
namespace Domain.Enums;
public enum DownloadClient
{
QBittorrent,
Deluge,
Transmission
}

View File

@@ -0,0 +1,7 @@
namespace Domain.Enums;
public enum StrikeType
{
Stalled,
ImportFailed
}

View File

@@ -1,4 +1,4 @@
namespace Domain.Arr.Queue;
namespace Domain.Models.Arr.Queue;
public record QueueListResponse
{

View File

@@ -1,9 +1,10 @@
namespace Domain.Arr.Queue;
namespace Domain.Models.Arr.Queue;
public record QueueRecord
{
public int SeriesId { get; init; }
public int EpisodeId { get; init; }
public int SeasonNumber { get; init; }
public int MovieId { get; init; }
public required string Title { get; init; }
public string Status { get; init; }

View File

@@ -0,0 +1,21 @@
namespace Domain.Models.Arr;
public class SearchItem
{
public long Id { get; set; }
public override bool Equals(object? obj)
{
if (obj is not SearchItem other)
{
return false;
}
return Id == other.Id;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}

View File

@@ -0,0 +1,25 @@
using Common.Configuration.Arr;
namespace Domain.Models.Arr;
public sealed class SonarrSearchItem : SearchItem
{
public long SeriesId { get; set; }
public SonarrSearchType SearchType { get; set; }
public override bool Equals(object? obj)
{
if (obj is not SonarrSearchItem other)
{
return false;
}
return Id == other.Id && SeriesId == other.SeriesId;
}
public override int GetHashCode()
{
return HashCode.Combine(Id, SeriesId);
}
}

View File

@@ -5,7 +5,7 @@ namespace Domain.Models.Deluge.Response;
public sealed record DelugeContents
{
[JsonPropertyName("contents")]
public Dictionary<string, DelugeFileOrDirectory> Contents { get; set; }
public Dictionary<string, DelugeFileOrDirectory>? Contents { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; } // Always "dir" for the root

View File

@@ -1,6 +0,0 @@
namespace Domain.Models.Deluge.Response;
public sealed record DelugeMinimalStatus
{
public string? Hash { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace Domain.Models.Deluge.Response;
public sealed record TorrentStatus
{
public string? Hash { get; set; }
public string? State { get; set; }
public string? Name { get; set; }
public ulong Eta { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace Domain.Models.Radarr;
public sealed record Movie
{
public required long Id { get; init; }
public required string Title { get; init; }
}

View File

@@ -4,5 +4,5 @@ public sealed record RadarrCommand
{
public required string Name { get; init; }
public required HashSet<int> MovieIds { get; init; }
public required List<long> MovieIds { get; init; }
}

View File

@@ -0,0 +1,12 @@
namespace Domain.Models.Sonarr;
public sealed record Episode
{
public long Id { get; set; }
public int EpisodeNumber { get; set; }
public int SeasonNumber { get; set; }
public long SeriesId { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace Domain.Models.Sonarr;
public sealed record Series
{
public required long Id { get; init; }
public required string Title { get; init; }
}

View File

@@ -1,8 +1,16 @@
namespace Domain.Models.Sonarr;
using Common.Configuration.Arr;
namespace Domain.Models.Sonarr;
public sealed record SonarrCommand
{
public required string Name { get; init; }
public string Name { get; set; }
public required int SeriesId { get; set; }
public long? SeriesId { get; set; }
public long? SeasonNumber { get; set; }
public List<long>? EpisodeIds { get; set; }
public SonarrSearchType SearchType { get; set; }
}

View File

@@ -1,5 +1,10 @@
using Common.Configuration;
using Common.Configuration.Arr;
using Common.Configuration.ContentBlocker;
using Common.Configuration.DownloadClient;
using Common.Configuration.Logging;
using Common.Configuration.QueueCleaner;
using Domain.Enums;
namespace Executable.DependencyInjection;
@@ -7,10 +12,12 @@ public static class ConfigurationDI
{
public static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
services
.Configure<QueueCleanerConfig>(configuration.GetSection(QueueCleanerConfig.SectionName))
.Configure<ContentBlockerConfig>(configuration.GetSection(ContentBlockerConfig.SectionName))
.Configure<QBitConfig>(configuration.GetSection(QBitConfig.SectionName))
.Configure<DelugeConfig>(configuration.GetSection(DelugeConfig.SectionName))
.Configure<TransmissionConfig>(configuration.GetSection(TransmissionConfig.SectionName))
.Configure<SonarrConfig>(configuration.GetSection(SonarrConfig.SectionName))
.Configure<RadarrConfig>(configuration.GetSection(RadarrConfig.SectionName));
.Configure<RadarrConfig>(configuration.GetSection(RadarrConfig.SectionName))
.Configure<LoggingConfig>(configuration.GetSection(LoggingConfig.SectionName));
}

View File

@@ -0,0 +1,67 @@
using Common.Configuration.Logging;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.QueueCleaner;
using Serilog;
using Serilog.Events;
using Serilog.Templates;
using Serilog.Templates.Themes;
namespace Executable.DependencyInjection;
public static class LoggingDI
{
public static ILoggingBuilder AddLogging(this ILoggingBuilder builder, IConfiguration configuration)
{
LoggingConfig? config = configuration.GetSection(LoggingConfig.SectionName).Get<LoggingConfig>();
if (!string.IsNullOrEmpty(config?.File?.Path) && !Directory.Exists(config.File.Path))
{
try
{
Directory.CreateDirectory(config.File.Path);
}
catch (Exception exception)
{
throw new Exception($"log file path is not a valid directory | {config.File.Path}", exception);
}
}
LoggerConfiguration logConfig = new();
const string consoleOutputTemplate = "[{@t:yyyy-MM-dd HH:mm:ss.fff} {@l:u3}]{#if JobName is not null} {Concat('[',JobName,']'),PAD}{#end} {@m}\n{@x}";
const string fileOutputTemplate = "{@t:yyyy-MM-dd HH:mm:ss.fff zzz} [{@l:u3}]{#if JobName is not null} {Concat('[',JobName,']'),PAD}{#end} {@m:lj}\n{@x}";
LogEventLevel level = LogEventLevel.Information;
List<string> jobNames = [nameof(ContentBlocker), nameof(QueueCleaner)];
int padding = jobNames.Max(x => x.Length) + 2;
if (config is not null)
{
level = config.LogLevel;
if (config.File?.Enabled is true)
{
logConfig.WriteTo.File(
path: Path.Combine(config.File.Path, "cleanuperr-.txt"),
formatter: new ExpressionTemplate(fileOutputTemplate.Replace("PAD", padding.ToString())),
fileSizeLimitBytes: 10L * 1024 * 1024,
rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: true
);
}
}
Log.Logger = logConfig
.MinimumLevel.Is(level)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.Extensions.Http", LogEventLevel.Warning)
.MinimumLevel.Override("Quartz", LogEventLevel.Warning)
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Error)
.WriteTo.Console(new ExpressionTemplate(consoleOutputTemplate.Replace("PAD", padding.ToString())))
.Enrich.FromLogContext()
.Enrich.WithProperty("ApplicationName", "cleanuperr")
.CreateLogger();
return builder
.ClearProviders()
.AddSerilog();
}
}

View File

@@ -19,6 +19,7 @@ public static class MainDI
.AddLogging(builder => builder.ClearProviders().AddConsole())
.AddHttpClients()
.AddConfiguration(configuration)
.AddMemoryCache()
.AddServices()
.AddQuartzServices(configuration);

View File

@@ -3,8 +3,10 @@ using Common.Configuration.ContentBlocker;
using Common.Configuration.QueueCleaner;
using Executable.Jobs;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.Jobs;
using Infrastructure.Verticals.QueueCleaner;
using Quartz;
using Quartz.Spi;
namespace Executable.DependencyInjection;
@@ -23,77 +25,104 @@ public static class QuartzDI
throw new NullReferenceException("triggers configuration is null");
}
q.AddQueueCleanerJob(configuration, config.QueueCleaner);
q.AddContentBlockerJob(configuration, config.ContentBlocker);
q.AddJobs(configuration, config);
})
.AddQuartzHostedService(opt =>
{
opt.WaitForJobsToComplete = true;
});
private static void AddQueueCleanerJob(
private static void AddJobs(
this IServiceCollectionQuartzConfigurator q,
IConfiguration configuration,
string trigger
TriggersConfig triggersConfig
)
{
QueueCleanerConfig? config = configuration
ContentBlockerConfig? contentBlockerConfig = configuration
.GetRequiredSection(ContentBlockerConfig.SectionName)
.Get<ContentBlockerConfig>();
q.AddJob<ContentBlocker>(contentBlockerConfig, triggersConfig.ContentBlocker);
QueueCleanerConfig? queueCleanerConfig = configuration
.GetRequiredSection(QueueCleanerConfig.SectionName)
.Get<QueueCleanerConfig>();
if (contentBlockerConfig?.Enabled is true && queueCleanerConfig is { Enabled: true, RunSequentially: true })
{
q.AddJob<QueueCleaner>(queueCleanerConfig, string.Empty);
q.AddJobListener(new JobChainingListener(nameof(QueueCleaner)));
}
else
{
q.AddJob<QueueCleaner>(queueCleanerConfig, triggersConfig.QueueCleaner);
}
}
private static void AddJob<T>(
this IServiceCollectionQuartzConfigurator q,
IJobConfig? config,
string trigger
) where T: GenericHandler
{
string typeName = typeof(T).Name;
if (config is null)
{
throw new NullReferenceException($"{nameof(QueueCleaner)} configuration is null");
throw new NullReferenceException($"{typeName} configuration is null");
}
if (!config.Enabled)
{
return;
}
bool hasTrigger = trigger.Length > 0;
q.AddJob<GenericJob<T>>(opts =>
{
opts.WithIdentity(typeName);
if (!hasTrigger)
{
// jobs with no triggers need to be stored durably
opts.StoreDurably();
}
});
// skip empty triggers
if (!hasTrigger)
{
return;
}
q.AddJob<QueueCleanerJob>(opts =>
{
opts.WithIdentity(nameof(QueueCleanerJob));
});
var triggerObj = (IOperableTrigger)TriggerBuilder.Create()
.WithIdentity("ExampleTrigger")
.StartNow()
.WithCronSchedule(trigger)
.Build();
var nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2);
if (nextFireTimes[1] - nextFireTimes[0] > TimeSpan.FromHours(1))
{
throw new Exception($"{trigger} should have a fire time of maximum 1 hour");
}
q.AddTrigger(opts =>
{
opts.ForJob(nameof(QueueCleanerJob))
.WithIdentity($"{nameof(QueueCleanerJob)}-trigger")
.WithCronSchedule(trigger, x =>x.WithMisfireHandlingInstructionDoNothing());
opts.ForJob(typeName)
.WithIdentity($"{typeName}-trigger")
.WithCronSchedule(trigger, x =>x.WithMisfireHandlingInstructionDoNothing())
.StartNow();
});
}
private static void AddContentBlockerJob(
this IServiceCollectionQuartzConfigurator q,
IConfiguration configuration,
string trigger
)
{
ContentBlockerConfig? config = configuration
.GetRequiredSection(ContentBlockerConfig.SectionName)
.Get<ContentBlockerConfig>();
if (config is null)
{
throw new NullReferenceException($"{nameof(ContentBlocker)} configuration is null");
}
if (!config.Enabled)
{
return;
}
q.AddJob<ContentBlockerJob>(opts =>
{
opts.WithIdentity(nameof(ContentBlockerJob));
});
// Startup trigger
q.AddTrigger(opts =>
{
opts.ForJob(nameof(ContentBlockerJob))
.WithIdentity($"{nameof(ContentBlockerJob)}-trigger")
.WithCronSchedule(trigger, x =>x.WithMisfireHandlingInstructionDoNothing());
opts.ForJob(typeName)
.WithIdentity($"{typeName}-startup-trigger")
.StartNow();
});
}
}

View File

@@ -1,10 +1,10 @@
using Executable.Jobs;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.DownloadClient.Deluge;
using Infrastructure.Verticals.DownloadClient.QBittorrent;
using Infrastructure.Verticals.DownloadClient.Transmission;
using Infrastructure.Verticals.ItemStriker;
using Infrastructure.Verticals.QueueCleaner;
namespace Executable.DependencyInjection;
@@ -15,8 +15,6 @@ public static class ServicesDI
services
.AddTransient<SonarrClient>()
.AddTransient<RadarrClient>()
.AddTransient<QueueCleanerJob>()
.AddTransient<ContentBlockerJob>()
.AddTransient<QueueCleaner>()
.AddTransient<ContentBlocker>()
.AddTransient<FilenameEvaluator>()
@@ -25,5 +23,6 @@ public static class ServicesDI
.AddTransient<TransmissionService>()
.AddTransient<ArrQueueIterator>()
.AddTransient<DownloadServiceFactory>()
.AddSingleton<BlocklistProvider>();
.AddSingleton<BlocklistProvider>()
.AddSingleton<Striker>();
}

View File

@@ -2,18 +2,24 @@
<PropertyGroup>
<AssemblyName>cleanuperr</AssemblyName>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-Executable-6108b2ba-f035-47bc-addf-aaf5e20da4b8</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1"/>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Quartz" Version="3.13.1" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.13.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.13.1" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,32 +0,0 @@
using Infrastructure.Verticals.ContentBlocker;
using Quartz;
namespace Executable.Jobs;
[DisallowConcurrentExecution]
public sealed class ContentBlockerJob : IJob
{
private readonly ILogger<QueueCleanerJob> _logger;
private readonly ContentBlocker _contentBlocker;
public ContentBlockerJob(
ILogger<QueueCleanerJob> logger,
ContentBlocker contentBlocker
)
{
_logger = logger;
_contentBlocker = contentBlocker;
}
public async Task Execute(IJobExecutionContext context)
{
try
{
await _contentBlocker.ExecuteAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(ContentBlockerJob)} failed");
}
}
}

View File

@@ -0,0 +1,33 @@
using Infrastructure.Verticals.Jobs;
using Quartz;
using Serilog.Context;
namespace Executable.Jobs;
[DisallowConcurrentExecution]
public sealed class GenericJob<T> : IJob
where T : GenericHandler
{
private readonly ILogger<GenericJob<T>> _logger;
private readonly T _handler;
public GenericJob(ILogger<GenericJob<T>> logger, T handler)
{
_logger = logger;
_handler = handler;
}
public async Task Execute(IJobExecutionContext context)
{
using var _ = LogContext.PushProperty("JobName", typeof(T).Name);
try
{
await _handler.ExecuteAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "{name} failed", typeof(T).Name);
}
}
}

View File

@@ -1,32 +0,0 @@
using Infrastructure.Verticals.QueueCleaner;
using Quartz;
namespace Executable.Jobs;
[DisallowConcurrentExecution]
public sealed class QueueCleanerJob : IJob
{
private readonly ILogger<QueueCleanerJob> _logger;
private readonly QueueCleaner _queueCleaner;
public QueueCleanerJob(
ILogger<QueueCleanerJob> logger,
QueueCleaner queueCleaner
)
{
_logger = logger;
_queueCleaner = queueCleaner;
}
public async Task Execute(IJobExecutionContext context)
{
try
{
await _queueCleaner.ExecuteAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(QueueCleanerJob)} failed");
}
}
}

View File

@@ -3,6 +3,7 @@ using Executable.DependencyInjection;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddInfrastructure(builder.Configuration);
builder.Logging.AddLogging(builder.Configuration);
var host = builder.Build();

View File

@@ -1,10 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.Hosting.Lifetime": "Information",
"Quartz": "Warning",
"System.Net.Http.HttpClient": "Error"
"LogLevel": "Debug",
"Enhanced": true,
"File": {
"Enabled": false,
"Path": ""
}
},
"Triggers": {
@@ -12,7 +12,7 @@
"ContentBlocker": "0/10 * * * * ?"
},
"ContentBlocker": {
"Enabled": false,
"Enabled": true,
"Blacklist": {
"Enabled": false,
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
@@ -23,27 +23,29 @@
}
},
"QueueCleaner": {
"Enabled": true
},
"qBittorrent": {
"Enabled": true,
"RunSequentially": true,
"IMPORT_FAILED_MAX_STRIKES": 5,
"STALLED_MAX_STRIKES": 5
},
"DOWNLOAD_CLIENT": "qbittorrent",
"qBittorrent": {
"Url": "http://localhost:8080",
"Username": "test",
"Password": "testing"
},
"Deluge": {
"Enabled": false,
"Url": "http://localhost:8112",
"Password": "testing"
},
"Transmission": {
"Enabled": false,
"Url": "http://localhost:9091",
"Username": "test",
"Password": "testing"
},
"Sonarr": {
"Enabled": true,
"SearchType": "Episode",
"Instances": [
{
"Url": "http://localhost:8989",

View File

@@ -1,10 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information",
"Quartz": "Warning",
"System.Net.Http.HttpClient": "Error"
"LogLevel": "Information",
"Enhanced": true,
"File": {
"Enabled": false,
"Path": ""
}
},
"Triggers": {
@@ -23,27 +23,29 @@
}
},
"QueueCleaner": {
"Enabled": true
},
"qBittorrent": {
"Enabled": true,
"RunSequentially": true,
"IMPORT_FAILED_MAX_STRIKES": 5,
"STALLED_MAX_STRIKES": 5
},
"DOWNLOAD_CLIENT": "qbittorrent",
"qBittorrent": {
"Url": "http://localhost:8080",
"Username": "",
"Password": ""
},
"Deluge": {
"Enabled": false,
"Url": "http://localhost:8112",
"Password": "testing"
},
"Transmission": {
"Enabled": false,
"Url": "http://localhost:9091",
"Username": "test",
"Password": "testing"
},
"Sonarr": {
"Enabled": true,
"SearchType": "Episode",
"Instances": [
{
"Url": "http://localhost:8989",

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -13,9 +13,10 @@
<ItemGroup>
<PackageReference Include="FLM.Transmission" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="QBittorrent.Client" Version="1.9.24285.1" />
<PackageReference Include="Quartz" Version="3.13.1" />
</ItemGroup>
</Project>

View File

@@ -1,24 +1,43 @@
using Common.Configuration;
using Domain.Arr.Queue;
using Common.Configuration.Arr;
using Common.Configuration.Logging;
using Common.Configuration.QueueCleaner;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Infrastructure.Verticals.Arr;
public abstract class ArrClient
{
private protected ILogger<ArrClient> _logger;
private protected HttpClient _httpClient;
protected readonly ILogger<ArrClient> _logger;
protected readonly HttpClient _httpClient;
protected readonly LoggingConfig _loggingConfig;
protected readonly QueueCleanerConfig _queueCleanerConfig;
protected readonly Striker _striker;
protected ArrClient(ILogger<ArrClient> logger, IHttpClientFactory httpClientFactory)
protected ArrClient(
ILogger<ArrClient> logger,
IHttpClientFactory httpClientFactory,
IOptions<LoggingConfig> loggingConfig,
IOptions<QueueCleanerConfig> queueCleanerConfig,
Striker striker
)
{
_logger = logger;
_striker = striker;
_httpClient = httpClientFactory.CreateClient();
_loggingConfig = loggingConfig.Value;
_queueCleanerConfig = queueCleanerConfig.Value;
_striker = striker;
}
public virtual async Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page)
{
Uri uri = new(arrInstance.Url, $"/api/v3/queue?page={page}&pageSize=200&sortKey=timeleft");
Uri uri = new(arrInstance.Url, GetQueueUrlPath(page));
using HttpRequestMessage request = new(HttpMethod.Get, uri);
SetApiKey(request, arrInstance.ApiKey);
@@ -46,6 +65,28 @@ public abstract class ArrClient
return queueResponse;
}
public virtual bool ShouldRemoveFromQueue(QueueRecord record)
{
bool hasWarn() => record.TrackedDownloadStatus
.Equals("warning", StringComparison.InvariantCultureIgnoreCase);
bool isImportBlocked() => record.TrackedDownloadState
.Equals("importBlocked", StringComparison.InvariantCultureIgnoreCase);
bool isImportPending() => record.TrackedDownloadState
.Equals("importPending", StringComparison.InvariantCultureIgnoreCase);
if (hasWarn() && (isImportBlocked() || isImportPending()))
{
return _striker.StrikeAndCheckLimit(
record.DownloadId,
record.Title,
_queueCleanerConfig.ImportFailedMaxStrikes,
StrikeType.ImportFailed
);
}
return false;
}
public virtual async Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord queueRecord)
{
Uri uri = new(arrInstance.Url, $"/api/v3/queue/{queueRecord.Id}?removeFromClient=true&blocklist=true&skipRedownload=true&changeCategory=false");
@@ -68,8 +109,27 @@ public abstract class ArrClient
}
}
public abstract Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<int> itemIds);
public abstract Task RefreshItemsAsync(ArrInstance arrInstance, ArrConfig config, HashSet<SearchItem>? items);
public virtual bool IsRecordValid(QueueRecord record)
{
if (string.IsNullOrEmpty(record.DownloadId))
{
_logger.LogDebug("skip | download id is null for {title}", record.Title);
return false;
}
if (record.DownloadId.Equals(record.Title, StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogDebug("skip | item is not ready yet | {title}", record.Title);
return false;
}
return true;
}
protected abstract string GetQueueUrlPath(int page);
protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
{
request.Headers.Add("x-api-key", apiKey);

View File

@@ -1,5 +1,6 @@
using Common.Configuration;
using Domain.Arr.Queue;
using Common.Configuration.Arr;
using Domain.Models.Arr.Queue;
using Microsoft.Extensions.Logging;
namespace Infrastructure.Verticals.Arr;

View File

@@ -1,30 +1,49 @@
using System.Text;
using Common.Configuration;
using Common.Configuration.Arr;
using Common.Configuration.Logging;
using Common.Configuration.QueueCleaner;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Domain.Models.Radarr;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Infrastructure.Verticals.Arr;
public sealed class RadarrClient : ArrClient
{
public RadarrClient(ILogger<ArrClient> logger, IHttpClientFactory httpClientFactory)
: base(logger, httpClientFactory)
public RadarrClient(
ILogger<ArrClient> logger,
IHttpClientFactory httpClientFactory,
IOptions<LoggingConfig> loggingConfig,
IOptions<QueueCleanerConfig> queueCleanerConfig,
Striker striker
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker)
{
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<int> itemIds)
protected override string GetQueueUrlPath(int page)
{
if (itemIds.Count is 0)
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownMovieItems=true&includeMovie=true";
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, ArrConfig config, HashSet<SearchItem>? items)
{
if (items?.Count is null or 0)
{
return;
}
List<long> ids = items.Select(item => item.Id).ToList();
Uri uri = new(arrInstance.Url, "/api/v3/command");
RadarrCommand command = new()
{
Name = "MoviesSearch",
MovieIds = itemIds
MovieIds = ids,
};
using HttpRequestMessage request = new(HttpMethod.Post, uri);
@@ -36,17 +55,83 @@ public sealed class RadarrClient : ArrClient
SetApiKey(request, arrInstance.ApiKey);
using HttpResponseMessage response = await _httpClient.SendAsync(request);
string? logContext = await ComputeCommandLogContextAsync(arrInstance, command);
try
{
response.EnsureSuccessStatusCode();
_logger.LogInformation("movie search triggered | {url} | movie ids: {ids}", arrInstance.Url, string.Join(",", itemIds));
_logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext));
}
catch
{
_logger.LogError("movie search failed | {url} | movie ids: {ids}", arrInstance.Url, string.Join(",", itemIds));
_logger.LogError("{log}", GetSearchLog(arrInstance.Url, command, false, logContext));
throw;
}
}
public override bool IsRecordValid(QueueRecord record)
{
if (record.MovieId is 0)
{
_logger.LogDebug("skip | item information missing | {title}", record.Title);
return false;
}
return base.IsRecordValid(record);
}
private static string GetSearchLog(Uri instanceUrl, RadarrCommand command, bool success, string? logContext)
{
string status = success ? "triggered" : "failed";
string message = logContext ?? $"movie ids: {string.Join(',', command.MovieIds)}";
return $"movie search {status} | {instanceUrl} | {message}";
}
private async Task<string?> ComputeCommandLogContextAsync(ArrInstance arrInstance, RadarrCommand command)
{
try
{
if (!_loggingConfig.Enhanced)
{
return null;
}
StringBuilder log = new();
foreach (long movieId in command.MovieIds)
{
Movie? movie = await GetMovie(arrInstance, movieId);
if (movie is null)
{
return null;
}
log.Append($"[{movie.Title}]");
}
return log.ToString();
}
catch (Exception exception)
{
_logger.LogDebug(exception, "failed to compute log context");
}
return null;
}
private async Task<Movie?> GetMovie(ArrInstance arrInstance, long movieId)
{
Uri uri = new(arrInstance.Url, $"api/v3/movie/{movieId}");
using HttpRequestMessage request = new(HttpMethod.Get, uri);
SetApiKey(request, arrInstance.ApiKey);
using HttpResponseMessage response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Movie>(responseBody);
}
}

View File

@@ -1,50 +1,265 @@
using System.Text;
using Common.Configuration;
using Common.Configuration.Arr;
using Common.Configuration.Logging;
using Common.Configuration.QueueCleaner;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Domain.Models.Sonarr;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Infrastructure.Verticals.Arr;
public sealed class SonarrClient : ArrClient
{
public SonarrClient(ILogger<SonarrClient> logger, IHttpClientFactory httpClientFactory)
: base(logger, httpClientFactory)
public SonarrClient(
ILogger<SonarrClient> logger,
IHttpClientFactory httpClientFactory,
IOptions<LoggingConfig> loggingConfig,
IOptions<QueueCleanerConfig> queueCleanerConfig,
Striker striker
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker)
{
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<int> itemIds)
protected override string GetQueueUrlPath(int page)
{
foreach (int itemId in itemIds)
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true";
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, ArrConfig config, HashSet<SearchItem>? items)
{
if (items?.Count is null or 0)
{
return;
}
Uri uri = new(arrInstance.Url, "/api/v3/command");
foreach (SonarrCommand command in GetSearchCommands(items.Cast<SonarrSearchItem>().ToHashSet()))
{
Uri uri = new(arrInstance.Url, "/api/v3/command");
SonarrCommand command = new()
{
Name = "SeriesSearch",
SeriesId = itemId
};
using HttpRequestMessage request = new(HttpMethod.Post, uri);
request.Content = new StringContent(
JsonConvert.SerializeObject(command),
JsonConvert.SerializeObject(command, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
Encoding.UTF8,
"application/json"
);
SetApiKey(request, arrInstance.ApiKey);
using HttpResponseMessage response = await _httpClient.SendAsync(request);
string? logContext = await ComputeCommandLogContextAsync(arrInstance, command, command.SearchType);
try
{
response.EnsureSuccessStatusCode();
_logger.LogInformation("series search triggered | {url} | series id: {id}", arrInstance.Url, itemId);
_logger.LogInformation("{log}", GetSearchLog(command.SearchType, arrInstance.Url, command, true, logContext));
}
catch
{
_logger.LogError("series search failed | {url} | series id: {id}", arrInstance.Url, itemId);
_logger.LogError("{log}", GetSearchLog(command.SearchType, arrInstance.Url, command, false, logContext));
throw;
}
}
}
public override bool IsRecordValid(QueueRecord record)
{
if (record.EpisodeId is 0 || record.SeriesId is 0)
{
_logger.LogDebug("skip | item information missing | {title}", record.Title);
return false;
}
return base.IsRecordValid(record);
}
private static string GetSearchLog(
SonarrSearchType searchType,
Uri instanceUrl,
SonarrCommand command,
bool success,
string? logContext
)
{
string status = success ? "triggered" : "failed";
return searchType switch
{
SonarrSearchType.Episode =>
$"episodes search {status} | {instanceUrl} | {logContext ?? $"episode ids: {string.Join(',', command.EpisodeIds)}"}",
SonarrSearchType.Season =>
$"season search {status} | {instanceUrl} | {logContext ?? $"season: {command.SeasonNumber} series id: {command.SeriesId}"}",
SonarrSearchType.Series => $"series search {status} | {instanceUrl} | {logContext ?? $"series id: {command.SeriesId}"}",
_ => throw new ArgumentOutOfRangeException(nameof(searchType), searchType, null)
};
}
private async Task<string?> ComputeCommandLogContextAsync(ArrInstance arrInstance, SonarrCommand command, SonarrSearchType searchType)
{
try
{
if (!_loggingConfig.Enhanced)
{
return null;
}
StringBuilder log = new();
if (searchType is SonarrSearchType.Episode)
{
var episodes = await GetEpisodesAsync(arrInstance, command.EpisodeIds);
if (episodes?.Count is null or 0)
{
return null;
}
var seriesIds = episodes
.Select(x => x.SeriesId)
.Distinct()
.ToList();
List<Series> series = [];
foreach (long id in seriesIds)
{
Series? show = await GetSeriesAsync(arrInstance, id);
if (show is null)
{
return null;
}
series.Add(show);
}
foreach (var group in command.EpisodeIds.GroupBy(id => episodes.First(x => x.Id == id).SeriesId))
{
var show = series.First(x => x.Id == group.Key);
var episode = episodes
.Where(ep => group.Any(x => x == ep.Id))
.OrderBy(x => x.SeasonNumber)
.ThenBy(x => x.EpisodeNumber)
.Select(x => $"S{x.SeasonNumber.ToString().PadLeft(2, '0')}E{x.EpisodeNumber.ToString().PadLeft(2, '0')}")
.ToList();
log.Append($"[{show.Title} {string.Join(',', episode)}]");
}
}
if (searchType is SonarrSearchType.Season)
{
Series? show = await GetSeriesAsync(arrInstance, command.SeriesId.Value);
if (show is null)
{
return null;
}
log.Append($"[{show.Title} season {command.SeasonNumber}]");
}
if (searchType is SonarrSearchType.Series)
{
Series? show = await GetSeriesAsync(arrInstance, command.SeriesId.Value);
if (show is null)
{
return null;
}
log.Append($"[{show.Title}]");
}
return log.ToString();
}
catch (Exception exception)
{
_logger.LogDebug(exception, "failed to compute log context");
}
return null;
}
private async Task<List<Episode>?> GetEpisodesAsync(ArrInstance arrInstance, List<long> episodeIds)
{
Uri uri = new(arrInstance.Url, $"api/v3/episode?{string.Join('&', episodeIds.Select(x => $"episodeIds={x}"))}");
using HttpRequestMessage request = new(HttpMethod.Get, uri);
SetApiKey(request, arrInstance.ApiKey);
using HttpResponseMessage response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<Episode>>(responseBody);
}
private async Task<Series?> GetSeriesAsync(ArrInstance arrInstance, long seriesId)
{
Uri uri = new(arrInstance.Url, $"api/v3/series/{seriesId}");
using HttpRequestMessage request = new(HttpMethod.Get, uri);
SetApiKey(request, arrInstance.ApiKey);
using HttpResponseMessage response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
string responseBody = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Series>(responseBody);
}
private List<SonarrCommand> GetSearchCommands(HashSet<SonarrSearchItem> items)
{
const string episodeSearch = "EpisodeSearch";
const string seasonSearch = "SeasonSearch";
const string seriesSearch = "SeriesSearch";
List<SonarrCommand> commands = new();
foreach (SonarrSearchItem item in items)
{
SonarrCommand command = item.SearchType is SonarrSearchType.Episode
? commands.FirstOrDefault() ?? new() { Name = episodeSearch, EpisodeIds = new() }
: new();
switch (item.SearchType)
{
case SonarrSearchType.Episode when command.EpisodeIds is null:
command.EpisodeIds = [item.Id];
break;
case SonarrSearchType.Episode when command.EpisodeIds is not null:
command.EpisodeIds.Add(item.Id);
break;
case SonarrSearchType.Season:
command.Name = seasonSearch;
command.SeasonNumber = item.Id;
command.SeriesId = ((SonarrSearchItem)item).SeriesId;
break;
case SonarrSearchType.Series:
command.Name = seriesSearch;
command.SeriesId = item.Id;
break;
default:
throw new ArgumentOutOfRangeException(nameof(item.SearchType), item.SearchType, null);
}
if (item.SearchType is SonarrSearchType.Episode && commands.Count > 0)
{
// only one command will be generated for episodes search
continue;
}
command.SearchType = item.SearchType;
commands.Add(command);
}
return commands;
}
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.RegularExpressions;
using Common.Configuration.ContentBlocker;
using Domain.Enums;
@@ -15,9 +16,9 @@ public sealed class BlocklistProvider
public BlocklistType BlocklistType { get; }
public List<string> Patterns { get; } = [];
public ConcurrentBag<string> Patterns { get; } = [];
public List<Regex> Regexes { get; } = [];
public ConcurrentBag<Regex> Regexes { get; } = [];
public BlocklistProvider(
ILogger<BlocklistProvider> logger,
@@ -75,9 +76,18 @@ public sealed class BlocklistProvider
long startTime = Stopwatch.GetTimestamp();
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
const string regexId = "regex:";
Parallel.ForEach(patterns, options, pattern =>
{
if (!pattern.StartsWith(regexId))
{
Patterns.Add(pattern);
return;
}
pattern = pattern[regexId.Length..];
try
{
Regex regex = new(pattern, RegexOptions.Compiled);
@@ -85,7 +95,7 @@ public sealed class BlocklistProvider
}
catch (ArgumentException)
{
Patterns.Add(pattern);
_logger.LogWarning("invalid regex | {pattern}", pattern);
}
});

View File

@@ -1,23 +1,18 @@
using Common.Configuration;
using Domain.Arr.Queue;
using Common.Configuration.Arr;
using Domain.Enums;
using Domain.Models.Arr.Queue;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.Jobs;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Infrastructure.Verticals.ContentBlocker;
public sealed class ContentBlocker : IDisposable
public sealed class ContentBlocker : GenericHandler
{
private readonly ILogger<ContentBlocker> _logger;
private readonly SonarrConfig _sonarrConfig;
private readonly RadarrConfig _radarrConfig;
private readonly SonarrClient _sonarrClient;
private readonly RadarrClient _radarrClient;
private readonly ArrQueueIterator _arrArrQueueIterator;
private readonly BlocklistProvider _blocklistProvider;
private readonly IDownloadService _downloadService;
public ContentBlocker(
ILogger<ContentBlocker> logger,
@@ -28,48 +23,18 @@ public sealed class ContentBlocker : IDisposable
ArrQueueIterator arrArrQueueIterator,
BlocklistProvider blocklistProvider,
DownloadServiceFactory downloadServiceFactory
)
) : base(logger, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
{
_logger = logger;
_sonarrConfig = sonarrConfig.Value;
_radarrConfig = radarrConfig.Value;
_sonarrClient = sonarrClient;
_radarrClient = radarrClient;
_arrArrQueueIterator = arrArrQueueIterator;
_blocklistProvider = blocklistProvider;
_downloadService = downloadServiceFactory.CreateDownloadClient();
}
public async Task ExecuteAsync()
public override async Task ExecuteAsync()
{
await _blocklistProvider.LoadBlocklistAsync();
await _downloadService.LoginAsync();
await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr);
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr);
await base.ExecuteAsync();
}
private async Task ProcessArrConfigAsync(ArrConfig config, InstanceType instanceType)
{
if (!config.Enabled)
{
return;
}
foreach (ArrInstance arrInstance in config.Instances)
{
try
{
await ProcessInstanceAsync(arrInstance, instanceType);
}
catch (Exception exception)
{
_logger.LogError(exception, "failed to block content for {type} instance | {url}", instanceType, arrInstance.Url);
}
}
}
private async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
{
ArrClient arrClient = GetClient(instanceType);
@@ -77,6 +42,11 @@ public sealed class ContentBlocker : IDisposable
{
foreach (QueueRecord record in items)
{
if (record.Protocol is not "torrent")
{
continue;
}
if (string.IsNullOrEmpty(record.DownloadId))
{
_logger.LogDebug("skip | download id is null for {title}", record.Title);
@@ -88,17 +58,4 @@ public sealed class ContentBlocker : IDisposable
}
});
}
private ArrClient GetClient(InstanceType type) =>
type switch
{
InstanceType.Sonarr => _sonarrClient,
InstanceType.Radarr => _radarrClient,
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
};
public void Dispose()
{
_downloadService.Dispose();
}
}

View File

@@ -1,6 +1,7 @@
using System.Net.Http.Headers;
using System.Text.Json.Serialization;
using Common.Configuration;
using Common.Configuration.DownloadClient;
using Domain.Models.Deluge.Exceptions;
using Domain.Models.Deluge.Request;
using Domain.Models.Deluge.Response;

View File

@@ -1,42 +1,46 @@
using Common.Configuration;
using Common.Configuration.DownloadClient;
using Common.Configuration.QueueCleaner;
using Domain.Models.Deluge.Response;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Infrastructure.Verticals.DownloadClient.Deluge;
public sealed class DelugeService : IDownloadService
public sealed class DelugeService : DownloadServiceBase
{
private readonly ILogger<DelugeService> _logger;
private readonly DelugeClient _client;
private readonly FilenameEvaluator _filenameEvaluator;
public DelugeService(
ILogger<DelugeService> logger,
IOptions<DelugeConfig> config,
IHttpClientFactory httpClientFactory,
FilenameEvaluator filenameEvaluator
)
IOptions<QueueCleanerConfig> queueCleanerConfig,
FilenameEvaluator filenameEvaluator,
Striker striker
) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
{
_logger = logger;
config.Value.Validate();
_client = new (config, httpClientFactory);
_filenameEvaluator = filenameEvaluator;
}
public async Task LoginAsync()
public override async Task LoginAsync()
{
await _client.LoginAsync();
}
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
{
hash = hash.ToLowerInvariant();
DelugeContents? contents = null;
TorrentStatus? status = await GetTorrentStatus(hash);
if (!await HasMinimalStatus(hash))
if (status?.Hash is null)
{
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
return false;
}
@@ -49,12 +53,7 @@ public sealed class DelugeService : IDownloadService
_logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
}
if (contents is null)
{
return false;
}
bool shouldRemove = true;
bool shouldRemove = contents?.Contents?.Count > 0;
ProcessFiles(contents.Contents, (_, file) =>
{
@@ -64,15 +63,18 @@ public sealed class DelugeService : IDownloadService
}
});
return shouldRemove;
return shouldRemove || IsItemStuckAndShouldRemove(status);
}
public async Task BlockUnwantedFilesAsync(string hash)
public override async Task BlockUnwantedFilesAsync(string hash)
{
hash = hash.ToLowerInvariant();
if (!await HasMinimalStatus(hash))
TorrentStatus? status = await GetTorrentStatus(hash);
if (status?.Hash is null)
{
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
return;
}
@@ -123,22 +125,29 @@ public sealed class DelugeService : IDownloadService
await _client.ChangeFilesPriority(hash, sortedPriorities);
}
private async Task<bool> HasMinimalStatus(string hash)
private bool IsItemStuckAndShouldRemove(TorrentStatus status)
{
DelugeMinimalStatus? status = await _client.SendRequest<DelugeMinimalStatus?>(
"web.get_torrent_status",
hash,
new[] { "hash" }
);
if (status?.Hash is null)
if (status.State is null || !status.State.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase))
{
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
return false;
}
return true;
if (status.Eta > 0)
{
return false;
}
return StrikeAndCheckLimit(status.Hash!, status.Name!);
}
private async Task<TorrentStatus?> GetTorrentStatus(string hash)
{
return await _client.SendRequest<TorrentStatus?>(
"web.get_torrent_status",
hash,
new[] { "hash", "state", "name", "eta" }
);
}
private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory> contents, Action<string, DelugeFileOrDirectory> processFile)
@@ -158,7 +167,7 @@ public sealed class DelugeService : IDownloadService
}
}
public void Dispose()
public override void Dispose()
{
}
}

View File

@@ -0,0 +1,42 @@
using Common.Configuration.QueueCleaner;
using Domain.Enums;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Infrastructure.Verticals.DownloadClient;
public abstract class DownloadServiceBase : IDownloadService
{
protected readonly ILogger<DownloadServiceBase> _logger;
protected readonly QueueCleanerConfig _queueCleanerConfig;
protected readonly FilenameEvaluator _filenameEvaluator;
protected readonly Striker _striker;
protected DownloadServiceBase(
ILogger<DownloadServiceBase> logger,
IOptions<QueueCleanerConfig> queueCleanerConfig,
FilenameEvaluator filenameEvaluator,
Striker striker
)
{
_logger = logger;
_queueCleanerConfig = queueCleanerConfig.Value;
_filenameEvaluator = filenameEvaluator;
_striker = striker;
}
public abstract void Dispose();
public abstract Task LoginAsync();
public abstract Task<bool> ShouldRemoveFromArrQueueAsync(string hash);
public abstract Task BlockUnwantedFilesAsync(string hash);
protected bool StrikeAndCheckLimit(string hash, string itemName)
{
return _striker.StrikeAndCheckLimit(hash, itemName, _queueCleanerConfig.StalledMaxStrikes, StrikeType.Stalled);
}
}

View File

@@ -1,7 +1,9 @@
using Common.Configuration;
using Common.Configuration.DownloadClient;
using Infrastructure.Verticals.DownloadClient.Deluge;
using Infrastructure.Verticals.DownloadClient.QBittorrent;
using Infrastructure.Verticals.DownloadClient.Transmission;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -9,57 +11,25 @@ namespace Infrastructure.Verticals.DownloadClient;
public sealed class DownloadServiceFactory
{
private readonly QBitConfig _qBitConfig;
private readonly DelugeConfig _delugeConfig;
private readonly TransmissionConfig _transmissionConfig;
private readonly IServiceProvider _serviceProvider;
private readonly Domain.Enums.DownloadClient _downloadClient;
public DownloadServiceFactory(
IOptions<QBitConfig> qBitConfig,
IOptions<DelugeConfig> delugeConfig,
IOptions<TransmissionConfig> transmissionConfig,
IServiceProvider serviceProvider)
public DownloadServiceFactory(IServiceProvider serviceProvider, IConfiguration configuration)
{
_qBitConfig = qBitConfig.Value;
_delugeConfig = delugeConfig.Value;
_transmissionConfig = transmissionConfig.Value;
_serviceProvider = serviceProvider;
_qBitConfig.Validate();
_delugeConfig.Validate();
_transmissionConfig.Validate();
int enabledCount = new[] { _qBitConfig.Enabled, _delugeConfig.Enabled, _transmissionConfig.Enabled }
.Count(enabled => enabled);
if (enabledCount > 1)
{
throw new Exception("only one download client can be enabled");
}
if (enabledCount == 0)
{
throw new Exception("no download client is enabled");
}
_downloadClient = (Domain.Enums.DownloadClient)Enum.Parse(
typeof(Domain.Enums.DownloadClient),
configuration[EnvironmentVariables.DownloadClient] ?? Domain.Enums.DownloadClient.QBittorrent.ToString(),
true
);
}
public IDownloadService CreateDownloadClient()
{
if (_qBitConfig.Enabled)
public IDownloadService CreateDownloadClient() =>
_downloadClient switch
{
return _serviceProvider.GetRequiredService<QBitService>();
}
if (_delugeConfig.Enabled)
{
return _serviceProvider.GetRequiredService<DelugeService>();
}
if (_transmissionConfig.Enabled)
{
return _serviceProvider.GetRequiredService<TransmissionService>();
}
throw new NotSupportedException();
}
Domain.Enums.DownloadClient.QBittorrent => _serviceProvider.GetRequiredService<QBitService>(),
Domain.Enums.DownloadClient.Deluge => _serviceProvider.GetRequiredService<DelugeService>(),
Domain.Enums.DownloadClient.Transmission => _serviceProvider.GetRequiredService<TransmissionService>(),
_ => throw new ArgumentOutOfRangeException()
};
}

View File

@@ -1,42 +1,49 @@
using Common.Configuration;
using Common.Configuration.DownloadClient;
using Common.Configuration.QueueCleaner;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using QBittorrent.Client;
namespace Infrastructure.Verticals.DownloadClient.QBittorrent;
public sealed class QBitService : IDownloadService
public sealed class QBitService : DownloadServiceBase
{
private readonly ILogger<QBitService> _logger;
private readonly QBitConfig _config;
private readonly QBittorrentClient _client;
private readonly FilenameEvaluator _filenameEvaluator;
public QBitService(
ILogger<QBitService> logger,
IOptions<QBitConfig> config,
FilenameEvaluator filenameEvaluator
)
IOptions<QueueCleanerConfig> queueCleanerConfig,
FilenameEvaluator filenameEvaluator,
Striker striker
) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
{
_logger = logger;
_config = config.Value;
_config.Validate();
_client = new(_config.Url);
_filenameEvaluator = filenameEvaluator;
}
public async Task LoginAsync()
public override async Task LoginAsync()
{
if (string.IsNullOrEmpty(_config.Username) && string.IsNullOrEmpty(_config.Password))
{
return;
}
await _client.LoginAsync(_config.Username, _config.Password);
}
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
{
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
.FirstOrDefault();
if (torrent is null)
{
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
return false;
}
@@ -48,21 +55,16 @@ public sealed class QBitService : IDownloadService
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
if (files is null)
{
return false;
}
// if all files are marked as skip
if (files.All(x => x.Priority is TorrentContentPriority.Skip))
if (files?.Count is > 0 && files.All(x => x.Priority is TorrentContentPriority.Skip))
{
return true;
}
return false;
return IsItemStuckAndShouldRemove(torrent);
}
public async Task BlockUnwantedFilesAsync(string hash)
public override async Task BlockUnwantedFilesAsync(string hash)
{
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
@@ -88,8 +90,20 @@ public sealed class QBitService : IDownloadService
}
}
public void Dispose()
public override void Dispose()
{
_client.Dispose();
}
private bool IsItemStuckAndShouldRemove(TorrentInfo torrent)
{
if (torrent.State is not TorrentState.StalledDownload and not TorrentState.FetchingMetadata
and not TorrentState.ForcedFetchingMetadata)
{
// ignore other states
return false;
}
return StrikeAndCheckLimit(torrent.Hash, torrent.Name);
}
}

View File

@@ -1,5 +1,7 @@
using Common.Configuration;
using Common.Configuration.DownloadClient;
using Common.Configuration.QueueCleaner;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Transmission.API.RPC;
@@ -8,64 +10,66 @@ using Transmission.API.RPC.Entity;
namespace Infrastructure.Verticals.DownloadClient.Transmission;
public sealed class TransmissionService : IDownloadService
public sealed class TransmissionService : DownloadServiceBase
{
private readonly ILogger<TransmissionService> _logger;
private readonly TransmissionConfig _config;
private readonly Client _client;
private readonly FilenameEvaluator _filenameEvaluator;
private TorrentInfo[]? _torrentsCache;
public TransmissionService(
ILogger<TransmissionService> logger,
IOptions<TransmissionConfig> config,
FilenameEvaluator filenameEvaluator
)
IOptions<QueueCleanerConfig> queueCleanerConfig,
FilenameEvaluator filenameEvaluator,
Striker striker
) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
{
_logger = logger;
_config = config.Value;
_config.Validate();
_client = new(
new Uri(_config.Url, "/transmission/rpc").ToString(),
login: _config.Username,
password: _config.Password
);
_filenameEvaluator = filenameEvaluator;
}
public async Task LoginAsync()
public override async Task LoginAsync()
{
await _client.GetSessionInformationAsync();
}
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
{
TorrentInfo? torrent = await GetTorrentAsync(hash);
if (torrent is null)
{
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
return false;
}
bool shouldRemove = torrent.FileStats?.Length > 0;
foreach (TransmissionTorrentFileStats? stats in torrent.FileStats ?? [])
{
if (!stats.Wanted.HasValue)
{
// if any files stats are missing, do not remove
return false;
shouldRemove = false;
}
if (stats.Wanted.HasValue && stats.Wanted.Value)
{
// if any files are wanted, do not remove
return false;
shouldRemove = false;
}
}
// remove if all files are unwanted
return true;
return shouldRemove || IsItemStuckAndShouldRemove(torrent);
}
public async Task BlockUnwantedFilesAsync(string hash)
public override async Task BlockUnwantedFilesAsync(string hash)
{
TorrentInfo? torrent = await GetTorrentAsync(hash);
@@ -105,10 +109,26 @@ public sealed class TransmissionService : IDownloadService
FilesUnwanted = unwantedFiles.ToArray(),
});
}
public void Dispose()
public override void Dispose()
{
}
private bool IsItemStuckAndShouldRemove(TorrentInfo torrent)
{
if (torrent.Status is not 4)
{
// not in downloading state
return false;
}
if (torrent.Eta > 0)
{
return false;
}
return StrikeAndCheckLimit(torrent.HashString!, torrent.Name!);
}
private async Task<TorrentInfo?> GetTorrentAsync(string hash)
{
@@ -117,7 +137,15 @@ public sealed class TransmissionService : IDownloadService
if (_torrentsCache is null || torrent is null)
{
string[] fields = [TorrentFields.FILES, TorrentFields.FILE_STATS, TorrentFields.HASH_STRING, TorrentFields.ID];
string[] fields = [
TorrentFields.FILES,
TorrentFields.FILE_STATS,
TorrentFields.HASH_STRING,
TorrentFields.ID,
TorrentFields.ETA,
TorrentFields.NAME,
TorrentFields.STATUS
];
// refresh cache
_torrentsCache = (await _client.TorrentGetAsync(fields))

View File

@@ -0,0 +1,57 @@
using Domain.Enums;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Infrastructure.Verticals.ItemStriker;
public class Striker
{
private readonly ILogger<Striker> _logger;
private readonly IMemoryCache _cache;
private readonly MemoryCacheEntryOptions _cacheOptions;
public Striker(ILogger<Striker> logger, IMemoryCache cache)
{
_logger = logger;
_cache = cache;
_cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromHours(2));
}
public bool StrikeAndCheckLimit(string hash, string itemName, ushort maxStrikes, StrikeType strikeType)
{
if (maxStrikes is 0)
{
return false;
}
string key = $"{strikeType.ToString()}_{hash}";
if (!_cache.TryGetValue(key, out int? strikeCount))
{
strikeCount = 1;
}
else
{
++strikeCount;
}
_logger.LogDebug("item on strike number {strike} | reason {reason} | {name}", strikeCount, strikeType.ToString(), itemName);
_cache.Set(key, strikeCount, _cacheOptions);
if (strikeCount < maxStrikes)
{
return false;
}
if (strikeCount > maxStrikes)
{
_logger.LogWarning("blocked item keeps coming back | {name}", itemName);
_logger.LogWarning("be sure to enable \"Reject Blocklisted Torrent Hashes While Grabbing\" on your indexers to reject blocked items");
}
_logger.LogInformation("removing item with max strikes | reason {reason} | {name}", strikeType.ToString(), itemName);
return true;
}
}

View File

@@ -0,0 +1,125 @@
using Common.Configuration.Arr;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.DownloadClient;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Infrastructure.Verticals.Jobs;
public abstract class GenericHandler : IDisposable
{
protected readonly ILogger<GenericHandler> _logger;
protected readonly SonarrConfig _sonarrConfig;
protected readonly RadarrConfig _radarrConfig;
protected readonly SonarrClient _sonarrClient;
protected readonly RadarrClient _radarrClient;
protected readonly ArrQueueIterator _arrArrQueueIterator;
protected readonly IDownloadService _downloadService;
protected GenericHandler(
ILogger<GenericHandler> logger,
SonarrConfig sonarrConfig,
RadarrConfig radarrConfig,
SonarrClient sonarrClient,
RadarrClient radarrClient,
ArrQueueIterator arrArrQueueIterator,
DownloadServiceFactory downloadServiceFactory
)
{
_logger = logger;
_sonarrConfig = sonarrConfig;
_radarrConfig = radarrConfig;
_sonarrClient = sonarrClient;
_radarrClient = radarrClient;
_arrArrQueueIterator = arrArrQueueIterator;
_downloadService = downloadServiceFactory.CreateDownloadClient();
}
public virtual async Task ExecuteAsync()
{
await _downloadService.LoginAsync();
await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr);
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr);
}
public virtual void Dispose()
{
_downloadService.Dispose();
}
protected abstract Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType);
private async Task ProcessArrConfigAsync(ArrConfig config, InstanceType instanceType)
{
if (!config.Enabled)
{
return;
}
foreach (ArrInstance arrInstance in config.Instances)
{
try
{
await ProcessInstanceAsync(arrInstance, instanceType);
}
catch (Exception exception)
{
_logger.LogError(exception, "failed to clean {type} instance | {url}", instanceType, arrInstance.Url);
}
}
}
protected ArrClient GetClient(InstanceType type) =>
type switch
{
InstanceType.Sonarr => _sonarrClient,
InstanceType.Radarr => _radarrClient,
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
};
protected ArrConfig GetConfig(InstanceType type) =>
type switch
{
InstanceType.Sonarr => _sonarrConfig,
InstanceType.Radarr => _radarrConfig,
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
};
protected SearchItem GetRecordSearchItem(InstanceType type, QueueRecord record, bool isPack = false)
{
return type switch
{
InstanceType.Sonarr when _sonarrConfig.SearchType is SonarrSearchType.Episode && !isPack => new SonarrSearchItem
{
Id = record.EpisodeId,
SeriesId = record.SeriesId,
SearchType = SonarrSearchType.Episode
},
InstanceType.Sonarr when _sonarrConfig.SearchType is SonarrSearchType.Episode && isPack => new SonarrSearchItem
{
Id = record.SeasonNumber,
SeriesId = record.SeriesId,
SearchType = SonarrSearchType.Season
},
InstanceType.Sonarr when _sonarrConfig.SearchType is SonarrSearchType.Season => new SonarrSearchItem
{
Id = record.SeasonNumber,
SeriesId = record.SeriesId,
SearchType = SonarrSearchType.Series
},
InstanceType.Sonarr when _sonarrConfig.SearchType is SonarrSearchType.Series => new SonarrSearchItem
{
Id = record.SeriesId,
},
InstanceType.Radarr => new SearchItem
{
Id = record.MovieId,
},
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
};
}
}

View File

@@ -0,0 +1,35 @@
using Quartz;
namespace Infrastructure.Verticals.Jobs;
public class JobChainingListener : IJobListener
{
private readonly string _nextJobName;
public JobChainingListener(string nextJobName)
{
_nextJobName = nextJobName;
}
public string Name => nameof(JobChainingListener);
public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken) => Task.CompletedTask;
public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken) => Task.CompletedTask;
public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(_nextJobName) || context.JobDetail.Key.Name == _nextJobName)
{
return;
}
IScheduler scheduler = context.Scheduler;
JobKey nextJobKey = new(_nextJobName);
if (await scheduler.CheckExists(nextJobKey, cancellationToken))
{
await scheduler.TriggerJob(nextJobKey, cancellationToken);
}
}
}

View File

@@ -1,23 +1,18 @@
using Common.Configuration;
using Domain.Arr.Queue;
using Common.Configuration.Arr;
using Common.Configuration.QueueCleaner;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.Jobs;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Infrastructure.Verticals.QueueCleaner;
public sealed class QueueCleaner : IDisposable
public sealed class QueueCleaner : GenericHandler
{
private readonly ILogger<QueueCleaner> _logger;
private readonly SonarrConfig _sonarrConfig;
private readonly RadarrConfig _radarrConfig;
private readonly SonarrClient _sonarrClient;
private readonly RadarrClient _radarrClient;
private readonly ArrQueueIterator _arrArrQueueIterator;
private readonly IDownloadService _downloadService;
public QueueCleaner(
ILogger<QueueCleaner> logger,
IOptions<SonarrConfig> sonarrConfig,
@@ -26,101 +21,53 @@ public sealed class QueueCleaner : IDisposable
RadarrClient radarrClient,
ArrQueueIterator arrArrQueueIterator,
DownloadServiceFactory downloadServiceFactory
)
) : base(logger, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
{
_logger = logger;
_sonarrConfig = sonarrConfig.Value;
_radarrConfig = radarrConfig.Value;
_sonarrClient = sonarrClient;
_radarrClient = radarrClient;
_arrArrQueueIterator = arrArrQueueIterator;
_downloadService = downloadServiceFactory.CreateDownloadClient();
}
public async Task ExecuteAsync()
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
{
await _downloadService.LoginAsync();
await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr);
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr);
// await _downloadClient.LogoutAsync();
}
private async Task ProcessArrConfigAsync(ArrConfig config, InstanceType instanceType)
{
if (!config.Enabled)
{
return;
}
foreach (ArrInstance arrInstance in config.Instances)
{
try
{
await ProcessInstanceAsync(arrInstance, instanceType);
}
catch (Exception exception)
{
_logger.LogError(exception, "failed to clean {type} instance | {url}", instanceType, arrInstance.Url);
}
}
}
private async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
{
HashSet<int> itemsToBeRefreshed = [];
HashSet<SearchItem> itemsToBeRefreshed = [];
ArrClient arrClient = GetClient(instanceType);
ArrConfig arrConfig = GetConfig(instanceType);
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
{
foreach (QueueRecord record in items)
var groups = items
.GroupBy(x => x.DownloadId)
.ToList();
foreach (var group in groups)
{
if (group.Any(x => !arrClient.IsRecordValid(x)))
{
continue;
}
QueueRecord record = group.First();
if (record.Protocol is not "torrent")
{
continue;
}
if (string.IsNullOrEmpty(record.DownloadId))
if (!arrClient.IsRecordValid(record))
{
_logger.LogDebug("skip | download id is null for {title}", record.Title);
continue;
}
if (!await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId))
if (!arrClient.ShouldRemoveFromQueue(record) && !await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId))
{
_logger.LogInformation("skip | {title}", record.Title);
continue;
}
itemsToBeRefreshed.Add(GetRecordId(instanceType, record));
itemsToBeRefreshed.Add(GetRecordSearchItem(instanceType, record, group.Count() > 1));
await arrClient.DeleteQueueItemAsync(instance, record);
}
});
await arrClient.RefreshItemsAsync(instance, itemsToBeRefreshed);
}
private ArrClient GetClient(InstanceType type) =>
type switch
{
InstanceType.Sonarr => _sonarrClient,
InstanceType.Radarr => _radarrClient,
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
};
private int GetRecordId(InstanceType type, QueueRecord record) =>
type switch
{
// TODO add episode id
InstanceType.Sonarr => record.SeriesId,
InstanceType.Radarr => record.MovieId,
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
};
public void Dispose()
{
_downloadService.Dispose();
await arrClient.RefreshItemsAsync(instance, arrConfig, itemsToBeRefreshed);
}
}

View File

@@ -1,97 +0,0 @@
{
"file": 1,
"format": 1
}{
"add_paused": false,
"allow_remote": false,
"auto_manage_prefer_seeds": false,
"auto_managed": true,
"cache_expiry": 60,
"cache_size": 512,
"copy_torrent_file": false,
"daemon_port": 58846,
"del_copy_torrent_file": false,
"dht": true,
"dont_count_slow_torrents": false,
"download_location": "/downloads",
"download_location_paths_list": [],
"enabled_plugins": [
"Label"
],
"enc_in_policy": 1,
"enc_level": 2,
"enc_out_policy": 1,
"geoip_db_location": "/usr/share/GeoIP/GeoIP.dat",
"ignore_limits_on_local_network": true,
"info_sent": 0.0,
"listen_interface": "",
"listen_ports": [
6882,
6882
],
"listen_random_port": null,
"listen_reuse_port": true,
"listen_use_sys_port": false,
"lsd": true,
"max_active_downloading": 3,
"max_active_limit": 8,
"max_active_seeding": 5,
"max_connections_global": 200,
"max_connections_per_second": 20,
"max_connections_per_torrent": -1,
"max_download_speed": -1.0,
"max_download_speed_per_torrent": -1,
"max_half_open_connections": 50,
"max_upload_slots_global": 4,
"max_upload_slots_per_torrent": -1,
"max_upload_speed": -1.0,
"max_upload_speed_per_torrent": -1,
"move_completed": false,
"move_completed_path": "/downloads",
"move_completed_paths_list": [],
"natpmp": true,
"new_release_check": true,
"outgoing_interface": "",
"outgoing_ports": [
0,
0
],
"path_chooser_accelerator_string": "Tab",
"path_chooser_auto_complete_enabled": true,
"path_chooser_max_popup_rows": 20,
"path_chooser_show_chooser_button_on_localhost": true,
"path_chooser_show_hidden_files": false,
"peer_tos": "0x00",
"plugins_location": "/config/plugins",
"pre_allocate_storage": false,
"prioritize_first_last_pieces": false,
"proxy": {
"anonymous_mode": false,
"force_proxy": false,
"hostname": "",
"password": "",
"port": 8080,
"proxy_hostnames": true,
"proxy_peer_connections": true,
"proxy_tracker_connections": true,
"type": 0,
"username": ""
},
"queue_new_to_top": false,
"random_outgoing_ports": true,
"random_port": false,
"rate_limit_ip_overhead": true,
"remove_seed_at_ratio": false,
"seed_time_limit": 180,
"seed_time_ratio_limit": 7.0,
"send_info": false,
"sequential_download": false,
"share_ratio_limit": 2.0,
"shared": false,
"stop_seed_at_ratio": false,
"stop_seed_ratio": 2.0,
"super_seeding": false,
"torrentfiles_location": "/config/torrents",
"upnp": true,
"utpex": true
}

View File

@@ -1,50 +0,0 @@
{
"file": 1,
"format": 1
}{
"labels": {
"radarr": {
"apply_max": false,
"apply_move_completed": false,
"apply_queue": false,
"auto_add": false,
"auto_add_trackers": [],
"is_auto_managed": false,
"max_connections": -1,
"max_download_speed": -1,
"max_upload_slots": -1,
"max_upload_speed": -1,
"move_completed": false,
"move_completed_path": "",
"prioritize_first_last": false,
"remove_at_ratio": false,
"stop_at_ratio": false,
"stop_ratio": 2.0
},
"tv-sonarr": {
"apply_max": false,
"apply_move_completed": false,
"apply_queue": false,
"auto_add": false,
"auto_add_trackers": [],
"is_auto_managed": false,
"max_connections": -1,
"max_download_speed": -1,
"max_upload_slots": -1,
"max_upload_speed": -1,
"move_completed": false,
"move_completed_path": "",
"prioritize_first_last": false,
"remove_at_ratio": false,
"stop_at_ratio": false,
"stop_ratio": 2.0
}
},
"torrent_labels": {
"59ab2bc053430fe53e06a93e2eadb7acb6a6bf2c": "tv-sonarr",
"5a31d5f1689f5f45fd85c275a37acd2c7b82fde1": "tv-sonarr",
"6c890ff85b5317d5df291c3c23a782774e10e6fe": "radarr",
"a4a1d1dd1db25763caa8f5e4d25ad72ef304094b": "radarr",
"b72541215214be2a1d96ef6b29ca1305f5e5e1f6": "tv-sonarr"
}
}

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -16,571 +16,6 @@
"pwd_sha1": "3ac8756d294abe4f6c9dfa084b7fc2c84ce32f68",
"session_timeout": 3600,
"sessions": {
"00390c773fafe30cb393f53a920b48ac353b58ca27ac9ed64a1cbc61d5026677": {
"expires": 1731936939.0,
"level": 10,
"login": "admin"
},
"0379df23e58eb57a0ec781168c5acb1527be9ce1dc48a6dec201905358dbedd8": {
"expires": 1731665164.0,
"level": 10,
"login": "admin"
},
"03d38494090a758cbe3ecc1e8a004986528297c7200e58b36649e197276c95e3": {
"expires": 1731718770.0,
"level": 10,
"login": "admin"
},
"03fe5879beed5c299cd18472b64d31c4c610cd413059d7582312b002bb0eef03": {
"expires": 1731689401.0,
"level": 10,
"login": "admin"
},
"05c8e71ff1e411beb45e278e786fbde8c893854e2906f3111dd48f943082eba5": {
"expires": 1731593711.0,
"level": 10,
"login": "admin"
},
"0cce3c4e10dcdebd921e19905c9ce3c162cefafadbf35b3c64a1932860af0e7d": {
"expires": 1731721234.0,
"level": 10,
"login": "admin"
},
"0d5bce647f6368877290f7be8a0f63f070039dd76027158278143b2ea6078a42": {
"expires": 1731665495.0,
"level": 10,
"login": "admin"
},
"0f95357a4b9994584b429a5facaad735bc1e0adb0f994b7fad82318f589de991": {
"expires": 1731718993.0,
"level": 10,
"login": "admin"
},
"15c8cf06252ea0039bc2569d4121378baa3287594f9148d4fb26e999966e5538": {
"expires": 1731714201.0,
"level": 10,
"login": "admin"
},
"15d5edb14093bb821dbef9080853e00f969860add39bd21e301172ae911713b1": {
"expires": 1731665102.0,
"level": 10,
"login": "admin"
},
"17f7a731a26bdde434e8f4edb6043c4699efa29b982ad1f5df26676747b400a7": {
"expires": 1731658344.0,
"level": 10,
"login": "admin"
},
"1800115899b60e88b29483c4656f9c56c58d38c008d96149bb70fb5e9d26a10c": {
"expires": 1731939230.0,
"level": 10,
"login": "admin"
},
"1970022246cbb41f07d1242920163980e93e4e96f11864ffcf047c8cb5cf9908": {
"expires": 1731706575.0,
"level": 10,
"login": "admin"
},
"19daad7642cf7f056083ff2868c9565ff8b2f6750eae91d5b235989089239bdc": {
"expires": 1731689407.0,
"level": 10,
"login": "admin"
},
"1b525ddd164c645ec47f1b2b58044cdf32f90800bc1973afb7b56a5814b813da": {
"expires": 1731711800.0,
"level": 10,
"login": "admin"
},
"1da54bf8d0c73023d11e1cb91586b088898ef5a37d146c108bfe2a9633499b63": {
"expires": 1731693609.0,
"level": 10,
"login": "admin"
},
"2095c759cb9b9ce96ab3bf3f07301e8dd71de75aeed2d4db957d2227adbc56f5": {
"expires": 1731664924.0,
"level": 10,
"login": "admin"
},
"2185646cfe4fa4f9892ea3df734b02b31ab7dfeb0f0868a6c730f04328b1a87a": {
"expires": 1731716848.0,
"level": 10,
"login": "admin"
},
"2270be625fa3bf61e919e8c495bc6c7868e907709e6b06c533727d0469df61c9": {
"expires": 1731714357.0,
"level": 10,
"login": "admin"
},
"2438a71a5850697dfb99ba24afa21f82a99ca32bf59a05f5dbd8c0f8bf645e4f": {
"expires": 1731691862.0,
"level": 10,
"login": "admin"
},
"24930581ae0aa9a0e0a520f3cccfb71f50308be43b15cb5b0fb3404a7d9a8a2f": {
"expires": 1731617366.0,
"level": 10,
"login": "admin"
},
"28920404a5f4638ebcdb4eb4addad4db19ee0bdd8505457cce7b6f81ba06b363": {
"expires": 1731712508.0,
"level": 10,
"login": "admin"
},
"29f26fc40c0be11ba12209d142e8a662ad3c5f58f4c9e2a4dcb9bf81a9eac0ef": {
"expires": 1731692292.0,
"level": 10,
"login": "admin"
},
"2b4e3dc65d727790957c28d570f474ef0ffacc98bf1372b11ef4c2eacfde585c": {
"expires": 1731711706.0,
"level": 10,
"login": "admin"
},
"2c1d2ff923df8718a46c89e575a2ebbcede10f9c585e5fe2ddb3a4a43ddabafa": {
"expires": 1731719810.0,
"level": 10,
"login": "admin"
},
"2c9235163d9dc1af694609caf5624465e872a9a9efaae9c5dd7de97190911970": {
"expires": 1731592559.0,
"level": 10,
"login": "admin"
},
"2ee0d1c4f504c080441ae4a8e61546077405ef9dce3ec291923d761b69f69586": {
"expires": 1731659695.0,
"level": 10,
"login": "admin"
},
"317679509ed59022afd20bd8a891cc759fadd7ca9c85c88ac0b05cf9b9ea1791": {
"expires": 1731716722.0,
"level": 10,
"login": "admin"
},
"31b14f8de2ba1de58011ecf8dfe7f8681ae4af543928f3903a9c080374a7fb08": {
"expires": 1731692998.0,
"level": 10,
"login": "admin"
},
"337641d938548a3261f67b1b1e295e8d09b248d2c7358a84a2914c803e2c9827": {
"expires": 1731719800.0,
"level": 10,
"login": "admin"
},
"33b864b8ab214816273040e0456297da90a7f2e5bf352368f906e64cc363ccdf": {
"expires": 1731659084.0,
"level": 10,
"login": "admin"
},
"36d3798ad875c70807a02eac3b7fd4279550cb9dfa6ccb530b66c74f0d577a52": {
"expires": 1731939210.0,
"level": 10,
"login": "admin"
},
"3b14137d9b57080d81e0a4532ad703dc91e2542be56415f58feca56231851eb0": {
"expires": 1731714081.0,
"level": 10,
"login": "admin"
},
"3b6c35a4cde25fa4a846a3df5d41562e798089db25eb21b9363cccdba2a3e093": {
"expires": 1731939483.0,
"level": 10,
"login": "admin"
},
"3d7bf91cb3e15eb82297ab54bae4cc6e06a42b54679e5f963a580fdf0d4bcf57": {
"expires": 1731719791.0,
"level": 10,
"login": "admin"
},
"3e7234797204e1672caf4b5b5ef450898f931ee48dcffae0b3b89e138434c036": {
"expires": 1731723178.0,
"level": 10,
"login": "admin"
},
"3e79e3bd1e01d10a6017e61b152151150deeb5185781a6325cd0aa4b9bfec47d": {
"expires": 1731719395.0,
"level": 10,
"login": "admin"
},
"3e9020e47c34087dc02ff6a5b396b7f338cc3249c9812e320490c33d9e7ce245": {
"expires": 1731719545.0,
"level": 10,
"login": "admin"
},
"44c3d9b212384742b7e0ac2a8c9a2bb48cf146a403ba01b6137051f54953a38a": {
"expires": 1731938866.0,
"level": 10,
"login": "admin"
},
"470c34122403b8dec692f9079e892ca92d8bf13cdd6c2814b6d829996e5f8b67": {
"expires": 1731664624.0,
"level": 10,
"login": "admin"
},
"4a47ca4a624fff02d9970d7e8a341ec08b8076cade949348795e45002c18556d": {
"expires": 1731718983.0,
"level": 10,
"login": "admin"
},
"4e88bdb753b76965909d1a2eabcc6ef5c12dab11cb0f32185506a19192f9cae2": {
"expires": 1731935979.0,
"level": 10,
"login": "admin"
},
"51ce01dfe69d8ae6afb019a05046154f4b51ef64569e3424e78a80957a11ba8c": {
"expires": 1731693589.0,
"level": 10,
"login": "admin"
},
"538a9fd86a56727c571da77b70353ac0fa5568442ea17d69a817a579d37679ac": {
"expires": 1731939264.0,
"level": 10,
"login": "admin"
},
"566fe0345702f6e2f30effef42ef664c46ac0e7f21aa3d2b414a1f290230fba6": {
"expires": 1731664385.0,
"level": 10,
"login": "admin"
},
"57df9cb450fd2dd9717b9ab03bbce3492603188c11f5e83af8d1bb38ab36ed01": {
"expires": 1731938872.0,
"level": 10,
"login": "admin"
},
"587362acb21285d814a9861a93b3f0e017ee9efb1bfe63343c13c09e7ea80f91": {
"expires": 1731716759.0,
"level": 10,
"login": "admin"
},
"58f7b011411dbad96eadc573eb164e1cdb6f96d52febbb8d4adb2cdda6ed80ef": {
"expires": 1731936289.0,
"level": 10,
"login": "admin"
},
"5cb4123b4b425f1d8a6accf4c02386feea7f20bf6171291f715c2cd99bfb02c0": {
"expires": 1731665482.0,
"level": 10,
"login": "admin"
},
"5d9819c229e5f79b767a88db9c78b24998b424a66e8a7ba0039553b7c54051ec": {
"expires": 1731617370.0,
"level": 10,
"login": "admin"
},
"5e6cb57d0f9d97aa3ed75fefd40c8060a085c04727886511f0e7db126b203d43": {
"expires": 1731721196.0,
"level": 10,
"login": "admin"
},
"5e74fdaca0c6c7e7afec714793677646ff89d00ae35908a7125d6cf50ea0702c": {
"expires": 1731603184.0,
"level": 10,
"login": "admin"
},
"5fbe1b057086c9f17b5a4d7d9fd9f41eab0305ea89b5b1de2ca633b0b38aab50": {
"expires": 1731937913.0,
"level": 10,
"login": "admin"
},
"644aae458bd0a092fe342d1f020d4b7eccff9cf6cfc0677fe0d9531754edaff0": {
"expires": 1731692249.0,
"level": 10,
"login": "admin"
},
"653493a22274078fc44d52acb337b56bcd4084de2f0a1b6b79be186550a30cb3": {
"expires": 1731591365.0,
"level": 10,
"login": "admin"
},
"66618b25addefa9f12946c5afb65e8d690c5a871fff3d03fb796751a9eee0d41": {
"expires": 1731591683.0,
"level": 10,
"login": "admin"
},
"684613e83cfde35d79917108a4091de4c585e1ff627eee0d904920759dc3ea53": {
"expires": 1731712313.0,
"level": 10,
"login": "admin"
},
"68ecec112a27957857622d4c0ba02824f5e03981db6f51fa01c0be8ac893c6f1": {
"expires": 1731591683.0,
"level": 10,
"login": "admin"
},
"69d6aa5b5eae433a357dc92fe12bbfb7fd29629425f0edbe55e0ac4e8df112aa": {
"expires": 1731718574.0,
"level": 10,
"login": "admin"
},
"6ade7fdf14f334646e2ad2f6b627146a69e0b896d0930a996b4f9df7bc4cf28e": {
"expires": 1731939300.0,
"level": 10,
"login": "admin"
},
"6c19cb32acd64f6c1543bf07dba3aee5ca5d3ba71f754e1e95acc3da5dc6aa27": {
"expires": 1731718629.0,
"level": 10,
"login": "admin"
},
"71c8d2cf33b22285f43c5855bdabd9ea25a18b177cfe28056c8bed41776d0ced": {
"expires": 1731664874.0,
"level": 10,
"login": "admin"
},
"745592d9be94482df01bc76010f58844175050e2bb7c0974de4e1f852a589554": {
"expires": 1731689116.0,
"level": 10,
"login": "admin"
},
"82ee9a0b4fde768d0580e85633012235fdf4683c0115ec121ba17282075483e7": {
"expires": 1731708818.0,
"level": 10,
"login": "admin"
},
"8346f9faa70bd614131a9115bcb33168ae9af221be0270e967c01e9c1c58129e": {
"expires": 1731693061.0,
"level": 10,
"login": "admin"
},
"861dbf07e0df2c29fedc8fcd5a346b4dcb1a0ece0f741befc3c72d3fadc82268": {
"expires": 1731722565.0,
"level": 10,
"login": "admin"
},
"870afa2ad6a0832fb19680b3bbf0bfd99de377c9cbaaff3cf6bf5a633fe541c3": {
"expires": 1731933969.0,
"level": 10,
"login": "admin"
},
"88be8381b68afe4c56b949c2588dec6b57f2dbe5ae20faacb06c37b7cbdc8a8b": {
"expires": 1731718780.0,
"level": 10,
"login": "admin"
},
"8a010b80566da20d356b77df700e2444292eea50a928eb7613bc874462436f36": {
"expires": 1731602951.0,
"level": 10,
"login": "admin"
},
"8be44833b5b32f8ed1eb2e6dba5ec7aa49fc4307dbd18d03859c96d227a9058b": {
"expires": 1731658179.0,
"level": 10,
"login": "admin"
},
"8d5c74abebafb8bfc72a3c33c17195d8e6a52c4505b1ccff9eda1a81f9a74ef7": {
"expires": 1731939423.0,
"level": 10,
"login": "admin"
},
"8e2e998fabc8429ef4ba385a4d4ed401fbe2508192b65dc72280cdfc086948e0": {
"expires": 1731692291.0,
"level": 10,
"login": "admin"
},
"8e8ed10c9cb8ced98d7736a81b68990fff019ca2b32dd9093209b132906b68c3": {
"expires": 1731692267.0,
"level": 10,
"login": "admin"
},
"8fbfaad4b9fc517f848b6f052be51b3a2cd494247078bf053460f8b80e53065a": {
"expires": 1731595963.0,
"level": 10,
"login": "admin"
},
"8fddab944e5a0d45750d5da5b78230387228e12f27ad040316394a4d6b166b5d": {
"expires": 1731939300.0,
"level": 10,
"login": "admin"
},
"9364816c6b2e739b647c43ad27b2d52593c2d59a5181aefcda12d04ea31d9fc6": {
"expires": 1731718765.0,
"level": 10,
"login": "admin"
},
"9a29ef6e50f415e5ccd36140eeab85a4409ae271470c1d51a32e0791c75ea588": {
"expires": 1731658513.0,
"level": 10,
"login": "admin"
},
"9ac87d5bf293ab9dc0bb5dcefe1e65eb7e166c4b3f5a162529683d5d866d7f9f": {
"expires": 1731937503.0,
"level": 10,
"login": "admin"
},
"9b266dafeef7c956bb4d9e987791975b64e7333de860a0d9173e211524cc8540": {
"expires": 1731591553.0,
"level": 10,
"login": "admin"
},
"9c03a8e39a5ff42c889c73ad9d4d5d84e76747322dfdf714ccb74a0d37923682": {
"expires": 1731719394.0,
"level": 10,
"login": "admin"
},
"9d1df8609ca6ada923c85b2721c0b3e606b92372478b5ca943cb52a7b8886951": {
"expires": 1731939230.0,
"level": 10,
"login": "admin"
},
"9f9de499b714f5c7dcb873f1141ad463a34e7c34eaa62cc988167505e4f5ac54": {
"expires": 1731939220.0,
"level": 10,
"login": "admin"
},
"a484b99afebac4ce74b6f52cad90448496fb80f2d8756e503776072739748a57": {
"expires": 1731938872.0,
"level": 10,
"login": "admin"
},
"a48e673783b5c56edfd5b5bc73b1f9c527dc1ca6da3aef82f61692b40095b39c": {
"expires": 1731939445.0,
"level": 10,
"login": "admin"
},
"a7ba418572d761f89d13fd7a11d682a3c821406ccd29f3e6d683966d0fb3d3ab": {
"expires": 1731720041.0,
"level": 10,
"login": "admin"
},
"a7cc53afae41ab5e800893c7271acf272f34b604f7e1ace8c3f0232606d01e2b": {
"expires": 1731719810.0,
"level": 10,
"login": "admin"
},
"a8c19794aaac3afd733588d42d7ffddc5f8de336d4bd8aa5cf9c1ff36cc9d590": {
"expires": 1731933957.0,
"level": 10,
"login": "admin"
},
"aa301c95f4890c38baa814503e940f28143304ad55dbfaea2db8aaec90169031": {
"expires": 1731599734.0,
"level": 10,
"login": "admin"
},
"ac98337f159cdc03b4865b80c78d53ab39502291fa19cc0b60233209e1d92bb7": {
"expires": 1731689363.0,
"level": 10,
"login": "admin"
},
"adf9d353a020d93e54b32b14c28525a7e6f33fd735144d7b778a4e517192b7c5": {
"expires": 1731691941.0,
"level": 10,
"login": "admin"
},
"ae14e4adfe0504f8dfb3fcb575606e67f57e950cdf88f00ca76e0af215c2b413": {
"expires": 1731935980.0,
"level": 10,
"login": "admin"
},
"ae48fe0ecea0704704bc6327b6bf3258de377b08fa4d76b4f69b8470852960c6": {
"expires": 1731591544.0,
"level": 10,
"login": "admin"
},
"ae967211fa06ac26d96e3207e67c136efc6367acc92d608e52e1f204b5bd3da4": {
"expires": 1731664535.0,
"level": 10,
"login": "admin"
},
"b0f55c6cad8bc2230754fd1bbeebfedd31ba244eac96e8d02be6d6a33b542b4f": {
"expires": 1731592529.0,
"level": 10,
"login": "admin"
},
"b689520f2e3ccf2164da5dba4bac111d4cbc6d3bdcf44361127baa0068623cd0": {
"expires": 1731717015.0,
"level": 10,
"login": "admin"
},
"bb92c09264296d8c12fac5d2abed2224b25a85d5a46d87f4a4351da76d00566e": {
"expires": 1731720040.0,
"level": 10,
"login": "admin"
},
"bbba0f20f9c1639ebfcaedb54a87d4b968bc953f72b06105f6d995f5eee9bff7": {
"expires": 1731591455.0,
"level": 10,
"login": "admin"
},
"c1a50b58a364f294755666d0e748ba2a06fbe55de758453180920774525214c8": {
"expires": 1731719336.0,
"level": 10,
"login": "admin"
},
"c6af2f6a6627cbbf18696f1b8b904346726800d05b137d251ee28b699fbd858c": {
"expires": 1731665294.0,
"level": 10,
"login": "admin"
},
"c86527a0aae330a71fc324a233ad876d420a54d387794374d126e7d6a0f19f92": {
"expires": 1731692356.0,
"level": 10,
"login": "admin"
},
"cd65741faf869e193c4e2a51e9454cf88598f58987b2ce559ef3d6bfa98e7605": {
"expires": 1731591572.0,
"level": 10,
"login": "admin"
},
"cdb4f2d14ab7de91b8be1261be40bf59f2bfa0e5e6327166ffa08cfa74eb357a": {
"expires": 1731692237.0,
"level": 10,
"login": "admin"
},
"d0f2c477a500bffcdcfbc56907df1b322200d9f7a801285bbe5440e5bca5e8c4": {
"expires": 1731711859.0,
"level": 10,
"login": "admin"
},
"d3a9186f1c2c81d8085d86c09ddd4fc5e205f61f25f3cabbc1ee379e52304c77": {
"expires": 1731718780.0,
"level": 10,
"login": "admin"
},
"d74ba4649e1a5a098d42c2f62472c94d4484f43b0979cbbd48b464ac5f20e49b": {
"expires": 1731716625.0,
"level": 10,
"login": "admin"
},
"d769dedd53059553cb54961641c0cbdf818533db06764fbcd5557a7200247c28": {
"expires": 1731718992.0,
"level": 10,
"login": "admin"
},
"d95d211a839a71c0bb00d2504f18deb81af2d0ab7183478543a5d28654a37197": {
"expires": 1731658629.0,
"level": 10,
"login": "admin"
},
"da6f45956cf8b0b4a5552166dfe1372437b25179e0d24bfbeaba745d28febb53": {
"expires": 1731937514.0,
"level": 10,
"login": "admin"
},
"df84bacd66f8dbf2d1f150d839029e11b3ce56c0536183fd33656685bd446c44": {
"expires": 1731719340.0,
"level": 10,
"login": "admin"
},
"e8be12b35aa13a67bc7d3332d373b4117493319625c1ed87e3335ab8d09b9054": {
"expires": 1731686353.0,
"level": 10,
"login": "admin"
},
"f04dce449eb7dc69f7973906564c4aa224ff20595a3f2a3bfedbcd23ffaa9117": {
"expires": 1731658418.0,
"level": 10,
"login": "admin"
},
"f4472d087a796ef1f431c44d4f6e9d46ecfe88acce21368a7d85fc31e1efc1e9": {
"expires": 1731711611.0,
"level": 10,
"login": "admin"
},
"f62a451ea2933e56ff9dae2299e374477adb54d8c8285ea59d2ab15b9a80bd13": {
"expires": 1731712500.0,
"level": 10,
"login": "admin"
}
},
"show_session_speed": false,
"show_sidebar": true,

View File

@@ -1,480 +0,0 @@
{
"file": 2,
"format": 1
}{
"base": "/",
"cert": "ssl/daemon.cert",
"default_daemon": "",
"enabled_plugins": [],
"first_login": false,
"https": false,
"interface": "0.0.0.0",
"language": "",
"pkey": "ssl/daemon.pkey",
"port": 8112,
"pwd_salt": "2bc0ed67acc6876dda1a1632594090478fdeab60",
"pwd_sha1": "3ac8756d294abe4f6c9dfa084b7fc2c84ce32f68",
"session_timeout": 3600,
"sessions": {
"0379df23e58eb57a0ec781168c5acb1527be9ce1dc48a6dec201905358dbedd8": {
"expires": 1731665164.0,
"level": 10,
"login": "admin"
},
"03d38494090a758cbe3ecc1e8a004986528297c7200e58b36649e197276c95e3": {
"expires": 1731718770.0,
"level": 10,
"login": "admin"
},
"03fe5879beed5c299cd18472b64d31c4c610cd413059d7582312b002bb0eef03": {
"expires": 1731689401.0,
"level": 10,
"login": "admin"
},
"05c8e71ff1e411beb45e278e786fbde8c893854e2906f3111dd48f943082eba5": {
"expires": 1731593711.0,
"level": 10,
"login": "admin"
},
"0cce3c4e10dcdebd921e19905c9ce3c162cefafadbf35b3c64a1932860af0e7d": {
"expires": 1731721234.0,
"level": 10,
"login": "admin"
},
"0d5bce647f6368877290f7be8a0f63f070039dd76027158278143b2ea6078a42": {
"expires": 1731665495.0,
"level": 10,
"login": "admin"
},
"0f95357a4b9994584b429a5facaad735bc1e0adb0f994b7fad82318f589de991": {
"expires": 1731718993.0,
"level": 10,
"login": "admin"
},
"15c8cf06252ea0039bc2569d4121378baa3287594f9148d4fb26e999966e5538": {
"expires": 1731714201.0,
"level": 10,
"login": "admin"
},
"15d5edb14093bb821dbef9080853e00f969860add39bd21e301172ae911713b1": {
"expires": 1731665102.0,
"level": 10,
"login": "admin"
},
"17f7a731a26bdde434e8f4edb6043c4699efa29b982ad1f5df26676747b400a7": {
"expires": 1731658344.0,
"level": 10,
"login": "admin"
},
"1970022246cbb41f07d1242920163980e93e4e96f11864ffcf047c8cb5cf9908": {
"expires": 1731706575.0,
"level": 10,
"login": "admin"
},
"19daad7642cf7f056083ff2868c9565ff8b2f6750eae91d5b235989089239bdc": {
"expires": 1731689407.0,
"level": 10,
"login": "admin"
},
"1b525ddd164c645ec47f1b2b58044cdf32f90800bc1973afb7b56a5814b813da": {
"expires": 1731711800.0,
"level": 10,
"login": "admin"
},
"1da54bf8d0c73023d11e1cb91586b088898ef5a37d146c108bfe2a9633499b63": {
"expires": 1731693609.0,
"level": 10,
"login": "admin"
},
"2095c759cb9b9ce96ab3bf3f07301e8dd71de75aeed2d4db957d2227adbc56f5": {
"expires": 1731664924.0,
"level": 10,
"login": "admin"
},
"2185646cfe4fa4f9892ea3df734b02b31ab7dfeb0f0868a6c730f04328b1a87a": {
"expires": 1731716848.0,
"level": 10,
"login": "admin"
},
"2270be625fa3bf61e919e8c495bc6c7868e907709e6b06c533727d0469df61c9": {
"expires": 1731714357.0,
"level": 10,
"login": "admin"
},
"2438a71a5850697dfb99ba24afa21f82a99ca32bf59a05f5dbd8c0f8bf645e4f": {
"expires": 1731691862.0,
"level": 10,
"login": "admin"
},
"24930581ae0aa9a0e0a520f3cccfb71f50308be43b15cb5b0fb3404a7d9a8a2f": {
"expires": 1731617366.0,
"level": 10,
"login": "admin"
},
"28920404a5f4638ebcdb4eb4addad4db19ee0bdd8505457cce7b6f81ba06b363": {
"expires": 1731712508.0,
"level": 10,
"login": "admin"
},
"29f26fc40c0be11ba12209d142e8a662ad3c5f58f4c9e2a4dcb9bf81a9eac0ef": {
"expires": 1731692292.0,
"level": 10,
"login": "admin"
},
"2b4e3dc65d727790957c28d570f474ef0ffacc98bf1372b11ef4c2eacfde585c": {
"expires": 1731711706.0,
"level": 10,
"login": "admin"
},
"2c1d2ff923df8718a46c89e575a2ebbcede10f9c585e5fe2ddb3a4a43ddabafa": {
"expires": 1731719810.0,
"level": 10,
"login": "admin"
},
"2c9235163d9dc1af694609caf5624465e872a9a9efaae9c5dd7de97190911970": {
"expires": 1731592559.0,
"level": 10,
"login": "admin"
},
"2ee0d1c4f504c080441ae4a8e61546077405ef9dce3ec291923d761b69f69586": {
"expires": 1731659695.0,
"level": 10,
"login": "admin"
},
"317679509ed59022afd20bd8a891cc759fadd7ca9c85c88ac0b05cf9b9ea1791": {
"expires": 1731716722.0,
"level": 10,
"login": "admin"
},
"31b14f8de2ba1de58011ecf8dfe7f8681ae4af543928f3903a9c080374a7fb08": {
"expires": 1731692998.0,
"level": 10,
"login": "admin"
},
"337641d938548a3261f67b1b1e295e8d09b248d2c7358a84a2914c803e2c9827": {
"expires": 1731719800.0,
"level": 10,
"login": "admin"
},
"33b864b8ab214816273040e0456297da90a7f2e5bf352368f906e64cc363ccdf": {
"expires": 1731659084.0,
"level": 10,
"login": "admin"
},
"3b14137d9b57080d81e0a4532ad703dc91e2542be56415f58feca56231851eb0": {
"expires": 1731714081.0,
"level": 10,
"login": "admin"
},
"3d7bf91cb3e15eb82297ab54bae4cc6e06a42b54679e5f963a580fdf0d4bcf57": {
"expires": 1731719791.0,
"level": 10,
"login": "admin"
},
"3e7234797204e1672caf4b5b5ef450898f931ee48dcffae0b3b89e138434c036": {
"expires": 1731723178.0,
"level": 10,
"login": "admin"
},
"3e79e3bd1e01d10a6017e61b152151150deeb5185781a6325cd0aa4b9bfec47d": {
"expires": 1731719395.0,
"level": 10,
"login": "admin"
},
"3e9020e47c34087dc02ff6a5b396b7f338cc3249c9812e320490c33d9e7ce245": {
"expires": 1731719545.0,
"level": 10,
"login": "admin"
},
"470c34122403b8dec692f9079e892ca92d8bf13cdd6c2814b6d829996e5f8b67": {
"expires": 1731664624.0,
"level": 10,
"login": "admin"
},
"4a47ca4a624fff02d9970d7e8a341ec08b8076cade949348795e45002c18556d": {
"expires": 1731718983.0,
"level": 10,
"login": "admin"
},
"51ce01dfe69d8ae6afb019a05046154f4b51ef64569e3424e78a80957a11ba8c": {
"expires": 1731693589.0,
"level": 10,
"login": "admin"
},
"566fe0345702f6e2f30effef42ef664c46ac0e7f21aa3d2b414a1f290230fba6": {
"expires": 1731664385.0,
"level": 10,
"login": "admin"
},
"587362acb21285d814a9861a93b3f0e017ee9efb1bfe63343c13c09e7ea80f91": {
"expires": 1731716759.0,
"level": 10,
"login": "admin"
},
"5cb4123b4b425f1d8a6accf4c02386feea7f20bf6171291f715c2cd99bfb02c0": {
"expires": 1731665482.0,
"level": 10,
"login": "admin"
},
"5d9819c229e5f79b767a88db9c78b24998b424a66e8a7ba0039553b7c54051ec": {
"expires": 1731617370.0,
"level": 10,
"login": "admin"
},
"5e6cb57d0f9d97aa3ed75fefd40c8060a085c04727886511f0e7db126b203d43": {
"expires": 1731721196.0,
"level": 10,
"login": "admin"
},
"5e74fdaca0c6c7e7afec714793677646ff89d00ae35908a7125d6cf50ea0702c": {
"expires": 1731603184.0,
"level": 10,
"login": "admin"
},
"644aae458bd0a092fe342d1f020d4b7eccff9cf6cfc0677fe0d9531754edaff0": {
"expires": 1731692249.0,
"level": 10,
"login": "admin"
},
"653493a22274078fc44d52acb337b56bcd4084de2f0a1b6b79be186550a30cb3": {
"expires": 1731591365.0,
"level": 10,
"login": "admin"
},
"66618b25addefa9f12946c5afb65e8d690c5a871fff3d03fb796751a9eee0d41": {
"expires": 1731591683.0,
"level": 10,
"login": "admin"
},
"684613e83cfde35d79917108a4091de4c585e1ff627eee0d904920759dc3ea53": {
"expires": 1731712313.0,
"level": 10,
"login": "admin"
},
"68ecec112a27957857622d4c0ba02824f5e03981db6f51fa01c0be8ac893c6f1": {
"expires": 1731591683.0,
"level": 10,
"login": "admin"
},
"69d6aa5b5eae433a357dc92fe12bbfb7fd29629425f0edbe55e0ac4e8df112aa": {
"expires": 1731718574.0,
"level": 10,
"login": "admin"
},
"6c19cb32acd64f6c1543bf07dba3aee5ca5d3ba71f754e1e95acc3da5dc6aa27": {
"expires": 1731718629.0,
"level": 10,
"login": "admin"
},
"71c8d2cf33b22285f43c5855bdabd9ea25a18b177cfe28056c8bed41776d0ced": {
"expires": 1731664874.0,
"level": 10,
"login": "admin"
},
"745592d9be94482df01bc76010f58844175050e2bb7c0974de4e1f852a589554": {
"expires": 1731689116.0,
"level": 10,
"login": "admin"
},
"82ee9a0b4fde768d0580e85633012235fdf4683c0115ec121ba17282075483e7": {
"expires": 1731708818.0,
"level": 10,
"login": "admin"
},
"8346f9faa70bd614131a9115bcb33168ae9af221be0270e967c01e9c1c58129e": {
"expires": 1731693061.0,
"level": 10,
"login": "admin"
},
"861dbf07e0df2c29fedc8fcd5a346b4dcb1a0ece0f741befc3c72d3fadc82268": {
"expires": 1731722565.0,
"level": 10,
"login": "admin"
},
"88be8381b68afe4c56b949c2588dec6b57f2dbe5ae20faacb06c37b7cbdc8a8b": {
"expires": 1731718780.0,
"level": 10,
"login": "admin"
},
"8a010b80566da20d356b77df700e2444292eea50a928eb7613bc874462436f36": {
"expires": 1731602951.0,
"level": 10,
"login": "admin"
},
"8be44833b5b32f8ed1eb2e6dba5ec7aa49fc4307dbd18d03859c96d227a9058b": {
"expires": 1731658179.0,
"level": 10,
"login": "admin"
},
"8e2e998fabc8429ef4ba385a4d4ed401fbe2508192b65dc72280cdfc086948e0": {
"expires": 1731692291.0,
"level": 10,
"login": "admin"
},
"8e8ed10c9cb8ced98d7736a81b68990fff019ca2b32dd9093209b132906b68c3": {
"expires": 1731692267.0,
"level": 10,
"login": "admin"
},
"8fbfaad4b9fc517f848b6f052be51b3a2cd494247078bf053460f8b80e53065a": {
"expires": 1731595963.0,
"level": 10,
"login": "admin"
},
"9364816c6b2e739b647c43ad27b2d52593c2d59a5181aefcda12d04ea31d9fc6": {
"expires": 1731718765.0,
"level": 10,
"login": "admin"
},
"9a29ef6e50f415e5ccd36140eeab85a4409ae271470c1d51a32e0791c75ea588": {
"expires": 1731658513.0,
"level": 10,
"login": "admin"
},
"9b266dafeef7c956bb4d9e987791975b64e7333de860a0d9173e211524cc8540": {
"expires": 1731591553.0,
"level": 10,
"login": "admin"
},
"9c03a8e39a5ff42c889c73ad9d4d5d84e76747322dfdf714ccb74a0d37923682": {
"expires": 1731719394.0,
"level": 10,
"login": "admin"
},
"a7ba418572d761f89d13fd7a11d682a3c821406ccd29f3e6d683966d0fb3d3ab": {
"expires": 1731720041.0,
"level": 10,
"login": "admin"
},
"a7cc53afae41ab5e800893c7271acf272f34b604f7e1ace8c3f0232606d01e2b": {
"expires": 1731719810.0,
"level": 10,
"login": "admin"
},
"aa301c95f4890c38baa814503e940f28143304ad55dbfaea2db8aaec90169031": {
"expires": 1731599734.0,
"level": 10,
"login": "admin"
},
"ac98337f159cdc03b4865b80c78d53ab39502291fa19cc0b60233209e1d92bb7": {
"expires": 1731689363.0,
"level": 10,
"login": "admin"
},
"adf9d353a020d93e54b32b14c28525a7e6f33fd735144d7b778a4e517192b7c5": {
"expires": 1731691941.0,
"level": 10,
"login": "admin"
},
"ae48fe0ecea0704704bc6327b6bf3258de377b08fa4d76b4f69b8470852960c6": {
"expires": 1731591544.0,
"level": 10,
"login": "admin"
},
"ae967211fa06ac26d96e3207e67c136efc6367acc92d608e52e1f204b5bd3da4": {
"expires": 1731664535.0,
"level": 10,
"login": "admin"
},
"b0f55c6cad8bc2230754fd1bbeebfedd31ba244eac96e8d02be6d6a33b542b4f": {
"expires": 1731592529.0,
"level": 10,
"login": "admin"
},
"b689520f2e3ccf2164da5dba4bac111d4cbc6d3bdcf44361127baa0068623cd0": {
"expires": 1731717015.0,
"level": 10,
"login": "admin"
},
"bb92c09264296d8c12fac5d2abed2224b25a85d5a46d87f4a4351da76d00566e": {
"expires": 1731720040.0,
"level": 10,
"login": "admin"
},
"bbba0f20f9c1639ebfcaedb54a87d4b968bc953f72b06105f6d995f5eee9bff7": {
"expires": 1731591455.0,
"level": 10,
"login": "admin"
},
"c1a50b58a364f294755666d0e748ba2a06fbe55de758453180920774525214c8": {
"expires": 1731719336.0,
"level": 10,
"login": "admin"
},
"c6af2f6a6627cbbf18696f1b8b904346726800d05b137d251ee28b699fbd858c": {
"expires": 1731665294.0,
"level": 10,
"login": "admin"
},
"c86527a0aae330a71fc324a233ad876d420a54d387794374d126e7d6a0f19f92": {
"expires": 1731692356.0,
"level": 10,
"login": "admin"
},
"cd65741faf869e193c4e2a51e9454cf88598f58987b2ce559ef3d6bfa98e7605": {
"expires": 1731591572.0,
"level": 10,
"login": "admin"
},
"cdb4f2d14ab7de91b8be1261be40bf59f2bfa0e5e6327166ffa08cfa74eb357a": {
"expires": 1731692237.0,
"level": 10,
"login": "admin"
},
"d0f2c477a500bffcdcfbc56907df1b322200d9f7a801285bbe5440e5bca5e8c4": {
"expires": 1731711859.0,
"level": 10,
"login": "admin"
},
"d3a9186f1c2c81d8085d86c09ddd4fc5e205f61f25f3cabbc1ee379e52304c77": {
"expires": 1731718780.0,
"level": 10,
"login": "admin"
},
"d74ba4649e1a5a098d42c2f62472c94d4484f43b0979cbbd48b464ac5f20e49b": {
"expires": 1731716625.0,
"level": 10,
"login": "admin"
},
"d769dedd53059553cb54961641c0cbdf818533db06764fbcd5557a7200247c28": {
"expires": 1731718992.0,
"level": 10,
"login": "admin"
},
"d95d211a839a71c0bb00d2504f18deb81af2d0ab7183478543a5d28654a37197": {
"expires": 1731658629.0,
"level": 10,
"login": "admin"
},
"df84bacd66f8dbf2d1f150d839029e11b3ce56c0536183fd33656685bd446c44": {
"expires": 1731719340.0,
"level": 10,
"login": "admin"
},
"e8be12b35aa13a67bc7d3332d373b4117493319625c1ed87e3335ab8d09b9054": {
"expires": 1731686353.0,
"level": 10,
"login": "admin"
},
"f04dce449eb7dc69f7973906564c4aa224ff20595a3f2a3bfedbcd23ffaa9117": {
"expires": 1731658418.0,
"level": 10,
"login": "admin"
},
"f4472d087a796ef1f431c44d4f6e9d46ecfe88acce21368a7d85fc31e1efc1e9": {
"expires": 1731711611.0,
"level": 10,
"login": "admin"
},
"f62a451ea2933e56ff9dae2299e374477adb54d8c8285ea59d2ab15b9a80bd13": {
"expires": 1731712500.0,
"level": 10,
"login": "admin"
}
},
"show_session_speed": false,
"show_sidebar": true,
"sidebar_multiple_filters": true,
"sidebar_show_zero": false,
"theme": "gray"
}

View File

@@ -1,7 +1,7 @@
<rss version="2.0">
<channel>
<title>Test feed</title>
<link>http://nginx/custom/radarr_bad_single.xml</link>
<link>http://nginx/custom/radarr.xml</link>
<description>
Test
</description>
@@ -11,6 +11,17 @@
<lastBuildDate>Tue, 5 Nov 2024 22:02:13 -0400</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<ttl>30</ttl>
<item>
<title>Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB</title>
<description>Test</description>
<size>4138858110</size>
<link>http://nginx/custom/radarr_bad_nested.torrent</link>
<guid isPermaLink="false">
174674a88c8927f6f9057ac3f81efde384ed216cade43564ec450f2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
<item>
<title>The.Wild.Robot.2024.2160p.AMZN.WEB-DL.DDP5.1.Atmos.H.265-FLUX</title>
<description>Test</description>

View File

@@ -1,25 +0,0 @@
<rss version="2.0">
<channel>
<title>Test feed</title>
<link>http://nginx/custom/radarr_bad_nested.xml</link>
<description>
Test
</description>
<language>en-CA</language>
<copyright> Test </copyright>
<pubDate>Tue, 5 Nov 2024 22:02:13 -0400</pubDate>
<lastBuildDate>Tue, 5 Nov 2024 22:02:13 -0400</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<ttl>30</ttl>
<item>
<title>Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB</title>
<description>Test</description>
<size>4138858110</size>
<link>http://nginx/custom/radarr_bad_nested.torrent</link>
<guid isPermaLink="false">
174674a88c8947f6f9057ac3f81efde384ed216cade43564ec450f2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,91 @@
<rss version="2.0">
<channel>
<title>Test feed</title>
<link>http://nginx/custom/sonarr.xml</link>
<description>
Test
</description>
<language>en-CA</language>
<copyright> Test </copyright>
<pubDate>Tue, 5 Nov 2024 22:02:13 -0400</pubDate>
<lastBuildDate>Tue, 5 Nov 2024 22:02:13 -0400</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<ttl>30</ttl>
<item>
<title>Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos.H.265-VARYG</title>
<description>Test</description>
<size>4138858110</size>
<link>http://nginx/custom/sonarr_bad_nested.torrent</link>
<guid isPermaLink="false">
174674a88c8947f6f9057a23f81efde384ed216cade43564ec450f2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
<item>
<title>Agatha.All.Along.S01E02.Circle.Sewn.With.Fate.Unlock.Thy.Hidden.Gate.2160p.DSNP.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX</title>
<description>Test</description>
<size>4138858110</size>
<link>http://nginx/custom/sonarr_bad_single.torrent</link>
<guid isPermaLink="false">
174674a88c8947f689057ac3f81efde384ed216cade43564ec450f2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
<item>
<title>Top.Gear.S23E01.720p.x265.HDTV.HEVC.-.YSTEAM</title>
<description>Test</description>
<size>4138858110</size>
<link>magnet:?xt=urn:btih:cf82cf859b110af0ad3d94b846e006828417b193&amp;dn=TPG.2301.720p.x265.yourserie.com.mkv</link>
<guid isPermaLink="false">
174674a88c8947f6f5057ac3f81efde384ed216cade43564ec450f2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
<item>
<title>Top.Gear.S23E03.720p.x265.HDTV.HEVC.-.YSTEAM</title>
<description>Test</description>
<size>4138858110</size>
<link>magnet:?xt=urn:btih:cf92cf859b110af0ad3d94b846e006828417b193&amp;dn=TPG.2303.720p.x265.yourserie.com.mkv</link>
<guid isPermaLink="false">
174674a88c8947f6f5057ac3f81efde384ed216c2de43564ec450f2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
<item>
<title>Top.Gear.S23E01.720p.x265.HDTV.HEVC.-.YSTEAM</title>
<description>Test</description>
<size>4138858110</size>
<link>http://nginx/custom/sonarr_bad_stuck_stalled.torrent</link>
<guid isPermaLink="false">
174674a88c8947f6f9057ac3f81efde384ed216cade43564ec450f2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
<item>
<title>Top.Gear.S23E02.720p.x265.HDTV.HEVC.-.YSTEAM</title>
<description>Test</description>
<size>4138858110</size>
<link>http://nginx/custom/sonarr_bad_nested_top.torrent</link>
<guid isPermaLink="false">
174674a88c8947f6f9057ac3f82efde384ed216cade43564ec450f2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
<item>
<title>Sherlock.S01.1080p.BluRay.DD5.1.x264-DON</title>
<description>Test</description>
<size>4138858110</size>
<link>http://nginx/custom/sonarr_bad_pack.torrent</link>
<guid isPermaLink="false">
174674a88c8947f6f9057ac3f82efde384ed216cade43564ec45gf2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
</channel>
</rss>

View File

@@ -1,25 +0,0 @@
<rss version="2.0">
<channel>
<title>Test feed</title>
<link>http://nginx/custom/sonarr_bad_nested.xml</link>
<description>
Test
</description>
<language>en-CA</language>
<copyright> Test </copyright>
<pubDate>Tue, 5 Nov 2024 22:02:13 -0400</pubDate>
<lastBuildDate>Tue, 5 Nov 2024 22:02:13 -0400</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<ttl>30</ttl>
<item>
<title>Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos.H.265-VARYG</title>
<description>Test</description>
<size>4138858110</size>
<link>http://nginx/custom/sonarr_bad_nested.torrent</link>
<guid isPermaLink="false">
174674a88c8947f6f9057ac3f81efde384ed216cade43564ec450f2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
</channel>
</rss>

View File

@@ -0,0 +1 @@
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1732896923e4:infod5:filesld6:lengthi2604e4:pathl4:Dir15:Dir1111:test11.zipxeed6:lengthi2604e4:pathl4:Dir110:sample.txteed6:lengthi2604e4:pathl4:Dir210:test2.zipxeed6:lengthi2604e4:pathl9:test.zipxeed6:lengthi2604e4:pathl49:Top.Gear.S23E02.720p.x265.HDTV.HEVC.-.YSTEAM.zipxeee4:name44:Top.Gear.S23E02.720p.x265.HDTV.HEVC.-.YSTEAM12:piece lengthi262144e6:pieces20:w<77><02>̳R<CCB3><52>'6F<36>o<EFBFBD>}<7D><>ee

View File

@@ -0,0 +1 @@
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1734129464e4:infod5:filesld6:lengthi7e4:pathl47:Sherlock.S01E01.1080p.BluRay.DD5.1.x264-DON.mkveed6:lengthi7e4:pathl47:Sherlock.S01E02.1080p.BluRay.DD5.1.x264-DON.mkveed6:lengthi7e4:pathl47:Sherlock.S01E03.1080p.BluRay.DD5.1.x264-DON.mkveee4:name40:Sherlock.S01.1080p.BluRay.DD5.1.x264-DON12:piece lengthi262144e6:pieces20:/<2F><>r<04><><EFBFBD><EFBFBD>lY<1A><><EFBFBD><EFBFBD>|<7C>7ee

View File

@@ -1,25 +0,0 @@
<rss version="2.0">
<channel>
<title>Test feed</title>
<link>http://nginx/custom/sonarr_bad_single.xml</link>
<description>
Test
</description>
<language>en-CA</language>
<copyright> Test </copyright>
<pubDate>Tue, 5 Nov 2024 22:02:13 -0400</pubDate>
<lastBuildDate>Tue, 5 Nov 2024 22:02:13 -0400</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<ttl>30</ttl>
<item>
<title>Agatha.All.Along.S01E02.Circle.Sewn.With.Fate.Unlock.Thy.Hidden.Gate.2160p.DSNP.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX</title>
<description>Test</description>
<size>4138858110</size>
<link>http://nginx/custom/sonarr_bad_single.torrent</link>
<guid isPermaLink="false">
174674a88c8947f6f9057ac3f81efde384ed216cade43564ec450f2cb4677554
</guid>
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
</item>
</channel>
</rss>

View File

Binary file not shown.

View File

@@ -0,0 +1 @@
d10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1734129464e4:infod5:filesld6:lengthi7e4:pathl47:Sherlock.S01E01.1080p.BluRay.DD5.1.x264-DON.mkveed6:lengthi7e4:pathl47:Sherlock.S01E02.1080p.BluRay.DD5.1.x264-DON.mkveed6:lengthi7e4:pathl47:Sherlock.S01E03.1080p.BluRay.DD5.1.x264-DON.mkveee4:name40:Sherlock.S01.1080p.BluRay.DD5.1.x264-DON12:piece lengthi262144e6:pieces20:/<2F><>r<04><><EFBFBD><EFBFBD>lY<1A><><EFBFBD><EFBFBD>|<7C>7ee

View File

@@ -1,4 +1 @@
b72541215214be2a1d96ef6b29ca1305f5e5e1f6
a4a1d1dd1db25763caa8f5e4d25ad72ef304094b
2b2ec156461d77bc48b8fe4d62cede50dcdff8e0
59ab2bc053430fe53e06a93e2eadb7acb6a6bf2c
11cece7f8721c484126b66f609d52738ff1bbf1e

View File

@@ -1,2 +1,2 @@
[Stats]
AllStats=@Variant(\0\0\0\x1c\0\0\0\x2\0\0\0\x12\0\x41\0l\0l\0t\0i\0m\0\x65\0\x44\0L\0\0\0\x4\0\0\0\0\0Z\xd2\x1b\0\0\0\x12\0\x41\0l\0l\0t\0i\0m\0\x65\0U\0L\0\0\0\x4\0\0\0\0\0\x90\xf9\xfc)
AllStats=@Variant(\0\0\0\x1c\0\0\0\x2\0\0\0\x12\0\x41\0l\0l\0t\0i\0m\0\x65\0U\0L\0\0\0\x4\0\0\0\0\0\x9dm\x4\0\0\0\x12\0\x41\0l\0l\0t\0i\0m\0\x65\0\x44\0L\0\0\0\x4\0\0\0\0\0\x62_.)

View File

@@ -0,0 +1 @@
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest

View File

@@ -0,0 +1 @@
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest

View File

@@ -0,0 +1 @@
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest

View File

@@ -0,0 +1 @@
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest

View File

@@ -0,0 +1 @@
testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest

View File

@@ -0,0 +1 @@
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1732896923e4:infod5:filesld6:lengthi2604e4:pathl4:Dir15:Dir1111:test11.zipxeed6:lengthi2604e4:pathl4:Dir110:sample.txteed6:lengthi2604e4:pathl4:Dir210:test2.zipxeed6:lengthi2604e4:pathl9:test.zipxeed6:lengthi2604e4:pathl49:Top.Gear.S23E02.720p.x265.HDTV.HEVC.-.YSTEAM.zipxeee4:name44:Top.Gear.S23E02.720p.x265.HDTV.HEVC.-.YSTEAM12:piece lengthi262144e6:pieces20:w<77><02>̳R<CCB3><52>'6F<36>o<EFBFBD>}<7D><>ee

View File

@@ -0,0 +1 @@
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1734129464e4:infod5:filesld6:lengthi7e4:pathl47:Sherlock.S01E01.1080p.BluRay.DD5.1.x264-DON.mkveed6:lengthi7e4:pathl47:Sherlock.S01E02.1080p.BluRay.DD5.1.x264-DON.mkveed6:lengthi7e4:pathl47:Sherlock.S01E03.1080p.BluRay.DD5.1.x264-DON.mkveee4:name40:Sherlock.S01.1080p.BluRay.DD5.1.x264-DON12:piece lengthi262144e6:pieces20:/<2F><>r<04><><EFBFBD><EFBFBD>lY<1A><><EFBFBD><EFBFBD>|<7C>7ee

View File

@@ -1 +1 @@
de6996481f4e318e7baff03b4043929c585a7c4e
cf82cf859b110af0ad3d94b846e006828417b193

View File

@@ -1,2 +1,2 @@
[Stats]
AllStats=@Variant(\0\0\0\x1c\0\0\0\x2\0\0\0\x12\0\x41\0l\0l\0t\0i\0m\0\x65\0\x44\0L\0\0\0\x4\0\0\0\0\0\x1e\xc7?\0\0\0\x12\0\x41\0l\0l\0t\0i\0m\0\x65\0U\0L\0\0\0\x4\0\0\0\0\0+1q)
AllStats=@Variant(\0\0\0\x1c\0\0\0\x2\0\0\0\x12\0\x41\0l\0l\0t\0i\0m\0\x65\0\x44\0L\0\0\0\x4\0\0\0\0\0!\x9d\x8e\0\0\0\x12\0\x41\0l\0l\0t\0i\0m\0\x65\0U\0L\0\0\0\x4\0\0\0\0\0.\xe6I)

View File

@@ -1 +1 @@
{"update":{"sid":"3e34254f4ee14bf4bec2d0359c3f8ee4","did":"92eba3c5-a8d0-44d5-836d-25bc4aa81a85","init":true,"started":"2024-11-18T17:39:31.1848496+00:00","timestamp":"2024-11-18T17:39:31.1852902+00:00","seq":0,"duration":0,"errors":0,"attrs":{"release":"Radarr@5.14.0.9383-master","environment":"master"}}}
{"update":{"sid":"743459ae24ef4f4c8a85171b21fd99a8","did":"92eba3c5-a8d0-44d5-836d-25bc4aa81a85","init":true,"started":"2024-11-29T15:46:38.3721409+00:00","timestamp":"2024-11-29T15:46:38.3728803+00:00","seq":0,"duration":0,"errors":0,"attrs":{"release":"Radarr@5.14.0.9383-master","environment":"master"}}}

Some files were not shown because too many files have changed in this diff Show More