mirror of
https://github.com/knightcrawler-stremio/knightcrawler.git
synced 2024-12-20 03:29:51 +00:00
Run pre-commit
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/.env
|
||||
**/.env
|
||||
|
||||
@@ -1236,11 +1236,11 @@ dotnet_naming_rule.unity_serialized_field_rule_1.style = lower_camel_case_style
|
||||
dotnet_naming_rule.unity_serialized_field_rule_1.symbols = unity_serialized_field_symbols_1
|
||||
dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
|
||||
dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds =
|
||||
dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds =
|
||||
dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field
|
||||
dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance
|
||||
dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_kinds =
|
||||
dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_kinds =
|
||||
dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_applicable_kinds = unity_serialised_field
|
||||
dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_required_modifiers = instance
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
|
||||
@@ -1264,7 +1264,7 @@ resharper_enforce_line_ending_style = true
|
||||
resharper_formatter_off_tag = @formatter:off
|
||||
resharper_formatter_on_tag = @formatter:on
|
||||
resharper_formatter_tags_enabled = true
|
||||
resharper_instance_members_qualify_declared_in =
|
||||
resharper_instance_members_qualify_declared_in =
|
||||
resharper_object_creation_when_type_not_evident = target_typed
|
||||
resharper_place_accessorholder_attribute_on_same_line = false
|
||||
resharper_place_expr_property_on_single_line = false
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -17,7 +17,7 @@ Steps to reproduce the behavior:
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Logs**
|
||||
Provide logs for all containers when this issue occurs.
|
||||
Provide logs for all containers when this issue occurs.
|
||||
If the logs are short, make sure to triple backtick them, or use https://pastebin.com/
|
||||
|
||||
**Hardware:**
|
||||
|
||||
2
.github/workflows/build_images.yaml
vendored
2
.github/workflows/build_images.yaml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
type=edge,branch=master,commit=${{ github.sha }}
|
||||
type=sha,commit=${{ github.sha }}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
|
||||
- name: Build image for scanning ${{ matrix.image_name }}
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -410,4 +410,4 @@ src/producer/.run/
|
||||
|
||||
# Caddy logs
|
||||
deployment/docker/optional_reverse_proxy/logs/**
|
||||
!deployment/docker/optional_reverse_proxy/logs/.gitkeep
|
||||
!deployment/docker/optional_reverse_proxy/logs/.gitkeep
|
||||
|
||||
26
README.md
26
README.md
@@ -7,7 +7,7 @@
|
||||
|
||||
## Contents
|
||||
|
||||
> [!CAUTION]
|
||||
> [!CAUTION]
|
||||
> Until we reach `v1.0.0`, please consider releases as alpha.
|
||||
|
||||
> [!IMPORTANT]
|
||||
@@ -75,9 +75,9 @@ You can also increase `JOB_CONCURRENCY` from `5` to `10`.
|
||||
|
||||
### DebridMediaManager setup (optional)
|
||||
|
||||
There are some optional steps you should take to maximise the number of movies/tv shows we can find.
|
||||
There are some optional steps you should take to maximise the number of movies/tv shows we can find.
|
||||
|
||||
We can search DebridMediaManager hash lists which are hosted on GitHub. This allows us to add hundreds of thousands of movies and tv shows, but it requires a Personal Access Token to be generated. The software only needs read access and only for public respositories. To generate one, please follow these steps:
|
||||
We can search DebridMediaManager hash lists which are hosted on GitHub. This allows us to add hundreds of thousands of movies and tv shows, but it requires a Personal Access Token to be generated. The software only needs read access and only for public repositories. To generate one, please follow these steps:
|
||||
|
||||
1. Navigate to GitHub settings -> Developer Settings -> Personal access tokens -> Fine-grained tokens (click [here](https://github.com/settings/tokens?type=beta) for a direct link)
|
||||
2. Press `Generate new token`
|
||||
@@ -89,8 +89,8 @@ We can search DebridMediaManager hash lists which are hosted on GitHub. This all
|
||||
90 days
|
||||
Description:
|
||||
<blank>
|
||||
Respository access
|
||||
(checked) Public Repositories (read-only)
|
||||
Repository access
|
||||
(checked) Public Repositories (read-only)
|
||||
```
|
||||
4. Click `Generate token`
|
||||
5. Take the new token and add it to the bottom of the [.env](deployment/docker/.env) file
|
||||
@@ -109,7 +109,7 @@ Please choose which applies to you:
|
||||
|
||||
You can use either a paid domain `your-domain.com` or a free reverse dns service like [DuckDNS](https://www.duckdns.org/) (you can [automate the update of your IP address](https://www.duckdns.org/install.jsp)).
|
||||
|
||||
Before continuing you need to open up port `80` and `443` in your firewall and configure any [port forwarding](https://portforward.com/) as necessary. You should not do this unless you understand the security implications. Please note that Knightcrawler and its contributors cannot be held responsible for any damage or loss of data from exposing your service publically.
|
||||
Before continuing you need to open up port `80` and `443` in your firewall and configure any [port forwarding](https://portforward.com/) as necessary. You should not do this unless you understand the security implications. Please note that Knightcrawler and its contributors cannot be held responsible for any damage or loss of data from exposing your service publicly.
|
||||
|
||||
You may find it safer to [use a tunnel/vpn](#i-will-be-using-a-tunnelvpn-cgnat-dont-want-to-open-ports-etc), but this will require the use of a paid domain or will not be accessible without being connected to your vpn.
|
||||
|
||||
@@ -119,7 +119,7 @@ For this you can use a VPN like [Tailscale](https://tailscale.com/) which has it
|
||||
|
||||
To use a Cloudflare tunnel you __will__ need a domain name.
|
||||
|
||||
Theres a sample compose for a Cloudflare tunnel [here](deployment/docker/example_cloudflare_tunnel/docker-compose.yml).
|
||||
There's a sample compose for a Cloudflare tunnel [here](deployment/docker/example_cloudflare_tunnel/docker-compose.yml).
|
||||
|
||||
If you are going to go this route, you will want to connect caddy to the cloudflare-tunnel network. It's all in Caddy's [docker-compose.yaml](deployment/docker/optional_reverse_proxy/docker-compose.yaml) you will just need to uncomment it.
|
||||
|
||||
@@ -209,7 +209,7 @@ Here's how to set up and use Grafana and Prometheus for monitoring RabbitMQ:
|
||||
|
||||
- You can use the following dashboard from Grafana's official library: [RabbitMQ Overview Dashboard](https://grafana.com/grafana/dashboards/10991-rabbitmq-overview/).
|
||||
|
||||
- You can alse use the following dashboard [PostgreSQL Database](https://grafana.com/grafana/dashboards/9628-postgresql-database/) to monitor Postgres metrics.
|
||||
- You can also use the following dashboard [PostgreSQL Database](https://grafana.com/grafana/dashboards/9628-postgresql-database/) to monitor Postgres metrics.
|
||||
|
||||
The Prometheus data source is already configured in Grafana, you just have to select it when importing the dashboard.
|
||||
|
||||
@@ -252,7 +252,7 @@ with include drop, create tables, create indexes, reset sequences
|
||||
|
||||
> [!NOTE]
|
||||
> If you have changed the default password for PostgreSQL (RECOMMENDED) please change it above accordingly
|
||||
>
|
||||
>
|
||||
> `postgresql://postgres:<password here>@postgres:5432/knightcrawler`
|
||||
|
||||
Then run the following docker run command to import the database:
|
||||
@@ -295,7 +295,7 @@ with include drop, create tables, create indexes, reset sequences
|
||||
|
||||
> [!NOTE]
|
||||
> If you have changed the default password for PostgreSQL (RECOMMENDED) please change it above accordingly
|
||||
>
|
||||
>
|
||||
> `postgresql://postgres:<password here>@<docker-ip>/knightcrawler`
|
||||
|
||||
> [!TIP]
|
||||
@@ -308,7 +308,7 @@ Then run `pgloader db.load`.
|
||||
|
||||
### Process the data we have imported
|
||||
|
||||
The data we have imported is not usable immediately. It is loaded into a new table called `items`. We need to move this data into the `ingested_torrents` table to be processed. The producer and atleast one consumer need to be running to process this data.
|
||||
The data we have imported is not usable immediately. It is loaded into a new table called `items`. We need to move this data into the `ingested_torrents` table to be processed. The producer and at least one consumer need to be running to process this data.
|
||||
|
||||
We are going to concatenate all of the different movie categories into one e.g. movies_uhd, movies_hd, movies_sd -> `movies`. This will give us a lot more data to work with.
|
||||
|
||||
@@ -364,8 +364,8 @@ With the renaming of the project, you will have to change your database name in
|
||||
**With your existing stack still running**, run:
|
||||
```
|
||||
docker exec -it torrentio-selfhostio-postgres-1 psql -c "
|
||||
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
||||
WHERE pid <> pg_backend_pid() AND datname = 'selfhostio';
|
||||
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
||||
WHERE pid <> pg_backend_pid() AND datname = 'selfhostio';
|
||||
ALTER DATABASE selfhostio RENAME TO knightcrawler;"
|
||||
```
|
||||
Make sure your postgres container is named `torrentio-selfhostio-postgres-1`, otherwise, adjust accordingly.
|
||||
|
||||
@@ -26,7 +26,7 @@ RABBITMQ_MAX_PUBLISH_BATCH_SIZE=500
|
||||
RABBITMQ_PUBLISH_INTERVAL_IN_SECONDS=10
|
||||
|
||||
# Metadata
|
||||
## Only used if DATA_ONCE is set to false. If true, the schedule is ignored
|
||||
## Only used if DATA_ONCE is set to false. If true, the schedule is ignored
|
||||
METADATA_DOWNLOAD_IMDB_DATA_SCHEDULE="0 0 1 * *"
|
||||
## If true, the metadata will be downloaded once and then the schedule will be ignored
|
||||
METADATA_DOWNLOAD_IMDB_DATA_ONCE=true
|
||||
|
||||
@@ -130,7 +130,7 @@ services:
|
||||
ports:
|
||||
- "7000:7000"
|
||||
|
||||
|
||||
|
||||
networks:
|
||||
knightcrawler-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -1 +1 @@
|
||||
TOKEN=cloudflare-tunnel-token-here
|
||||
TOKEN=cloudflare-tunnel-token-here
|
||||
|
||||
@@ -5,4 +5,4 @@ providers:
|
||||
folder: Dashboards
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
||||
@@ -578,4 +578,4 @@
|
||||
"uid": "knightcrawler-logs",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,4 @@ datasources:
|
||||
basicAuth: false
|
||||
isDefault: false
|
||||
version: 1
|
||||
editable: true
|
||||
editable: true
|
||||
|
||||
@@ -32,4 +32,4 @@ schema_config:
|
||||
schema: v11
|
||||
index:
|
||||
prefix: index_
|
||||
period: 24h
|
||||
period: 24h
|
||||
|
||||
@@ -7,7 +7,7 @@ networks:
|
||||
volumes:
|
||||
grafana-data:
|
||||
loki-data:
|
||||
|
||||
|
||||
services:
|
||||
prometheus:
|
||||
command:
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
image: prometheuscommunity/postgres-exporter
|
||||
networks:
|
||||
- knightcrawler-network
|
||||
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:2.9.4
|
||||
volumes:
|
||||
@@ -61,7 +61,7 @@ services:
|
||||
networks:
|
||||
- knightcrawler-network
|
||||
|
||||
|
||||
|
||||
loki:
|
||||
command: '-config.file=/etc/loki/local-config.yml'
|
||||
depends_on:
|
||||
@@ -73,4 +73,3 @@ services:
|
||||
volumes:
|
||||
- ./config/loki/config.yml:/etc/loki/local-config.yml
|
||||
- loki-data:/loki
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
## NOTE: I have dramatically lowered the above for testing.
|
||||
## Once you have confirmed that everything works, start increasing the number
|
||||
## the goal is to have HSTS set to one year with subdomains and preloading :
|
||||
##
|
||||
##
|
||||
# `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`
|
||||
##
|
||||
##
|
||||
## Warning: You should ensure that you fully understand the implications
|
||||
## of HSTS preloading before you include the directive in your policy and
|
||||
## before you submit. It means that your entire domain and all subdomains,
|
||||
@@ -41,7 +41,7 @@
|
||||
(cloudflare-tunnel-protection) {
|
||||
import ./snippets/cloudflare-replace-X-Forwarded-For
|
||||
trusted_proxies 172.17.0.0/16 # This needs to be your docker subnet
|
||||
# I beleive this is what is configured by default.
|
||||
# I believe this is what is configured by default.
|
||||
# If you can't make it work ask for my help on discord.
|
||||
}
|
||||
|
||||
|
||||
@@ -16,16 +16,16 @@
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "{Timestamp:HH:mm:ss} [{Level}] [{SourceContext}] {Message}{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
|
||||
"Properties": {
|
||||
"Application": "Metadata"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,41 +4,41 @@ public static class ConfigurationExtensions
|
||||
{
|
||||
private const string ConfigurationFolder = "Configuration";
|
||||
private const string LoggingConfig = "logging.json";
|
||||
|
||||
|
||||
public static IConfigurationBuilder AddServiceConfiguration(this IConfigurationBuilder configuration)
|
||||
{
|
||||
configuration.SetBasePath(Path.Combine(AppContext.BaseDirectory, ConfigurationFolder));
|
||||
|
||||
|
||||
configuration.AddJsonFile(LoggingConfig, false, true);
|
||||
|
||||
|
||||
configuration.AddEnvironmentVariables();
|
||||
|
||||
configuration.AddUserSecrets<Program>();
|
||||
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ 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;
|
||||
@@ -21,11 +21,11 @@ public static class EnvironmentExtensions
|
||||
_ => 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))
|
||||
@@ -35,11 +35,11 @@ public static class EnvironmentExtensions
|
||||
|
||||
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))
|
||||
@@ -49,11 +49,11 @@ public static class EnvironmentExtensions
|
||||
|
||||
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))
|
||||
@@ -63,6 +63,6 @@ public static class EnvironmentExtensions
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
private static string GetFullVariableName(string prefix, string varName) => $"{prefix}_{varName}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ namespace Metadata.Extensions;
|
||||
public static class JsonSerializerExtensions
|
||||
{
|
||||
public static string ToJson<T>(this T value) => JsonSerializer.Serialize(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,15 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
internal static IServiceCollection AddMongoDb(this IServiceCollection services)
|
||||
{
|
||||
services.LoadConfigurationFromEnv<MongoConfiguration>();
|
||||
services.AddTransient<ImdbMongoDbService>();
|
||||
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
internal static IServiceCollection AddJobSupport(this IServiceCollection services)
|
||||
{
|
||||
services.LoadConfigurationFromEnv<JobConfiguration>();
|
||||
@@ -24,7 +24,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScheduler()
|
||||
.AddTransient<DownloadImdbDataJob>()
|
||||
.AddHostedService<JobScheduler>();
|
||||
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ internal static class WebApplicationBuilderExtensions
|
||||
{
|
||||
options.DefaultExecutionTimeout = 6.Hours();
|
||||
});
|
||||
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ public class JobConfiguration
|
||||
private const string DownloadImdbDataVariable = "DOWNLOAD_IMDB_DATA_SCHEDULE";
|
||||
private const string DownloadImdbDataOnceVariable = "DOWNLOAD_IMDB_DATA_ONCE";
|
||||
private const string InsertBatchSizeVariable = "INSERT_BATCH_SIZE";
|
||||
|
||||
|
||||
public int InsertBatchSize { get; init; } = Prefix.GetEnvironmentVariableAsInt(InsertBatchSizeVariable, 25_000);
|
||||
public string DownloadImdbCronSchedule { get; init; } = Prefix.GetOptionalEnvironmentVariableAsString(DownloadImdbDataVariable, CronExpressions.EveryHour);
|
||||
public bool DownloadImdbOnce { get; init; } = Prefix.GetEnvironmentVariableAsBool(DownloadImdbDataOnceVariable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ public class MongoConfiguration
|
||||
private const string DbVariable = "DB";
|
||||
private const string UsernameVariable = "USER";
|
||||
private const string PasswordVariable = "PASSWORD";
|
||||
|
||||
|
||||
|
||||
private string Host { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(HostVariable);
|
||||
private int Port { get; init; } = Prefix.GetEnvironmentVariableAsInt(PortVariable, 27017);
|
||||
private string Username { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(UsernameVariable);
|
||||
private string Password { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(PasswordVariable);
|
||||
public string DbName { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(DbVariable);
|
||||
|
||||
|
||||
public string ConnectionString => $"mongodb://{Username}:{Password}@{Host}:{Port}/{DbName}?tls=false&directConnection=true&authSource=admin";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace Metadata.Features.DeleteDownloadedImdbData;
|
||||
|
||||
public record DeleteDownloadedImdbDataRequest(string FilePath);
|
||||
public record DeleteDownloadedImdbDataRequest(string FilePath);
|
||||
|
||||
@@ -5,9 +5,9 @@ public class DeleteDownloadedImdbDataRequestHandler(ILogger<DeleteDownloadedImdb
|
||||
public Task Handle(DeleteDownloadedImdbDataRequest request, CancellationToken _)
|
||||
{
|
||||
logger.LogInformation("Deleting file {FilePath}", request.FilePath);
|
||||
|
||||
|
||||
File.Delete(request.FilePath);
|
||||
|
||||
|
||||
logger.LogInformation("File Deleted");
|
||||
|
||||
if (configuration.DownloadImdbOnce)
|
||||
@@ -18,4 +18,4 @@ public class DeleteDownloadedImdbDataRequestHandler(ILogger<DeleteDownloadedImdb
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ public class DownloadImdbDataJob(IMessageBus messageBus, JobConfiguration config
|
||||
public override bool IsScheduelable => !configuration.DownloadImdbOnce && !string.IsNullOrEmpty(configuration.DownloadImdbCronSchedule);
|
||||
public override string JobName => nameof(DownloadImdbDataJob);
|
||||
public override async Task Invoke() => await messageBus.SendAsync(new GetImdbDataRequest());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace Metadata.Features.DownloadImdbData;
|
||||
|
||||
public record GetImdbDataRequest;
|
||||
public record GetImdbDataRequest;
|
||||
|
||||
@@ -3,28 +3,28 @@ namespace Metadata.Features.DownloadImdbData;
|
||||
public class GetImdbDataRequestHandler(IHttpClientFactory clientFactory, ILogger<GetImdbDataRequestHandler> logger)
|
||||
{
|
||||
private const string TitleBasicsFileName = "title.basics.tsv";
|
||||
|
||||
|
||||
public async Task<ImportImdbDataRequest> Handle(GetImdbDataRequest _, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Downloading IMDB data");
|
||||
|
||||
|
||||
var client = clientFactory.CreateClient("imdb-data");
|
||||
var response = await client.GetAsync($"{TitleBasicsFileName}.gz", cancellationToken);
|
||||
|
||||
|
||||
var tempFile = Path.Combine(Path.GetTempPath(), TitleBasicsFileName);
|
||||
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
await using var gzipStream = new GZipStream(stream, CompressionMode.Decompress);
|
||||
await using var fileStream = File.Create(tempFile);
|
||||
|
||||
|
||||
await gzipStream.CopyToAsync(fileStream, cancellationToken);
|
||||
|
||||
|
||||
logger.LogInformation("Downloaded IMDB data to {TempFile}", tempFile);
|
||||
|
||||
|
||||
fileStream.Close();
|
||||
|
||||
return new(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ public class ImdbEntry
|
||||
public string? EndYear { get; set; }
|
||||
public string? RuntimeMinutes { get; set; }
|
||||
public string? Genres { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public class ImdbMongoDbService
|
||||
public ImdbMongoDbService(MongoConfiguration configuration, ILogger<ImdbMongoDbService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
|
||||
var client = new MongoClient(configuration.ConnectionString);
|
||||
var database = client.GetDatabase(configuration.DbName);
|
||||
|
||||
@@ -37,7 +37,7 @@ public class ImdbMongoDbService
|
||||
|
||||
await _imdbCollection.BulkWriteAsync(operations);
|
||||
}
|
||||
|
||||
|
||||
public bool IsDatabaseInitialized()
|
||||
{
|
||||
try
|
||||
@@ -61,4 +61,4 @@ public class ImdbMongoDbService
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace Metadata.Features.ImportImdbData;
|
||||
|
||||
public record ImportImdbDataRequest(string FilePath);
|
||||
public record ImportImdbDataRequest(string FilePath);
|
||||
|
||||
@@ -21,16 +21,16 @@ public class ImportImdbDataRequestHandler(ILogger<ImportImdbDataRequestHandler>
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Skip the header row
|
||||
await csv.ReadAsync();
|
||||
|
||||
|
||||
var batchInsertTask = CreateBatchOfEntries(channel, cancellationToken);
|
||||
|
||||
await ReadEntries(csv, channel, cancellationToken);
|
||||
|
||||
channel.Writer.Complete();
|
||||
|
||||
|
||||
await batchInsertTask;
|
||||
|
||||
return new(request.FilePath);
|
||||
@@ -45,7 +45,7 @@ public class ImportImdbDataRequestHandler(ILogger<ImportImdbDataRequestHandler>
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var batch = new List<ImdbEntry>
|
||||
{
|
||||
movieData,
|
||||
@@ -63,7 +63,7 @@ public class ImportImdbDataRequestHandler(ILogger<ImportImdbDataRequestHandler>
|
||||
}
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
|
||||
private static async Task ReadEntries(CsvReader csv, Channel<ImdbEntry, ImdbEntry> channel, CancellationToken cancellationToken)
|
||||
{
|
||||
while (await csv.ReadAsync())
|
||||
@@ -80,13 +80,13 @@ public class ImportImdbDataRequestHandler(ILogger<ImportImdbDataRequestHandler>
|
||||
RuntimeMinutes = csv.GetField(7),
|
||||
Genres = csv.GetField(8),
|
||||
};
|
||||
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await channel.Writer.WriteAsync(movieData, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ public abstract class BaseJob : IMetadataJob
|
||||
public abstract bool IsScheduelable { get; }
|
||||
|
||||
public abstract string JobName { get; }
|
||||
|
||||
|
||||
public abstract Task Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ public interface IMetadataJob : IInvocable
|
||||
{
|
||||
bool IsScheduelable { get; }
|
||||
string JobName { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ public class JobScheduler(IServiceProvider serviceProvider) : IHostedService
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = serviceProvider.CreateAsyncScope();
|
||||
|
||||
|
||||
var mongoDbService = scope.ServiceProvider.GetRequiredService<ImdbMongoDbService>();
|
||||
|
||||
|
||||
if (!mongoDbService.IsDatabaseInitialized())
|
||||
{
|
||||
throw new InvalidOperationException("MongoDb is not initialized");
|
||||
}
|
||||
|
||||
|
||||
var jobConfigurations = scope.ServiceProvider.GetRequiredService<JobConfiguration>();
|
||||
var downloadJob = scope.ServiceProvider.GetRequiredService<DownloadImdbDataJob>();
|
||||
|
||||
@@ -20,15 +20,15 @@ public class JobScheduler(IServiceProvider serviceProvider) : IHostedService
|
||||
{
|
||||
return downloadJob.Invoke();
|
||||
}
|
||||
|
||||
|
||||
var scheduler = scope.ServiceProvider.GetRequiredService<IScheduler>();
|
||||
|
||||
|
||||
scheduler.Schedule<DownloadImdbDataJob>()
|
||||
.Cron(jobConfigurations.DownloadImdbCronSchedule)
|
||||
.PreventOverlapping(nameof(downloadJob.JobName));
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ public static class CronExpressions
|
||||
public const string EveryDay = "0 0 0 * *";
|
||||
public const string EveryWeek = "0 0 * * 0";
|
||||
public const string EveryMonth = "0 0 0 * *";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ public static class HttpClients
|
||||
{
|
||||
public const string ImdbDataClientName = "imdb-data";
|
||||
public const string ImdbClientBaseAddress = "https://datasets.imdbws.com/";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,4 @@ builder.Services
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
await host.RunAsync();
|
||||
await host.RunAsync();
|
||||
|
||||
@@ -1 +1 @@
|
||||
*.ts
|
||||
*.ts
|
||||
|
||||
@@ -36,4 +36,4 @@ module.exports = {
|
||||
"one-var": ["error", { uninitialized: "consecutive" }],
|
||||
"prefer-destructuring": "warn",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker build -t ippexdeploymentscr.azurecr.io/dave/stremio-addon-jackett:latest . --platform linux/amd64
|
||||
docker push ippexdeploymentscr.azurecr.io/dave/stremio-addon-jackett:latest
|
||||
docker push ippexdeploymentscr.azurecr.io/dave/stremio-addon-jackett:latest
|
||||
|
||||
@@ -18,4 +18,4 @@
|
||||
"typeRoots": ["node_modules/@types", "src/@types"]
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ builder.defineStreamHandler((args) => {
|
||||
if (!args.id.match(/tt\d+/i) && !args.id.match(/kitsu:\d+/i)) {
|
||||
return Promise.resolve({ streams: [] });
|
||||
}
|
||||
|
||||
|
||||
if (processConfig.DEBUG) {
|
||||
console.log(`Incoming stream ${args.id} request`)
|
||||
console.log('args', args);
|
||||
}
|
||||
|
||||
|
||||
return cacheWrapStream(args.id, () => limiter.schedule(() =>
|
||||
streamHandler(args)
|
||||
.then(records => records.map(record => toStreamInfo(record, args.type))))
|
||||
|
||||
@@ -111,7 +111,7 @@ export const transformData = async (data, query) => {
|
||||
console.log("Transforming data for query " + data);
|
||||
|
||||
let results = [];
|
||||
|
||||
|
||||
const parsedData = await parseString(data);
|
||||
|
||||
if (!parsedData.rss.channel[0]?.item) {
|
||||
@@ -126,7 +126,7 @@ export const transformData = async (data, query) => {
|
||||
[torznabDataItem.$.name]: torznabDataItem.$.value,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
if (torznabData.infohash) {
|
||||
|
||||
const [title, pubDate, category, size] = [rssItem.title[0], rssItem.pubDate[0], rssItem.category[0], rssItem.size[0]];
|
||||
@@ -148,4 +148,4 @@ export const transformData = async (data, query) => {
|
||||
|
||||
|
||||
return results;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,4 +38,4 @@ export const searchJackett = async (query) => {
|
||||
}
|
||||
|
||||
return sortedResults;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ const getMovieSearchQueries = (cleanName, year) => {
|
||||
const getSeriesSearchQueries = (cleanName, year, season, episode) => {
|
||||
return {
|
||||
seriesByEpisode: seriesByEpisode(cleanName, season, episode),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const jackettSearchQueries = (cleanName, type, year, season, episode) => {
|
||||
@@ -27,8 +27,8 @@ export const jackettSearchQueries = (cleanName, type, year, season, episode) =>
|
||||
return getMovieSearchQueries(cleanName, year);
|
||||
case Type.SERIES:
|
||||
return getSeriesSearchQueries(cleanName, year, season, episode);
|
||||
|
||||
|
||||
default:
|
||||
return { };
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ const cinemetaUri = cinemetaConfig.URI;
|
||||
export const getMetaData = (args) => {
|
||||
const [imdbId] = args.id.split(':');
|
||||
const {type} = args;
|
||||
|
||||
|
||||
return cacheWrapImdbMetaData(args.id, () => getInfoForImdbId(imdbId, type));
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const getInfoForImdbId = async (imdbId, type) => {
|
||||
console.log(`Getting info for ${imdbId} of type ${type}`);
|
||||
console.log(`Request URI: ${requestUri}`);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const { data: response } = await axios.get(requestUri, options);
|
||||
return response.meta;
|
||||
@@ -28,4 +28,4 @@ const getInfoForImdbId = async (imdbId, type) => {
|
||||
console.log(error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,4 +22,4 @@ export function parseConfiguration(configuration) {
|
||||
.map(value => keysToUppercase.includes(key) ? value.toUpperCase() : value.toLowerCase()))
|
||||
|
||||
return configValues;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ a.install-link {
|
||||
}
|
||||
|
||||
.input:focus, .btn:focus {
|
||||
outline: none;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2pt rgb(30, 144, 255, 0.7);
|
||||
}
|
||||
`;
|
||||
@@ -210,7 +210,7 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
const debridOptionsHTML = Object.values(DebridOptions.options)
|
||||
.map(option => `<option value="${option.key}">${option.description}</option>`)
|
||||
.join('\n');
|
||||
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html style="background-image: url(${background});">
|
||||
@@ -239,54 +239,54 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
<h2 class="description">${manifest.description || ''}</h2>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
|
||||
<label class="label" id="iLimitLabel" for="iLimit">Max results per quality:</label>
|
||||
<input type="text" inputmode="numeric" pattern="[0-9]*" id="iLimit" onchange="generateInstallLink()" class="input" placeholder="All results">
|
||||
|
||||
|
||||
<label class="label" for="iDebridProviders">Debrid provider:</label>
|
||||
<select id="iDebridProviders" class="input" onchange="debridProvidersChange()">
|
||||
<option value="none" selected>None</option>
|
||||
${debridProvidersHTML}
|
||||
</select>
|
||||
|
||||
|
||||
<div id="dRealDebrid">
|
||||
<label class="label" for="iRealDebrid">RealDebrid API Key (Find it <a href='https://real-debrid.com/apitoken' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iRealDebrid" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dAllDebrid">
|
||||
<label class="label" for="iAllDebrid">AllDebrid API Key (Create it <a href='https://alldebrid.com/apikeys' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iAllDebrid" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dPremiumize">
|
||||
<label class="label" for="iPremiumize">Premiumize API Key (Find it <a href='https://www.premiumize.me/account' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iPremiumize" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dDebridLink">
|
||||
<label class="label" for="iDebridLink">DebridLink API Key (Find it <a href='https://debrid-link.fr/webapp/apikey' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iDebridLink" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dOffcloud">
|
||||
<label class="label" for="iOffcloud">Offcloud API Key (Find it <a href='https://offcloud.com/#/account' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iOffcloud" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dPutio">
|
||||
<label class="label" for="iPutio">Put.io ClientId and Token (Create new OAuth App <a href='https://app.put.io/oauth' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iPutioClientId" placeholder="ClientId" onchange="generateInstallLink()" class="input">
|
||||
<input type="text" id="iPutioToken" placeholder="Token" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dDebridOptions">
|
||||
<label class="label" for="iDebridOptions">Debrid options:</label>
|
||||
<select id="iDebridOptions" class="input" onchange="generateInstallLink()" name="debridOptions[]" multiple="multiple">
|
||||
${debridOptionsHTML}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<a id="installLink" class="install-link" href="#">
|
||||
@@ -295,7 +295,7 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
<div class="contact">
|
||||
<p>Or paste into Stremio search bar after clicking install</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="separator"></div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
@@ -304,7 +304,7 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
const isTvAgent = /\\b(?:tv|wv)\\b/i.test(navigator.userAgent)
|
||||
const isDesktopMedia = window.matchMedia("(pointer:fine)").matches;
|
||||
if (isDesktopMedia && !isTvMedia && !isTvAgent) {
|
||||
$('#iDebridOptions').multiselect({
|
||||
$('#iDebridOptions').multiselect({
|
||||
nonSelectedText: 'None',
|
||||
buttonTextAlignment: 'left',
|
||||
onChange: () => generateInstallLink()
|
||||
@@ -325,7 +325,7 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
generateInstallLink();
|
||||
debridProvidersChange();
|
||||
});
|
||||
|
||||
|
||||
function debridProvidersChange() {
|
||||
const provider = $('#iDebridProviders').val()
|
||||
$('#dDebridOptions').toggle(provider !== 'none');
|
||||
@@ -336,10 +336,10 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
$('#dOffcloud').toggle(provider === '${MochOptions.offcloud.key}');
|
||||
$('#dPutio').toggle(provider === '${MochOptions.putio.key}');
|
||||
}
|
||||
|
||||
|
||||
function generateInstallLink() {
|
||||
const limitValue = $('#iLimit').val() || '';
|
||||
|
||||
|
||||
const debridOptionsValue = $('#iDebridOptions').val().join(',') || '';
|
||||
const realDebridValue = $('#iRealDebrid').val() || '';
|
||||
const allDebridValue = $('#iAllDebrid').val() || '';
|
||||
@@ -348,9 +348,9 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
const offcloudValue = $('#iOffcloud').val() || ''
|
||||
const putioClientIdValue = $('#iPutioClientId').val() || '';
|
||||
const putioTokenValue = $('#iPutioToken').val() || '';
|
||||
|
||||
|
||||
const limit = /^[1-9][0-9]{0,2}$/.test(limitValue) && limitValue;
|
||||
|
||||
|
||||
const debridOptions = debridOptionsValue.length && debridOptionsValue.trim();
|
||||
const realDebrid = realDebridValue.length && realDebridValue.trim();
|
||||
const premiumize = premiumizeValue.length && premiumizeValue.trim();
|
||||
@@ -361,7 +361,7 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
|
||||
let configurationValue = [
|
||||
['limit', limit],
|
||||
['${DebridOptions.key}', debridOptions],
|
||||
['${DebridOptions.key}', debridOptions],
|
||||
['${MochOptions.realdebrid.key}', realDebrid],
|
||||
['${MochOptions.premiumize.key}', premiumize],
|
||||
['${MochOptions.alldebrid.key}', allDebrid],
|
||||
|
||||
@@ -56,4 +56,4 @@ export function getSources(magnetInfo) {
|
||||
}
|
||||
const trackers = Array.isArray(magnetInfo.announce) ? magnetInfo.announce : magnetInfo.announce.split(',');
|
||||
return trackers.map(tracker => `tracker:${tracker}`).concat(`dht:${magnetInfo.infohash}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@ export const cacheConfig = {
|
||||
STALE_REVALIDATE_AGE: parseInt(process.env.STALE_REVALIDATE_AGE) || 4 * 60 * 60, // 4 hours
|
||||
STALE_ERROR_AGE: parseInt(process.env.STALE_ERROR_AGE) || 7 * 24 * 60 * 60, // 7 days
|
||||
GLOBAL_KEY_PREFIX: process.env.GLOBAL_KEY_PREFIX || 'jackettio-addon',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function toStreamInfo(record, type) {
|
||||
const behaviorHints = bingeGroup ? { bingeGroup } : undefined;
|
||||
|
||||
const magnetInfo = decode(record.magneturl)
|
||||
|
||||
|
||||
return cleanOutputObject({
|
||||
name: name,
|
||||
title: title,
|
||||
@@ -61,7 +61,7 @@ function formatSize(size) {
|
||||
function getBingeGroupParts(record, sameInfo, quality, torrentInfo, fileInfo, type) {
|
||||
if (type === Type.MOVIE) {
|
||||
return [quality];
|
||||
|
||||
|
||||
} else if (sameInfo) {
|
||||
return [quality];
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ export const Type = {
|
||||
SERIES: 'series',
|
||||
ANIME: 'anime',
|
||||
OTHER: 'other'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,4 +13,4 @@ export function isStaticUrl(url) {
|
||||
return Object.values(staticVideoUrls).some(videoUrl => url?.endsWith(videoUrl));
|
||||
}
|
||||
|
||||
export default staticVideoUrls
|
||||
export default staticVideoUrls
|
||||
|
||||
@@ -1 +1 @@
|
||||
*.ts
|
||||
*.ts
|
||||
|
||||
@@ -36,4 +36,4 @@ module.exports = {
|
||||
"one-var": ["error", { uninitialized: "consecutive" }],
|
||||
"prefer-destructuring": "warn",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,4 +11,4 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -65,4 +65,4 @@ try {
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,4 @@
|
||||
"typeRoots": ["node_modules/@types", "src/@types"]
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ builder.defineStreamHandler((args) => {
|
||||
if (!args.id.match(/tt\d+/i) && !args.id.match(/kitsu:\d+/i)) {
|
||||
return Promise.resolve({ streams: [] });
|
||||
}
|
||||
|
||||
|
||||
return cacheWrapStream(args.id, () => limiter.schedule(() =>
|
||||
streamHandler(args)
|
||||
.then(records => records
|
||||
|
||||
@@ -25,4 +25,4 @@ export function parseConfiguration(configuration) {
|
||||
.map(value => keysToUppercase.includes(key) ? value.toUpperCase() : value.toLowerCase()))
|
||||
|
||||
return configValues;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ a.install-link {
|
||||
}
|
||||
|
||||
.input:focus, .btn:focus {
|
||||
outline: none;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2pt rgb(30, 144, 255, 0.7);
|
||||
}
|
||||
`;
|
||||
@@ -228,7 +228,7 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
.join('\n');
|
||||
const stylizedTypes = manifest.types
|
||||
.map(t => t[0].toUpperCase() + t.slice(1) + (t !== 'series' ? 's' : ''));
|
||||
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html style="background-image: url(${background});">
|
||||
@@ -264,73 +264,73 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
</ul>
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
|
||||
<label class="label" for="iSort">Sorting:</label>
|
||||
<select id="iSort" class="input" onchange="sortModeChange()">
|
||||
${sortOptionsHTML}
|
||||
</select>
|
||||
|
||||
|
||||
<label class="label" for="iLanguages">Priority foreign language:</label>
|
||||
<select id="iLanguages" class="input" onchange="generateInstallLink()" name="languages[]" multiple="multiple" title="Streams with the selected dubs/subs language will be shown on the top">
|
||||
${languagesOptionsHTML}
|
||||
</select>
|
||||
|
||||
|
||||
<label class="label" for="iQualityFilter">Exclude qualities/resolutions:</label>
|
||||
<select id="iQualityFilter" class="input" onchange="generateInstallLink()" name="qualityFilters[]" multiple="multiple">
|
||||
${qualityFiltersHTML}
|
||||
</select>
|
||||
|
||||
|
||||
<label class="label" id="iLimitLabel" for="iLimit">Max results per quality:</label>
|
||||
<input type="text" inputmode="numeric" pattern="[0-9]*" id="iLimit" onchange="generateInstallLink()" class="input" placeholder="All results">
|
||||
|
||||
|
||||
<label class="label" id="iSizeFilterLabel" for="iSizeFilter">Video size limit:</label>
|
||||
<input type="text" pattern="([0-9.]*(?:MB|GB),?)+" id="iSizeFilter" onchange="generateInstallLink()" class="input" placeholder="No limit" title="Returned videos cannot exceed this size, use comma to have different size for movies and series. Examples: 5GB ; 800MB ; 10GB,2GB">
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="label" for="iDebridProviders">Debrid provider:</label>
|
||||
<select id="iDebridProviders" class="input" onchange="debridProvidersChange()">
|
||||
<option value="none" selected>None</option>
|
||||
${debridProvidersHTML}
|
||||
</select>
|
||||
|
||||
|
||||
<div id="dRealDebrid">
|
||||
<label class="label" for="iRealDebrid">RealDebrid API Key (Find it <a href='https://real-debrid.com/apitoken' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iRealDebrid" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dAllDebrid">
|
||||
<label class="label" for="iAllDebrid">AllDebrid API Key (Create it <a href='https://alldebrid.com/apikeys' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iAllDebrid" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dPremiumize">
|
||||
<label class="label" for="iPremiumize">Premiumize API Key (Find it <a href='https://www.premiumize.me/account' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iPremiumize" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dDebridLink">
|
||||
<label class="label" for="iDebridLink">DebridLink API Key (Find it <a href='https://debrid-link.fr/webapp/apikey' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iDebridLink" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dOffcloud">
|
||||
<label class="label" for="iOffcloud">Offcloud API Key (Find it <a href='https://offcloud.com/#/account' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iOffcloud" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dPutio">
|
||||
<label class="label" for="iPutio">Put.io ClientId and Token (Create new OAuth App <a href='https://app.put.io/oauth' target="_blank">here</a>):</label>
|
||||
<input type="text" id="iPutioClientId" placeholder="ClientId" onchange="generateInstallLink()" class="input">
|
||||
<input type="text" id="iPutioToken" placeholder="Token" onchange="generateInstallLink()" class="input">
|
||||
</div>
|
||||
|
||||
|
||||
<div id="dDebridOptions">
|
||||
<label class="label" for="iDebridOptions">Debrid options:</label>
|
||||
<select id="iDebridOptions" class="input" onchange="generateInstallLink()" name="debridOptions[]" multiple="multiple">
|
||||
${debridOptionsHTML}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="separator"></div>
|
||||
|
||||
<a id="installLink" class="install-link" href="#">
|
||||
@@ -339,7 +339,7 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
<div class="contact">
|
||||
<p>Or paste into Stremio search bar after clicking install</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="separator"></div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
@@ -348,19 +348,19 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
const isTvAgent = /\\b(?:tv|wv)\\b/i.test(navigator.userAgent)
|
||||
const isDesktopMedia = window.matchMedia("(pointer:fine)").matches;
|
||||
if (isDesktopMedia && !isTvMedia && !isTvAgent) {
|
||||
$('#iLanguages').multiselect({
|
||||
$('#iLanguages').multiselect({
|
||||
nonSelectedText: 'None',
|
||||
buttonTextAlignment: 'left',
|
||||
onChange: () => generateInstallLink()
|
||||
});
|
||||
$('#iLanguages').multiselect('select', [${languages.map(language => '"' + language + '"')}]);
|
||||
$('#iQualityFilter').multiselect({
|
||||
$('#iQualityFilter').multiselect({
|
||||
nonSelectedText: 'None',
|
||||
buttonTextAlignment: 'left',
|
||||
onChange: () => generateInstallLink()
|
||||
});
|
||||
$('#iQualityFilter').multiselect('select', [${qualityFilters.map(filter => '"' + filter + '"')}]);
|
||||
$('#iDebridOptions').multiselect({
|
||||
$('#iDebridOptions').multiselect({
|
||||
nonSelectedText: 'None',
|
||||
buttonTextAlignment: 'left',
|
||||
onChange: () => generateInstallLink()
|
||||
@@ -385,7 +385,7 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
generateInstallLink();
|
||||
debridProvidersChange();
|
||||
});
|
||||
|
||||
|
||||
function sortModeChange() {
|
||||
if (['${SortOptions.options.seeders.key}', '${SortOptions.options.size.key}'].includes($('#iSort').val())) {
|
||||
$("#iLimitLabel").text("Max results:");
|
||||
@@ -394,7 +394,7 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
}
|
||||
generateInstallLink();
|
||||
}
|
||||
|
||||
|
||||
function debridProvidersChange() {
|
||||
const provider = $('#iDebridProviders').val()
|
||||
$('#dDebridOptions').toggle(provider !== 'none');
|
||||
@@ -405,14 +405,14 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
$('#dOffcloud').toggle(provider === '${MochOptions.offcloud.key}');
|
||||
$('#dPutio').toggle(provider === '${MochOptions.putio.key}');
|
||||
}
|
||||
|
||||
|
||||
function generateInstallLink() {
|
||||
const qualityFilterValue = $('#iQualityFilter').val().join(',') || '';
|
||||
const sortValue = $('#iSort').val() || '';
|
||||
const languagesValue = $('#iLanguages').val().join(',') || [];
|
||||
const limitValue = $('#iLimit').val() || '';
|
||||
const sizeFilterValue = $('#iSizeFilter').val() || '';
|
||||
|
||||
|
||||
const debridOptionsValue = $('#iDebridOptions').val().join(',') || '';
|
||||
const realDebridValue = $('#iRealDebrid').val() || '';
|
||||
const allDebridValue = $('#iAllDebrid').val() || '';
|
||||
@@ -421,13 +421,13 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
const offcloudValue = $('#iOffcloud').val() || ''
|
||||
const putioClientIdValue = $('#iPutioClientId').val() || '';
|
||||
const putioTokenValue = $('#iPutioToken').val() || '';
|
||||
|
||||
|
||||
const qualityFilters = qualityFilterValue.length && qualityFilterValue;
|
||||
const sort = sortValue !== '${SortOptions.options.qualitySeeders.key}' && sortValue;
|
||||
const languages = languagesValue.length && languagesValue;
|
||||
const limit = /^[1-9][0-9]{0,2}$/.test(limitValue) && limitValue;
|
||||
const sizeFilter = sizeFilterValue.length && sizeFilterValue;
|
||||
|
||||
|
||||
const debridOptions = debridOptionsValue.length && debridOptionsValue.trim();
|
||||
const realDebrid = realDebridValue.length && realDebridValue.trim();
|
||||
const premiumize = premiumizeValue.length && premiumizeValue.trim();
|
||||
@@ -442,7 +442,7 @@ export default function landingTemplate(manifest, config = {}) {
|
||||
['${QualityFilter.key}', qualityFilters],
|
||||
['limit', limit],
|
||||
['${SizeFilter.key}', sizeFilter],
|
||||
['${DebridOptions.key}', debridOptions],
|
||||
['${DebridOptions.key}', debridOptions],
|
||||
['${MochOptions.realdebrid.key}', realDebrid],
|
||||
['${MochOptions.premiumize.key}', premiumize],
|
||||
['${MochOptions.alldebrid.key}', allDebrid],
|
||||
|
||||
@@ -85,4 +85,4 @@ export function getSources(trackersInput, infoHash) {
|
||||
|
||||
function unique(array) {
|
||||
return Array.from(new Set(array));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function sortStreams(streams, config) {
|
||||
|
||||
function _sortStreams(streams, config) {
|
||||
const limit = /^[1-9][0-9]*$/.test(config.limit) && parseInt(config.limit) || undefined;
|
||||
|
||||
|
||||
return sortBySize(streams, limit);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@ export const Type = {
|
||||
SERIES: 'series',
|
||||
ANIME: 'anime',
|
||||
OTHER: 'other'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,4 +13,4 @@ export function isStaticUrl(url) {
|
||||
return Object.values(staticVideoUrls).some(videoUrl => url?.endsWith(videoUrl));
|
||||
}
|
||||
|
||||
export default staticVideoUrls
|
||||
export default staticVideoUrls
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
dist/
|
||||
esbuild.ts
|
||||
jest.config.ts
|
||||
jest.config.ts
|
||||
|
||||
@@ -81,4 +81,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v20.10.0
|
||||
v20.10.0
|
||||
|
||||
@@ -44,4 +44,4 @@ try {
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ export default {
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export enum CacheType {
|
||||
Memory = 'memory',
|
||||
MongoDb = 'mongodb'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ export enum TorrentType {
|
||||
Series = 'Series',
|
||||
Movie = 'Movie',
|
||||
Anime = 'anime',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ export const BooleanHelpers = {
|
||||
throw new Error(`Invalid boolean value: '${value}'. Allowed values are 'true', 'false', 'yes', 'no', '1', or '0'.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,4 +63,4 @@ export const ExtensionHelpers = {
|
||||
const extensionMatch = filename.match(/\.(\w{2,4})$/);
|
||||
return extensionMatch !== null && extensions.includes(extensionMatch[1].toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,4 +34,4 @@ export const PromiseHelpers = {
|
||||
return array.sort((a, b) => array.filter(v => v === a).length - array.filter(v => v === b).length).pop();
|
||||
}
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface ICacheOptions {
|
||||
ttl: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@ export interface ICacheService {
|
||||
cacheTrackers: (method: CacheMethod) => Promise<any>;
|
||||
}
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
@@ -81,4 +81,4 @@ export interface ICinemetaLink {
|
||||
export interface ICinemetaBehaviorHints {
|
||||
defaultVideoId?: null;
|
||||
hasScheduledVideos?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ export interface ICommonVideoMetadata {
|
||||
name?: string;
|
||||
id?: string;
|
||||
imdb_id?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ export interface IIngestedRabbitTorrent {
|
||||
|
||||
export interface IIngestedRabbitMessage {
|
||||
message: IIngestedRabbitTorrent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,4 @@ export interface IKitsuCatalogMetaData {
|
||||
background: string;
|
||||
trailers: IKitsuTrailer[];
|
||||
links: IKitsuLink[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,4 +46,4 @@ export interface IKitsuLink {
|
||||
name?: string;
|
||||
category?: string;
|
||||
url?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,4 @@ export interface ILoggingService {
|
||||
warn(message: string, ...args: any[]): void;
|
||||
}
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
@@ -6,4 +6,4 @@ export interface IMetaDataQuery {
|
||||
season?: number
|
||||
episode?: number
|
||||
id?: string | number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ export interface IMetadataResponse {
|
||||
videos?: ICommonVideoMetadata[];
|
||||
episodeCount?: number[];
|
||||
totalCount?: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ export interface IMetadataService {
|
||||
isEpisodeImdbId(imdbId: string | undefined): Promise<boolean>;
|
||||
|
||||
escapeTitle(title: string): string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,4 @@ export interface IParseTorrentTitleResult {
|
||||
videoFile?: IFileAttributes;
|
||||
folderName?: string;
|
||||
fileName?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ export interface IParsedTorrent extends IParseTorrentTitleResult {
|
||||
seeders?: number;
|
||||
torrentId?: string;
|
||||
fileCollection?: ITorrentFileCollection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface IProcessTorrentsJob {
|
||||
listenToQueue: () => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ import {ITorrentFileCollection} from "@interfaces/torrent_file_collection";
|
||||
|
||||
export interface ITorrentDownloadService {
|
||||
getTorrentFiles(torrent: IParsedTorrent, timeout: number): Promise<ITorrentFileCollection>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ export interface ITorrentEntriesService {
|
||||
createTorrentContents(torrent: Torrent): Promise<void>;
|
||||
|
||||
updateTorrentSeeders(torrent: ITorrentAttributes): Promise<[number] | undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ export interface ITorrentFileCollection {
|
||||
contents?: IContentAttributes[];
|
||||
videos?: IFileAttributes[];
|
||||
subtitles?: ISubtitleAttributes[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ export interface ITorrentFileService {
|
||||
parseTorrentFiles(torrent: IParsedTorrent): Promise<ITorrentFileCollection>;
|
||||
|
||||
isPackTorrent(torrent: IParsedTorrent): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ import {IIngestedTorrentAttributes} from "@repository/interfaces/ingested_torren
|
||||
|
||||
export interface ITorrentProcessingService {
|
||||
processTorrentRecord(torrent: IIngestedTorrentAttributes): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ import {ITorrentFileCollection} from "@interfaces/torrent_file_collection";
|
||||
|
||||
export interface ITorrentSubtitleService {
|
||||
assignSubtitles(fileCollection: ITorrentFileCollection): ITorrentFileCollection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface ITrackerService {
|
||||
getTrackers(): Promise<string[]>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,4 +60,4 @@ export class ProcessTorrentsJob implements IProcessTorrentsJob {
|
||||
this.logger.error('Failed to setup channel', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ export const cacheConfig = {
|
||||
get MONGO_URI(): string {
|
||||
return `mongodb://${this.MONGODB_USER}:${this.MONGODB_PASSWORD}@${this.MONGODB_HOST}:${this.MONGODB_PORT}/${this.MONGODB_DB}?authSource=admin`;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,4 +11,4 @@ export const databaseConfig = {
|
||||
get POSTGRES_URI(): string {
|
||||
return `postgres://${this.POSTGRES_USER}:${this.POSTGRES_PASSWORD}@${this.POSTGRES_HOST}:${this.POSTGRES_PORT}/${this.POSTGRES_DB}`;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user