add content blocker (#5)
* refactored code added deluge support added transmission support added content blocker added blacklist and whitelist * increased level on some logs; updated test docker compose; updated dev appsettings * updated docker compose and readme * moved some logs * fixed env var typo; fixed sonarr and radarr default download client
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
namespace Common.Configuration.ContentBlocker;
|
||||
|
||||
public sealed record ContentBlockerConfig : IConfig
|
||||
{
|
||||
public const string SectionName = "ContentBlocker";
|
||||
|
||||
public required bool Enabled { get; init; }
|
||||
|
||||
public PatternConfig? Blacklist { get; init; }
|
||||
|
||||
public PatternConfig? Whitelist { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Blacklist is null && Whitelist is null)
|
||||
{
|
||||
throw new Exception("content blocker is enabled, but both blacklist and whitelist are missing");
|
||||
}
|
||||
|
||||
if (Blacklist?.Enabled is true && Whitelist?.Enabled is true)
|
||||
{
|
||||
throw new Exception("only one exclusion (blacklist/whitelist) list is allowed");
|
||||
}
|
||||
|
||||
if (Blacklist?.Enabled is true && string.IsNullOrEmpty(Blacklist.Path))
|
||||
{
|
||||
throw new Exception("blacklist path is required");
|
||||
}
|
||||
|
||||
if (Whitelist?.Enabled is true && string.IsNullOrEmpty(Whitelist.Path))
|
||||
{
|
||||
throw new Exception("blacklist path is required");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Common.Configuration.ContentBlocker;
|
||||
|
||||
public sealed record PatternConfig
|
||||
{
|
||||
public bool Enabled { get; init; }
|
||||
|
||||
public string? Path { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Security;
|
||||
|
||||
namespace Common.Configuration;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public interface IConfig
|
||||
{
|
||||
void Validate();
|
||||
}
|
||||
@@ -1,12 +1,39 @@
|
||||
namespace Common.Configuration;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
public sealed class QBitConfig
|
||||
namespace Common.Configuration;
|
||||
|
||||
public sealed class QBitConfig : IConfig
|
||||
{
|
||||
public const string SectionName = "qBittorrent";
|
||||
|
||||
public required Uri Url { get; set; }
|
||||
public required bool Enabled { get; init; }
|
||||
|
||||
public required string Username { get; set; }
|
||||
public Uri? Url { get; init; }
|
||||
|
||||
public required string Password { get; set; }
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Common.Configuration.QueueCleaner;
|
||||
|
||||
public sealed record QueueCleanerConfig
|
||||
{
|
||||
public const string SectionName = "QueueCleaner";
|
||||
|
||||
public required bool Enabled { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public record TransmissionConfig
|
||||
{
|
||||
public const string SectionName = "Transmission";
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,6 @@ public sealed class TriggersConfig
|
||||
public const string SectionName = "Triggers";
|
||||
|
||||
public required string QueueCleaner { get; init; }
|
||||
|
||||
public required string ContentBlocker { get; init; }
|
||||
}
|
||||
@@ -6,4 +6,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Domain.Enums;
|
||||
|
||||
public enum BlocklistType
|
||||
{
|
||||
Blacklist,
|
||||
Whitelist
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Domain.Arr.Enums;
|
||||
namespace Domain.Enums;
|
||||
|
||||
public enum InstanceType
|
||||
{
|
||||
@@ -3,6 +3,7 @@
|
||||
public record QueueRecord
|
||||
{
|
||||
public int SeriesId { get; init; }
|
||||
public int EpisodeId { get; init; }
|
||||
public int MovieId { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public string Status { get; init; }
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Domain.Models.Deluge.Exceptions;
|
||||
|
||||
public class DelugeClientException : Exception
|
||||
{
|
||||
public DelugeClientException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Domain.Models.Deluge.Exceptions;
|
||||
|
||||
public sealed class DelugeLoginException : DelugeClientException
|
||||
{
|
||||
public DelugeLoginException() : base("login failed")
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Domain.Models.Deluge.Exceptions;
|
||||
|
||||
public sealed class DelugeLogoutException : DelugeClientException
|
||||
{
|
||||
public DelugeLogoutException() : base("logout failed")
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Models.Deluge.Request;
|
||||
|
||||
public class DelugeRequest
|
||||
{
|
||||
[JsonProperty(PropertyName = "id")]
|
||||
public int RequestId { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "method")]
|
||||
public String Method { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "params")]
|
||||
public List<Object> Params { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public NullValueHandling NullValueHandling { get; set; }
|
||||
|
||||
public DelugeRequest(int requestId, String method, params object[] parameters)
|
||||
{
|
||||
RequestId = requestId;
|
||||
Method = method;
|
||||
Params = new List<Object>();
|
||||
|
||||
if (parameters != null)
|
||||
{
|
||||
Params.AddRange(parameters);
|
||||
}
|
||||
|
||||
NullValueHandling = NullValueHandling.Include;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public sealed record DelugeContents
|
||||
{
|
||||
[JsonPropertyName("contents")]
|
||||
public Dictionary<string, DelugeFileOrDirectory> Contents { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } // Always "dir" for the root
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public sealed record DelugeError
|
||||
{
|
||||
[JsonProperty(PropertyName = "message")]
|
||||
public String Message { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "code")]
|
||||
public int Code { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public class DelugeFileOrDirectory
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } // "file" or "dir"
|
||||
|
||||
[JsonPropertyName("contents")]
|
||||
public Dictionary<string, DelugeFileOrDirectory>? Contents { get; set; } // Recursive property for directories
|
||||
|
||||
[JsonPropertyName("index")]
|
||||
public required int Index { get; set; }
|
||||
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public int? Size { get; set; }
|
||||
|
||||
[JsonPropertyName("offset")]
|
||||
public int? Offset { get; set; }
|
||||
|
||||
[JsonPropertyName("progress")]
|
||||
public double? Progress { get; set; }
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public required int Priority { get; set; }
|
||||
|
||||
[JsonPropertyName("progresses")]
|
||||
public List<double> Progresses { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public sealed record DelugeMinimalStatus
|
||||
{
|
||||
public string? Hash { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public sealed record DelugeResponse<T>
|
||||
{
|
||||
[JsonProperty(PropertyName = "id")]
|
||||
public int ResponseId { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "result")]
|
||||
public T? Result { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "error")]
|
||||
public DelugeError? Error { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public record DelugeTorrent
|
||||
{
|
||||
[JsonProperty(PropertyName = "comment")]
|
||||
public string Comment { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "is_seed")]
|
||||
public bool IsSeed { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "hash")]
|
||||
public string Hash { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "paused")]
|
||||
public bool Paused { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "ratio")]
|
||||
public double Ratio { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "label")]
|
||||
public string Label { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Domain.Models.Deluge.Response;
|
||||
|
||||
public sealed record DelugeTorrentExtended : DelugeTorrent
|
||||
{
|
||||
[JsonProperty(PropertyName = "total_done")]
|
||||
public long TotalDone { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_payload_download")]
|
||||
public long TotalPayloadDownload { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_uploaded")]
|
||||
public long TotalUploaded { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "next_announce")]
|
||||
public int NextAnnounce { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "tracker_status")]
|
||||
public string TrackerStatus { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "num_pieces")]
|
||||
public int NumPieces { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "piece_length")]
|
||||
public long PieceLength { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "is_auto_managed")]
|
||||
public bool IsAutoManaged { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "active_time")]
|
||||
public long ActiveTime { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "seeding_time")]
|
||||
public long SeedingTime { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "time_since_transfer")]
|
||||
public long TimeSinceTransfer { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "seed_rank")]
|
||||
public int SeedRank { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "last_seen_complete")]
|
||||
public long LastSeenComplete { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "completed_time")]
|
||||
public long CompletedTime { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "owner")] public string Owner { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "public")]
|
||||
public bool Public { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "shared")]
|
||||
public bool Shared { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "queue")] public int Queue { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_wanted")]
|
||||
public long TotalWanted { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "state")] public string State { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "progress")]
|
||||
public float Progress { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "num_seeds")]
|
||||
public int NumSeeds { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_seeds")]
|
||||
public int TotalSeeds { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "num_peers")]
|
||||
public int NumPeers { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_peers")]
|
||||
public int TotalPeers { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "download_payload_rate")]
|
||||
public long DownloadPayloadRate { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "upload_payload_rate")]
|
||||
public long UploadPayloadRate { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "eta")] public long Eta { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "distributed_copies")]
|
||||
public float DistributedCopies { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "time_added")]
|
||||
public int TimeAdded { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "tracker_host")]
|
||||
public string TrackerHost { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "download_location")]
|
||||
public string DownloadLocation { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "total_remaining")]
|
||||
public long TotalRemaining { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "max_download_speed")]
|
||||
public long MaxDownloadSpeed { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "max_upload_speed")]
|
||||
public long MaxUploadSpeed { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "seeds_peers_ratio")]
|
||||
public float SeedsPeersRatio { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Domain.Radarr;
|
||||
namespace Domain.Models.Radarr;
|
||||
|
||||
public sealed record RadarrCommand
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Domain.Sonarr;
|
||||
namespace Domain.Models.Sonarr;
|
||||
|
||||
public sealed record SonarrCommand
|
||||
{
|
||||
@@ -1,64 +0,0 @@
|
||||
using Common.Configuration;
|
||||
using Executable.Jobs;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.QueueCleaner;
|
||||
|
||||
namespace Executable;
|
||||
using Quartz;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.AddLogging(builder => builder.ClearProviders().AddConsole())
|
||||
.AddHttpClient()
|
||||
.AddConfiguration(configuration)
|
||||
.AddServices()
|
||||
.AddQuartzServices(configuration);
|
||||
|
||||
private static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.Configure<QBitConfig>(configuration.GetSection(QBitConfig.SectionName))
|
||||
.Configure<SonarrConfig>(configuration.GetSection(SonarrConfig.SectionName))
|
||||
.Configure<RadarrConfig>(configuration.GetSection(RadarrConfig.SectionName));
|
||||
|
||||
private static IServiceCollection AddServices(this IServiceCollection services) =>
|
||||
services
|
||||
.AddTransient<SonarrClient>()
|
||||
.AddTransient<RadarrClient>()
|
||||
.AddTransient<QueueCleanerJob>()
|
||||
.AddTransient<QueueCleanerHandler>();
|
||||
|
||||
private static IServiceCollection AddQuartzServices(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.AddQuartz(q =>
|
||||
{
|
||||
TriggersConfig? config = configuration.GetRequiredSection(TriggersConfig.SectionName).Get<TriggersConfig>();
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
throw new NullReferenceException("Quartz configuration is null");
|
||||
}
|
||||
|
||||
q.AddQueueCleanerJob(config.QueueCleaner);
|
||||
})
|
||||
.AddQuartzHostedService(opt =>
|
||||
{
|
||||
opt.WaitForJobsToComplete = true;
|
||||
});
|
||||
|
||||
private static void AddQueueCleanerJob(this IServiceCollectionQuartzConfigurator q, string trigger)
|
||||
{
|
||||
q.AddJob<QueueCleanerJob>(opts =>
|
||||
{
|
||||
opts.WithIdentity(nameof(QueueCleanerJob));
|
||||
});
|
||||
|
||||
q.AddTrigger(opts =>
|
||||
{
|
||||
opts.ForJob(nameof(QueueCleanerJob))
|
||||
.WithIdentity($"{nameof(QueueCleanerJob)}-trigger")
|
||||
.WithCronSchedule(trigger);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Common.Configuration;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
|
||||
namespace Executable.DependencyInjection;
|
||||
|
||||
public static class ConfigurationDI
|
||||
{
|
||||
public static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.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));
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Net;
|
||||
using Common.Configuration;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Executable.Jobs;
|
||||
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.QueueCleaner;
|
||||
|
||||
namespace Executable.DependencyInjection;
|
||||
|
||||
public static class MainDI
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.AddLogging(builder => builder.ClearProviders().AddConsole())
|
||||
.AddHttpClients()
|
||||
.AddConfiguration(configuration)
|
||||
.AddServices()
|
||||
.AddQuartzServices(configuration);
|
||||
|
||||
private static IServiceCollection AddHttpClients(this IServiceCollection services)
|
||||
{
|
||||
// add default HttpClient
|
||||
services.AddHttpClient();
|
||||
|
||||
// add Deluge HttpClient
|
||||
services
|
||||
.AddHttpClient(nameof(DelugeService), x =>
|
||||
{
|
||||
x.Timeout = TimeSpan.FromSeconds(5);
|
||||
})
|
||||
.ConfigurePrimaryHttpMessageHandler(_ =>
|
||||
{
|
||||
return new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = true,
|
||||
UseCookies = true,
|
||||
CookieContainer = new CookieContainer(),
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
|
||||
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
|
||||
};
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using Common.Configuration;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Executable.Jobs;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.QueueCleaner;
|
||||
using Quartz;
|
||||
|
||||
namespace Executable.DependencyInjection;
|
||||
|
||||
public static class QuartzDI
|
||||
{
|
||||
public static IServiceCollection AddQuartzServices(this IServiceCollection services, IConfiguration configuration) =>
|
||||
services
|
||||
.AddQuartz(q =>
|
||||
{
|
||||
TriggersConfig? config = configuration
|
||||
.GetRequiredSection(TriggersConfig.SectionName)
|
||||
.Get<TriggersConfig>();
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
throw new NullReferenceException("triggers configuration is null");
|
||||
}
|
||||
|
||||
q.AddQueueCleanerJob(configuration, config.QueueCleaner);
|
||||
q.AddContentBlockerJob(configuration, config.ContentBlocker);
|
||||
})
|
||||
.AddQuartzHostedService(opt =>
|
||||
{
|
||||
opt.WaitForJobsToComplete = true;
|
||||
});
|
||||
|
||||
private static void AddQueueCleanerJob(
|
||||
this IServiceCollectionQuartzConfigurator q,
|
||||
IConfiguration configuration,
|
||||
string trigger
|
||||
)
|
||||
{
|
||||
QueueCleanerConfig? config = configuration
|
||||
.GetRequiredSection(QueueCleanerConfig.SectionName)
|
||||
.Get<QueueCleanerConfig>();
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
throw new NullReferenceException($"{nameof(QueueCleaner)} configuration is null");
|
||||
}
|
||||
|
||||
if (!config.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
q.AddJob<QueueCleanerJob>(opts =>
|
||||
{
|
||||
opts.WithIdentity(nameof(QueueCleanerJob));
|
||||
});
|
||||
|
||||
q.AddTrigger(opts =>
|
||||
{
|
||||
opts.ForJob(nameof(QueueCleanerJob))
|
||||
.WithIdentity($"{nameof(QueueCleanerJob)}-trigger")
|
||||
.WithCronSchedule(trigger, x =>x.WithMisfireHandlingInstructionDoNothing());
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
q.AddTrigger(opts =>
|
||||
{
|
||||
opts.ForJob(nameof(ContentBlockerJob))
|
||||
.WithIdentity($"{nameof(ContentBlockerJob)}-trigger")
|
||||
.WithCronSchedule(trigger, x =>x.WithMisfireHandlingInstructionDoNothing());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Executable.Jobs;
|
||||
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.QueueCleaner;
|
||||
|
||||
namespace Executable.DependencyInjection;
|
||||
|
||||
public static class ServicesDI
|
||||
{
|
||||
public static IServiceCollection AddServices(this IServiceCollection services) =>
|
||||
services
|
||||
.AddTransient<SonarrClient>()
|
||||
.AddTransient<RadarrClient>()
|
||||
.AddTransient<QueueCleanerJob>()
|
||||
.AddTransient<ContentBlockerJob>()
|
||||
.AddTransient<QueueCleaner>()
|
||||
.AddTransient<ContentBlocker>()
|
||||
.AddTransient<FilenameEvaluator>()
|
||||
.AddTransient<QBitService>()
|
||||
.AddTransient<DelugeService>()
|
||||
.AddTransient<TransmissionService>()
|
||||
.AddTransient<ArrQueueIterator>()
|
||||
.AddTransient<DownloadServiceFactory>()
|
||||
.AddSingleton<BlocklistProvider>();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,20 +6,23 @@ namespace Executable.Jobs;
|
||||
[DisallowConcurrentExecution]
|
||||
public sealed class QueueCleanerJob : IJob
|
||||
{
|
||||
private ILogger<QueueCleanerJob> _logger;
|
||||
private QueueCleanerHandler _handler;
|
||||
private readonly ILogger<QueueCleanerJob> _logger;
|
||||
private readonly QueueCleaner _queueCleaner;
|
||||
|
||||
public QueueCleanerJob(ILogger<QueueCleanerJob> logger, QueueCleanerHandler handler)
|
||||
public QueueCleanerJob(
|
||||
ILogger<QueueCleanerJob> logger,
|
||||
QueueCleaner queueCleaner
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_handler = handler;
|
||||
_queueCleaner = queueCleaner;
|
||||
}
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _handler.HandleAsync();
|
||||
await _queueCleaner.ExecuteAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Executable;
|
||||
using Executable.DependencyInjection;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
|
||||
@@ -1,11 +1,63 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
"Default": "Debug",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Quartz": "Warning",
|
||||
"System.Net.Http.HttpClient": "Error"
|
||||
}
|
||||
},
|
||||
"Triggers": {
|
||||
"QueueCleaner": "0 0/1 * * * ?"
|
||||
"QueueCleaner": "0/10 * * * * ?",
|
||||
"ContentBlocker": "0/10 * * * * ?"
|
||||
},
|
||||
"ContentBlocker": {
|
||||
"Enabled": false,
|
||||
"Blacklist": {
|
||||
"Enabled": false,
|
||||
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
|
||||
},
|
||||
"Whitelist": {
|
||||
"Enabled": false,
|
||||
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist"
|
||||
}
|
||||
},
|
||||
"QueueCleaner": {
|
||||
"Enabled": true
|
||||
},
|
||||
"qBittorrent": {
|
||||
"Enabled": true,
|
||||
"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,
|
||||
"Instances": [
|
||||
{
|
||||
"Url": "http://localhost:8989",
|
||||
"ApiKey": "96736c3eb3144936b8f1d62d27be8cee"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Radarr": {
|
||||
"Enabled": true,
|
||||
"Instances": [
|
||||
{
|
||||
"Url": "http://localhost:7878",
|
||||
"ApiKey": "705b553732ab4167ab23909305d60600"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,40 @@
|
||||
}
|
||||
},
|
||||
"Triggers": {
|
||||
"QueueCleaner": "0 0/5 * * * ?"
|
||||
"QueueCleaner": "0 0/5 * * * ?",
|
||||
"ContentBlocker": "0 0/5 * * * ?"
|
||||
},
|
||||
"ContentBlocker": {
|
||||
"Enabled": false,
|
||||
"Blacklist": {
|
||||
"Enabled": false,
|
||||
"Path": ""
|
||||
},
|
||||
"Whitelist": {
|
||||
"Enabled": false,
|
||||
"Path": ""
|
||||
}
|
||||
},
|
||||
"QueueCleaner": {
|
||||
"Enabled": true
|
||||
},
|
||||
"qBittorrent": {
|
||||
"Enabled": true,
|
||||
"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,
|
||||
"Instances": [
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<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="QBittorrent.Client" Version="1.9.24285.1" />
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Arr.Queue;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Verticals.Arr;
|
||||
|
||||
public sealed class ArrQueueIterator
|
||||
{
|
||||
private readonly ILogger<ArrQueueIterator> _logger;
|
||||
|
||||
public ArrQueueIterator(ILogger<ArrQueueIterator> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Iterate(ArrClient arrClient, ArrInstance arrInstance, Func<IReadOnlyList<QueueRecord>, Task> action)
|
||||
{
|
||||
const ushort maxPage = 100;
|
||||
ushort page = 1;
|
||||
int totalRecords = 0;
|
||||
int processedRecords = 0;
|
||||
|
||||
do
|
||||
{
|
||||
QueueListResponse queueResponse = await arrClient.GetQueueItemsAsync(arrInstance, page);
|
||||
|
||||
if (totalRecords is 0)
|
||||
{
|
||||
totalRecords = queueResponse.TotalRecords;
|
||||
|
||||
_logger.LogInformation(
|
||||
"{items} items found in queue | {url}",
|
||||
queueResponse.TotalRecords, arrInstance.Url);
|
||||
}
|
||||
|
||||
if (queueResponse.Records.Count is 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await action(queueResponse.Records);
|
||||
|
||||
processedRecords += queueResponse.Records.Count;
|
||||
|
||||
if (processedRecords >= totalRecords)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
} while (processedRecords < totalRecords && page < maxPage);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text;
|
||||
using Common.Configuration;
|
||||
using Domain.Radarr;
|
||||
using Domain.Models.Radarr;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text;
|
||||
using Common.Configuration;
|
||||
using Domain.Sonarr;
|
||||
using Domain.Models.Sonarr;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Domain.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.ContentBlocker;
|
||||
|
||||
public sealed class BlocklistProvider
|
||||
{
|
||||
private readonly ILogger<BlocklistProvider> _logger;
|
||||
private readonly ContentBlockerConfig _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public BlocklistType BlocklistType { get; }
|
||||
|
||||
public List<string> Patterns { get; } = [];
|
||||
|
||||
public List<Regex> Regexes { get; } = [];
|
||||
|
||||
public BlocklistProvider(
|
||||
ILogger<BlocklistProvider> logger,
|
||||
IOptions<ContentBlockerConfig> config,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
|
||||
_config.Validate();
|
||||
|
||||
if (_config.Blacklist?.Enabled is true)
|
||||
{
|
||||
BlocklistType = BlocklistType.Blacklist;
|
||||
}
|
||||
|
||||
if (_config.Whitelist?.Enabled is true)
|
||||
{
|
||||
BlocklistType = BlocklistType.Whitelist;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadBlocklistAsync()
|
||||
{
|
||||
if (Patterns.Count > 0 || Regexes.Count > 0)
|
||||
{
|
||||
_logger.LogDebug("blocklist already loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await LoadPatternsAndRegexesAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("failed to load {type}", BlocklistType.ToString());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPatternsAndRegexesAsync()
|
||||
{
|
||||
string[] patterns;
|
||||
|
||||
if (BlocklistType is BlocklistType.Blacklist)
|
||||
{
|
||||
patterns = await ReadContentAsync(_config.Blacklist.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
patterns = await ReadContentAsync(_config.Whitelist.Path);
|
||||
}
|
||||
|
||||
long startTime = Stopwatch.GetTimestamp();
|
||||
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
|
||||
|
||||
Parallel.ForEach(patterns, options, pattern =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Regex regex = new(pattern, RegexOptions.Compiled);
|
||||
Regexes.Add(regex);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
Patterns.Add(pattern);
|
||||
}
|
||||
});
|
||||
|
||||
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||
|
||||
_logger.LogDebug("loaded {count} patterns", Patterns.Count);
|
||||
_logger.LogDebug("loaded {count} regexes", Regexes.Count);
|
||||
_logger.LogDebug("blocklist loaded in {elapsed} ms", elapsed.TotalMilliseconds);
|
||||
}
|
||||
|
||||
private async Task<string[]> ReadContentAsync(string path)
|
||||
{
|
||||
if (Uri.TryCreate(path, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
||||
{
|
||||
// http(s) url
|
||||
return await ReadFromUrlAsync(path);
|
||||
}
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
// local file path
|
||||
return await File.ReadAllLinesAsync(path);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"blocklist not found | {path}");
|
||||
}
|
||||
|
||||
private async Task<string[]> ReadFromUrlAsync(string url)
|
||||
{
|
||||
using HttpResponseMessage response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return (await response.Content.ReadAsStringAsync())
|
||||
.Split(['\r','\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Arr.Queue;
|
||||
using Domain.Enums;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.DownloadClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.ContentBlocker;
|
||||
|
||||
public sealed class ContentBlocker : IDisposable
|
||||
{
|
||||
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,
|
||||
IOptions<SonarrConfig> sonarrConfig,
|
||||
IOptions<RadarrConfig> radarrConfig,
|
||||
SonarrClient sonarrClient,
|
||||
RadarrClient radarrClient,
|
||||
ArrQueueIterator arrArrQueueIterator,
|
||||
BlocklistProvider blocklistProvider,
|
||||
DownloadServiceFactory downloadServiceFactory
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_sonarrConfig = sonarrConfig.Value;
|
||||
_radarrConfig = radarrConfig.Value;
|
||||
_sonarrClient = sonarrClient;
|
||||
_radarrClient = radarrClient;
|
||||
_arrArrQueueIterator = arrArrQueueIterator;
|
||||
_blocklistProvider = blocklistProvider;
|
||||
_downloadService = downloadServiceFactory.CreateDownloadClient();
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
await _blocklistProvider.LoadBlocklistAsync();
|
||||
await _downloadService.LoginAsync();
|
||||
|
||||
await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr);
|
||||
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
ArrClient arrClient = GetClient(instanceType);
|
||||
|
||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||
{
|
||||
foreach (QueueRecord record in items)
|
||||
{
|
||||
_logger.LogDebug("searching unwanted files for {title}", record.Title);
|
||||
await _downloadService.BlockUnwantedFilesAsync(record.DownloadId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Domain.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Verticals.ContentBlocker;
|
||||
|
||||
public sealed class FilenameEvaluator
|
||||
{
|
||||
private readonly ILogger<FilenameEvaluator> _logger;
|
||||
private readonly BlocklistProvider _blocklistProvider;
|
||||
|
||||
public FilenameEvaluator(ILogger<FilenameEvaluator> logger, BlocklistProvider blocklistProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_blocklistProvider = blocklistProvider;
|
||||
}
|
||||
|
||||
// TODO create unit tests
|
||||
public bool IsValid(string filename)
|
||||
{
|
||||
return IsValidAgainstPatterns(filename) && IsValidAgainstRegexes(filename);
|
||||
}
|
||||
|
||||
private bool IsValidAgainstPatterns(string filename)
|
||||
{
|
||||
if (_blocklistProvider.Patterns.Count is 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _blocklistProvider.BlocklistType switch
|
||||
{
|
||||
BlocklistType.Blacklist => !_blocklistProvider.Patterns.Any(pattern => MatchesPattern(filename, pattern)),
|
||||
BlocklistType.Whitelist => _blocklistProvider.Patterns.Any(pattern => MatchesPattern(filename, pattern)),
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsValidAgainstRegexes(string filename)
|
||||
{
|
||||
if (_blocklistProvider.Regexes.Count is 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _blocklistProvider.BlocklistType switch
|
||||
{
|
||||
BlocklistType.Blacklist => !_blocklistProvider.Regexes.Any(regex => regex.IsMatch(filename)),
|
||||
BlocklistType.Whitelist => _blocklistProvider.Regexes.Any(regex => regex.IsMatch(filename)),
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private static bool MatchesPattern(string filename, string pattern)
|
||||
{
|
||||
bool hasStartWildcard = pattern.StartsWith('*');
|
||||
bool hasEndWildcard = pattern.EndsWith('*');
|
||||
|
||||
if (hasStartWildcard && hasEndWildcard)
|
||||
{
|
||||
return filename.Contains(
|
||||
pattern.Substring(1, pattern.Length - 2),
|
||||
StringComparison.InvariantCultureIgnoreCase
|
||||
);
|
||||
}
|
||||
|
||||
if (hasStartWildcard)
|
||||
{
|
||||
return filename.EndsWith(pattern.Substring(1), StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
if (hasEndWildcard)
|
||||
{
|
||||
return filename.StartsWith(
|
||||
pattern.Substring(0, pattern.Length - 1),
|
||||
StringComparison.InvariantCultureIgnoreCase
|
||||
);
|
||||
}
|
||||
|
||||
return filename == pattern;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json.Serialization;
|
||||
using Common.Configuration;
|
||||
using Domain.Models.Deluge.Exceptions;
|
||||
using Domain.Models.Deluge.Request;
|
||||
using Domain.Models.Deluge.Response;
|
||||
using Infrastructure.Verticals.DownloadClient.Deluge.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.Deluge;
|
||||
|
||||
public sealed class DelugeClient
|
||||
{
|
||||
private readonly DelugeConfig _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public DelugeClient(IOptions<DelugeConfig> config, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_config = config.Value;
|
||||
_httpClient = httpClientFactory.CreateClient(nameof(DelugeService));
|
||||
}
|
||||
|
||||
public async Task<bool> LoginAsync()
|
||||
{
|
||||
return await SendRequest<bool>("auth.login", _config.Password);
|
||||
}
|
||||
|
||||
public async Task<bool> Logout()
|
||||
{
|
||||
return await SendRequest<bool>("auth.delete_session");
|
||||
}
|
||||
|
||||
public async Task<List<DelugeTorrent>> ListTorrents(Dictionary<string, string>? filters = null)
|
||||
{
|
||||
filters ??= new Dictionary<string, string>();
|
||||
var keys = typeof(DelugeTorrent).GetAllJsonPropertyFromType();
|
||||
Dictionary<string, DelugeTorrent> result =
|
||||
await SendRequest<Dictionary<string, DelugeTorrent>>("core.get_torrents_status", filters, keys);
|
||||
return result.Values.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<DelugeTorrentExtended>> ListTorrentsExtended(Dictionary<string, string>? filters = null)
|
||||
{
|
||||
filters ??= new Dictionary<string, string>();
|
||||
var keys = typeof(DelugeTorrentExtended).GetAllJsonPropertyFromType();
|
||||
Dictionary<string, DelugeTorrentExtended> result =
|
||||
await SendRequest<Dictionary<string, DelugeTorrentExtended>>("core.get_torrents_status", filters, keys);
|
||||
return result.Values.ToList();
|
||||
}
|
||||
|
||||
public async Task<DelugeTorrent?> GetTorrent(string hash)
|
||||
{
|
||||
List<DelugeTorrent> torrents = await ListTorrents(new Dictionary<string, string>() { { "hash", hash } });
|
||||
return torrents.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<DelugeTorrentExtended?> GetTorrentExtended(string hash)
|
||||
{
|
||||
List<DelugeTorrentExtended> torrents =
|
||||
await ListTorrentsExtended(new Dictionary<string, string> { { "hash", hash } });
|
||||
return torrents.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<DelugeContents?> GetTorrentFiles(string hash)
|
||||
{
|
||||
return await SendRequest<DelugeContents?>("web.get_torrent_files", hash);
|
||||
}
|
||||
|
||||
public async Task ChangeFilesPriority(string hash, List<int> priorities)
|
||||
{
|
||||
Dictionary<string, List<int>> filePriorities = new()
|
||||
{
|
||||
{ "file_priorities", priorities }
|
||||
};
|
||||
|
||||
await SendRequest<DelugeResponse<object>>("core.set_torrent_options", hash, filePriorities);
|
||||
}
|
||||
|
||||
private async Task<String> PostJson(String json)
|
||||
{
|
||||
StringContent content = new StringContent(json);
|
||||
content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");
|
||||
|
||||
var responseMessage = await _httpClient.PostAsync(new Uri(_config.Url, "/json"), content);
|
||||
responseMessage.EnsureSuccessStatusCode();
|
||||
|
||||
var responseJson = await responseMessage.Content.ReadAsStringAsync();
|
||||
return responseJson;
|
||||
}
|
||||
|
||||
private DelugeRequest CreateRequest(string method, params object[] parameters)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(method))
|
||||
{
|
||||
throw new ArgumentException(nameof(method));
|
||||
}
|
||||
|
||||
return new DelugeRequest(1, method, parameters);
|
||||
}
|
||||
|
||||
public async Task<T> SendRequest<T>(string method, params object[] parameters)
|
||||
{
|
||||
return await SendRequest<T>(CreateRequest(method, parameters));
|
||||
}
|
||||
|
||||
public async Task<T> SendRequest<T>(DelugeRequest webRequest)
|
||||
{
|
||||
var requestJson = JsonConvert.SerializeObject(webRequest, Formatting.None, new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = webRequest.NullValueHandling
|
||||
});
|
||||
|
||||
var responseJson = await PostJson(requestJson);
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
Error = (_, args) =>
|
||||
{
|
||||
// Suppress the error and continue
|
||||
args.ErrorContext.Handled = true;
|
||||
}
|
||||
};
|
||||
|
||||
DelugeResponse<T>? webResponse = JsonConvert.DeserializeObject<DelugeResponse<T>>(responseJson, settings);
|
||||
|
||||
if (webResponse?.Error != null)
|
||||
{
|
||||
throw new DelugeClientException(webResponse.Error.Message);
|
||||
}
|
||||
|
||||
if (webResponse?.ResponseId != webRequest.RequestId)
|
||||
{
|
||||
throw new DelugeClientException("desync");
|
||||
}
|
||||
|
||||
return webResponse.Result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Models.Deluge.Response;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.Deluge;
|
||||
|
||||
public sealed class DelugeService : IDownloadService
|
||||
{
|
||||
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
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_client = new (config, httpClientFactory);
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
}
|
||||
|
||||
public async Task LoginAsync()
|
||||
{
|
||||
await _client.LoginAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
{
|
||||
hash = hash.ToLowerInvariant();
|
||||
|
||||
DelugeContents? contents = null;
|
||||
|
||||
if (!await HasMinimalStatus(hash))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
contents = await _client.GetTorrentFiles(hash);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
|
||||
}
|
||||
|
||||
if (contents is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool shouldRemove = true;
|
||||
|
||||
ProcessFiles(contents.Contents, (_, file) =>
|
||||
{
|
||||
if (file.Priority > 0)
|
||||
{
|
||||
shouldRemove = false;
|
||||
}
|
||||
});
|
||||
|
||||
return shouldRemove;
|
||||
}
|
||||
|
||||
public async Task BlockUnwantedFilesAsync(string hash)
|
||||
{
|
||||
hash = hash.ToLowerInvariant();
|
||||
|
||||
if (!await HasMinimalStatus(hash))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DelugeContents? contents = null;
|
||||
|
||||
try
|
||||
{
|
||||
contents = await _client.GetTorrentFiles(hash);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
|
||||
}
|
||||
|
||||
if (contents is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<int, int> priorities = [];
|
||||
bool hasPriorityUpdates = false;
|
||||
|
||||
ProcessFiles(contents.Contents, (name, file) =>
|
||||
{
|
||||
int priority = file.Priority;
|
||||
|
||||
if (file.Priority is not 0 && !_filenameEvaluator.IsValid(name))
|
||||
{
|
||||
priority = 0;
|
||||
hasPriorityUpdates = true;
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Path);
|
||||
}
|
||||
|
||||
priorities.Add(file.Index, priority);
|
||||
});
|
||||
|
||||
if (!hasPriorityUpdates)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
||||
|
||||
List<int> sortedPriorities = priorities
|
||||
.OrderBy(x => x.Key)
|
||||
.Select(x => x.Value)
|
||||
.ToList();
|
||||
|
||||
await _client.ChangeFilesPriority(hash, sortedPriorities);
|
||||
}
|
||||
|
||||
private async Task<bool> HasMinimalStatus(string hash)
|
||||
{
|
||||
DelugeMinimalStatus? status = await _client.SendRequest<DelugeMinimalStatus?>(
|
||||
"web.get_torrent_status",
|
||||
hash,
|
||||
new[] { "hash" }
|
||||
);
|
||||
|
||||
if (status?.Hash is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory> contents, Action<string, DelugeFileOrDirectory> processFile)
|
||||
{
|
||||
foreach (var (name, data) in contents)
|
||||
{
|
||||
switch (data.Type)
|
||||
{
|
||||
case "file":
|
||||
processFile(name, data);
|
||||
break;
|
||||
case "dir" when data.Contents is not null:
|
||||
// Recurse into subdirectories
|
||||
ProcessFiles(data.Contents, processFile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.Deluge.Extensions;
|
||||
|
||||
internal static class DelugeExtensions
|
||||
{
|
||||
public static List<String?> GetAllJsonPropertyFromType(this Type t)
|
||||
{
|
||||
var type = typeof(JsonPropertyAttribute);
|
||||
var props = t.GetProperties()
|
||||
.Where(prop => Attribute.IsDefined(prop, type))
|
||||
.ToList();
|
||||
|
||||
return props
|
||||
.Select(x => x.GetCustomAttributes(type, true).Single())
|
||||
.Cast<JsonPropertyAttribute>()
|
||||
.Select(x => x.PropertyName)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Common.Configuration;
|
||||
using Infrastructure.Verticals.DownloadClient.Deluge;
|
||||
using Infrastructure.Verticals.DownloadClient.QBittorrent;
|
||||
using Infrastructure.Verticals.DownloadClient.Transmission;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient;
|
||||
|
||||
public sealed class DownloadServiceFactory
|
||||
{
|
||||
private readonly QBitConfig _qBitConfig;
|
||||
private readonly DelugeConfig _delugeConfig;
|
||||
private readonly TransmissionConfig _transmissionConfig;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public DownloadServiceFactory(
|
||||
IOptions<QBitConfig> qBitConfig,
|
||||
IOptions<DelugeConfig> delugeConfig,
|
||||
IOptions<TransmissionConfig> transmissionConfig,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_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");
|
||||
}
|
||||
}
|
||||
|
||||
public IDownloadService CreateDownloadClient()
|
||||
{
|
||||
if (_qBitConfig.Enabled)
|
||||
{
|
||||
return _serviceProvider.GetRequiredService<QBitService>();
|
||||
}
|
||||
|
||||
if (_delugeConfig.Enabled)
|
||||
{
|
||||
return _serviceProvider.GetRequiredService<DelugeService>();
|
||||
}
|
||||
|
||||
if (_transmissionConfig.Enabled)
|
||||
{
|
||||
return _serviceProvider.GetRequiredService<TransmissionService>();
|
||||
}
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Infrastructure.Verticals.DownloadClient;
|
||||
|
||||
public interface IDownloadService : IDisposable
|
||||
{
|
||||
public Task LoginAsync();
|
||||
|
||||
public Task<bool> ShouldRemoveFromArrQueueAsync(string hash);
|
||||
|
||||
public Task BlockUnwantedFilesAsync(string hash);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using Common.Configuration;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using QBittorrent.Client;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.QBittorrent;
|
||||
|
||||
public sealed class QBitService : IDownloadService
|
||||
{
|
||||
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
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
_client = new(_config.Url);
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
}
|
||||
|
||||
public async Task LoginAsync()
|
||||
{
|
||||
await _client.LoginAsync(_config.Username, _config.Password);
|
||||
}
|
||||
|
||||
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (torrent is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// if all files were blocked by qBittorrent
|
||||
if (torrent is { CompletionOn: not null, Downloaded: null or 0 })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task BlockUnwantedFilesAsync(string hash)
|
||||
{
|
||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
||||
|
||||
if (files is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (TorrentContent file in files)
|
||||
{
|
||||
if (!file.Index.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.Priority is TorrentContentPriority.Skip || _filenameEvaluator.IsValid(file.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Name);
|
||||
await _client.SetFilePriorityAsync(hash, file.Index.Value, TorrentContentPriority.Skip);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using Common.Configuration;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Transmission.API.RPC;
|
||||
using Transmission.API.RPC.Arguments;
|
||||
using Transmission.API.RPC.Entity;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.Transmission;
|
||||
|
||||
public sealed class TransmissionService : IDownloadService
|
||||
{
|
||||
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
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
_client = new(
|
||||
new Uri(_config.Url, "/transmission/rpc").ToString(),
|
||||
login: _config.Username,
|
||||
password: _config.Password
|
||||
);
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
}
|
||||
|
||||
public async Task LoginAsync()
|
||||
{
|
||||
await _client.GetSessionInformationAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||
|
||||
if (torrent is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (TransmissionTorrentFileStats? stats in torrent.FileStats ?? [])
|
||||
{
|
||||
if (!stats.Wanted.HasValue)
|
||||
{
|
||||
// if any files stats are missing, do not remove
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stats.Wanted.HasValue && stats.Wanted.Value)
|
||||
{
|
||||
// if any files are wanted, do not remove
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// remove if all files are unwanted
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task BlockUnwantedFilesAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||
|
||||
if (torrent?.FileStats is null || torrent.Files is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<long> unwantedFiles = [];
|
||||
|
||||
for (int i = 0; i < torrent.Files.Length; i++)
|
||||
{
|
||||
if (torrent.FileStats?[i].Wanted == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!torrent.FileStats[i].Wanted.Value || _filenameEvaluator.IsValid(torrent.Files[i].Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("unwanted file found | {file}", torrent.Files[i].Name);
|
||||
unwantedFiles.Add(i);
|
||||
}
|
||||
|
||||
if (unwantedFiles.Count is 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
||||
|
||||
await _client.TorrentSetAsync(new TorrentSettings
|
||||
{
|
||||
Ids = [ torrent.Id ],
|
||||
FilesUnwanted = unwantedFiles.ToArray(),
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private async Task<TorrentInfo?> GetTorrentAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = _torrentsCache?
|
||||
.FirstOrDefault(x => x.HashString.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (_torrentsCache is null || torrent is null)
|
||||
{
|
||||
string[] fields = [TorrentFields.FILES, TorrentFields.FILE_STATS, TorrentFields.HASH_STRING, TorrentFields.ID];
|
||||
|
||||
// refresh cache
|
||||
_torrentsCache = (await _client.TorrentGetAsync(fields))
|
||||
?.Torrents;
|
||||
}
|
||||
|
||||
if (_torrentsCache?.Length is null or 0)
|
||||
{
|
||||
_logger.LogDebug("could not list torrents | {url}", _config.Url);
|
||||
}
|
||||
|
||||
torrent = _torrentsCache?.FirstOrDefault(x => x.HashString.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (torrent is null)
|
||||
{
|
||||
_logger.LogDebug("could not find torrent | {hash} | {url}", hash, _config.Url);
|
||||
}
|
||||
|
||||
return torrent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Arr.Queue;
|
||||
using Domain.Enums;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.DownloadClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.QueueCleaner;
|
||||
|
||||
public sealed class QueueCleaner : IDisposable
|
||||
{
|
||||
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,
|
||||
IOptions<RadarrConfig> radarrConfig,
|
||||
SonarrClient sonarrClient,
|
||||
RadarrClient radarrClient,
|
||||
ArrQueueIterator arrArrQueueIterator,
|
||||
DownloadServiceFactory downloadServiceFactory
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_sonarrConfig = sonarrConfig.Value;
|
||||
_radarrConfig = radarrConfig.Value;
|
||||
_sonarrClient = sonarrClient;
|
||||
_radarrClient = radarrClient;
|
||||
_arrArrQueueIterator = arrArrQueueIterator;
|
||||
_downloadService = downloadServiceFactory.CreateDownloadClient();
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
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 = [];
|
||||
ArrClient arrClient = GetClient(instanceType);
|
||||
|
||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||
{
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId))
|
||||
{
|
||||
_logger.LogInformation("skip | {title}", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
itemsToBeRefreshed.Add(GetRecordId(instanceType, record));
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Arr.Enums;
|
||||
using Domain.Arr.Queue;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using QBittorrent.Client;
|
||||
|
||||
namespace Infrastructure.Verticals.QueueCleaner;
|
||||
|
||||
public sealed class QueueCleanerHandler
|
||||
{
|
||||
private readonly ILogger<QueueCleanerHandler> _logger;
|
||||
private readonly QBitConfig _qBitConfig;
|
||||
private readonly SonarrConfig _sonarrConfig;
|
||||
private readonly RadarrConfig _radarrConfig;
|
||||
private readonly SonarrClient _sonarrClient;
|
||||
private readonly RadarrClient _radarrClient;
|
||||
|
||||
public QueueCleanerHandler(
|
||||
ILogger<QueueCleanerHandler> logger,
|
||||
IOptions<QBitConfig> qBitConfig,
|
||||
IOptions<SonarrConfig> sonarrConfig,
|
||||
IOptions<RadarrConfig> radarrConfig,
|
||||
SonarrClient sonarrClient,
|
||||
RadarrClient radarrClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_qBitConfig = qBitConfig.Value;
|
||||
_sonarrConfig = sonarrConfig.Value;
|
||||
_radarrConfig = radarrConfig.Value;
|
||||
_sonarrClient = sonarrClient;
|
||||
_radarrClient = radarrClient;
|
||||
}
|
||||
|
||||
public async Task HandleAsync()
|
||||
{
|
||||
QBittorrentClient qBitClient = new(_qBitConfig.Url);
|
||||
await qBitClient.LoginAsync(_qBitConfig.Username, _qBitConfig.Password);
|
||||
|
||||
await ProcessArrConfigAsync(qBitClient, _sonarrConfig, InstanceType.Sonarr);
|
||||
await ProcessArrConfigAsync(qBitClient, _radarrConfig, InstanceType.Radarr);
|
||||
}
|
||||
|
||||
private async Task ProcessArrConfigAsync(QBittorrentClient qBitClient, ArrConfig config, InstanceType instanceType)
|
||||
{
|
||||
if (!config.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ArrInstance arrInstance in config.Instances)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessInstanceAsync(qBitClient, arrInstance, instanceType);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "failed to clean {type} instance | {url}", instanceType, arrInstance.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessInstanceAsync(QBittorrentClient qBitClient, ArrInstance instance, InstanceType instanceType)
|
||||
{
|
||||
ushort page = 1;
|
||||
int totalRecords = 0;
|
||||
int processedRecords = 0;
|
||||
HashSet<int> itemsToBeRefreshed = [];
|
||||
ArrClient arrClient = GetClient(instanceType);
|
||||
|
||||
do
|
||||
{
|
||||
QueueListResponse queueResponse = await arrClient.GetQueueItemsAsync(instance, page);
|
||||
|
||||
if (totalRecords is 0)
|
||||
{
|
||||
totalRecords = queueResponse.TotalRecords;
|
||||
|
||||
_logger.LogInformation(
|
||||
"{items} items found in queue | {url}",
|
||||
queueResponse.TotalRecords, instance.Url);
|
||||
}
|
||||
|
||||
foreach (QueueRecord record in queueResponse.Records)
|
||||
{
|
||||
if (record.Protocol is not "torrent")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TorrentInfo? torrent = (await qBitClient.GetTorrentListAsync(new TorrentListQuery { Hashes = [record.DownloadId] }))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (torrent is not { CompletionOn: not null, Downloaded: null or 0 })
|
||||
{
|
||||
_logger.LogInformation("skip | {torrent}", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
itemsToBeRefreshed.Add(GetRecordId(instanceType, record));
|
||||
|
||||
await arrClient.DeleteQueueItemAsync(instance, record);
|
||||
}
|
||||
|
||||
if (queueResponse.Records.Count is 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
processedRecords += queueResponse.Records.Count;
|
||||
|
||||
if (processedRecords >= totalRecords)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
} while (processedRecords < totalRecords);
|
||||
|
||||
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
|
||||
{
|
||||
InstanceType.Sonarr => record.SeriesId,
|
||||
InstanceType.Radarr => record.MovieId,
|
||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
.*sample.*
|
||||
*.zipx
|
||||
@@ -0,0 +1 @@
|
||||
*.mkv
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
localclient:da4d4b43be734d48c1bb8b9ab0e39894520994e3:10
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"file": 1,
|
||||
"format": 1
|
||||
}{
|
||||
"check_after_days": 4,
|
||||
"last_update": 0.0,
|
||||
"list_compression": "",
|
||||
"list_size": 0,
|
||||
"list_type": "",
|
||||
"load_on_start": false,
|
||||
"timeout": 180,
|
||||
"try_times": 3,
|
||||
"url": "",
|
||||
"whitelisted": []
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"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": false,
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"file": 3,
|
||||
"format": 1
|
||||
}{
|
||||
"hosts": [
|
||||
[
|
||||
"b5408e9794dd432789c55d8c46d15275",
|
||||
"127.0.0.1",
|
||||
58846,
|
||||
"localclient",
|
||||
"da4d4b43be734d48c1bb8b9ab0e39894520994e3"
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"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": {
|
||||
"2b2ec156461d77bc48b8fe4d62cede50dcdff8e0": "radarr",
|
||||
"59ab2bc053430fe53e06a93e2eadb7acb6a6bf2c": "tv-sonarr",
|
||||
"5a31d5f1689f5f45fd85c275a37acd2c7b82fde1": "tv-sonarr",
|
||||
"6c890ff85b5317d5df291c3c23a782774e10e6fe": "radarr",
|
||||
"a4a1d1dd1db25763caa8f5e4d25ad72ef304094b": "radarr",
|
||||
"b72541215214be2a1d96ef6b29ca1305f5e5e1f6": "tv-sonarr"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
+429
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* blocklist.js
|
||||
*
|
||||
* Copyright (C) Omar Alvarez 2014 <omar.alvarez@udc.es>
|
||||
*
|
||||
* This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
|
||||
* the additional special exception to link portions of this program with the OpenSSL library.
|
||||
* See LICENSE for more details.
|
||||
*
|
||||
*/
|
||||
|
||||
Ext.ns('Deluge.ux.preferences');
|
||||
|
||||
/**
|
||||
* @class Deluge.ux.preferences.BlocklistPage
|
||||
* @extends Ext.Panel
|
||||
*/
|
||||
Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
|
||||
title: _('Blocklist'),
|
||||
header: false,
|
||||
layout: 'fit',
|
||||
border: false,
|
||||
autoScroll: true,
|
||||
|
||||
initComponent: function () {
|
||||
Deluge.ux.preferences.BlocklistPage.superclass.initComponent.call(this);
|
||||
|
||||
this.URLFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('General'),
|
||||
autoHeight: true,
|
||||
defaultType: 'textfield',
|
||||
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
autoWidth: true,
|
||||
labelWidth: 40,
|
||||
});
|
||||
|
||||
this.URL = this.URLFset.add({
|
||||
fieldLabel: _('URL:'),
|
||||
labelSeparator: '',
|
||||
name: 'url',
|
||||
width: '80%',
|
||||
});
|
||||
|
||||
this.SettingsFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Settings'),
|
||||
autoHeight: true,
|
||||
defaultType: 'spinnerfield',
|
||||
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
autoWidth: true,
|
||||
labelWidth: 160,
|
||||
});
|
||||
|
||||
this.checkListDays = this.SettingsFset.add({
|
||||
fieldLabel: _('Check for new list every (days):'),
|
||||
labelSeparator: '',
|
||||
name: 'check_list_days',
|
||||
value: 4,
|
||||
decimalPrecision: 0,
|
||||
width: 80,
|
||||
});
|
||||
|
||||
this.chkImportOnStart = this.SettingsFset.add({
|
||||
xtype: 'checkbox',
|
||||
fieldLabel: _('Import blocklist on startup'),
|
||||
name: 'check_import_startup',
|
||||
});
|
||||
|
||||
this.OptionsFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Options'),
|
||||
autoHeight: true,
|
||||
defaultType: 'button',
|
||||
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
autoWidth: false,
|
||||
width: '80%',
|
||||
labelWidth: 0,
|
||||
});
|
||||
|
||||
this.checkDownload = this.OptionsFset.add({
|
||||
fieldLabel: _(''),
|
||||
name: 'check_download',
|
||||
xtype: 'container',
|
||||
layout: 'hbox',
|
||||
margins: '4 0 0 5',
|
||||
items: [
|
||||
{
|
||||
xtype: 'button',
|
||||
text: ' Check Download and Import ',
|
||||
scale: 'medium',
|
||||
},
|
||||
{
|
||||
xtype: 'box',
|
||||
autoEl: {
|
||||
tag: 'img',
|
||||
src: '../icons/ok.png',
|
||||
},
|
||||
margins: '4 0 0 3',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.forceDownload = this.OptionsFset.add({
|
||||
fieldLabel: _(''),
|
||||
name: 'force_download',
|
||||
text: ' Force Download and Import ',
|
||||
margins: '2 0 0 0',
|
||||
//icon: '../icons/blocklist_import24.png',
|
||||
scale: 'medium',
|
||||
});
|
||||
|
||||
this.ProgressFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Info'),
|
||||
autoHeight: true,
|
||||
defaultType: 'progress',
|
||||
style: 'margin-top: 1px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
autoWidth: true,
|
||||
labelWidth: 0,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
this.downProgBar = this.ProgressFset.add({
|
||||
fieldLabel: _(''),
|
||||
name: 'progress_bar',
|
||||
width: '90%',
|
||||
});
|
||||
|
||||
this.InfoFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Info'),
|
||||
autoHeight: true,
|
||||
defaultType: 'label',
|
||||
style: 'margin-top: 0px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
labelWidth: 60,
|
||||
});
|
||||
|
||||
this.lblFileSize = this.InfoFset.add({
|
||||
fieldLabel: _('File Size:'),
|
||||
labelSeparator: '',
|
||||
name: 'file_size',
|
||||
});
|
||||
|
||||
this.lblDate = this.InfoFset.add({
|
||||
fieldLabel: _('Date:'),
|
||||
labelSeparator: '',
|
||||
name: 'date',
|
||||
});
|
||||
|
||||
this.lblType = this.InfoFset.add({
|
||||
fieldLabel: _('Type:'),
|
||||
labelSeparator: '',
|
||||
name: 'type',
|
||||
});
|
||||
|
||||
this.lblURL = this.InfoFset.add({
|
||||
fieldLabel: _('URL:'),
|
||||
labelSeparator: '',
|
||||
name: 'lbl_URL',
|
||||
});
|
||||
|
||||
this.WhitelistFset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Whitelist'),
|
||||
autoHeight: true,
|
||||
defaultType: 'editorgrid',
|
||||
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
|
||||
autoWidth: true,
|
||||
labelWidth: 0,
|
||||
items: [
|
||||
{
|
||||
fieldLabel: _(''),
|
||||
name: 'whitelist',
|
||||
margins: '2 0 5 5',
|
||||
height: 100,
|
||||
width: 260,
|
||||
autoExpandColumn: 'ip',
|
||||
viewConfig: {
|
||||
emptyText: _('Add an IP...'),
|
||||
deferEmptyText: false,
|
||||
},
|
||||
colModel: new Ext.grid.ColumnModel({
|
||||
columns: [
|
||||
{
|
||||
id: 'ip',
|
||||
header: _('IP'),
|
||||
dataIndex: 'ip',
|
||||
sortable: true,
|
||||
hideable: false,
|
||||
editable: true,
|
||||
editor: {
|
||||
xtype: 'textfield',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
selModel: new Ext.grid.RowSelectionModel({
|
||||
singleSelect: false,
|
||||
moveEditorOnEnter: false,
|
||||
}),
|
||||
store: new Ext.data.ArrayStore({
|
||||
autoDestroy: true,
|
||||
fields: [{ name: 'ip' }],
|
||||
}),
|
||||
listeners: {
|
||||
afteredit: function (e) {
|
||||
e.record.commit();
|
||||
},
|
||||
},
|
||||
setEmptyText: function (text) {
|
||||
if (this.viewReady) {
|
||||
this.getView().emptyText = text;
|
||||
this.getView().refresh();
|
||||
} else {
|
||||
Ext.apply(this.viewConfig, { emptyText: text });
|
||||
}
|
||||
},
|
||||
loadData: function (data) {
|
||||
this.getStore().loadData(data);
|
||||
if (this.viewReady) {
|
||||
this.getView().updateHeaders();
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.ipButtonsContainer = this.WhitelistFset.add({
|
||||
xtype: 'container',
|
||||
layout: 'hbox',
|
||||
margins: '4 0 0 5',
|
||||
items: [
|
||||
{
|
||||
xtype: 'button',
|
||||
text: ' Add IP ',
|
||||
margins: '0 5 0 0',
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
text: ' Delete IP ',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.updateTask = Ext.TaskMgr.start({
|
||||
interval: 2000,
|
||||
run: this.onUpdate,
|
||||
scope: this,
|
||||
});
|
||||
|
||||
this.on('show', this.updateConfig, this);
|
||||
|
||||
this.ipButtonsContainer.getComponent(0).setHandler(this.addIP, this);
|
||||
this.ipButtonsContainer.getComponent(1).setHandler(this.deleteIP, this);
|
||||
|
||||
this.checkDownload.getComponent(0).setHandler(this.checkDown, this);
|
||||
this.forceDownload.setHandler(this.forceDown, this);
|
||||
},
|
||||
|
||||
onApply: function () {
|
||||
var config = {};
|
||||
|
||||
config['url'] = this.URL.getValue();
|
||||
config['check_after_days'] = this.checkListDays.getValue();
|
||||
config['load_on_start'] = this.chkImportOnStart.getValue();
|
||||
|
||||
var ipList = [];
|
||||
var store = this.WhitelistFset.getComponent(0).getStore();
|
||||
|
||||
for (var i = 0; i < store.getCount(); i++) {
|
||||
var record = store.getAt(i);
|
||||
var ip = record.get('ip');
|
||||
ipList.push(ip);
|
||||
}
|
||||
|
||||
config['whitelisted'] = ipList;
|
||||
|
||||
deluge.client.blocklist.set_config(config);
|
||||
},
|
||||
|
||||
onOk: function () {
|
||||
this.onApply();
|
||||
},
|
||||
|
||||
onUpdate: function () {
|
||||
deluge.client.blocklist.get_status({
|
||||
success: function (status) {
|
||||
if (status['state'] == 'Downloading') {
|
||||
this.InfoFset.hide();
|
||||
this.checkDownload.getComponent(0).setDisabled(true);
|
||||
this.checkDownload.getComponent(1).hide();
|
||||
this.forceDownload.setDisabled(true);
|
||||
|
||||
this.ProgressFset.show();
|
||||
this.downProgBar.updateProgress(
|
||||
status['file_progress'],
|
||||
'Downloading '
|
||||
.concat((status['file_progress'] * 100).toFixed(2))
|
||||
.concat('%'),
|
||||
true
|
||||
);
|
||||
} else if (status['state'] == 'Importing') {
|
||||
this.InfoFset.hide();
|
||||
this.checkDownload.getComponent(0).setDisabled(true);
|
||||
this.checkDownload.getComponent(1).hide();
|
||||
this.forceDownload.setDisabled(true);
|
||||
|
||||
this.ProgressFset.show();
|
||||
this.downProgBar.updateText(
|
||||
'Importing '.concat(status['num_blocked'])
|
||||
);
|
||||
} else if (status['state'] == 'Idle') {
|
||||
this.ProgressFset.hide();
|
||||
this.checkDownload.getComponent(0).setDisabled(false);
|
||||
this.forceDownload.setDisabled(false);
|
||||
if (status['up_to_date']) {
|
||||
this.checkDownload.getComponent(1).show();
|
||||
this.checkDownload.doLayout();
|
||||
} else {
|
||||
this.checkDownload.getComponent(1).hide();
|
||||
}
|
||||
this.InfoFset.show();
|
||||
this.lblFileSize.setText(fsize(status['file_size']));
|
||||
this.lblDate.setText(fdate(status['file_date']));
|
||||
this.lblType.setText(status['file_type']);
|
||||
this.lblURL.setText(
|
||||
status['file_url'].substr(0, 40).concat('...')
|
||||
);
|
||||
}
|
||||
},
|
||||
scope: this,
|
||||
});
|
||||
},
|
||||
|
||||
checkDown: function () {
|
||||
this.onApply();
|
||||
deluge.client.blocklist.check_import();
|
||||
},
|
||||
|
||||
forceDown: function () {
|
||||
this.onApply();
|
||||
deluge.client.blocklist.check_import((force = true));
|
||||
},
|
||||
|
||||
updateConfig: function () {
|
||||
deluge.client.blocklist.get_config({
|
||||
success: function (config) {
|
||||
this.URL.setValue(config['url']);
|
||||
this.checkListDays.setValue(config['check_after_days']);
|
||||
this.chkImportOnStart.setValue(config['load_on_start']);
|
||||
|
||||
var data = [];
|
||||
var keys = Ext.keys(config['whitelisted']);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
data.push([config['whitelisted'][key]]);
|
||||
}
|
||||
|
||||
this.WhitelistFset.getComponent(0).loadData(data);
|
||||
},
|
||||
scope: this,
|
||||
});
|
||||
|
||||
deluge.client.blocklist.get_status({
|
||||
success: function (status) {
|
||||
this.lblFileSize.setText(fsize(status['file_size']));
|
||||
this.lblDate.setText(fdate(status['file_date']));
|
||||
this.lblType.setText(status['file_type']);
|
||||
this.lblURL.setText(
|
||||
status['file_url'].substr(0, 40).concat('...')
|
||||
);
|
||||
},
|
||||
scope: this,
|
||||
});
|
||||
},
|
||||
|
||||
addIP: function () {
|
||||
var store = this.WhitelistFset.getComponent(0).getStore();
|
||||
var IP = store.recordType;
|
||||
var i = new IP({
|
||||
ip: '',
|
||||
});
|
||||
this.WhitelistFset.getComponent(0).stopEditing();
|
||||
store.insert(0, i);
|
||||
this.WhitelistFset.getComponent(0).startEditing(0, 0);
|
||||
},
|
||||
|
||||
deleteIP: function () {
|
||||
var selections = this.WhitelistFset.getComponent(0)
|
||||
.getSelectionModel()
|
||||
.getSelections();
|
||||
var store = this.WhitelistFset.getComponent(0).getStore();
|
||||
|
||||
this.WhitelistFset.getComponent(0).stopEditing();
|
||||
for (var i = 0; i < selections.length; i++) store.remove(selections[i]);
|
||||
store.commitChanges();
|
||||
},
|
||||
|
||||
onDestroy: function () {
|
||||
Ext.TaskMgr.stop(this.updateTask);
|
||||
|
||||
deluge.preferences.un('show', this.updateConfig, this);
|
||||
|
||||
Deluge.ux.preferences.BlocklistPage.superclass.onDestroy.call(this);
|
||||
},
|
||||
});
|
||||
|
||||
Deluge.plugins.BlocklistPlugin = Ext.extend(Deluge.Plugin, {
|
||||
name: 'Blocklist',
|
||||
|
||||
onDisable: function () {
|
||||
deluge.preferences.removePage(this.prefsPage);
|
||||
},
|
||||
|
||||
onEnable: function () {
|
||||
this.prefsPage = deluge.preferences.addPage(
|
||||
new Deluge.ux.preferences.BlocklistPage()
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Deluge.registerPlugin('Blocklist', Deluge.plugins.BlocklistPlugin);
|
||||
+635
@@ -0,0 +1,635 @@
|
||||
/**
|
||||
* label.js
|
||||
*
|
||||
* Copyright (C) Damien Churchill 2010 <damoxc@gmail.com>
|
||||
*
|
||||
* This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
|
||||
* the additional special exception to link portions of this program with the OpenSSL library.
|
||||
* See LICENSE for more details.
|
||||
*
|
||||
*/
|
||||
|
||||
Ext.ns('Deluge.ux.preferences');
|
||||
|
||||
/**
|
||||
* @class Deluge.ux.preferences.LabelPage
|
||||
* @extends Ext.Panel
|
||||
*/
|
||||
Deluge.ux.preferences.LabelPage = Ext.extend(Ext.Panel, {
|
||||
title: _('Label'),
|
||||
layout: 'fit',
|
||||
border: false,
|
||||
|
||||
initComponent: function () {
|
||||
Deluge.ux.preferences.LabelPage.superclass.initComponent.call(this);
|
||||
fieldset = this.add({
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
title: _('Label Preferences'),
|
||||
autoHeight: true,
|
||||
labelWidth: 1,
|
||||
defaultType: 'panel',
|
||||
});
|
||||
fieldset.add({
|
||||
border: false,
|
||||
bodyCfg: {
|
||||
html: _(
|
||||
'<p>The Label plugin is enabled.</p><br>' +
|
||||
'<p>To add, remove or edit labels right-click on the Label filter ' +
|
||||
'entry in the sidebar.</p><br>' +
|
||||
'<p>To apply a label right-click on torrent(s).<p>'
|
||||
),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Ext.ns('Deluge.ux');
|
||||
|
||||
/**
|
||||
* @class Deluge.ux.AddLabelWindow
|
||||
* @extends Ext.Window
|
||||
*/
|
||||
Deluge.ux.AddLabelWindow = Ext.extend(Ext.Window, {
|
||||
title: _('Add Label'),
|
||||
width: 300,
|
||||
height: 100,
|
||||
closeAction: 'hide',
|
||||
|
||||
initComponent: function () {
|
||||
Deluge.ux.AddLabelWindow.superclass.initComponent.call(this);
|
||||
this.addButton(_('Cancel'), this.onCancelClick, this);
|
||||
this.addButton(_('Ok'), this.onOkClick, this);
|
||||
|
||||
this.form = this.add({
|
||||
xtype: 'form',
|
||||
height: 35,
|
||||
baseCls: 'x-plain',
|
||||
bodyStyle: 'padding:5px 5px 0',
|
||||
defaultType: 'textfield',
|
||||
labelWidth: 50,
|
||||
items: [
|
||||
{
|
||||
fieldLabel: _('Name'),
|
||||
name: 'name',
|
||||
allowBlank: false,
|
||||
width: 220,
|
||||
listeners: {
|
||||
specialkey: {
|
||||
fn: function (field, e) {
|
||||
if (e.getKey() == 13) this.onOkClick();
|
||||
},
|
||||
scope: this,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
onCancelClick: function () {
|
||||
this.hide();
|
||||
},
|
||||
|
||||
onOkClick: function () {
|
||||
var label = this.form.getForm().getValues().name;
|
||||
deluge.client.label.add(label, {
|
||||
success: function () {
|
||||
deluge.ui.update();
|
||||
this.fireEvent('labeladded', label);
|
||||
},
|
||||
scope: this,
|
||||
});
|
||||
this.hide();
|
||||
},
|
||||
|
||||
onHide: function (comp) {
|
||||
Deluge.ux.AddLabelWindow.superclass.onHide.call(this, comp);
|
||||
this.form.getForm().reset();
|
||||
},
|
||||
|
||||
onShow: function (comp) {
|
||||
Deluge.ux.AddLabelWindow.superclass.onShow.call(this, comp);
|
||||
this.form.getForm().findField('name').focus(false, 150);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @class Deluge.ux.LabelOptionsWindow
|
||||
* @extends Ext.Window
|
||||
*/
|
||||
Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
|
||||
title: _('Label Options'),
|
||||
width: 325,
|
||||
height: 240,
|
||||
closeAction: 'hide',
|
||||
|
||||
initComponent: function () {
|
||||
Deluge.ux.LabelOptionsWindow.superclass.initComponent.call(this);
|
||||
this.addButton(_('Cancel'), this.onCancelClick, this);
|
||||
this.addButton(_('Ok'), this.onOkClick, this);
|
||||
|
||||
this.form = this.add({
|
||||
xtype: 'form',
|
||||
});
|
||||
|
||||
this.tabs = this.form.add({
|
||||
xtype: 'tabpanel',
|
||||
height: 175,
|
||||
border: false,
|
||||
items: [
|
||||
{
|
||||
title: _('Maximum'),
|
||||
items: [
|
||||
{
|
||||
border: false,
|
||||
items: [
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||
items: [
|
||||
{
|
||||
xtype: 'checkbox',
|
||||
name: 'apply_max',
|
||||
fieldLabel: '',
|
||||
boxLabel: _(
|
||||
'Apply per torrent max settings:'
|
||||
),
|
||||
listeners: {
|
||||
check: this.onFieldChecked,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
defaultType: 'spinnerfield',
|
||||
style: 'margin-top: 0px; padding-top: 0px;',
|
||||
items: [
|
||||
{
|
||||
fieldLabel: _('Download Speed'),
|
||||
name: 'max_download_speed',
|
||||
width: 80,
|
||||
disabled: true,
|
||||
value: -1,
|
||||
minValue: -1,
|
||||
},
|
||||
{
|
||||
fieldLabel: _('Upload Speed'),
|
||||
name: 'max_upload_speed',
|
||||
width: 80,
|
||||
disabled: true,
|
||||
value: -1,
|
||||
minValue: -1,
|
||||
},
|
||||
{
|
||||
fieldLabel: _('Upload Slots'),
|
||||
name: 'max_upload_slots',
|
||||
width: 80,
|
||||
disabled: true,
|
||||
value: -1,
|
||||
minValue: -1,
|
||||
},
|
||||
{
|
||||
fieldLabel: _('Connections'),
|
||||
name: 'max_connections',
|
||||
width: 80,
|
||||
disabled: true,
|
||||
value: -1,
|
||||
minValue: -1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: _('Queue'),
|
||||
items: [
|
||||
{
|
||||
border: false,
|
||||
items: [
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||
items: [
|
||||
{
|
||||
xtype: 'checkbox',
|
||||
name: 'apply_queue',
|
||||
fieldLabel: '',
|
||||
boxLabel: _(
|
||||
'Apply queue settings:'
|
||||
),
|
||||
listeners: {
|
||||
check: this.onFieldChecked,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
defaultType: 'checkbox',
|
||||
style: 'margin-top: 0px; padding-top: 0px;',
|
||||
defaults: {
|
||||
style: 'margin-left: 20px',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
boxLabel: _('Auto Managed'),
|
||||
name: 'is_auto_managed',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
boxLabel: _('Stop seed at ratio:'),
|
||||
name: 'stop_at_ratio',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
xtype: 'spinnerfield',
|
||||
name: 'stop_ratio',
|
||||
width: 60,
|
||||
decimalPrecision: 2,
|
||||
incrementValue: 0.1,
|
||||
style: 'position: relative; left: 100px',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
boxLabel: _('Remove at ratio'),
|
||||
name: 'remove_at_ratio',
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: _('Folders'),
|
||||
items: [
|
||||
{
|
||||
border: false,
|
||||
items: [
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||
items: [
|
||||
{
|
||||
xtype: 'checkbox',
|
||||
name: 'apply_move_completed',
|
||||
fieldLabel: '',
|
||||
boxLabel: _(
|
||||
'Apply folder settings:'
|
||||
),
|
||||
listeners: {
|
||||
check: this.onFieldChecked,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
defaultType: 'checkbox',
|
||||
labelWidth: 1,
|
||||
style: 'margin-top: 0px; padding-top: 0px;',
|
||||
defaults: {
|
||||
style: 'margin-left: 20px',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
boxLabel: _('Move completed to:'),
|
||||
name: 'move_completed',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
xtype: 'textfield',
|
||||
name: 'move_completed_path',
|
||||
width: 250,
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: _('Trackers'),
|
||||
items: [
|
||||
{
|
||||
border: false,
|
||||
items: [
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||
items: [
|
||||
{
|
||||
xtype: 'checkbox',
|
||||
name: 'auto_add',
|
||||
fieldLabel: '',
|
||||
boxLabel: _(
|
||||
'Automatically apply label:'
|
||||
),
|
||||
listeners: {
|
||||
check: this.onFieldChecked,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'fieldset',
|
||||
border: false,
|
||||
labelWidth: 1,
|
||||
style: 'margin-top: 0px; padding-top: 0px;',
|
||||
defaults: {
|
||||
style: 'margin-left: 20px',
|
||||
},
|
||||
defaultType: 'textarea',
|
||||
items: [
|
||||
{
|
||||
boxLabel: _('Move completed to:'),
|
||||
name: 'auto_add_trackers',
|
||||
width: 250,
|
||||
height: 100,
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
getLabelOptions: function () {
|
||||
deluge.client.label.get_options(this.label, {
|
||||
success: this.gotOptions,
|
||||
scope: this,
|
||||
});
|
||||
},
|
||||
|
||||
gotOptions: function (options) {
|
||||
this.form.getForm().setValues(options);
|
||||
},
|
||||
|
||||
show: function (label) {
|
||||
Deluge.ux.LabelOptionsWindow.superclass.show.call(this);
|
||||
this.label = label;
|
||||
this.setTitle(_('Label Options') + ': ' + this.label);
|
||||
this.tabs.setActiveTab(0);
|
||||
this.getLabelOptions();
|
||||
},
|
||||
|
||||
onCancelClick: function () {
|
||||
this.hide();
|
||||
},
|
||||
|
||||
onOkClick: function () {
|
||||
var values = this.form.getForm().getFieldValues();
|
||||
if (values['auto_add_trackers']) {
|
||||
values['auto_add_trackers'] =
|
||||
values['auto_add_trackers'].split('\n');
|
||||
}
|
||||
deluge.client.label.set_options(this.label, values);
|
||||
this.hide();
|
||||
},
|
||||
|
||||
onFieldChecked: function (field, checked) {
|
||||
var fs = field.ownerCt.nextSibling();
|
||||
fs.items.each(function (field) {
|
||||
field.setDisabled(!checked);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Ext.ns('Deluge.plugins');
|
||||
|
||||
/**
|
||||
* @class Deluge.plugins.LabelPlugin
|
||||
* @extends Deluge.Plugin
|
||||
*/
|
||||
Deluge.plugins.LabelPlugin = Ext.extend(Deluge.Plugin, {
|
||||
name: 'Label',
|
||||
|
||||
createMenu: function () {
|
||||
this.labelMenu = new Ext.menu.Menu({
|
||||
items: [
|
||||
{
|
||||
text: _('Add Label'),
|
||||
iconCls: 'icon-add',
|
||||
handler: this.onLabelAddClick,
|
||||
scope: this,
|
||||
},
|
||||
{
|
||||
text: _('Remove Label'),
|
||||
disabled: true,
|
||||
iconCls: 'icon-remove',
|
||||
handler: this.onLabelRemoveClick,
|
||||
scope: this,
|
||||
},
|
||||
{
|
||||
text: _('Label Options'),
|
||||
disabled: true,
|
||||
handler: this.onLabelOptionsClick,
|
||||
scope: this,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
setFilter: function (filter) {
|
||||
filter.show_zero = true;
|
||||
|
||||
filter.list.on('contextmenu', this.onLabelContextMenu, this);
|
||||
filter.header.on('contextmenu', this.onLabelHeaderContextMenu, this);
|
||||
this.filter = filter;
|
||||
},
|
||||
|
||||
updateTorrentMenu: function (states) {
|
||||
this.torrentMenu.removeAll(true);
|
||||
this.torrentMenu.addMenuItem({
|
||||
text: _('No Label'),
|
||||
label: '',
|
||||
handler: this.onTorrentMenuClick,
|
||||
scope: this,
|
||||
});
|
||||
for (var state in states) {
|
||||
if (!state || state == 'All') continue;
|
||||
this.torrentMenu.addMenuItem({
|
||||
text: state,
|
||||
label: state,
|
||||
handler: this.onTorrentMenuClick,
|
||||
scope: this,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onDisable: function () {
|
||||
deluge.sidebar.un('filtercreate', this.onFilterCreate);
|
||||
deluge.sidebar.un('afterfiltercreate', this.onAfterFilterCreate);
|
||||
delete Deluge.FilterPanel.templates.label;
|
||||
this.deregisterTorrentStatus('label');
|
||||
deluge.menus.torrent.remove(this.tmSep);
|
||||
deluge.menus.torrent.remove(this.tm);
|
||||
deluge.preferences.removePage(this.prefsPage);
|
||||
},
|
||||
|
||||
onEnable: function () {
|
||||
this.prefsPage = deluge.preferences.addPage(
|
||||
new Deluge.ux.preferences.LabelPage()
|
||||
);
|
||||
this.torrentMenu = new Ext.menu.Menu();
|
||||
|
||||
this.tmSep = deluge.menus.torrent.add({
|
||||
xtype: 'menuseparator',
|
||||
});
|
||||
|
||||
this.tm = deluge.menus.torrent.add({
|
||||
text: _('Label'),
|
||||
menu: this.torrentMenu,
|
||||
});
|
||||
|
||||
var lbltpl =
|
||||
'<div class="x-deluge-filter">' +
|
||||
'<tpl if="filter">{filter}</tpl>' +
|
||||
'<tpl if="!filter">No Label</tpl>' +
|
||||
' ({count})' +
|
||||
'</div>';
|
||||
|
||||
if (deluge.sidebar.hasFilter('label')) {
|
||||
var filter = deluge.sidebar.getFilter('label');
|
||||
filter.list.columns[0].tpl = new Ext.XTemplate(lbltpl);
|
||||
this.setFilter(filter);
|
||||
this.updateTorrentMenu(filter.getStates());
|
||||
filter.list.refresh();
|
||||
} else {
|
||||
deluge.sidebar.on('filtercreate', this.onFilterCreate, this);
|
||||
deluge.sidebar.on(
|
||||
'afterfiltercreate',
|
||||
this.onAfterFilterCreate,
|
||||
this
|
||||
);
|
||||
Deluge.FilterPanel.templates.label = lbltpl;
|
||||
}
|
||||
this.registerTorrentStatus('label', _('Label'));
|
||||
},
|
||||
|
||||
onAfterFilterCreate: function (sidebar, filter) {
|
||||
if (filter.filter != 'label') return;
|
||||
this.updateTorrentMenu(filter.getStates());
|
||||
},
|
||||
|
||||
onFilterCreate: function (sidebar, filter) {
|
||||
if (filter.filter != 'label') return;
|
||||
this.setFilter(filter);
|
||||
},
|
||||
|
||||
onLabelAddClick: function () {
|
||||
if (!this.addWindow) {
|
||||
this.addWindow = new Deluge.ux.AddLabelWindow();
|
||||
this.addWindow.on('labeladded', this.onLabelAdded, this);
|
||||
}
|
||||
this.addWindow.show();
|
||||
},
|
||||
|
||||
onLabelAdded: function (label) {
|
||||
var filter = deluge.sidebar.getFilter('label');
|
||||
var states = filter.getStates();
|
||||
var statesArray = [];
|
||||
|
||||
for (state in states) {
|
||||
if (!state || state == 'All') continue;
|
||||
statesArray.push(state);
|
||||
}
|
||||
|
||||
statesArray.push(label.toLowerCase());
|
||||
statesArray.sort();
|
||||
|
||||
//console.log(states);
|
||||
//console.log(statesArray);
|
||||
|
||||
states = {};
|
||||
|
||||
for (i = 0; i < statesArray.length; ++i) {
|
||||
states[statesArray[i]] = 0;
|
||||
}
|
||||
|
||||
this.updateTorrentMenu(states);
|
||||
},
|
||||
|
||||
onLabelContextMenu: function (dv, i, node, e) {
|
||||
e.preventDefault();
|
||||
if (!this.labelMenu) this.createMenu();
|
||||
var r = dv.getRecord(node).get('filter');
|
||||
if (!r || r == 'All') {
|
||||
this.labelMenu.items.get(1).setDisabled(true);
|
||||
this.labelMenu.items.get(2).setDisabled(true);
|
||||
} else {
|
||||
this.labelMenu.items.get(1).setDisabled(false);
|
||||
this.labelMenu.items.get(2).setDisabled(false);
|
||||
}
|
||||
dv.select(i);
|
||||
this.labelMenu.showAt(e.getXY());
|
||||
},
|
||||
|
||||
onLabelHeaderContextMenu: function (e, t) {
|
||||
e.preventDefault();
|
||||
if (!this.labelMenu) this.createMenu();
|
||||
this.labelMenu.items.get(1).setDisabled(true);
|
||||
this.labelMenu.items.get(2).setDisabled(true);
|
||||
this.labelMenu.showAt(e.getXY());
|
||||
},
|
||||
|
||||
onLabelOptionsClick: function () {
|
||||
if (!this.labelOpts)
|
||||
this.labelOpts = new Deluge.ux.LabelOptionsWindow();
|
||||
this.labelOpts.show(this.filter.getState());
|
||||
},
|
||||
|
||||
onLabelRemoveClick: function () {
|
||||
var state = this.filter.getState();
|
||||
deluge.client.label.remove(state, {
|
||||
success: function () {
|
||||
deluge.ui.update();
|
||||
this.torrentMenu.items.each(function (item) {
|
||||
if (item.text != state) return;
|
||||
this.torrentMenu.remove(item);
|
||||
var i = item;
|
||||
}, this);
|
||||
},
|
||||
scope: this,
|
||||
});
|
||||
},
|
||||
|
||||
onTorrentMenuClick: function (item, e) {
|
||||
var ids = deluge.torrents.getSelectedIds();
|
||||
Ext.each(ids, function (id, i) {
|
||||
if (ids.length == i + 1) {
|
||||
deluge.client.label.set_torrent(id, item.label, {
|
||||
success: function () {
|
||||
deluge.ui.update();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
deluge.client.label.set_torrent(id, item.label);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
Deluge.registerPlugin('Label', Deluge.plugins.LabelPlugin);
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICpDCCAYwCAQAwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNRGVsdWdlIERh
|
||||
ZW1vbjAeFw0yNDExMTQxMjI0MjVaFw0yNzExMTQxMjI0MjVaMBgxFjAUBgNVBAMM
|
||||
DURlbHVnZSBEYWVtb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDk
|
||||
hV3jiOW40yERiQ6F0GgsM8doBRaCzdJ5du7JRKY0bxNzpkgsmjJp8HFljeROPbOl
|
||||
plyWzJuol02sQ5WlcnUppPZCFlm3Hnw4wdsM2F7n4MC3i2M8/M73pIBbXw/7Ekro
|
||||
ZijmS02DT3o6c4Urdh89w3GRs6MWESikBdzTDAVPV8REASAfoI1JVUFznxqEMysx
|
||||
H9ANqdlkO0sMnBvOFvNxAyuVMOwCUEFsw7ynutJB/yrMUk1itoX21CigOH+pkNDe
|
||||
JnfIKRa6BvU4aLCFGynAR3bk7TcwRiPoIiWPmwxktFVc+sr26fuGWd8KSPjOJZGV
|
||||
+WZjYAqtiZRFX67VgAf1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAC3vWaRkzIue
|
||||
9onnmcpv0uXPNDANefAL4B9sPVmWVdBrWGJTGm9umWZGOa2Z1VRGaC8+LpHJ074f
|
||||
gmv/PE61GxL5usmfRRLJK4ZKIPzoIspqVKfuWJH6kbsAzg3x43RF0WvokJQKC+Y/
|
||||
tWY8a6ewJ1Uh1YDJEwxgR+WBguN64w4QdujPGoDnAWAEW7VJsc2PlYzYConpwKXy
|
||||
RUOpcnZnAV3z98zRU6m0G/RyYZF8H00hXsEitDeuh5Kdu1tLCbhUYvVXxB6BY559
|
||||
bJ+DrxblqpM71wamFq9MDv+Z78XgGZFymoLLLRV3gBO1RsKVnl81Ywvo2LMkRp52
|
||||
XuvUJJPD6po=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDkhV3jiOW40yER
|
||||
iQ6F0GgsM8doBRaCzdJ5du7JRKY0bxNzpkgsmjJp8HFljeROPbOlplyWzJuol02s
|
||||
Q5WlcnUppPZCFlm3Hnw4wdsM2F7n4MC3i2M8/M73pIBbXw/7EkroZijmS02DT3o6
|
||||
c4Urdh89w3GRs6MWESikBdzTDAVPV8REASAfoI1JVUFznxqEMysxH9ANqdlkO0sM
|
||||
nBvOFvNxAyuVMOwCUEFsw7ynutJB/yrMUk1itoX21CigOH+pkNDeJnfIKRa6BvU4
|
||||
aLCFGynAR3bk7TcwRiPoIiWPmwxktFVc+sr26fuGWd8KSPjOJZGV+WZjYAqtiZRF
|
||||
X67VgAf1AgMBAAECggEAA4T6ToghNu4pfYz60vIZaJ+I2//tZNOpVi2QEpF4GH6i
|
||||
x2PcNgj56x/E31Ixc0hdUpkeppk9cc9CvFBzfDooYR0lSHHyV8ZPFiCw2vXKIGVv
|
||||
EmSXLAKeErpPL8O7CfGHgyTE+dGsaUVOwJpe3AMptVh5O0vk9cYLNjDQ7H8sOxiQ
|
||||
0uCTu7JyKRVOtmp9EFy/KqnHPaGVFuNmQH5byiDhuHWFC3lbC2QeYrlhMnPv38jw
|
||||
NVuUI10E+ZlJJuhuSOwTdKTj0XxhtvMclsbkXOCGm4nL13EYqcyrTiHFbxDCV5c3
|
||||
V33xmtH+ABvdvF/68Ouk6ph1BRLTpAW/UmURcUvpUQKBgQD3O2clEmBac3yA487T
|
||||
/uBhqId5JU5vAYltH2Cfs46aaDUjHe4QshuMPUiyyiZgFh2oc2JXbmDgbXkyrkyq
|
||||
2K5sOcixKef/eoAIkT+o2Nd8PDMpApF0AcqyWAe0xNR0thWJvjdMon4aFHHZoCcW
|
||||
+zyWYB8cxVdZCcPnnykpvJgSdwKBgQDsoBZxrq9Ta3S0Ho7NEYu4b6sh9rCfXCU7
|
||||
94+eeWPc6+oO+jQDtIS+RYWKwOKmqMzhzq1MdTl9+2yGTCa1st05gsyUpEpso2r5
|
||||
BlHUVBzrDEMhRN5FqOcKuRZ+G2HMAsTxJbVZnOS9Z0mf5nwCvJxs8jUEPbFDWQVr
|
||||
AcKRApTH8wKBgQDw3T3TH0EiPks5Izh4z2L5ogBCZbcxbNTfrGctj/jJs+a5DMrI
|
||||
F03BZl9yWIHksQc5+xf/SDk3zU/7sVZeSHY+WFmPSN2OyGD+d8wGiyP9FIVfWfIt
|
||||
jCVXdW4kjnLSNidrqBcmIVUrwWld9aq/uAtCEemd1SERTPNAsI6g6+1YZwKBgQCL
|
||||
P55Voi4NElRoVv9EUMn/bL+xygGgllJXGtWKtfb9oFtqGvWXJJlle3Yd9GqtFvMT
|
||||
A1RahTWjHN19nry8+phTatTHuHMPwY+HIp/vKtylud6banK/XakxV0CUT7ramtqY
|
||||
6s7xAHJfv7PFBJb/6UzIlDR83W0+q9mTYkLEoVc63wKBgCV2ok/C7+7e189gxUir
|
||||
3ezxBlY9Cv+1/wIE5IjAwpmOPPzsZMKZ2SbDxJMoDBXACJtgOEleiUSYss9qkhVt
|
||||
o0kUkEOWPM3vydRLCLcsdfpM826WAmGA/w6MsqWyZDoc/kw7UhEBqMhQVXzc/cGF
|
||||
gRXRDfyZj4MpvAOBPTlTWbme
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,2 @@
|
||||
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731591387e4:infod6:lengthi6e4:name97:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos.H.265-VARYG.mkv.zipx12:piece lengthi262144e6:pieces20:ýçs¡‹²Ÿ^Ö^o
|
||||
z§ý¤…Ôee
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,590 @@
|
||||
{
|
||||
"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": {
|
||||
"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,
|
||||
"sidebar_multiple_filters": true,
|
||||
"sidebar_show_zero": false,
|
||||
"theme": "gray"
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
146
|
||||
145
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731398141e4:infod6:lengthi4e4:name68:Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB.mkv.zipx12:piece lengthi262144e6:pieces20:©J�å̱›¦Lsӑ釘/»Óee
|
||||
@@ -0,0 +1 @@
|
||||
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931728e4:infod5:filesld6:lengthi2604e4:pathl4:Dir15:Dir1111:test11.zipxeed6:lengthi2604e4:pathl4:Dir110:sample.txteed6:lengthi2604e4:pathl4:Dir210:test2.zipxeed6:lengthi2604e4:pathl68:Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB.mkv.zipxeed6:lengthi2604e4:pathl9:test.zipxeee4:name59:Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB12:piece lengthi262144e6:pieces20:w¤ŸÌ³RÇþ'6Fíoð}ä°ee
|
||||
@@ -1,7 +1,7 @@
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Test feed</title>
|
||||
<link>http://nginx/custom/radarr_bad.xml</link>
|
||||
<link>http://nginx/custom/radarr_bad_nested.xml</link>
|
||||
<description>
|
||||
Test
|
||||
</description>
|
||||
@@ -15,7 +15,7 @@
|
||||
<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.torrent</link>
|
||||
<link>http://nginx/custom/radarr_bad_nested.torrent</link>
|
||||
<guid isPermaLink="false">
|
||||
174674a88c8947f6f9057ac3f81efde384ed216cade43564ec450f2cb4677554
|
||||
</guid>
|
||||
@@ -0,0 +1 @@
|
||||
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931618e4:infod6:lengthi2604e4:name70:The.Wild.Robot.2024.2160p.AMZN.WEB-DL.DDP5.1.Atmos.H.265-FLUX.mkv.zipx12:piece lengthi262144e6:pieces20:ûù@;9fS” â›E:¯Iá1ee
|
||||
@@ -0,0 +1,25 @@
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Test feed</title>
|
||||
<link>http://nginx/custom/radarr_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>The.Wild.Robot.2024.2160p.AMZN.WEB-DL.DDP5.1.Atmos.H.265-FLUX</title>
|
||||
<description>Test</description>
|
||||
<size>4138858110</size>
|
||||
<link>http://nginx/custom/radarr_bad_single.torrent</link>
|
||||
<guid isPermaLink="false">
|
||||
174674a88c8947f6f9057ac3f81efde384ed216cade43564ec450f2cb4677554
|
||||
</guid>
|
||||
<pubDate>Sat, 24 Sep 2022 22:02:13 -0300</pubDate>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -1 +0,0 @@
|
||||
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731357387e4:infod6:lengthi5e4:name93:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos.H.265-VARYG.zipx12:piece lengthi262144e6:pieces20:NC½"ÆnvºžÝÁù”埃ee
|
||||
@@ -0,0 +1 @@
|
||||
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931738e4:infod5:filesld6:lengthi2604e4:pathl89:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos. - Copy.zipxeed6:lengthi2604e4:pathl4:Dir15:Dir1111:test11.zipxeed6:lengthi2604e4:pathl4:Dir110:sample.txteed6:lengthi2604e4:pathl4:Dir210:test2.zipxeed6:lengthi2604e4:pathl9:test.zipxeee4:name88:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos.H.265-VARYG12:piece lengthi262144e6:pieces20:w¤ŸÌ³RÇþ'6Fíoð}ä°ee
|
||||
@@ -1,7 +1,7 @@
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Test feed</title>
|
||||
<link>http://nginx/custom/sonarr_bad.xml</link>
|
||||
<link>http://nginx/custom/sonarr_bad_nested.xml</link>
|
||||
<description>
|
||||
Test
|
||||
</description>
|
||||
@@ -15,7 +15,7 @@
|
||||
<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.torrent</link>
|
||||
<link>http://nginx/custom/sonarr_bad_nested.torrent</link>
|
||||
<guid isPermaLink="false">
|
||||
174674a88c8947f6f9057ac3f81efde384ed216cade43564ec450f2cb4677554
|
||||
</guid>
|
||||
@@ -0,0 +1 @@
|
||||
d8:announce28:http://tracker:6969/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931604e4:infod6:lengthi2604e4:name126:Agatha.All.Along.S01E02.Circle.Sewn.With.Fate.Unlock.Thy.Hidden.Gate.2160p.DSNP.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX.mkv.zipx12:piece lengthi262144e6:pieces20:ûù@;9fS” â›E:¯Iá1ee
|
||||
@@ -0,0 +1,25 @@
|
||||
<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>
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
d10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931618e4:infod6:lengthi2604e4:name70:The.Wild.Robot.2024.2160p.AMZN.WEB-DL.DDP5.1.Atmos.H.265-FLUX.mkv.zipx12:piece lengthi262144e6:pieces20:ûù@;9fS” â›E:¯Iá1ee
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
d10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931738e4:infod5:filesld6:lengthi2604e4:pathl89:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos. - Copy.zipxeed6:lengthi2604e4:pathl4:Dir15:Dir1111:test11.zipxeed6:lengthi2604e4:pathl4:Dir110:sample.txteed6:lengthi2604e4:pathl4:Dir210:test2.zipxeed6:lengthi2604e4:pathl9:test.zipxeee4:name88:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos.H.265-VARYG12:piece lengthi262144e6:pieces20:w¤ŸÌ³RÇþ'6Fíoð}ä°ee
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
d10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731357387e4:infod6:lengthi5e4:name93:Agatha.All.Along.S01E01.Seekest.Thou.the.Road.2160p.APPS.WEB-DL.DDP5.1.Atmos.H.265-VARYG.zipx12:piece lengthi262144e6:pieces20:NC½"ÆnvºžÝÁù”埃ee
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
d10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931728e4:infod5:filesld6:lengthi2604e4:pathl4:Dir15:Dir1111:test11.zipxeed6:lengthi2604e4:pathl4:Dir110:sample.txteed6:lengthi2604e4:pathl4:Dir210:test2.zipxeed6:lengthi2604e4:pathl68:Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB.mkv.zipxeed6:lengthi2604e4:pathl9:test.zipxeee4:name59:Speak.No.Evil.2024.2160p.MA.WEB-DL.DDP5.1.Atmos.H.265-HHWEB12:piece lengthi262144e6:pieces20:w¤ŸÌ³RÇþ'6Fíoð}ä°ee
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
d10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1731931604e4:infod6:lengthi2604e4:name126:Agatha.All.Along.S01E02.Circle.Sewn.With.Fate.Unlock.Thy.Hidden.Gate.2160p.DSNP.WEB-DL.DDP5.1.Atmos.DV.HDR.H.265-FLUX.mkv.zipx12:piece lengthi262144e6:pieces20:ûù@;9fS” â›E:¯Iá1ee
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user