Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
684dbba2f0 | ||
|
|
c75ecd2707 | ||
|
|
c493ef3376 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -612,3 +612,7 @@ fabric.properties
|
|||||||
# Mac directory indexes
|
# Mac directory indexes
|
||||||
.DS_Store
|
.DS_Store
|
||||||
deployment/docker/stack.env
|
deployment/docker/stack.env
|
||||||
|
|
||||||
|
src/producer/src/python/
|
||||||
|
src/debrid-collector/python/
|
||||||
|
src/qbit-collector/python/
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file: stack.env
|
env_file: stack.env
|
||||||
hostname: knightcrawler-addon
|
hostname: knightcrawler-addon
|
||||||
image: gabisonfire/knightcrawler-addon:2.0.15
|
image: gabisonfire/knightcrawler-addon:2.0.18
|
||||||
labels:
|
labels:
|
||||||
logging: promtail
|
logging: promtail
|
||||||
networks:
|
networks:
|
||||||
@@ -117,7 +117,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file: stack.env
|
env_file: stack.env
|
||||||
image: gabisonfire/knightcrawler-consumer:2.0.15
|
image: gabisonfire/knightcrawler-consumer:2.0.18
|
||||||
labels:
|
labels:
|
||||||
logging: promtail
|
logging: promtail
|
||||||
networks:
|
networks:
|
||||||
@@ -138,7 +138,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file: stack.env
|
env_file: stack.env
|
||||||
image: gabisonfire/knightcrawler-debrid-collector:2.0.15
|
image: gabisonfire/knightcrawler-debrid-collector:2.0.18
|
||||||
labels:
|
labels:
|
||||||
logging: promtail
|
logging: promtail
|
||||||
networks:
|
networks:
|
||||||
@@ -152,7 +152,7 @@ services:
|
|||||||
migrator:
|
migrator:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
env_file: stack.env
|
env_file: stack.env
|
||||||
image: gabisonfire/knightcrawler-metadata:2.0.15
|
image: gabisonfire/knightcrawler-metadata:2.0.18
|
||||||
networks:
|
networks:
|
||||||
- knightcrawler-network
|
- knightcrawler-network
|
||||||
restart: "no"
|
restart: "no"
|
||||||
@@ -163,7 +163,7 @@ services:
|
|||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file: stack.env
|
env_file: stack.env
|
||||||
image: gabisonfire/knightcrawler-migrator:2.0.15
|
image: gabisonfire/knightcrawler-migrator:2.0.18
|
||||||
networks:
|
networks:
|
||||||
- knightcrawler-network
|
- knightcrawler-network
|
||||||
restart: "no"
|
restart: "no"
|
||||||
@@ -182,7 +182,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file: stack.env
|
env_file: stack.env
|
||||||
image: gabisonfire/knightcrawler-producer:2.0.15
|
image: gabisonfire/knightcrawler-producer:2.0.18
|
||||||
labels:
|
labels:
|
||||||
logging: promtail
|
logging: promtail
|
||||||
networks:
|
networks:
|
||||||
@@ -207,7 +207,7 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
replicas: ${QBIT_REPLICAS:-0}
|
replicas: ${QBIT_REPLICAS:-0}
|
||||||
env_file: stack.env
|
env_file: stack.env
|
||||||
image: gabisonfire/knightcrawler-qbit-collector:2.0.15
|
image: gabisonfire/knightcrawler-qbit-collector:2.0.18
|
||||||
labels:
|
labels:
|
||||||
logging: promtail
|
logging: promtail
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ x-depends: &knightcrawler-app-depends
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
metadata:
|
metadata:
|
||||||
image: gabisonfire/knightcrawler-metadata:2.0.15
|
image: gabisonfire/knightcrawler-metadata:2.0.18
|
||||||
env_file: ../../.env
|
env_file: ../../.env
|
||||||
networks:
|
networks:
|
||||||
- knightcrawler-network
|
- knightcrawler-network
|
||||||
@@ -30,7 +30,7 @@ services:
|
|||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|
||||||
migrator:
|
migrator:
|
||||||
image: gabisonfire/knightcrawler-migrator:2.0.15
|
image: gabisonfire/knightcrawler-migrator:2.0.18
|
||||||
env_file: ../../.env
|
env_file: ../../.env
|
||||||
networks:
|
networks:
|
||||||
- knightcrawler-network
|
- knightcrawler-network
|
||||||
@@ -40,7 +40,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
addon:
|
addon:
|
||||||
image: gabisonfire/knightcrawler-addon:2.0.15
|
image: gabisonfire/knightcrawler-addon:2.0.18
|
||||||
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
hostname: knightcrawler-addon
|
hostname: knightcrawler-addon
|
||||||
@@ -48,22 +48,22 @@ services:
|
|||||||
- "7000:7000"
|
- "7000:7000"
|
||||||
|
|
||||||
consumer:
|
consumer:
|
||||||
image: gabisonfire/knightcrawler-consumer:2.0.15
|
image: gabisonfire/knightcrawler-consumer:2.0.18
|
||||||
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
debridcollector:
|
debridcollector:
|
||||||
image: gabisonfire/knightcrawler-debrid-collector:2.0.15
|
image: gabisonfire/knightcrawler-debrid-collector:2.0.18
|
||||||
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
producer:
|
producer:
|
||||||
image: gabisonfire/knightcrawler-producer:2.0.15
|
image: gabisonfire/knightcrawler-producer:2.0.18
|
||||||
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
qbitcollector:
|
qbitcollector:
|
||||||
image: gabisonfire/knightcrawler-qbit-collector:2.0.15
|
image: gabisonfire/knightcrawler-qbit-collector:2.0.18
|
||||||
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.3" />
|
||||||
<PackageReference Include="Polly" Version="8.3.1" />
|
<PackageReference Include="Polly" Version="8.3.1" />
|
||||||
<PackageReference Include="PromKnight.ParseTorrentTitle" Version="1.0.4" />
|
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||||
@@ -29,10 +28,30 @@
|
|||||||
<None Include="Configuration\logging.json">
|
<None Include="Configuration\logging.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="requirements.txt">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<Content Remove="eng\**" />
|
||||||
|
<None Remove="eng\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
|
<Content Remove="python\**" />
|
||||||
|
<None Include="python\**">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\shared\SharedContracts.csproj" />
|
<ProjectReference Include="..\shared\SharedContracts.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="eng\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Remove="eng\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedContracts", "..\share
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{2C0A0F53-28E6-404F-9EFE-DADFBEF8338B}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{2C0A0F53-28E6-404F-9EFE-DADFBEF8338B}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{72A042C3-B4F3-45C5-AC20-041FE8F41EFC}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
eng\install-python-reqs.ps1 = eng\install-python-reqs.ps1
|
||||||
|
eng\install-python-reqs.sh = eng\install-python-reqs.sh
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|||||||
@@ -9,12 +9,23 @@ RUN dotnet restore -a $TARGETARCH
|
|||||||
RUN dotnet publish -c Release --no-restore -o /src/out -a $TARGETARCH
|
RUN dotnet publish -c Release --no-restore -o /src/out -a $TARGETARCH
|
||||||
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.19
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
RUN apk add --update --no-cache python3=~3.11.8-r0 py3-pip && ln -sf python3 /usr/bin/python
|
||||||
|
|
||||||
COPY --from=build /src/out .
|
COPY --from=build /src/out .
|
||||||
|
|
||||||
|
RUN rm -rf /app/python && mkdir -p /app/python
|
||||||
|
|
||||||
|
RUN pip3 install -r /app/requirements.txt -t /app/python
|
||||||
|
|
||||||
RUN addgroup -S debrid && adduser -S -G debrid debrid
|
RUN addgroup -S debrid && adduser -S -G debrid debrid
|
||||||
USER debrid
|
USER debrid
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||||
CMD pgrep -f dotnet || exit 1
|
CMD pgrep -f dotnet || exit 1
|
||||||
|
ENV PYTHONNET_PYDLL=/usr/lib/libpython3.11.so.1.0
|
||||||
ENTRYPOINT ["dotnet", "DebridCollector.dll"]
|
ENTRYPOINT ["dotnet", "DebridCollector.dll"]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using DebridCollector.Features.Configuration;
|
|
||||||
|
|
||||||
namespace DebridCollector.Extensions;
|
namespace DebridCollector.Extensions;
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
@@ -17,7 +15,8 @@ public static class ServiceCollectionExtensions
|
|||||||
var serviceConfiguration = services.LoadConfigurationFromEnv<DebridCollectorConfiguration>();
|
var serviceConfiguration = services.LoadConfigurationFromEnv<DebridCollectorConfiguration>();
|
||||||
|
|
||||||
services.AddRealDebridClient(serviceConfiguration);
|
services.AddRealDebridClient(serviceConfiguration);
|
||||||
services.AddSingleton<IParseTorrentTitle, ParseTorrentTitle>();
|
services.RegisterPythonEngine();
|
||||||
|
services.AddSingleton<IRankTorrentName, RankTorrentName>();
|
||||||
services.AddHostedService<DebridRequestProcessor>();
|
services.AddHostedService<DebridRequestProcessor>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using DebridCollector.Features.Configuration;
|
namespace DebridCollector.Features.Debrid;
|
||||||
|
|
||||||
namespace DebridCollector.Features.Debrid;
|
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ namespace DebridCollector.Features.Worker;
|
|||||||
public static class DebridMetaToTorrentMeta
|
public static class DebridMetaToTorrentMeta
|
||||||
{
|
{
|
||||||
public static IReadOnlyList<TorrentFile> MapMetadataToFilesCollection(
|
public static IReadOnlyList<TorrentFile> MapMetadataToFilesCollection(
|
||||||
IParseTorrentTitle torrentTitle,
|
IRankTorrentName rankTorrentName,
|
||||||
Torrent torrent,
|
Torrent torrent,
|
||||||
string ImdbId,
|
string ImdbId,
|
||||||
FileDataDictionary Metadata)
|
FileDataDictionary Metadata,
|
||||||
|
ILogger<WriteMetadataConsumer> logger)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -26,23 +27,30 @@ public static class DebridMetaToTorrentMeta
|
|||||||
Size = metadataEntry.Value.Filesize.GetValueOrDefault(),
|
Size = metadataEntry.Value.Filesize.GetValueOrDefault(),
|
||||||
};
|
};
|
||||||
|
|
||||||
var parsedTitle = torrentTitle.Parse(file.Title);
|
var parsedTitle = rankTorrentName.Parse(file.Title, false);
|
||||||
|
|
||||||
file.ImdbSeason = parsedTitle.Seasons.FirstOrDefault();
|
if (!parsedTitle.Success)
|
||||||
file.ImdbEpisode = parsedTitle.Episodes.FirstOrDefault();
|
{
|
||||||
|
logger.LogWarning("Failed to parse title {Title} for metadata mapping", file.Title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.ImdbSeason = parsedTitle.Response?.Season?.FirstOrDefault() ?? 0;
|
||||||
|
file.ImdbEpisode = parsedTitle.Response?.Episode?.FirstOrDefault() ?? 0;
|
||||||
|
|
||||||
files.Add(file);
|
files.Add(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
logger.LogWarning("Failed to map metadata to files collection: {Exception}", ex.Message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IReadOnlyList<SubtitleFile>> MapMetadataToSubtitlesCollection(IDataStorage storage, string InfoHash, FileDataDictionary Metadata)
|
public static async Task<IReadOnlyList<SubtitleFile>> MapMetadataToSubtitlesCollection(IDataStorage storage, string InfoHash, FileDataDictionary Metadata, ILogger<WriteMetadataConsumer> logger)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -74,8 +82,9 @@ public static class DebridMetaToTorrentMeta
|
|||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
logger.LogWarning("Failed to map metadata to subtitles collection: {Exception}", ex.Message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ public class InfohashMetadataSagaStateMachine : MassTransitStateMachine<Infohash
|
|||||||
.Then(
|
.Then(
|
||||||
context =>
|
context =>
|
||||||
{
|
{
|
||||||
|
if (!context.Message.WithFiles)
|
||||||
|
{
|
||||||
|
logger.LogInformation("No files written for torrent {InfoHash} in Saga {SagaId}", context.Saga.Torrent.InfoHash, context.Saga.CorrelationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogInformation("Metadata Written for torrent {InfoHash} in Saga {SagaId}", context.Saga.Torrent.InfoHash, context.Saga.CorrelationId);
|
logger.LogInformation("Metadata Written for torrent {InfoHash} in Saga {SagaId}", context.Saga.Torrent.InfoHash, context.Saga.CorrelationId);
|
||||||
})
|
})
|
||||||
.TransitionTo(Completed)
|
.TransitionTo(Completed)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public record WriteMetadata(Torrent Torrent, TorrentMetadataResponse Metadata, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
[EntityName("metadata-written-debrid-colloctor")]
|
[EntityName("metadata-written-debrid-colloctor")]
|
||||||
public record MetadataWritten(TorrentMetadataResponse Metadata) : CorrelatedBy<Guid>
|
public record MetadataWritten(TorrentMetadataResponse Metadata, bool WithFiles) : CorrelatedBy<Guid>
|
||||||
{
|
{
|
||||||
public Guid CorrelationId { get; init; } = Metadata.CorrelationId;
|
public Guid CorrelationId { get; init; } = Metadata.CorrelationId;
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,28 @@
|
|||||||
namespace DebridCollector.Features.Worker;
|
namespace DebridCollector.Features.Worker;
|
||||||
|
|
||||||
public class WriteMetadataConsumer(IParseTorrentTitle parseTorrentTitle, IDataStorage dataStorage) : IConsumer<WriteMetadata>
|
public class WriteMetadataConsumer(IRankTorrentName rankTorrentName, IDataStorage dataStorage, ILogger<WriteMetadataConsumer> logger) : IConsumer<WriteMetadata>
|
||||||
{
|
{
|
||||||
public async Task Consume(ConsumeContext<WriteMetadata> context)
|
public async Task Consume(ConsumeContext<WriteMetadata> context)
|
||||||
{
|
{
|
||||||
var request = context.Message;
|
var request = context.Message;
|
||||||
|
|
||||||
var torrentFiles = DebridMetaToTorrentMeta.MapMetadataToFilesCollection(parseTorrentTitle, request.Torrent, request.ImdbId, request.Metadata.Metadata);
|
var torrentFiles = DebridMetaToTorrentMeta.MapMetadataToFilesCollection(rankTorrentName, request.Torrent, request.ImdbId, request.Metadata.Metadata, logger);
|
||||||
|
|
||||||
if (torrentFiles.Any())
|
if (!torrentFiles.Any())
|
||||||
{
|
{
|
||||||
await dataStorage.InsertFiles(torrentFiles);
|
await context.Publish(new MetadataWritten(request.Metadata, false));
|
||||||
|
return;
|
||||||
var subtitles = await DebridMetaToTorrentMeta.MapMetadataToSubtitlesCollection(dataStorage, request.Torrent.InfoHash, request.Metadata.Metadata);
|
|
||||||
|
|
||||||
if (subtitles.Any())
|
|
||||||
{
|
|
||||||
await dataStorage.InsertSubtitles(subtitles);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.Publish(new MetadataWritten(request.Metadata));
|
await dataStorage.InsertFiles(torrentFiles);
|
||||||
|
|
||||||
|
var subtitles = await DebridMetaToTorrentMeta.MapMetadataToSubtitlesCollection(dataStorage, request.Torrent.InfoHash, request.Metadata.Metadata, logger);
|
||||||
|
|
||||||
|
if (subtitles.Any())
|
||||||
|
{
|
||||||
|
await dataStorage.InsertSubtitles(subtitles);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Publish(new MetadataWritten(request.Metadata, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,17 +4,18 @@ global using System.Text.Json;
|
|||||||
global using System.Text.Json.Serialization;
|
global using System.Text.Json.Serialization;
|
||||||
global using System.Threading.Channels;
|
global using System.Threading.Channels;
|
||||||
global using DebridCollector.Extensions;
|
global using DebridCollector.Extensions;
|
||||||
|
global using DebridCollector.Features.Configuration;
|
||||||
global using DebridCollector.Features.Debrid;
|
global using DebridCollector.Features.Debrid;
|
||||||
global using DebridCollector.Features.Worker;
|
global using DebridCollector.Features.Worker;
|
||||||
global using MassTransit;
|
global using MassTransit;
|
||||||
global using MassTransit.Mediator;
|
|
||||||
global using Microsoft.AspNetCore.Builder;
|
global using Microsoft.AspNetCore.Builder;
|
||||||
global using Microsoft.Extensions.DependencyInjection;
|
global using Microsoft.Extensions.DependencyInjection;
|
||||||
global using Polly;
|
global using Polly;
|
||||||
global using Polly.Extensions.Http;
|
global using Polly.Extensions.Http;
|
||||||
global using PromKnight.ParseTorrentTitle;
|
|
||||||
global using SharedContracts.Configuration;
|
global using SharedContracts.Configuration;
|
||||||
global using SharedContracts.Dapper;
|
global using SharedContracts.Dapper;
|
||||||
global using SharedContracts.Extensions;
|
global using SharedContracts.Extensions;
|
||||||
global using SharedContracts.Models;
|
global using SharedContracts.Models;
|
||||||
|
global using SharedContracts.Python;
|
||||||
|
global using SharedContracts.Python.RTN;
|
||||||
global using SharedContracts.Requests;
|
global using SharedContracts.Requests;
|
||||||
2
src/debrid-collector/eng/install-python-reqs.ps1
Normal file
2
src/debrid-collector/eng/install-python-reqs.ps1
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mkdir -p ../python
|
||||||
|
python -m pip install -r ../requirements.txt -t ../python/
|
||||||
5
src/debrid-collector/eng/install-python-reqs.sh
Normal file
5
src/debrid-collector/eng/install-python-reqs.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
rm -rf ../python
|
||||||
|
mkdir -p ../python
|
||||||
|
python3 -m pip install -r ../requirements.txt -t ../python/
|
||||||
1
src/debrid-collector/requirements.txt
Normal file
1
src/debrid-collector/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rank-torrent-name==0.2.5
|
||||||
@@ -108,14 +108,18 @@ public partial class DebridMediaManagerCrawler(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsedTorrent = rankTorrentName.Parse(torrentTitle.CleanTorrentTitleForImdb());
|
var parsedTorrent = rankTorrentName.Parse(torrentTitle);
|
||||||
|
|
||||||
if (!parsedTorrent.Success)
|
if (!parsedTorrent.Success)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (cached, cachedResult) = await CheckIfInCacheAndReturn(parsedTorrent.Response.ParsedTitle);
|
var torrentType = parsedTorrent.Response.IsMovie ? "movie" : "tvSeries";
|
||||||
|
|
||||||
|
var cacheKey = GetCacheKey(torrentType, parsedTorrent.Response.ParsedTitle, parsedTorrent.Response.Year);
|
||||||
|
|
||||||
|
var (cached, cachedResult) = await CheckIfInCacheAndReturn(cacheKey);
|
||||||
|
|
||||||
if (cached)
|
if (cached)
|
||||||
{
|
{
|
||||||
@@ -124,14 +128,14 @@ public partial class DebridMediaManagerCrawler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
int? year = parsedTorrent.Response.Year != 0 ? parsedTorrent.Response.Year : null;
|
int? year = parsedTorrent.Response.Year != 0 ? parsedTorrent.Response.Year : null;
|
||||||
var imdbEntry = await Storage.FindImdbMetadata(parsedTorrent.Response.ParsedTitle, parsedTorrent.Response.IsMovie ? "movies" : "tv", year);
|
var imdbEntry = await Storage.FindImdbMetadata(parsedTorrent.Response.ParsedTitle, torrentType, year);
|
||||||
|
|
||||||
if (imdbEntry is null)
|
if (imdbEntry is null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await AddToCache(parsedTorrent.Response.ParsedTitle.ToLowerInvariant(), imdbEntry);
|
await AddToCache(cacheKey, imdbEntry);
|
||||||
|
|
||||||
logger.LogInformation("[{ImdbId}] Found best match for {Title}: {BestMatch} with score {Score}", imdbEntry.ImdbId, parsedTorrent.Response.ParsedTitle, imdbEntry.Title, imdbEntry.Score);
|
logger.LogInformation("[{ImdbId}] Found best match for {Title}: {BestMatch} with score {Score}", imdbEntry.ImdbId, parsedTorrent.Response.ParsedTitle, imdbEntry.Title, imdbEntry.Score);
|
||||||
|
|
||||||
@@ -153,19 +157,19 @@ public partial class DebridMediaManagerCrawler(
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
private Task AddToCache(string lowerCaseTitle, ImdbEntry best)
|
private Task AddToCache(string cacheKey, ImdbEntry best)
|
||||||
{
|
{
|
||||||
var cacheOptions = new DistributedCacheEntryOptions
|
var cacheOptions = new DistributedCacheEntryOptions
|
||||||
{
|
{
|
||||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1),
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1),
|
||||||
};
|
};
|
||||||
|
|
||||||
return cache.SetStringAsync(lowerCaseTitle, JsonSerializer.Serialize(best), cacheOptions);
|
return cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(best), cacheOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool Success, ImdbEntry? Entry)> CheckIfInCacheAndReturn(string title)
|
private async Task<(bool Success, ImdbEntry? Entry)> CheckIfInCacheAndReturn(string cacheKey)
|
||||||
{
|
{
|
||||||
var cachedImdbId = await cache.GetStringAsync(title.ToLowerInvariant());
|
var cachedImdbId = await cache.GetStringAsync(cacheKey);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(cachedImdbId))
|
if (!string.IsNullOrEmpty(cachedImdbId))
|
||||||
{
|
{
|
||||||
@@ -207,16 +211,12 @@ public partial class DebridMediaManagerCrawler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string AssignCategory(ImdbEntry entry) =>
|
private static string AssignCategory(ImdbEntry entry) =>
|
||||||
entry.Category switch
|
entry.Category.ToLower() switch
|
||||||
{
|
{
|
||||||
"movie" => "movies",
|
var category when string.Equals(category, "movie", StringComparison.OrdinalIgnoreCase) => "movies",
|
||||||
"tvMovie" => "movies",
|
var category when string.Equals(category, "tvSeries", StringComparison.OrdinalIgnoreCase) => "tv",
|
||||||
"tvSeries" => "tv",
|
|
||||||
"tvEpisode" => "tv",
|
|
||||||
"tvSpecial" => "tv",
|
|
||||||
"tvMiniSeries" => "tv",
|
|
||||||
"tv" => "tv",
|
|
||||||
"short" => "tv",
|
|
||||||
_ => "unknown",
|
_ => "unknown",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string GetCacheKey(string category, string title, int year) => $"{category.ToLowerInvariant()}:{year}:{title.ToLowerInvariant()}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
rank-torrent-name==0.1.9
|
rank-torrent-name==0.2.5
|
||||||
@@ -9,12 +9,23 @@ RUN dotnet restore -a $TARGETARCH
|
|||||||
RUN dotnet publish -c Release --no-restore -o /src/out -a $TARGETARCH
|
RUN dotnet publish -c Release --no-restore -o /src/out -a $TARGETARCH
|
||||||
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.19
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
RUN apk add --update --no-cache python3=~3.11.8-r0 py3-pip && ln -sf python3 /usr/bin/python
|
||||||
|
|
||||||
COPY --from=build /src/out .
|
COPY --from=build /src/out .
|
||||||
|
|
||||||
|
RUN rm -rf /app/python && mkdir -p /app/python
|
||||||
|
|
||||||
|
RUN pip3 install -r /app/requirements.txt -t /app/python
|
||||||
|
|
||||||
RUN addgroup -S qbit && adduser -S -G qbit qbit
|
RUN addgroup -S qbit && adduser -S -G qbit qbit
|
||||||
USER qbit
|
USER qbit
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||||
CMD pgrep -f dotnet || exit 1
|
CMD pgrep -f dotnet || exit 1
|
||||||
|
ENV PYTHONNET_PYDLL=/usr/lib/libpython3.11.so.1.0
|
||||||
ENTRYPOINT ["dotnet", "QBitCollector.dll"]
|
ENTRYPOINT ["dotnet", "QBitCollector.dll"]
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ public static class ServiceCollectionExtensions
|
|||||||
internal static IServiceCollection AddServiceConfiguration(this IServiceCollection services)
|
internal static IServiceCollection AddServiceConfiguration(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddQBitTorrentClient();
|
services.AddQBitTorrentClient();
|
||||||
services.AddSingleton<IParseTorrentTitle, ParseTorrentTitle>();
|
services.RegisterPythonEngine();
|
||||||
|
services.AddSingleton<IRankTorrentName, RankTorrentName>();
|
||||||
services.AddSingleton<QbitRequestProcessor>();
|
services.AddSingleton<QbitRequestProcessor>();
|
||||||
services.AddHttpClient();
|
services.AddHttpClient();
|
||||||
services.AddSingleton<ITrackersService, TrackersService>();
|
services.AddSingleton<ITrackersService, TrackersService>();
|
||||||
services.AddHostedService<TrackersBackgroundService>();
|
services.AddHostedService<TrackersBackgroundService>();
|
||||||
|
services.AddHostedService<HousekeepingBackgroundService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
namespace QBitCollector.Features.Qbit;
|
||||||
|
|
||||||
|
public class HousekeepingBackgroundService(IQBittorrentClient client, ILogger<HousekeepingBackgroundService> logger) : BackgroundService
|
||||||
|
{
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Service is Running.");
|
||||||
|
|
||||||
|
await DoWork();
|
||||||
|
|
||||||
|
using PeriodicTimer timer = new(TimeSpan.FromMinutes(2));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||||
|
{
|
||||||
|
await DoWork();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Service stopping.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DoWork()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogInformation("Cleaning Stale Entries in Qbit...");
|
||||||
|
|
||||||
|
var torrents = await client.GetTorrentListAsync();
|
||||||
|
|
||||||
|
foreach (var torrentInfo in torrents)
|
||||||
|
{
|
||||||
|
if (!(torrentInfo.AddedOn < DateTimeOffset.UtcNow.AddMinutes(-1)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("Torrent [{InfoHash}] Identified as stale because was added at {AddedOn}", torrentInfo.Hash, torrentInfo.AddedOn);
|
||||||
|
|
||||||
|
await client.DeleteAsync(new[] {torrentInfo.Hash}, deleteDownloadedData: true);
|
||||||
|
logger.LogInformation("Cleaned up stale torrent: [{InfoHash}]", torrentInfo.Hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "Error cleaning up stale torrents this interval.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,11 @@ namespace QBitCollector.Features.Worker;
|
|||||||
public static class QbitMetaToTorrentMeta
|
public static class QbitMetaToTorrentMeta
|
||||||
{
|
{
|
||||||
public static IReadOnlyList<TorrentFile> MapMetadataToFilesCollection(
|
public static IReadOnlyList<TorrentFile> MapMetadataToFilesCollection(
|
||||||
IParseTorrentTitle torrentTitle,
|
IRankTorrentName rankTorrentName,
|
||||||
Torrent torrent,
|
Torrent torrent,
|
||||||
string ImdbId,
|
string ImdbId,
|
||||||
IReadOnlyList<TorrentContent> Metadata)
|
IReadOnlyList<TorrentContent> Metadata,
|
||||||
|
ILogger<WriteQbitMetadataConsumer> logger)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -24,23 +25,31 @@ public static class QbitMetaToTorrentMeta
|
|||||||
Size = metadataEntry.Size,
|
Size = metadataEntry.Size,
|
||||||
};
|
};
|
||||||
|
|
||||||
var parsedTitle = torrentTitle.Parse(file.Title);
|
var parsedTitle = rankTorrentName.Parse(file.Title, false);
|
||||||
|
|
||||||
|
if (!parsedTitle.Success)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Failed to parse title {Title} for metadata mapping", file.Title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
file.ImdbSeason = parsedTitle.Seasons.FirstOrDefault();
|
file.ImdbSeason = parsedTitle.Response?.Season?.FirstOrDefault() ?? 0;
|
||||||
file.ImdbEpisode = parsedTitle.Episodes.FirstOrDefault();
|
file.ImdbEpisode = parsedTitle.Response?.Episode?.FirstOrDefault() ?? 0;
|
||||||
|
|
||||||
files.Add(file);
|
files.Add(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
logger.LogWarning("Failed to map metadata to files collection: {Exception}", ex.Message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IReadOnlyList<SubtitleFile>> MapMetadataToSubtitlesCollection(IDataStorage storage, string InfoHash, IReadOnlyList<TorrentContent> Metadata)
|
public static async Task<IReadOnlyList<SubtitleFile>> MapMetadataToSubtitlesCollection(IDataStorage storage, string InfoHash, IReadOnlyList<TorrentContent> Metadata,
|
||||||
|
ILogger<WriteQbitMetadataConsumer> logger)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -70,8 +79,9 @@ public static class QbitMetaToTorrentMeta
|
|||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
logger.LogWarning("Failed to map metadata to subtitles collection: {Exception}", ex.Message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ public class QbitMetadataSagaStateMachine : MassTransitStateMachine<QbitMetadata
|
|||||||
.Then(
|
.Then(
|
||||||
context =>
|
context =>
|
||||||
{
|
{
|
||||||
|
if (!context.Message.WithFiles)
|
||||||
|
{
|
||||||
|
logger.LogInformation("No files written for torrent {InfoHash} in Saga {SagaId}", context.Saga.Torrent.InfoHash, context.Saga.CorrelationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogInformation("Metadata Written for torrent {InfoHash} in Saga {SagaId}", context.Saga.Torrent.InfoHash, context.Saga.CorrelationId);
|
logger.LogInformation("Metadata Written for torrent {InfoHash} in Saga {SagaId}", context.Saga.Torrent.InfoHash, context.Saga.CorrelationId);
|
||||||
})
|
})
|
||||||
.TransitionTo(Completed)
|
.TransitionTo(Completed)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public record WriteQbitMetadata(Torrent Torrent, QBitMetadataResponse Metadata,
|
|||||||
}
|
}
|
||||||
|
|
||||||
[EntityName("metadata-written-qbit-collector")]
|
[EntityName("metadata-written-qbit-collector")]
|
||||||
public record QbitMetadataWritten(QBitMetadataResponse Metadata) : CorrelatedBy<Guid>
|
public record QbitMetadataWritten(QBitMetadataResponse Metadata, bool WithFiles) : CorrelatedBy<Guid>
|
||||||
{
|
{
|
||||||
public Guid CorrelationId { get; init; } = Metadata.CorrelationId;
|
public Guid CorrelationId { get; init; } = Metadata.CorrelationId;
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
namespace QBitCollector.Features.Worker;
|
namespace QBitCollector.Features.Worker;
|
||||||
|
|
||||||
public class WriteQbitMetadataConsumer(IParseTorrentTitle parseTorrentTitle, IDataStorage dataStorage) : IConsumer<WriteQbitMetadata>
|
public class WriteQbitMetadataConsumer(IRankTorrentName rankTorrentName, IDataStorage dataStorage, ILogger<WriteQbitMetadataConsumer> logger) : IConsumer<WriteQbitMetadata>
|
||||||
{
|
{
|
||||||
public async Task Consume(ConsumeContext<WriteQbitMetadata> context)
|
public async Task Consume(ConsumeContext<WriteQbitMetadata> context)
|
||||||
{
|
{
|
||||||
var request = context.Message;
|
var request = context.Message;
|
||||||
|
|
||||||
var torrentFiles = QbitMetaToTorrentMeta.MapMetadataToFilesCollection(parseTorrentTitle, request.Torrent, request.ImdbId, request.Metadata.Metadata);
|
|
||||||
|
|
||||||
if (torrentFiles.Any())
|
var torrentFiles = QbitMetaToTorrentMeta.MapMetadataToFilesCollection(
|
||||||
|
rankTorrentName, request.Torrent, request.ImdbId, request.Metadata.Metadata, logger);
|
||||||
|
|
||||||
|
if (!torrentFiles.Any())
|
||||||
{
|
{
|
||||||
await dataStorage.InsertFiles(torrentFiles);
|
await context.Publish(new QbitMetadataWritten(request.Metadata, false));
|
||||||
|
return;
|
||||||
var subtitles = await QbitMetaToTorrentMeta.MapMetadataToSubtitlesCollection(dataStorage, request.Torrent.InfoHash, request.Metadata.Metadata);
|
|
||||||
|
|
||||||
if (subtitles.Any())
|
|
||||||
{
|
|
||||||
await dataStorage.InsertSubtitles(subtitles);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.Publish(new QbitMetadataWritten(request.Metadata));
|
await dataStorage.InsertFiles(torrentFiles);
|
||||||
|
|
||||||
|
var subtitles = await QbitMetaToTorrentMeta.MapMetadataToSubtitlesCollection(
|
||||||
|
dataStorage, request.Torrent.InfoHash, request.Metadata.Metadata, logger);
|
||||||
|
|
||||||
|
if (subtitles.Any())
|
||||||
|
{
|
||||||
|
await dataStorage.InsertSubtitles(subtitles);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.Publish(new QbitMetadataWritten(request.Metadata, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
// Global using directives
|
// Global using directives
|
||||||
|
|
||||||
global using System.Text.Json;
|
global using System.Text.Json;
|
||||||
global using System.Text.Json.Serialization;
|
|
||||||
global using System.Threading.Channels;
|
|
||||||
global using MassTransit;
|
global using MassTransit;
|
||||||
global using MassTransit.Mediator;
|
|
||||||
global using Microsoft.AspNetCore.Builder;
|
global using Microsoft.AspNetCore.Builder;
|
||||||
global using Microsoft.Extensions.Caching.Distributed;
|
global using Microsoft.Extensions.Caching.Distributed;
|
||||||
global using Microsoft.Extensions.Caching.Memory;
|
global using Microsoft.Extensions.Caching.Memory;
|
||||||
global using Microsoft.Extensions.DependencyInjection;
|
global using Microsoft.Extensions.DependencyInjection;
|
||||||
global using Polly;
|
|
||||||
global using Polly.Extensions.Http;
|
|
||||||
global using PromKnight.ParseTorrentTitle;
|
|
||||||
global using QBitCollector.Extensions;
|
global using QBitCollector.Extensions;
|
||||||
global using QBitCollector.Features.Qbit;
|
global using QBitCollector.Features.Qbit;
|
||||||
global using QBitCollector.Features.Trackers;
|
global using QBitCollector.Features.Trackers;
|
||||||
@@ -21,4 +15,6 @@ global using SharedContracts.Configuration;
|
|||||||
global using SharedContracts.Dapper;
|
global using SharedContracts.Dapper;
|
||||||
global using SharedContracts.Extensions;
|
global using SharedContracts.Extensions;
|
||||||
global using SharedContracts.Models;
|
global using SharedContracts.Models;
|
||||||
|
global using SharedContracts.Python;
|
||||||
|
global using SharedContracts.Python.RTN;
|
||||||
global using SharedContracts.Requests;
|
global using SharedContracts.Requests;
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.3" />
|
||||||
<PackageReference Include="Polly" Version="8.3.1" />
|
<PackageReference Include="Polly" Version="8.3.1" />
|
||||||
<PackageReference Include="PromKnight.ParseTorrentTitle" Version="1.0.4" />
|
|
||||||
<PackageReference Include="QBittorrent.Client" Version="1.9.23349.1" />
|
<PackageReference Include="QBittorrent.Client" Version="1.9.23349.1" />
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||||
@@ -31,10 +30,30 @@
|
|||||||
<None Include="Configuration\logging.json">
|
<None Include="Configuration\logging.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<Content Remove="eng\**" />
|
||||||
|
<None Remove="eng\**" />
|
||||||
|
<None Update="requirements.txt">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\shared\SharedContracts.csproj" />
|
<ProjectReference Include="..\shared\SharedContracts.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
|
<Content Remove="python\**" />
|
||||||
|
<None Include="python\**">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="eng\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Remove="eng\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{2C0A0F
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QBitCollector", "QBitCollector.csproj", "{1EF124BE-6EBE-4D9E-846C-FFF814999F3B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QBitCollector", "QBitCollector.csproj", "{1EF124BE-6EBE-4D9E-846C-FFF814999F3B}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{2F2EA33A-1303-405D-939B-E9394D262BC9}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
eng\install-python-reqs.ps1 = eng\install-python-reqs.ps1
|
||||||
|
eng\install-python-reqs.sh = eng\install-python-reqs.sh
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|||||||
3
src/qbit-collector/eng/install-python-reqs.ps1
Normal file
3
src/qbit-collector/eng/install-python-reqs.ps1
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Remove-Item -Recurse -Force ../python
|
||||||
|
mkdir -p ../python
|
||||||
|
python -m pip install -r ../requirements.txt -t ../python/
|
||||||
5
src/qbit-collector/eng/install-python-reqs.sh
Normal file
5
src/qbit-collector/eng/install-python-reqs.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
rm -rf ../python
|
||||||
|
mkdir -p ../python
|
||||||
|
python3 -m pip install -r ../requirements.txt -t ../python/
|
||||||
1
src/qbit-collector/requirements.txt
Normal file
1
src/qbit-collector/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rank-torrent-name==0.2.5
|
||||||
@@ -118,7 +118,7 @@ public class DapperDataStorage(PostgresConfiguration configuration, RabbitMqConf
|
|||||||
public async Task<ImdbEntry?> FindImdbMetadata(string? parsedTorrentTitle, string torrentType, int? year, CancellationToken cancellationToken = default) =>
|
public async Task<ImdbEntry?> FindImdbMetadata(string? parsedTorrentTitle, string torrentType, int? year, CancellationToken cancellationToken = default) =>
|
||||||
await ExecuteCommandAsync(async connection =>
|
await ExecuteCommandAsync(async connection =>
|
||||||
{
|
{
|
||||||
var query = $"select \"imdb_id\" as \"ImdbId\", \"title\" as \"Title\", \"year\" as \"Year\", \"score\" as Score from search_imdb_meta('{parsedTorrentTitle.Replace("'", "").Replace("\"", "")}', '{(torrentType.Equals("movie", StringComparison.OrdinalIgnoreCase) ? "movie" : "tvSeries")}'";
|
var query = $"select \"imdb_id\" as \"ImdbId\", \"title\" as \"Title\", \"year\" as \"Year\", \"score\" as Score, \"category\" as Category from search_imdb_meta('{parsedTorrentTitle.Replace("'", "").Replace("\"", "")}', '{torrentType}'";
|
||||||
query += year is not null ? $", {year}" : ", NULL";
|
query += year is not null ? $", {year}" : ", NULL";
|
||||||
query += ", 1)";
|
query += ", 1)";
|
||||||
|
|
||||||
@@ -167,12 +167,7 @@ public class DapperDataStorage(PostgresConfiguration configuration, RabbitMqConf
|
|||||||
INSERT INTO subtitles
|
INSERT INTO subtitles
|
||||||
("infoHash", "fileIndex", "fileId", "title")
|
("infoHash", "fileIndex", "fileId", "title")
|
||||||
VALUES
|
VALUES
|
||||||
(@InfoHash, @FileIndex, @FileId, @Title)
|
(@InfoHash, @FileIndex, @FileId, @Title);
|
||||||
ON CONFLICT
|
|
||||||
("infoHash", "fileIndex")
|
|
||||||
DO UPDATE SET
|
|
||||||
"fileId" = COALESCE(subtitles."fileId", EXCLUDED."fileId"),
|
|
||||||
"title" = COALESCE(subtitles."title", EXCLUDED."title");
|
|
||||||
""";
|
""";
|
||||||
|
|
||||||
await connection.ExecuteAsync(query, subtitles);
|
await connection.ExecuteAsync(query, subtitles);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace SharedContracts.Extensions;
|
namespace SharedContracts.Extensions;
|
||||||
|
|
||||||
public static partial class StringExtensions
|
public static partial class StringExtensions
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
global using System.Text.Json;
|
global using System.Text.Json;
|
||||||
global using System.Text.Json.Serialization;
|
global using System.Text.Json.Serialization;
|
||||||
|
global using System.Text.RegularExpressions;
|
||||||
global using Dapper;
|
global using Dapper;
|
||||||
global using MassTransit;
|
global using MassTransit;
|
||||||
global using Microsoft.AspNetCore.Builder;
|
global using Microsoft.AspNetCore.Builder;
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ namespace SharedContracts.Python.RTN;
|
|||||||
|
|
||||||
public interface IRankTorrentName
|
public interface IRankTorrentName
|
||||||
{
|
{
|
||||||
ParseTorrentTitleResponse Parse(string title);
|
ParseTorrentTitleResponse Parse(string title, bool trashGarbage = true);
|
||||||
}
|
}
|
||||||
@@ -13,15 +13,14 @@ public class RankTorrentName : IRankTorrentName
|
|||||||
InitModules();
|
InitModules();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ParseTorrentTitleResponse Parse(string title) =>
|
public ParseTorrentTitleResponse Parse(string title, bool trashGarbage = true) =>
|
||||||
_pythonEngineService.ExecutePythonOperationWithDefault(
|
_pythonEngineService.ExecutePythonOperationWithDefault(
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
var result = _rtn?.parse(title);
|
var result = _rtn?.parse(title, trashGarbage);
|
||||||
return ParseResult(result);
|
return ParseResult(result);
|
||||||
}, new ParseTorrentTitleResponse(false, null), nameof(Parse), throwOnErrors: false, logErrors: false);
|
}, new ParseTorrentTitleResponse(false, null), nameof(Parse), throwOnErrors: false, logErrors: false);
|
||||||
|
|
||||||
|
|
||||||
private static ParseTorrentTitleResponse ParseResult(dynamic result)
|
private static ParseTorrentTitleResponse ParseResult(dynamic result)
|
||||||
{
|
{
|
||||||
if (result == null)
|
if (result == null)
|
||||||
@@ -35,9 +34,18 @@ public class RankTorrentName : IRankTorrentName
|
|||||||
{
|
{
|
||||||
return new(false, null);
|
return new(false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mediaType = result.GetAttr("type")?.As<string>();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(mediaType))
|
||||||
|
{
|
||||||
|
return new(false, null);
|
||||||
|
}
|
||||||
|
|
||||||
var response = JsonSerializer.Deserialize<RtnResponse>(json);
|
var response = JsonSerializer.Deserialize<RtnResponse>(json);
|
||||||
|
|
||||||
|
response.IsMovie = mediaType.Equals("movie", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
return new(true, response);
|
return new(true, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ public class RtnResponse
|
|||||||
|
|
||||||
[JsonPropertyName("extended")]
|
[JsonPropertyName("extended")]
|
||||||
public bool Extended { get; set; }
|
public bool Extended { get; set; }
|
||||||
|
|
||||||
public bool IsMovie => (Season == null && Episode == null) || (Season?.Count == 0 && Episode?.Count == 0);
|
public bool IsMovie { get; set; }
|
||||||
|
|
||||||
public string ToJson() => this.AsJson();
|
public string ToJson() => this.AsJson();
|
||||||
}
|
}
|
||||||
@@ -82,11 +82,4 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
x.AddConsumer<PerformIngestionConsumer>();
|
x.AddConsumer<PerformIngestionConsumer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static IServiceCollection AddServiceConfiguration(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddSingleton<IParseTorrentTitle, ParseTorrentTitle>();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ global using MassTransit;
|
|||||||
global using MassTransit.Mediator;
|
global using MassTransit.Mediator;
|
||||||
global using Microsoft.AspNetCore.Builder;
|
global using Microsoft.AspNetCore.Builder;
|
||||||
global using Microsoft.Extensions.DependencyInjection;
|
global using Microsoft.Extensions.DependencyInjection;
|
||||||
global using PromKnight.ParseTorrentTitle;
|
|
||||||
global using SharedContracts.Configuration;
|
global using SharedContracts.Configuration;
|
||||||
global using SharedContracts.Dapper;
|
global using SharedContracts.Dapper;
|
||||||
global using SharedContracts.Extensions;
|
global using SharedContracts.Extensions;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ builder.Host
|
|||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.RegisterMassTransit()
|
.RegisterMassTransit()
|
||||||
.AddServiceConfiguration()
|
|
||||||
.AddDatabase();
|
.AddDatabase();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.3" />
|
||||||
<PackageReference Include="Polly" Version="8.3.1" />
|
<PackageReference Include="Polly" Version="8.3.1" />
|
||||||
<PackageReference Include="PromKnight.ParseTorrentTitle" Version="1.0.4" />
|
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||||
|
|||||||
Reference in New Issue
Block a user