Producer / Consumer / Collector rewrite (#160)

* Converted metadata service to redis

* move to postgres instead

* fix global usings

* [skip ci] optimize wolverine by prebuilding static types

* [skip ci] Stop indexing mac folder indexes

* [skip ci] producer, metadata and migrations

removed mongodb
added redis cache
imdb meta in postgres
Enable pgtrm
Create trigrams index
Add search meta postgres function

* [skip ci] get rid of node folder, replace mongo with redis in consumer

also wire up postgres metadata searches

* [skip ci] change mongo to redis in the addon

* [skip ci] jackettio to redis

* Rest of mongo removed...

* Cleaner rerunning of metadata - without conflicts

* Add akas import as well as basic metadata

* Include episodes file too

* cascade truncate pre-import

* reverse order to avoid cascadeing

* separate out clean to separate handler

* Switch producer to use metadata matching pre-preocessing dmm

* More work

* Still porting PTN

* PTN port, adding tests

* [skip ci] Codec tests

* [skip ci] Complete Collection handler tests

* [skip ci] container tests

* [skip ci] Convert handlers tests

* [skip ci] DateHandler tests

* [skip ci] Dual Audio matching tests

* [skip ci] episode code tests

* [skip ci] Extended handler tests

* [skip ci] group handler tests

* [skip ci] some broken stuff right now

* [skip ci] more ptn

* [skip ci] PTN now in a separate nuget package, rebased this on the redis changes - i need them.

* [skip ci] Wire up PTN port. Tired - will test tomorrow

* [skip ci] Needs a lot of work - too many titles being missed now

* cleaner. done?

* Handle the date in the imdb search

- add integer function to confirm its a valid integer
- use the input date as a range of -+1 year

* [skip ci] Start of collector service for RD

[skip ci] WIP

Implemented metadata saga, along with channels to process up to a maximum of 100 infohashes each time
The saga will rety for each infohas by requeuing up to three times, before just marking as complete for that infoHash - meaning no data will be updated in the db for that torrent.

[skip ci] Ready to test with queue publishing

Will provision a fanout exchange if it doesn't exist, and create and bind a queue to it. Listens to the queue with 50 prefetch count.
Still needs PTN rewrite bringing in to parse the filename response from real debrid, and extract season and episode numbers if the file is a tvshow

[skip ci] Add Debrid Collector Build Job

Debrid Collector ready for testing

New consumer, new collector, producer has meta lookup and anti porn measures

[skip ci] WIP - moving from wolverine to MassTransit.

 not happy that wolverine cannot effectively control saga concurrency. we need to really.

[skip ci] Producer and new Consumer moved to MassTransit

Just the debrid collector to go now, then to write the optional qbit collector.

Collector now switched to mass transit too

hide porn titles in logs, clean up cache name in redis for imdb titles

[skip ci] Allow control of queues

[skip ci] Update deployment

Remove old consumer, fix deployment files, fix dockerfiles for shared project import

fix base deployment

* Add collector missing env var

* edits to kick off builds

* Add optional qbit deployment which qbit collector will use

* Qbit collector done

* reorder compose, and bring both qbit and qbitcollector into the compose, with 0 replicas as default

* Clean up compose file

* Ensure debrid collector errors if no debrid api key
This commit is contained in:
iPromKnight
2024-03-25 23:32:28 +00:00
committed by GitHub
parent 9c6c1ac249
commit 9a831e92d0
443 changed files with 4154 additions and 476262 deletions

View File

@@ -0,0 +1,45 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace SharedContracts.Extensions;
public static class ConfigurationExtensions
{
private const string ConfigurationFolder = "Configuration";
private const string LoggingConfig = "logging.json";
public static IConfigurationBuilder AddServiceConfiguration(this IConfigurationBuilder builder)
{
builder.SetBasePath(Path.Combine(AppContext.BaseDirectory, ConfigurationFolder));
builder.AddJsonFile(LoggingConfig, false, true);
builder.AddEnvironmentVariables();
return builder;
}
public static TConfiguration LoadConfigurationFromConfig<TConfiguration>(this IServiceCollection services, IConfiguration configuration, string sectionName)
where TConfiguration : class
{
var instance = configuration.GetSection(sectionName).Get<TConfiguration>();
ArgumentNullException.ThrowIfNull(instance, nameof(instance));
services.TryAddSingleton(instance);
return instance;
}
public static TConfiguration LoadConfigurationFromEnv<TConfiguration>(this IServiceCollection services)
where TConfiguration : class
{
var instance = Activator.CreateInstance<TConfiguration>();
ArgumentNullException.ThrowIfNull(instance, nameof(instance));
services.TryAddSingleton(instance);
return instance;
}
}

View File

@@ -0,0 +1,68 @@
namespace SharedContracts.Extensions;
public static class EnvironmentExtensions
{
public static bool GetEnvironmentVariableAsBool(this string prefix, string varName, bool fallback = false)
{
var fullVarName = GetFullVariableName(prefix, varName);
var str = Environment.GetEnvironmentVariable(fullVarName);
if (string.IsNullOrEmpty(str))
{
return fallback;
}
return str.Trim().ToLower() switch
{
"true" => true,
"yes" => true,
"1" => true,
_ => false,
};
}
public static int GetEnvironmentVariableAsInt(this string prefix, string varName, int fallback = 0)
{
var fullVarName = GetFullVariableName(prefix, varName);
var str = Environment.GetEnvironmentVariable(fullVarName);
if (string.IsNullOrEmpty(str))
{
return fallback;
}
return int.TryParse(str, out var result) ? result : fallback;
}
public static string GetRequiredEnvironmentVariableAsString(this string prefix, string varName)
{
var fullVarName = GetFullVariableName(prefix, varName);
var str = Environment.GetEnvironmentVariable(fullVarName);
if (string.IsNullOrEmpty(str))
{
throw new InvalidOperationException($"Environment variable {fullVarName} is not set");
}
return str;
}
public static string GetOptionalEnvironmentVariableAsString(this string prefix, string varName, string? fallback = null)
{
var fullVarName = GetFullVariableName(prefix, varName);
var str = Environment.GetEnvironmentVariable(fullVarName);
if (string.IsNullOrEmpty(str))
{
return fallback;
}
return str;
}
private static string GetFullVariableName(string prefix, string varName) => $"{prefix}_{varName}";
}

View File

@@ -0,0 +1,8 @@
namespace SharedContracts.Extensions;
public static class HostBuilderExtensions
{
public static IHostBuilder SetupSerilog(this IHostBuilder builder, IConfiguration configuration) =>
builder.UseSerilog((_, c) =>
c.ReadFrom.Configuration(configuration));
}

View File

@@ -0,0 +1,6 @@
namespace SharedContracts.Extensions;
public static class JsonSerializerExtensions
{
public static string ToJson<T>(this T value) => JsonSerializer.Serialize(value);
}

View File

@@ -0,0 +1,44 @@
namespace SharedContracts.Extensions;
public static class RabbitMqBusFactoryExtensions
{
public static IRabbitMqBusFactoryConfigurator SetupExchangeEndpoint(this IRabbitMqBusFactoryConfigurator cfg,
string exchangeName,
bool durable = true,
bool autoDelete = false,
string type = "fanout")
{
cfg.ReceiveEndpoint(
exchangeName, e =>
{
e.Bind(
exchangeName, config =>
{
config.Durable = durable;
config.AutoDelete = autoDelete;
config.ExchangeType = type;
});
});
return cfg;
}
public static IRabbitMqMessagePublishTopologyConfigurator<TMessage> PublishToQueue<TMessage>(this IRabbitMqMessagePublishTopologyConfigurator<TMessage> cfg,
string exchangeName,
string queueName,
bool durable = true,
bool autoDelete = false,
string type = "fanout") where TMessage : class
{
cfg.Durable = durable;
cfg.AutoDelete = autoDelete;
cfg.ExchangeType = type;
cfg.BindQueue(
exchangeName, queueName, options =>
{
options.Durable = durable;
});
return cfg;
}
}

View File

@@ -0,0 +1,41 @@
using System.Text.RegularExpressions;
namespace SharedContracts.Extensions;
public static partial class StringExtensions
{
[GeneratedRegex("[^a-zA-Z0-9 ]")]
private static partial Regex NotAlphaNumeric();
[GeneratedRegex(@"\s*\([^)]*\)|\s*\b\d{4}\b")]
private static partial Regex CleanTitleForImdb();
private static readonly char[] separator = [' '];
public static bool IsNullOrEmpty(this string? value) =>
string.IsNullOrEmpty(value);
public static string NormalizeTitle(this string title)
{
var alphanumericTitle = NotAlphaNumeric().Replace(title, " ");
var words = alphanumericTitle.Split(separator, StringSplitOptions.RemoveEmptyEntries)
.Select(word => word.ToLower());
var normalizedTitle = string.Join(" ", words);
return normalizedTitle;
}
public static string RemoveMatches(this string input, IEnumerable<Func<Regex>> regexPatterns) =>
regexPatterns.Aggregate(input, (current, regex) => regex().Replace(current, string.Empty));
public static string CleanTorrentTitleForImdb(this string title)
{
var cleanTitle = CleanTitleForImdb().Replace(title, "").Trim();
cleanTitle = cleanTitle.ToLower();
return cleanTitle;
}
}

View File

@@ -0,0 +1,13 @@
namespace SharedContracts.Extensions;
public static class WebApplicationBuilderExtensions
{
public static void DisableIpPortBinding(this WebApplicationBuilder builder) =>
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(0, listenOptions =>
{
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.None;
});
});
}