Run pre-commit
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
**/node_modules
|
**/node_modules
|
||||||
**/npm-debug.log
|
**/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_rule.unity_serialized_field_rule_1.symbols = unity_serialized_field_symbols_1
|
||||||
dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
|
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_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_applicable_kinds = unity_serialised_field
|
||||||
dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance
|
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_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_applicable_kinds = unity_serialised_field
|
||||||
dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_required_modifiers = instance
|
dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_required_modifiers = instance
|
||||||
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
|
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_off_tag = @formatter:off
|
||||||
resharper_formatter_on_tag = @formatter:on
|
resharper_formatter_on_tag = @formatter:on
|
||||||
resharper_formatter_tags_enabled = true
|
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_object_creation_when_type_not_evident = target_typed
|
||||||
resharper_place_accessorholder_attribute_on_same_line = false
|
resharper_place_accessorholder_attribute_on_same_line = false
|
||||||
resharper_place_expr_property_on_single_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.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Logs**
|
**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/
|
If the logs are short, make sure to triple backtick them, or use https://pastebin.com/
|
||||||
|
|
||||||
**Hardware:**
|
**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=edge,branch=master,commit=${{ github.sha }}
|
||||||
type=sha,commit=${{ github.sha }}
|
type=sha,commit=${{ github.sha }}
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
- name: Build image for scanning ${{ matrix.image_name }}
|
- name: Build image for scanning ${{ matrix.image_name }}
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -410,4 +410,4 @@ src/producer/.run/
|
|||||||
|
|
||||||
# Caddy logs
|
# Caddy logs
|
||||||
deployment/docker/optional_reverse_proxy/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
|
## Contents
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> Until we reach `v1.0.0`, please consider releases as alpha.
|
> Until we reach `v1.0.0`, please consider releases as alpha.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
@@ -75,9 +75,9 @@ You can also increase `JOB_CONCURRENCY` from `5` to `10`.
|
|||||||
|
|
||||||
### DebridMediaManager setup (optional)
|
### 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)
|
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`
|
2. Press `Generate new token`
|
||||||
@@ -89,8 +89,8 @@ We can search DebridMediaManager hash lists which are hosted on GitHub. This all
|
|||||||
90 days
|
90 days
|
||||||
Description:
|
Description:
|
||||||
<blank>
|
<blank>
|
||||||
Respository access
|
Repository access
|
||||||
(checked) Public Repositories (read-only)
|
(checked) Public Repositories (read-only)
|
||||||
```
|
```
|
||||||
4. Click `Generate token`
|
4. Click `Generate token`
|
||||||
5. Take the new token and add it to the bottom of the [.env](deployment/docker/.env) file
|
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)).
|
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.
|
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.
|
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.
|
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 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.
|
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]
|
> [!NOTE]
|
||||||
> If you have changed the default password for PostgreSQL (RECOMMENDED) please change it above accordingly
|
> If you have changed the default password for PostgreSQL (RECOMMENDED) please change it above accordingly
|
||||||
>
|
>
|
||||||
> `postgresql://postgres:<password here>@postgres:5432/knightcrawler`
|
> `postgresql://postgres:<password here>@postgres:5432/knightcrawler`
|
||||||
|
|
||||||
Then run the following docker run command to import the database:
|
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]
|
> [!NOTE]
|
||||||
> If you have changed the default password for PostgreSQL (RECOMMENDED) please change it above accordingly
|
> If you have changed the default password for PostgreSQL (RECOMMENDED) please change it above accordingly
|
||||||
>
|
>
|
||||||
> `postgresql://postgres:<password here>@<docker-ip>/knightcrawler`
|
> `postgresql://postgres:<password here>@<docker-ip>/knightcrawler`
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
@@ -308,7 +308,7 @@ Then run `pgloader db.load`.
|
|||||||
|
|
||||||
### Process the data we have imported
|
### 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.
|
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:
|
**With your existing stack still running**, run:
|
||||||
```
|
```
|
||||||
docker exec -it torrentio-selfhostio-postgres-1 psql -c "
|
docker exec -it torrentio-selfhostio-postgres-1 psql -c "
|
||||||
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
|
||||||
WHERE pid <> pg_backend_pid() AND datname = 'selfhostio';
|
WHERE pid <> pg_backend_pid() AND datname = 'selfhostio';
|
||||||
ALTER DATABASE selfhostio RENAME TO knightcrawler;"
|
ALTER DATABASE selfhostio RENAME TO knightcrawler;"
|
||||||
```
|
```
|
||||||
Make sure your postgres container is named `torrentio-selfhostio-postgres-1`, otherwise, adjust accordingly.
|
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
|
RABBITMQ_PUBLISH_INTERVAL_IN_SECONDS=10
|
||||||
|
|
||||||
# Metadata
|
# 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 * *"
|
METADATA_DOWNLOAD_IMDB_DATA_SCHEDULE="0 0 1 * *"
|
||||||
## If true, the metadata will be downloaded once and then the schedule will be ignored
|
## If true, the metadata will be downloaded once and then the schedule will be ignored
|
||||||
METADATA_DOWNLOAD_IMDB_DATA_ONCE=true
|
METADATA_DOWNLOAD_IMDB_DATA_ONCE=true
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "7000:7000"
|
- "7000:7000"
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
knightcrawler-network:
|
knightcrawler-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
TOKEN=cloudflare-tunnel-token-here
|
TOKEN=cloudflare-tunnel-token-here
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ providers:
|
|||||||
folder: Dashboards
|
folder: Dashboards
|
||||||
type: file
|
type: file
|
||||||
options:
|
options:
|
||||||
path: /var/lib/grafana/dashboards
|
path: /var/lib/grafana/dashboards
|
||||||
|
|||||||
@@ -578,4 +578,4 @@
|
|||||||
"uid": "knightcrawler-logs",
|
"uid": "knightcrawler-logs",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"weekStart": ""
|
"weekStart": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ datasources:
|
|||||||
basicAuth: false
|
basicAuth: false
|
||||||
isDefault: false
|
isDefault: false
|
||||||
version: 1
|
version: 1
|
||||||
editable: true
|
editable: true
|
||||||
|
|||||||
@@ -32,4 +32,4 @@ schema_config:
|
|||||||
schema: v11
|
schema: v11
|
||||||
index:
|
index:
|
||||||
prefix: index_
|
prefix: index_
|
||||||
period: 24h
|
period: 24h
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ networks:
|
|||||||
volumes:
|
volumes:
|
||||||
grafana-data:
|
grafana-data:
|
||||||
loki-data:
|
loki-data:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
prometheus:
|
prometheus:
|
||||||
command:
|
command:
|
||||||
@@ -46,7 +46,7 @@ services:
|
|||||||
image: prometheuscommunity/postgres-exporter
|
image: prometheuscommunity/postgres-exporter
|
||||||
networks:
|
networks:
|
||||||
- knightcrawler-network
|
- knightcrawler-network
|
||||||
|
|
||||||
promtail:
|
promtail:
|
||||||
image: grafana/promtail:2.9.4
|
image: grafana/promtail:2.9.4
|
||||||
volumes:
|
volumes:
|
||||||
@@ -61,7 +61,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- knightcrawler-network
|
- knightcrawler-network
|
||||||
|
|
||||||
|
|
||||||
loki:
|
loki:
|
||||||
command: '-config.file=/etc/loki/local-config.yml'
|
command: '-config.file=/etc/loki/local-config.yml'
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -73,4 +73,3 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./config/loki/config.yml:/etc/loki/local-config.yml
|
- ./config/loki/config.yml:/etc/loki/local-config.yml
|
||||||
- loki-data:/loki
|
- loki-data:/loki
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,9 @@
|
|||||||
## NOTE: I have dramatically lowered the above for testing.
|
## NOTE: I have dramatically lowered the above for testing.
|
||||||
## Once you have confirmed that everything works, start increasing the number
|
## 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 :
|
## the goal is to have HSTS set to one year with subdomains and preloading :
|
||||||
##
|
##
|
||||||
# `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`
|
# `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`
|
||||||
##
|
##
|
||||||
## Warning: You should ensure that you fully understand the implications
|
## Warning: You should ensure that you fully understand the implications
|
||||||
## of HSTS preloading before you include the directive in your policy and
|
## of HSTS preloading before you include the directive in your policy and
|
||||||
## before you submit. It means that your entire domain and all subdomains,
|
## before you submit. It means that your entire domain and all subdomains,
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
(cloudflare-tunnel-protection) {
|
(cloudflare-tunnel-protection) {
|
||||||
import ./snippets/cloudflare-replace-X-Forwarded-For
|
import ./snippets/cloudflare-replace-X-Forwarded-For
|
||||||
trusted_proxies 172.17.0.0/16 # This needs to be your docker subnet
|
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.
|
# If you can't make it work ask for my help on discord.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,16 +16,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"WriteTo": [
|
"WriteTo": [
|
||||||
{
|
{
|
||||||
"Name": "Console",
|
"Name": "Console",
|
||||||
"Args": {
|
"Args": {
|
||||||
"outputTemplate": "{Timestamp:HH:mm:ss} [{Level}] [{SourceContext}] {Message}{NewLine}{Exception}"
|
"outputTemplate": "{Timestamp:HH:mm:ss} [{Level}] [{SourceContext}] {Message}{NewLine}{Exception}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
|
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
|
||||||
"Properties": {
|
"Properties": {
|
||||||
"Application": "Metadata"
|
"Application": "Metadata"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,41 +4,41 @@ public static class ConfigurationExtensions
|
|||||||
{
|
{
|
||||||
private const string ConfigurationFolder = "Configuration";
|
private const string ConfigurationFolder = "Configuration";
|
||||||
private const string LoggingConfig = "logging.json";
|
private const string LoggingConfig = "logging.json";
|
||||||
|
|
||||||
public static IConfigurationBuilder AddServiceConfiguration(this IConfigurationBuilder configuration)
|
public static IConfigurationBuilder AddServiceConfiguration(this IConfigurationBuilder configuration)
|
||||||
{
|
{
|
||||||
configuration.SetBasePath(Path.Combine(AppContext.BaseDirectory, ConfigurationFolder));
|
configuration.SetBasePath(Path.Combine(AppContext.BaseDirectory, ConfigurationFolder));
|
||||||
|
|
||||||
configuration.AddJsonFile(LoggingConfig, false, true);
|
configuration.AddJsonFile(LoggingConfig, false, true);
|
||||||
|
|
||||||
configuration.AddEnvironmentVariables();
|
configuration.AddEnvironmentVariables();
|
||||||
|
|
||||||
configuration.AddUserSecrets<Program>();
|
configuration.AddUserSecrets<Program>();
|
||||||
|
|
||||||
return configuration;
|
return configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TConfiguration LoadConfigurationFromConfig<TConfiguration>(this IServiceCollection services, IConfiguration configuration, string sectionName)
|
public static TConfiguration LoadConfigurationFromConfig<TConfiguration>(this IServiceCollection services, IConfiguration configuration, string sectionName)
|
||||||
where TConfiguration : class
|
where TConfiguration : class
|
||||||
{
|
{
|
||||||
var instance = configuration.GetSection(sectionName).Get<TConfiguration>();
|
var instance = configuration.GetSection(sectionName).Get<TConfiguration>();
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(instance, nameof(instance));
|
ArgumentNullException.ThrowIfNull(instance, nameof(instance));
|
||||||
|
|
||||||
services.TryAddSingleton(instance);
|
services.TryAddSingleton(instance);
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TConfiguration LoadConfigurationFromEnv<TConfiguration>(this IServiceCollection services)
|
public static TConfiguration LoadConfigurationFromEnv<TConfiguration>(this IServiceCollection services)
|
||||||
where TConfiguration : class
|
where TConfiguration : class
|
||||||
{
|
{
|
||||||
var instance = Activator.CreateInstance<TConfiguration>();
|
var instance = Activator.CreateInstance<TConfiguration>();
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(instance, nameof(instance));
|
ArgumentNullException.ThrowIfNull(instance, nameof(instance));
|
||||||
|
|
||||||
services.TryAddSingleton(instance);
|
services.TryAddSingleton(instance);
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ public static class EnvironmentExtensions
|
|||||||
public static bool GetEnvironmentVariableAsBool(this string prefix, string varName, bool fallback = false)
|
public static bool GetEnvironmentVariableAsBool(this string prefix, string varName, bool fallback = false)
|
||||||
{
|
{
|
||||||
var fullVarName = GetFullVariableName(prefix, varName);
|
var fullVarName = GetFullVariableName(prefix, varName);
|
||||||
|
|
||||||
var str = Environment.GetEnvironmentVariable(fullVarName);
|
var str = Environment.GetEnvironmentVariable(fullVarName);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(str))
|
if (string.IsNullOrEmpty(str))
|
||||||
{
|
{
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -21,11 +21,11 @@ public static class EnvironmentExtensions
|
|||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int GetEnvironmentVariableAsInt(this string prefix, string varName, int fallback = 0)
|
public static int GetEnvironmentVariableAsInt(this string prefix, string varName, int fallback = 0)
|
||||||
{
|
{
|
||||||
var fullVarName = GetFullVariableName(prefix, varName);
|
var fullVarName = GetFullVariableName(prefix, varName);
|
||||||
|
|
||||||
var str = Environment.GetEnvironmentVariable(fullVarName);
|
var str = Environment.GetEnvironmentVariable(fullVarName);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(str))
|
if (string.IsNullOrEmpty(str))
|
||||||
@@ -35,11 +35,11 @@ public static class EnvironmentExtensions
|
|||||||
|
|
||||||
return int.TryParse(str, out var result) ? result : fallback;
|
return int.TryParse(str, out var result) ? result : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetRequiredEnvironmentVariableAsString(this string prefix, string varName)
|
public static string GetRequiredEnvironmentVariableAsString(this string prefix, string varName)
|
||||||
{
|
{
|
||||||
var fullVarName = GetFullVariableName(prefix, varName);
|
var fullVarName = GetFullVariableName(prefix, varName);
|
||||||
|
|
||||||
var str = Environment.GetEnvironmentVariable(fullVarName);
|
var str = Environment.GetEnvironmentVariable(fullVarName);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(str))
|
if (string.IsNullOrEmpty(str))
|
||||||
@@ -49,11 +49,11 @@ public static class EnvironmentExtensions
|
|||||||
|
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetOptionalEnvironmentVariableAsString(this string prefix, string varName, string? fallback = null)
|
public static string GetOptionalEnvironmentVariableAsString(this string prefix, string varName, string? fallback = null)
|
||||||
{
|
{
|
||||||
var fullVarName = GetFullVariableName(prefix, varName);
|
var fullVarName = GetFullVariableName(prefix, varName);
|
||||||
|
|
||||||
var str = Environment.GetEnvironmentVariable(fullVarName);
|
var str = Environment.GetEnvironmentVariable(fullVarName);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(str))
|
if (string.IsNullOrEmpty(str))
|
||||||
@@ -63,6 +63,6 @@ public static class EnvironmentExtensions
|
|||||||
|
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetFullVariableName(string prefix, string varName) => $"{prefix}_{varName}";
|
private static string GetFullVariableName(string prefix, string varName) => $"{prefix}_{varName}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ namespace Metadata.Extensions;
|
|||||||
public static class JsonSerializerExtensions
|
public static class JsonSerializerExtensions
|
||||||
{
|
{
|
||||||
public static string ToJson<T>(this T value) => JsonSerializer.Serialize(value);
|
public static string ToJson<T>(this T value) => JsonSerializer.Serialize(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static IServiceCollection AddMongoDb(this IServiceCollection services)
|
internal static IServiceCollection AddMongoDb(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.LoadConfigurationFromEnv<MongoConfiguration>();
|
services.LoadConfigurationFromEnv<MongoConfiguration>();
|
||||||
services.AddTransient<ImdbMongoDbService>();
|
services.AddTransient<ImdbMongoDbService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static IServiceCollection AddJobSupport(this IServiceCollection services)
|
internal static IServiceCollection AddJobSupport(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.LoadConfigurationFromEnv<JobConfiguration>();
|
services.LoadConfigurationFromEnv<JobConfiguration>();
|
||||||
@@ -24,7 +24,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScheduler()
|
services.AddScheduler()
|
||||||
.AddTransient<DownloadImdbDataJob>()
|
.AddTransient<DownloadImdbDataJob>()
|
||||||
.AddHostedService<JobScheduler>();
|
.AddHostedService<JobScheduler>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ internal static class WebApplicationBuilderExtensions
|
|||||||
{
|
{
|
||||||
options.DefaultExecutionTimeout = 6.Hours();
|
options.DefaultExecutionTimeout = 6.Hours();
|
||||||
});
|
});
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ public class JobConfiguration
|
|||||||
private const string DownloadImdbDataVariable = "DOWNLOAD_IMDB_DATA_SCHEDULE";
|
private const string DownloadImdbDataVariable = "DOWNLOAD_IMDB_DATA_SCHEDULE";
|
||||||
private const string DownloadImdbDataOnceVariable = "DOWNLOAD_IMDB_DATA_ONCE";
|
private const string DownloadImdbDataOnceVariable = "DOWNLOAD_IMDB_DATA_ONCE";
|
||||||
private const string InsertBatchSizeVariable = "INSERT_BATCH_SIZE";
|
private const string InsertBatchSizeVariable = "INSERT_BATCH_SIZE";
|
||||||
|
|
||||||
public int InsertBatchSize { get; init; } = Prefix.GetEnvironmentVariableAsInt(InsertBatchSizeVariable, 25_000);
|
public int InsertBatchSize { get; init; } = Prefix.GetEnvironmentVariableAsInt(InsertBatchSizeVariable, 25_000);
|
||||||
public string DownloadImdbCronSchedule { get; init; } = Prefix.GetOptionalEnvironmentVariableAsString(DownloadImdbDataVariable, CronExpressions.EveryHour);
|
public string DownloadImdbCronSchedule { get; init; } = Prefix.GetOptionalEnvironmentVariableAsString(DownloadImdbDataVariable, CronExpressions.EveryHour);
|
||||||
public bool DownloadImdbOnce { get; init; } = Prefix.GetEnvironmentVariableAsBool(DownloadImdbDataOnceVariable);
|
public bool DownloadImdbOnce { get; init; } = Prefix.GetEnvironmentVariableAsBool(DownloadImdbDataOnceVariable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ public class MongoConfiguration
|
|||||||
private const string DbVariable = "DB";
|
private const string DbVariable = "DB";
|
||||||
private const string UsernameVariable = "USER";
|
private const string UsernameVariable = "USER";
|
||||||
private const string PasswordVariable = "PASSWORD";
|
private const string PasswordVariable = "PASSWORD";
|
||||||
|
|
||||||
|
|
||||||
private string Host { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(HostVariable);
|
private string Host { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(HostVariable);
|
||||||
private int Port { get; init; } = Prefix.GetEnvironmentVariableAsInt(PortVariable, 27017);
|
private int Port { get; init; } = Prefix.GetEnvironmentVariableAsInt(PortVariable, 27017);
|
||||||
private string Username { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(UsernameVariable);
|
private string Username { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(UsernameVariable);
|
||||||
private string Password { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(PasswordVariable);
|
private string Password { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(PasswordVariable);
|
||||||
public string DbName { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(DbVariable);
|
public string DbName { get; init; } = Prefix.GetRequiredEnvironmentVariableAsString(DbVariable);
|
||||||
|
|
||||||
public string ConnectionString => $"mongodb://{Username}:{Password}@{Host}:{Port}/{DbName}?tls=false&directConnection=true&authSource=admin";
|
public string ConnectionString => $"mongodb://{Username}:{Password}@{Host}:{Port}/{DbName}?tls=false&directConnection=true&authSource=admin";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
namespace Metadata.Features.DeleteDownloadedImdbData;
|
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 _)
|
public Task Handle(DeleteDownloadedImdbDataRequest request, CancellationToken _)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Deleting file {FilePath}", request.FilePath);
|
logger.LogInformation("Deleting file {FilePath}", request.FilePath);
|
||||||
|
|
||||||
File.Delete(request.FilePath);
|
File.Delete(request.FilePath);
|
||||||
|
|
||||||
logger.LogInformation("File Deleted");
|
logger.LogInformation("File Deleted");
|
||||||
|
|
||||||
if (configuration.DownloadImdbOnce)
|
if (configuration.DownloadImdbOnce)
|
||||||
@@ -18,4 +18,4 @@ public class DeleteDownloadedImdbDataRequestHandler(ILogger<DeleteDownloadedImdb
|
|||||||
|
|
||||||
return Task.CompletedTask;
|
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 bool IsScheduelable => !configuration.DownloadImdbOnce && !string.IsNullOrEmpty(configuration.DownloadImdbCronSchedule);
|
||||||
public override string JobName => nameof(DownloadImdbDataJob);
|
public override string JobName => nameof(DownloadImdbDataJob);
|
||||||
public override async Task Invoke() => await messageBus.SendAsync(new GetImdbDataRequest());
|
public override async Task Invoke() => await messageBus.SendAsync(new GetImdbDataRequest());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
namespace Metadata.Features.DownloadImdbData;
|
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)
|
public class GetImdbDataRequestHandler(IHttpClientFactory clientFactory, ILogger<GetImdbDataRequestHandler> logger)
|
||||||
{
|
{
|
||||||
private const string TitleBasicsFileName = "title.basics.tsv";
|
private const string TitleBasicsFileName = "title.basics.tsv";
|
||||||
|
|
||||||
public async Task<ImportImdbDataRequest> Handle(GetImdbDataRequest _, CancellationToken cancellationToken)
|
public async Task<ImportImdbDataRequest> Handle(GetImdbDataRequest _, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Downloading IMDB data");
|
logger.LogInformation("Downloading IMDB data");
|
||||||
|
|
||||||
var client = clientFactory.CreateClient("imdb-data");
|
var client = clientFactory.CreateClient("imdb-data");
|
||||||
var response = await client.GetAsync($"{TitleBasicsFileName}.gz", cancellationToken);
|
var response = await client.GetAsync($"{TitleBasicsFileName}.gz", cancellationToken);
|
||||||
|
|
||||||
var tempFile = Path.Combine(Path.GetTempPath(), TitleBasicsFileName);
|
var tempFile = Path.Combine(Path.GetTempPath(), TitleBasicsFileName);
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
await using var gzipStream = new GZipStream(stream, CompressionMode.Decompress);
|
await using var gzipStream = new GZipStream(stream, CompressionMode.Decompress);
|
||||||
await using var fileStream = File.Create(tempFile);
|
await using var fileStream = File.Create(tempFile);
|
||||||
|
|
||||||
await gzipStream.CopyToAsync(fileStream, cancellationToken);
|
await gzipStream.CopyToAsync(fileStream, cancellationToken);
|
||||||
|
|
||||||
logger.LogInformation("Downloaded IMDB data to {TempFile}", tempFile);
|
logger.LogInformation("Downloaded IMDB data to {TempFile}", tempFile);
|
||||||
|
|
||||||
fileStream.Close();
|
fileStream.Close();
|
||||||
|
|
||||||
return new(tempFile);
|
return new(tempFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ public class ImdbEntry
|
|||||||
public string? EndYear { get; set; }
|
public string? EndYear { get; set; }
|
||||||
public string? RuntimeMinutes { get; set; }
|
public string? RuntimeMinutes { get; set; }
|
||||||
public string? Genres { get; set; }
|
public string? Genres { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public class ImdbMongoDbService
|
|||||||
public ImdbMongoDbService(MongoConfiguration configuration, ILogger<ImdbMongoDbService> logger)
|
public ImdbMongoDbService(MongoConfiguration configuration, ILogger<ImdbMongoDbService> logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
var client = new MongoClient(configuration.ConnectionString);
|
var client = new MongoClient(configuration.ConnectionString);
|
||||||
var database = client.GetDatabase(configuration.DbName);
|
var database = client.GetDatabase(configuration.DbName);
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ public class ImdbMongoDbService
|
|||||||
|
|
||||||
await _imdbCollection.BulkWriteAsync(operations);
|
await _imdbCollection.BulkWriteAsync(operations);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsDatabaseInitialized()
|
public bool IsDatabaseInitialized()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -61,4 +61,4 @@ public class ImdbMongoDbService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
namespace Metadata.Features.ImportImdbData;
|
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,
|
FullMode = BoundedChannelFullMode.Wait,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Skip the header row
|
// Skip the header row
|
||||||
await csv.ReadAsync();
|
await csv.ReadAsync();
|
||||||
|
|
||||||
var batchInsertTask = CreateBatchOfEntries(channel, cancellationToken);
|
var batchInsertTask = CreateBatchOfEntries(channel, cancellationToken);
|
||||||
|
|
||||||
await ReadEntries(csv, channel, cancellationToken);
|
await ReadEntries(csv, channel, cancellationToken);
|
||||||
|
|
||||||
channel.Writer.Complete();
|
channel.Writer.Complete();
|
||||||
|
|
||||||
await batchInsertTask;
|
await batchInsertTask;
|
||||||
|
|
||||||
return new(request.FilePath);
|
return new(request.FilePath);
|
||||||
@@ -45,7 +45,7 @@ public class ImportImdbDataRequestHandler(ILogger<ImportImdbDataRequestHandler>
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var batch = new List<ImdbEntry>
|
var batch = new List<ImdbEntry>
|
||||||
{
|
{
|
||||||
movieData,
|
movieData,
|
||||||
@@ -63,7 +63,7 @@ public class ImportImdbDataRequestHandler(ILogger<ImportImdbDataRequestHandler>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
private static async Task ReadEntries(CsvReader csv, Channel<ImdbEntry, ImdbEntry> channel, CancellationToken cancellationToken)
|
private static async Task ReadEntries(CsvReader csv, Channel<ImdbEntry, ImdbEntry> channel, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
while (await csv.ReadAsync())
|
while (await csv.ReadAsync())
|
||||||
@@ -80,13 +80,13 @@ public class ImportImdbDataRequestHandler(ILogger<ImportImdbDataRequestHandler>
|
|||||||
RuntimeMinutes = csv.GetField(7),
|
RuntimeMinutes = csv.GetField(7),
|
||||||
Genres = csv.GetField(8),
|
Genres = csv.GetField(8),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await channel.Writer.WriteAsync(movieData, cancellationToken);
|
await channel.Writer.WriteAsync(movieData, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ public abstract class BaseJob : IMetadataJob
|
|||||||
public abstract bool IsScheduelable { get; }
|
public abstract bool IsScheduelable { get; }
|
||||||
|
|
||||||
public abstract string JobName { get; }
|
public abstract string JobName { get; }
|
||||||
|
|
||||||
public abstract Task Invoke();
|
public abstract Task Invoke();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ public interface IMetadataJob : IInvocable
|
|||||||
{
|
{
|
||||||
bool IsScheduelable { get; }
|
bool IsScheduelable { get; }
|
||||||
string JobName { get; }
|
string JobName { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ public class JobScheduler(IServiceProvider serviceProvider) : IHostedService
|
|||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var scope = serviceProvider.CreateAsyncScope();
|
using var scope = serviceProvider.CreateAsyncScope();
|
||||||
|
|
||||||
var mongoDbService = scope.ServiceProvider.GetRequiredService<ImdbMongoDbService>();
|
var mongoDbService = scope.ServiceProvider.GetRequiredService<ImdbMongoDbService>();
|
||||||
|
|
||||||
if (!mongoDbService.IsDatabaseInitialized())
|
if (!mongoDbService.IsDatabaseInitialized())
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("MongoDb is not initialized");
|
throw new InvalidOperationException("MongoDb is not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
var jobConfigurations = scope.ServiceProvider.GetRequiredService<JobConfiguration>();
|
var jobConfigurations = scope.ServiceProvider.GetRequiredService<JobConfiguration>();
|
||||||
var downloadJob = scope.ServiceProvider.GetRequiredService<DownloadImdbDataJob>();
|
var downloadJob = scope.ServiceProvider.GetRequiredService<DownloadImdbDataJob>();
|
||||||
|
|
||||||
@@ -20,15 +20,15 @@ public class JobScheduler(IServiceProvider serviceProvider) : IHostedService
|
|||||||
{
|
{
|
||||||
return downloadJob.Invoke();
|
return downloadJob.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
var scheduler = scope.ServiceProvider.GetRequiredService<IScheduler>();
|
var scheduler = scope.ServiceProvider.GetRequiredService<IScheduler>();
|
||||||
|
|
||||||
scheduler.Schedule<DownloadImdbDataJob>()
|
scheduler.Schedule<DownloadImdbDataJob>()
|
||||||
.Cron(jobConfigurations.DownloadImdbCronSchedule)
|
.Cron(jobConfigurations.DownloadImdbCronSchedule)
|
||||||
.PreventOverlapping(nameof(downloadJob.JobName));
|
.PreventOverlapping(nameof(downloadJob.JobName));
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => 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 EveryDay = "0 0 0 * *";
|
||||||
public const string EveryWeek = "0 0 * * 0";
|
public const string EveryWeek = "0 0 * * 0";
|
||||||
public const string EveryMonth = "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 ImdbDataClientName = "imdb-data";
|
||||||
public const string ImdbClientBaseAddress = "https://datasets.imdbws.com/";
|
public const string ImdbClientBaseAddress = "https://datasets.imdbws.com/";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ builder.Services
|
|||||||
|
|
||||||
var host = builder.Build();
|
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" }],
|
"one-var": ["error", { uninitialized: "consecutive" }],
|
||||||
"prefer-destructuring": "warn",
|
"prefer-destructuring": "warn",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
docker build -t ippexdeploymentscr.azurecr.io/dave/stremio-addon-jackett:latest . --platform linux/amd64
|
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"]
|
"typeRoots": ["node_modules/@types", "src/@types"]
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ builder.defineStreamHandler((args) => {
|
|||||||
if (!args.id.match(/tt\d+/i) && !args.id.match(/kitsu:\d+/i)) {
|
if (!args.id.match(/tt\d+/i) && !args.id.match(/kitsu:\d+/i)) {
|
||||||
return Promise.resolve({ streams: [] });
|
return Promise.resolve({ streams: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processConfig.DEBUG) {
|
if (processConfig.DEBUG) {
|
||||||
console.log(`Incoming stream ${args.id} request`)
|
console.log(`Incoming stream ${args.id} request`)
|
||||||
console.log('args', args);
|
console.log('args', args);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cacheWrapStream(args.id, () => limiter.schedule(() =>
|
return cacheWrapStream(args.id, () => limiter.schedule(() =>
|
||||||
streamHandler(args)
|
streamHandler(args)
|
||||||
.then(records => records.map(record => toStreamInfo(record, args.type))))
|
.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);
|
console.log("Transforming data for query " + data);
|
||||||
|
|
||||||
let results = [];
|
let results = [];
|
||||||
|
|
||||||
const parsedData = await parseString(data);
|
const parsedData = await parseString(data);
|
||||||
|
|
||||||
if (!parsedData.rss.channel[0]?.item) {
|
if (!parsedData.rss.channel[0]?.item) {
|
||||||
@@ -126,7 +126,7 @@ export const transformData = async (data, query) => {
|
|||||||
[torznabDataItem.$.name]: torznabDataItem.$.value,
|
[torznabDataItem.$.name]: torznabDataItem.$.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (torznabData.infohash) {
|
if (torznabData.infohash) {
|
||||||
|
|
||||||
const [title, pubDate, category, size] = [rssItem.title[0], rssItem.pubDate[0], rssItem.category[0], rssItem.size[0]];
|
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;
|
return results;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,4 +38,4 @@ export const searchJackett = async (query) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sortedResults;
|
return sortedResults;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const getMovieSearchQueries = (cleanName, year) => {
|
|||||||
const getSeriesSearchQueries = (cleanName, year, season, episode) => {
|
const getSeriesSearchQueries = (cleanName, year, season, episode) => {
|
||||||
return {
|
return {
|
||||||
seriesByEpisode: seriesByEpisode(cleanName, season, episode),
|
seriesByEpisode: seriesByEpisode(cleanName, season, episode),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jackettSearchQueries = (cleanName, type, year, 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);
|
return getMovieSearchQueries(cleanName, year);
|
||||||
case Type.SERIES:
|
case Type.SERIES:
|
||||||
return getSeriesSearchQueries(cleanName, year, season, episode);
|
return getSeriesSearchQueries(cleanName, year, season, episode);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { };
|
return { };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const cinemetaUri = cinemetaConfig.URI;
|
|||||||
export const getMetaData = (args) => {
|
export const getMetaData = (args) => {
|
||||||
const [imdbId] = args.id.split(':');
|
const [imdbId] = args.id.split(':');
|
||||||
const {type} = args;
|
const {type} = args;
|
||||||
|
|
||||||
return cacheWrapImdbMetaData(args.id, () => getInfoForImdbId(imdbId, type));
|
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(`Getting info for ${imdbId} of type ${type}`);
|
||||||
console.log(`Request URI: ${requestUri}`);
|
console.log(`Request URI: ${requestUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: response } = await axios.get(requestUri, options);
|
const { data: response } = await axios.get(requestUri, options);
|
||||||
return response.meta;
|
return response.meta;
|
||||||
@@ -28,4 +28,4 @@ const getInfoForImdbId = async (imdbId, type) => {
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ export function parseConfiguration(configuration) {
|
|||||||
.map(value => keysToUppercase.includes(key) ? value.toUpperCase() : value.toLowerCase()))
|
.map(value => keysToUppercase.includes(key) ? value.toUpperCase() : value.toLowerCase()))
|
||||||
|
|
||||||
return configValues;
|
return configValues;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ a.install-link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input:focus, .btn:focus {
|
.input:focus, .btn:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 2pt rgb(30, 144, 255, 0.7);
|
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)
|
const debridOptionsHTML = Object.values(DebridOptions.options)
|
||||||
.map(option => `<option value="${option.key}">${option.description}</option>`)
|
.map(option => `<option value="${option.key}">${option.description}</option>`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html style="background-image: url(${background});">
|
<html style="background-image: url(${background});">
|
||||||
@@ -239,54 +239,54 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
<h2 class="description">${manifest.description || ''}</h2>
|
<h2 class="description">${manifest.description || ''}</h2>
|
||||||
|
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
|
|
||||||
<label class="label" id="iLimitLabel" for="iLimit">Max results per quality:</label>
|
<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">
|
<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>
|
<label class="label" for="iDebridProviders">Debrid provider:</label>
|
||||||
<select id="iDebridProviders" class="input" onchange="debridProvidersChange()">
|
<select id="iDebridProviders" class="input" onchange="debridProvidersChange()">
|
||||||
<option value="none" selected>None</option>
|
<option value="none" selected>None</option>
|
||||||
${debridProvidersHTML}
|
${debridProvidersHTML}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div id="dRealDebrid">
|
<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>
|
<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">
|
<input type="text" id="iRealDebrid" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dAllDebrid">
|
<div id="dAllDebrid">
|
||||||
<label class="label" for="iAllDebrid">AllDebrid API Key (Create it <a href='https://alldebrid.com/apikeys' target="_blank">here</a>):</label>
|
<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">
|
<input type="text" id="iAllDebrid" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dPremiumize">
|
<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>
|
<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">
|
<input type="text" id="iPremiumize" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dDebridLink">
|
<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>
|
<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">
|
<input type="text" id="iDebridLink" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dOffcloud">
|
<div id="dOffcloud">
|
||||||
<label class="label" for="iOffcloud">Offcloud API Key (Find it <a href='https://offcloud.com/#/account' target="_blank">here</a>):</label>
|
<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">
|
<input type="text" id="iOffcloud" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dPutio">
|
<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>
|
<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="iPutioClientId" placeholder="ClientId" onchange="generateInstallLink()" class="input">
|
||||||
<input type="text" id="iPutioToken" placeholder="Token" onchange="generateInstallLink()" class="input">
|
<input type="text" id="iPutioToken" placeholder="Token" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dDebridOptions">
|
<div id="dDebridOptions">
|
||||||
<label class="label" for="iDebridOptions">Debrid options:</label>
|
<label class="label" for="iDebridOptions">Debrid options:</label>
|
||||||
<select id="iDebridOptions" class="input" onchange="generateInstallLink()" name="debridOptions[]" multiple="multiple">
|
<select id="iDebridOptions" class="input" onchange="generateInstallLink()" name="debridOptions[]" multiple="multiple">
|
||||||
${debridOptionsHTML}
|
${debridOptionsHTML}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
|
|
||||||
<a id="installLink" class="install-link" href="#">
|
<a id="installLink" class="install-link" href="#">
|
||||||
@@ -295,7 +295,7 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
<div class="contact">
|
<div class="contact">
|
||||||
<p>Or paste into Stremio search bar after clicking install</p>
|
<p>Or paste into Stremio search bar after clicking install</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
<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 isTvAgent = /\\b(?:tv|wv)\\b/i.test(navigator.userAgent)
|
||||||
const isDesktopMedia = window.matchMedia("(pointer:fine)").matches;
|
const isDesktopMedia = window.matchMedia("(pointer:fine)").matches;
|
||||||
if (isDesktopMedia && !isTvMedia && !isTvAgent) {
|
if (isDesktopMedia && !isTvMedia && !isTvAgent) {
|
||||||
$('#iDebridOptions').multiselect({
|
$('#iDebridOptions').multiselect({
|
||||||
nonSelectedText: 'None',
|
nonSelectedText: 'None',
|
||||||
buttonTextAlignment: 'left',
|
buttonTextAlignment: 'left',
|
||||||
onChange: () => generateInstallLink()
|
onChange: () => generateInstallLink()
|
||||||
@@ -325,7 +325,7 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
generateInstallLink();
|
generateInstallLink();
|
||||||
debridProvidersChange();
|
debridProvidersChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
function debridProvidersChange() {
|
function debridProvidersChange() {
|
||||||
const provider = $('#iDebridProviders').val()
|
const provider = $('#iDebridProviders').val()
|
||||||
$('#dDebridOptions').toggle(provider !== 'none');
|
$('#dDebridOptions').toggle(provider !== 'none');
|
||||||
@@ -336,10 +336,10 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
$('#dOffcloud').toggle(provider === '${MochOptions.offcloud.key}');
|
$('#dOffcloud').toggle(provider === '${MochOptions.offcloud.key}');
|
||||||
$('#dPutio').toggle(provider === '${MochOptions.putio.key}');
|
$('#dPutio').toggle(provider === '${MochOptions.putio.key}');
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateInstallLink() {
|
function generateInstallLink() {
|
||||||
const limitValue = $('#iLimit').val() || '';
|
const limitValue = $('#iLimit').val() || '';
|
||||||
|
|
||||||
const debridOptionsValue = $('#iDebridOptions').val().join(',') || '';
|
const debridOptionsValue = $('#iDebridOptions').val().join(',') || '';
|
||||||
const realDebridValue = $('#iRealDebrid').val() || '';
|
const realDebridValue = $('#iRealDebrid').val() || '';
|
||||||
const allDebridValue = $('#iAllDebrid').val() || '';
|
const allDebridValue = $('#iAllDebrid').val() || '';
|
||||||
@@ -348,9 +348,9 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
const offcloudValue = $('#iOffcloud').val() || ''
|
const offcloudValue = $('#iOffcloud').val() || ''
|
||||||
const putioClientIdValue = $('#iPutioClientId').val() || '';
|
const putioClientIdValue = $('#iPutioClientId').val() || '';
|
||||||
const putioTokenValue = $('#iPutioToken').val() || '';
|
const putioTokenValue = $('#iPutioToken').val() || '';
|
||||||
|
|
||||||
const limit = /^[1-9][0-9]{0,2}$/.test(limitValue) && limitValue;
|
const limit = /^[1-9][0-9]{0,2}$/.test(limitValue) && limitValue;
|
||||||
|
|
||||||
const debridOptions = debridOptionsValue.length && debridOptionsValue.trim();
|
const debridOptions = debridOptionsValue.length && debridOptionsValue.trim();
|
||||||
const realDebrid = realDebridValue.length && realDebridValue.trim();
|
const realDebrid = realDebridValue.length && realDebridValue.trim();
|
||||||
const premiumize = premiumizeValue.length && premiumizeValue.trim();
|
const premiumize = premiumizeValue.length && premiumizeValue.trim();
|
||||||
@@ -361,7 +361,7 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
|
|
||||||
let configurationValue = [
|
let configurationValue = [
|
||||||
['limit', limit],
|
['limit', limit],
|
||||||
['${DebridOptions.key}', debridOptions],
|
['${DebridOptions.key}', debridOptions],
|
||||||
['${MochOptions.realdebrid.key}', realDebrid],
|
['${MochOptions.realdebrid.key}', realDebrid],
|
||||||
['${MochOptions.premiumize.key}', premiumize],
|
['${MochOptions.premiumize.key}', premiumize],
|
||||||
['${MochOptions.alldebrid.key}', allDebrid],
|
['${MochOptions.alldebrid.key}', allDebrid],
|
||||||
|
|||||||
@@ -56,4 +56,4 @@ export function getSources(magnetInfo) {
|
|||||||
}
|
}
|
||||||
const trackers = Array.isArray(magnetInfo.announce) ? magnetInfo.announce : magnetInfo.announce.split(',');
|
const trackers = Array.isArray(magnetInfo.announce) ? magnetInfo.announce : magnetInfo.announce.split(',');
|
||||||
return trackers.map(tracker => `tracker:${tracker}`).concat(`dht:${magnetInfo.infohash}`);
|
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_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
|
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',
|
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 behaviorHints = bingeGroup ? { bingeGroup } : undefined;
|
||||||
|
|
||||||
const magnetInfo = decode(record.magneturl)
|
const magnetInfo = decode(record.magneturl)
|
||||||
|
|
||||||
return cleanOutputObject({
|
return cleanOutputObject({
|
||||||
name: name,
|
name: name,
|
||||||
title: title,
|
title: title,
|
||||||
@@ -61,7 +61,7 @@ function formatSize(size) {
|
|||||||
function getBingeGroupParts(record, sameInfo, quality, torrentInfo, fileInfo, type) {
|
function getBingeGroupParts(record, sameInfo, quality, torrentInfo, fileInfo, type) {
|
||||||
if (type === Type.MOVIE) {
|
if (type === Type.MOVIE) {
|
||||||
return [quality];
|
return [quality];
|
||||||
|
|
||||||
} else if (sameInfo) {
|
} else if (sameInfo) {
|
||||||
return [quality];
|
return [quality];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ export const Type = {
|
|||||||
SERIES: 'series',
|
SERIES: 'series',
|
||||||
ANIME: 'anime',
|
ANIME: 'anime',
|
||||||
OTHER: 'other'
|
OTHER: 'other'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ export function isStaticUrl(url) {
|
|||||||
return Object.values(staticVideoUrls).some(videoUrl => url?.endsWith(videoUrl));
|
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" }],
|
"one-var": ["error", { uninitialized: "consecutive" }],
|
||||||
"prefer-destructuring": "warn",
|
"prefer-destructuring": "warn",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,4 +65,4 @@ try {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,4 @@
|
|||||||
"typeRoots": ["node_modules/@types", "src/@types"]
|
"typeRoots": ["node_modules/@types", "src/@types"]
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ builder.defineStreamHandler((args) => {
|
|||||||
if (!args.id.match(/tt\d+/i) && !args.id.match(/kitsu:\d+/i)) {
|
if (!args.id.match(/tt\d+/i) && !args.id.match(/kitsu:\d+/i)) {
|
||||||
return Promise.resolve({ streams: [] });
|
return Promise.resolve({ streams: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
return cacheWrapStream(args.id, () => limiter.schedule(() =>
|
return cacheWrapStream(args.id, () => limiter.schedule(() =>
|
||||||
streamHandler(args)
|
streamHandler(args)
|
||||||
.then(records => records
|
.then(records => records
|
||||||
|
|||||||
@@ -25,4 +25,4 @@ export function parseConfiguration(configuration) {
|
|||||||
.map(value => keysToUppercase.includes(key) ? value.toUpperCase() : value.toLowerCase()))
|
.map(value => keysToUppercase.includes(key) ? value.toUpperCase() : value.toLowerCase()))
|
||||||
|
|
||||||
return configValues;
|
return configValues;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ a.install-link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input:focus, .btn:focus {
|
.input:focus, .btn:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 2pt rgb(30, 144, 255, 0.7);
|
box-shadow: 0 0 0 2pt rgb(30, 144, 255, 0.7);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -228,7 +228,7 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
const stylizedTypes = manifest.types
|
const stylizedTypes = manifest.types
|
||||||
.map(t => t[0].toUpperCase() + t.slice(1) + (t !== 'series' ? 's' : ''));
|
.map(t => t[0].toUpperCase() + t.slice(1) + (t !== 'series' ? 's' : ''));
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html style="background-image: url(${background});">
|
<html style="background-image: url(${background});">
|
||||||
@@ -264,73 +264,73 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
|
|
||||||
<label class="label" for="iSort">Sorting:</label>
|
<label class="label" for="iSort">Sorting:</label>
|
||||||
<select id="iSort" class="input" onchange="sortModeChange()">
|
<select id="iSort" class="input" onchange="sortModeChange()">
|
||||||
${sortOptionsHTML}
|
${sortOptionsHTML}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label class="label" for="iLanguages">Priority foreign language:</label>
|
<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">
|
<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}
|
${languagesOptionsHTML}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label class="label" for="iQualityFilter">Exclude qualities/resolutions:</label>
|
<label class="label" for="iQualityFilter">Exclude qualities/resolutions:</label>
|
||||||
<select id="iQualityFilter" class="input" onchange="generateInstallLink()" name="qualityFilters[]" multiple="multiple">
|
<select id="iQualityFilter" class="input" onchange="generateInstallLink()" name="qualityFilters[]" multiple="multiple">
|
||||||
${qualityFiltersHTML}
|
${qualityFiltersHTML}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label class="label" id="iLimitLabel" for="iLimit">Max results per quality:</label>
|
<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">
|
<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>
|
<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">
|
<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>
|
<label class="label" for="iDebridProviders">Debrid provider:</label>
|
||||||
<select id="iDebridProviders" class="input" onchange="debridProvidersChange()">
|
<select id="iDebridProviders" class="input" onchange="debridProvidersChange()">
|
||||||
<option value="none" selected>None</option>
|
<option value="none" selected>None</option>
|
||||||
${debridProvidersHTML}
|
${debridProvidersHTML}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div id="dRealDebrid">
|
<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>
|
<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">
|
<input type="text" id="iRealDebrid" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dAllDebrid">
|
<div id="dAllDebrid">
|
||||||
<label class="label" for="iAllDebrid">AllDebrid API Key (Create it <a href='https://alldebrid.com/apikeys' target="_blank">here</a>):</label>
|
<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">
|
<input type="text" id="iAllDebrid" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dPremiumize">
|
<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>
|
<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">
|
<input type="text" id="iPremiumize" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dDebridLink">
|
<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>
|
<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">
|
<input type="text" id="iDebridLink" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dOffcloud">
|
<div id="dOffcloud">
|
||||||
<label class="label" for="iOffcloud">Offcloud API Key (Find it <a href='https://offcloud.com/#/account' target="_blank">here</a>):</label>
|
<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">
|
<input type="text" id="iOffcloud" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dPutio">
|
<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>
|
<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="iPutioClientId" placeholder="ClientId" onchange="generateInstallLink()" class="input">
|
||||||
<input type="text" id="iPutioToken" placeholder="Token" onchange="generateInstallLink()" class="input">
|
<input type="text" id="iPutioToken" placeholder="Token" onchange="generateInstallLink()" class="input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dDebridOptions">
|
<div id="dDebridOptions">
|
||||||
<label class="label" for="iDebridOptions">Debrid options:</label>
|
<label class="label" for="iDebridOptions">Debrid options:</label>
|
||||||
<select id="iDebridOptions" class="input" onchange="generateInstallLink()" name="debridOptions[]" multiple="multiple">
|
<select id="iDebridOptions" class="input" onchange="generateInstallLink()" name="debridOptions[]" multiple="multiple">
|
||||||
${debridOptionsHTML}
|
${debridOptionsHTML}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
|
|
||||||
<a id="installLink" class="install-link" href="#">
|
<a id="installLink" class="install-link" href="#">
|
||||||
@@ -339,7 +339,7 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
<div class="contact">
|
<div class="contact">
|
||||||
<p>Or paste into Stremio search bar after clicking install</p>
|
<p>Or paste into Stremio search bar after clicking install</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
<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 isTvAgent = /\\b(?:tv|wv)\\b/i.test(navigator.userAgent)
|
||||||
const isDesktopMedia = window.matchMedia("(pointer:fine)").matches;
|
const isDesktopMedia = window.matchMedia("(pointer:fine)").matches;
|
||||||
if (isDesktopMedia && !isTvMedia && !isTvAgent) {
|
if (isDesktopMedia && !isTvMedia && !isTvAgent) {
|
||||||
$('#iLanguages').multiselect({
|
$('#iLanguages').multiselect({
|
||||||
nonSelectedText: 'None',
|
nonSelectedText: 'None',
|
||||||
buttonTextAlignment: 'left',
|
buttonTextAlignment: 'left',
|
||||||
onChange: () => generateInstallLink()
|
onChange: () => generateInstallLink()
|
||||||
});
|
});
|
||||||
$('#iLanguages').multiselect('select', [${languages.map(language => '"' + language + '"')}]);
|
$('#iLanguages').multiselect('select', [${languages.map(language => '"' + language + '"')}]);
|
||||||
$('#iQualityFilter').multiselect({
|
$('#iQualityFilter').multiselect({
|
||||||
nonSelectedText: 'None',
|
nonSelectedText: 'None',
|
||||||
buttonTextAlignment: 'left',
|
buttonTextAlignment: 'left',
|
||||||
onChange: () => generateInstallLink()
|
onChange: () => generateInstallLink()
|
||||||
});
|
});
|
||||||
$('#iQualityFilter').multiselect('select', [${qualityFilters.map(filter => '"' + filter + '"')}]);
|
$('#iQualityFilter').multiselect('select', [${qualityFilters.map(filter => '"' + filter + '"')}]);
|
||||||
$('#iDebridOptions').multiselect({
|
$('#iDebridOptions').multiselect({
|
||||||
nonSelectedText: 'None',
|
nonSelectedText: 'None',
|
||||||
buttonTextAlignment: 'left',
|
buttonTextAlignment: 'left',
|
||||||
onChange: () => generateInstallLink()
|
onChange: () => generateInstallLink()
|
||||||
@@ -385,7 +385,7 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
generateInstallLink();
|
generateInstallLink();
|
||||||
debridProvidersChange();
|
debridProvidersChange();
|
||||||
});
|
});
|
||||||
|
|
||||||
function sortModeChange() {
|
function sortModeChange() {
|
||||||
if (['${SortOptions.options.seeders.key}', '${SortOptions.options.size.key}'].includes($('#iSort').val())) {
|
if (['${SortOptions.options.seeders.key}', '${SortOptions.options.size.key}'].includes($('#iSort').val())) {
|
||||||
$("#iLimitLabel").text("Max results:");
|
$("#iLimitLabel").text("Max results:");
|
||||||
@@ -394,7 +394,7 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
}
|
}
|
||||||
generateInstallLink();
|
generateInstallLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
function debridProvidersChange() {
|
function debridProvidersChange() {
|
||||||
const provider = $('#iDebridProviders').val()
|
const provider = $('#iDebridProviders').val()
|
||||||
$('#dDebridOptions').toggle(provider !== 'none');
|
$('#dDebridOptions').toggle(provider !== 'none');
|
||||||
@@ -405,14 +405,14 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
$('#dOffcloud').toggle(provider === '${MochOptions.offcloud.key}');
|
$('#dOffcloud').toggle(provider === '${MochOptions.offcloud.key}');
|
||||||
$('#dPutio').toggle(provider === '${MochOptions.putio.key}');
|
$('#dPutio').toggle(provider === '${MochOptions.putio.key}');
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateInstallLink() {
|
function generateInstallLink() {
|
||||||
const qualityFilterValue = $('#iQualityFilter').val().join(',') || '';
|
const qualityFilterValue = $('#iQualityFilter').val().join(',') || '';
|
||||||
const sortValue = $('#iSort').val() || '';
|
const sortValue = $('#iSort').val() || '';
|
||||||
const languagesValue = $('#iLanguages').val().join(',') || [];
|
const languagesValue = $('#iLanguages').val().join(',') || [];
|
||||||
const limitValue = $('#iLimit').val() || '';
|
const limitValue = $('#iLimit').val() || '';
|
||||||
const sizeFilterValue = $('#iSizeFilter').val() || '';
|
const sizeFilterValue = $('#iSizeFilter').val() || '';
|
||||||
|
|
||||||
const debridOptionsValue = $('#iDebridOptions').val().join(',') || '';
|
const debridOptionsValue = $('#iDebridOptions').val().join(',') || '';
|
||||||
const realDebridValue = $('#iRealDebrid').val() || '';
|
const realDebridValue = $('#iRealDebrid').val() || '';
|
||||||
const allDebridValue = $('#iAllDebrid').val() || '';
|
const allDebridValue = $('#iAllDebrid').val() || '';
|
||||||
@@ -421,13 +421,13 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
const offcloudValue = $('#iOffcloud').val() || ''
|
const offcloudValue = $('#iOffcloud').val() || ''
|
||||||
const putioClientIdValue = $('#iPutioClientId').val() || '';
|
const putioClientIdValue = $('#iPutioClientId').val() || '';
|
||||||
const putioTokenValue = $('#iPutioToken').val() || '';
|
const putioTokenValue = $('#iPutioToken').val() || '';
|
||||||
|
|
||||||
const qualityFilters = qualityFilterValue.length && qualityFilterValue;
|
const qualityFilters = qualityFilterValue.length && qualityFilterValue;
|
||||||
const sort = sortValue !== '${SortOptions.options.qualitySeeders.key}' && sortValue;
|
const sort = sortValue !== '${SortOptions.options.qualitySeeders.key}' && sortValue;
|
||||||
const languages = languagesValue.length && languagesValue;
|
const languages = languagesValue.length && languagesValue;
|
||||||
const limit = /^[1-9][0-9]{0,2}$/.test(limitValue) && limitValue;
|
const limit = /^[1-9][0-9]{0,2}$/.test(limitValue) && limitValue;
|
||||||
const sizeFilter = sizeFilterValue.length && sizeFilterValue;
|
const sizeFilter = sizeFilterValue.length && sizeFilterValue;
|
||||||
|
|
||||||
const debridOptions = debridOptionsValue.length && debridOptionsValue.trim();
|
const debridOptions = debridOptionsValue.length && debridOptionsValue.trim();
|
||||||
const realDebrid = realDebridValue.length && realDebridValue.trim();
|
const realDebrid = realDebridValue.length && realDebridValue.trim();
|
||||||
const premiumize = premiumizeValue.length && premiumizeValue.trim();
|
const premiumize = premiumizeValue.length && premiumizeValue.trim();
|
||||||
@@ -442,7 +442,7 @@ export default function landingTemplate(manifest, config = {}) {
|
|||||||
['${QualityFilter.key}', qualityFilters],
|
['${QualityFilter.key}', qualityFilters],
|
||||||
['limit', limit],
|
['limit', limit],
|
||||||
['${SizeFilter.key}', sizeFilter],
|
['${SizeFilter.key}', sizeFilter],
|
||||||
['${DebridOptions.key}', debridOptions],
|
['${DebridOptions.key}', debridOptions],
|
||||||
['${MochOptions.realdebrid.key}', realDebrid],
|
['${MochOptions.realdebrid.key}', realDebrid],
|
||||||
['${MochOptions.premiumize.key}', premiumize],
|
['${MochOptions.premiumize.key}', premiumize],
|
||||||
['${MochOptions.alldebrid.key}', allDebrid],
|
['${MochOptions.alldebrid.key}', allDebrid],
|
||||||
|
|||||||
@@ -85,4 +85,4 @@ export function getSources(trackersInput, infoHash) {
|
|||||||
|
|
||||||
function unique(array) {
|
function unique(array) {
|
||||||
return Array.from(new Set(array));
|
return Array.from(new Set(array));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function sortStreams(streams, config) {
|
|||||||
|
|
||||||
function _sortStreams(streams, config) {
|
function _sortStreams(streams, config) {
|
||||||
const limit = /^[1-9][0-9]*$/.test(config.limit) && parseInt(config.limit) || undefined;
|
const limit = /^[1-9][0-9]*$/.test(config.limit) && parseInt(config.limit) || undefined;
|
||||||
|
|
||||||
return sortBySize(streams, limit);
|
return sortBySize(streams, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ export const Type = {
|
|||||||
SERIES: 'series',
|
SERIES: 'series',
|
||||||
ANIME: 'anime',
|
ANIME: 'anime',
|
||||||
OTHER: 'other'
|
OTHER: 'other'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ export function isStaticUrl(url) {
|
|||||||
return Object.values(staticVideoUrls).some(videoUrl => url?.endsWith(videoUrl));
|
return Object.values(staticVideoUrls).some(videoUrl => url?.endsWith(videoUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default staticVideoUrls
|
export default staticVideoUrls
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
dist/
|
dist/
|
||||||
esbuild.ts
|
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) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ export default {
|
|||||||
transform: {
|
transform: {
|
||||||
'^.+\\.tsx?$': 'ts-jest',
|
'^.+\\.tsx?$': 'ts-jest',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export enum CacheType {
|
export enum CacheType {
|
||||||
Memory = 'memory',
|
Memory = 'memory',
|
||||||
MongoDb = 'mongodb'
|
MongoDb = 'mongodb'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ export enum TorrentType {
|
|||||||
Series = 'Series',
|
Series = 'Series',
|
||||||
Movie = 'Movie',
|
Movie = 'Movie',
|
||||||
Anime = 'anime',
|
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'.`);
|
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})$/);
|
const extensionMatch = filename.match(/\.(\w{2,4})$/);
|
||||||
return extensionMatch !== null && extensions.includes(extensionMatch[1].toLowerCase());
|
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();
|
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 {
|
export interface ICacheOptions {
|
||||||
ttl: number;
|
ttl: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ export interface ICacheService {
|
|||||||
cacheTrackers: (method: CacheMethod) => Promise<any>;
|
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 {
|
export interface ICinemetaBehaviorHints {
|
||||||
defaultVideoId?: null;
|
defaultVideoId?: null;
|
||||||
hasScheduledVideos?: boolean;
|
hasScheduledVideos?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ export interface ICommonVideoMetadata {
|
|||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
imdb_id?: string;
|
imdb_id?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ export interface IIngestedRabbitTorrent {
|
|||||||
|
|
||||||
export interface IIngestedRabbitMessage {
|
export interface IIngestedRabbitMessage {
|
||||||
message: IIngestedRabbitTorrent;
|
message: IIngestedRabbitTorrent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ export interface IKitsuCatalogMetaData {
|
|||||||
background: string;
|
background: string;
|
||||||
trailers: IKitsuTrailer[];
|
trailers: IKitsuTrailer[];
|
||||||
links: IKitsuLink[];
|
links: IKitsuLink[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,4 +46,4 @@ export interface IKitsuLink {
|
|||||||
name?: string;
|
name?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ export interface ILoggingService {
|
|||||||
warn(message: string, ...args: any[]): void;
|
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
|
season?: number
|
||||||
episode?: number
|
episode?: number
|
||||||
id?: string | number
|
id?: string | number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ export interface IMetadataResponse {
|
|||||||
videos?: ICommonVideoMetadata[];
|
videos?: ICommonVideoMetadata[];
|
||||||
episodeCount?: number[];
|
episodeCount?: number[];
|
||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ export interface IMetadataService {
|
|||||||
isEpisodeImdbId(imdbId: string | undefined): Promise<boolean>;
|
isEpisodeImdbId(imdbId: string | undefined): Promise<boolean>;
|
||||||
|
|
||||||
escapeTitle(title: string): string;
|
escapeTitle(title: string): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ export interface IParseTorrentTitleResult {
|
|||||||
videoFile?: IFileAttributes;
|
videoFile?: IFileAttributes;
|
||||||
folderName?: string;
|
folderName?: string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ export interface IParsedTorrent extends IParseTorrentTitleResult {
|
|||||||
seeders?: number;
|
seeders?: number;
|
||||||
torrentId?: string;
|
torrentId?: string;
|
||||||
fileCollection?: ITorrentFileCollection;
|
fileCollection?: ITorrentFileCollection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface IProcessTorrentsJob {
|
export interface IProcessTorrentsJob {
|
||||||
listenToQueue: () => Promise<void>;
|
listenToQueue: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ import {ITorrentFileCollection} from "@interfaces/torrent_file_collection";
|
|||||||
|
|
||||||
export interface ITorrentDownloadService {
|
export interface ITorrentDownloadService {
|
||||||
getTorrentFiles(torrent: IParsedTorrent, timeout: number): Promise<ITorrentFileCollection>;
|
getTorrentFiles(torrent: IParsedTorrent, timeout: number): Promise<ITorrentFileCollection>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ export interface ITorrentEntriesService {
|
|||||||
createTorrentContents(torrent: Torrent): Promise<void>;
|
createTorrentContents(torrent: Torrent): Promise<void>;
|
||||||
|
|
||||||
updateTorrentSeeders(torrent: ITorrentAttributes): Promise<[number] | undefined>;
|
updateTorrentSeeders(torrent: ITorrentAttributes): Promise<[number] | undefined>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ export interface ITorrentFileCollection {
|
|||||||
contents?: IContentAttributes[];
|
contents?: IContentAttributes[];
|
||||||
videos?: IFileAttributes[];
|
videos?: IFileAttributes[];
|
||||||
subtitles?: ISubtitleAttributes[];
|
subtitles?: ISubtitleAttributes[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ export interface ITorrentFileService {
|
|||||||
parseTorrentFiles(torrent: IParsedTorrent): Promise<ITorrentFileCollection>;
|
parseTorrentFiles(torrent: IParsedTorrent): Promise<ITorrentFileCollection>;
|
||||||
|
|
||||||
isPackTorrent(torrent: IParsedTorrent): boolean;
|
isPackTorrent(torrent: IParsedTorrent): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ import {IIngestedTorrentAttributes} from "@repository/interfaces/ingested_torren
|
|||||||
|
|
||||||
export interface ITorrentProcessingService {
|
export interface ITorrentProcessingService {
|
||||||
processTorrentRecord(torrent: IIngestedTorrentAttributes): Promise<void>;
|
processTorrentRecord(torrent: IIngestedTorrentAttributes): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ import {ITorrentFileCollection} from "@interfaces/torrent_file_collection";
|
|||||||
|
|
||||||
export interface ITorrentSubtitleService {
|
export interface ITorrentSubtitleService {
|
||||||
assignSubtitles(fileCollection: ITorrentFileCollection): ITorrentFileCollection;
|
assignSubtitles(fileCollection: ITorrentFileCollection): ITorrentFileCollection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface ITrackerService {
|
export interface ITrackerService {
|
||||||
getTrackers(): Promise<string[]>;
|
getTrackers(): Promise<string[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,4 +60,4 @@ export class ProcessTorrentsJob implements IProcessTorrentsJob {
|
|||||||
this.logger.error('Failed to setup channel', error);
|
this.logger.error('Failed to setup channel', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ export const cacheConfig = {
|
|||||||
get MONGO_URI(): string {
|
get MONGO_URI(): string {
|
||||||
return `mongodb://${this.MONGODB_USER}:${this.MONGODB_PASSWORD}@${this.MONGODB_HOST}:${this.MONGODB_PORT}/${this.MONGODB_DB}?authSource=admin`;
|
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 {
|
get POSTGRES_URI(): string {
|
||||||
return `postgres://${this.POSTGRES_USER}:${this.POSTGRES_PASSWORD}@${this.POSTGRES_HOST}:${this.POSTGRES_PORT}/${this.POSTGRES_DB}`;
|
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