mirror of
https://github.com/knightcrawler-stremio/knightcrawler.git
synced 2024-12-20 03:29:51 +00:00
RTN-025 and title category parsing (#195)
* update rtn to 025 * Implement movie / show type parsing * switch to RTN in collectors * ensure env for pythonnet is loaded, and that requirements copy for qbit * version bump
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -612,3 +612,7 @@ fabric.properties
|
||||
# Mac directory indexes
|
||||
.DS_Store
|
||||
deployment/docker/stack.env
|
||||
|
||||
src/producer/src/python/
|
||||
src/debrid-collector/python/
|
||||
src/qbit-collector/python/
|
||||
|
||||
@@ -94,7 +94,7 @@ services:
|
||||
condition: service_healthy
|
||||
env_file: stack.env
|
||||
hostname: knightcrawler-addon
|
||||
image: gabisonfire/knightcrawler-addon:2.0.17
|
||||
image: gabisonfire/knightcrawler-addon:2.0.18
|
||||
labels:
|
||||
logging: promtail
|
||||
networks:
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
env_file: stack.env
|
||||
image: gabisonfire/knightcrawler-consumer:2.0.17
|
||||
image: gabisonfire/knightcrawler-consumer:2.0.18
|
||||
labels:
|
||||
logging: promtail
|
||||
networks:
|
||||
@@ -138,7 +138,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
env_file: stack.env
|
||||
image: gabisonfire/knightcrawler-debrid-collector:2.0.17
|
||||
image: gabisonfire/knightcrawler-debrid-collector:2.0.18
|
||||
labels:
|
||||
logging: promtail
|
||||
networks:
|
||||
@@ -152,7 +152,7 @@ services:
|
||||
migrator:
|
||||
condition: service_completed_successfully
|
||||
env_file: stack.env
|
||||
image: gabisonfire/knightcrawler-metadata:2.0.17
|
||||
image: gabisonfire/knightcrawler-metadata:2.0.18
|
||||
networks:
|
||||
- knightcrawler-network
|
||||
restart: "no"
|
||||
@@ -163,7 +163,7 @@ services:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
env_file: stack.env
|
||||
image: gabisonfire/knightcrawler-migrator:2.0.17
|
||||
image: gabisonfire/knightcrawler-migrator:2.0.18
|
||||
networks:
|
||||
- knightcrawler-network
|
||||
restart: "no"
|
||||
@@ -182,7 +182,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
env_file: stack.env
|
||||
image: gabisonfire/knightcrawler-producer:2.0.17
|
||||
image: gabisonfire/knightcrawler-producer:2.0.18
|
||||
labels:
|
||||
logging: promtail
|
||||
networks:
|
||||
@@ -207,7 +207,7 @@ services:
|
||||
deploy:
|
||||
replicas: ${QBIT_REPLICAS:-0}
|
||||
env_file: stack.env
|
||||
image: gabisonfire/knightcrawler-qbit-collector:2.0.17
|
||||
image: gabisonfire/knightcrawler-qbit-collector:2.0.18
|
||||
labels:
|
||||
logging: promtail
|
||||
networks:
|
||||
|
||||
@@ -20,7 +20,7 @@ x-depends: &knightcrawler-app-depends
|
||||
|
||||
services:
|
||||
metadata:
|
||||
image: gabisonfire/knightcrawler-metadata:2.0.17
|
||||
image: gabisonfire/knightcrawler-metadata:2.0.18
|
||||
env_file: ../../.env
|
||||
networks:
|
||||
- knightcrawler-network
|
||||
@@ -30,7 +30,7 @@ services:
|
||||
condition: service_completed_successfully
|
||||
|
||||
migrator:
|
||||
image: gabisonfire/knightcrawler-migrator:2.0.17
|
||||
image: gabisonfire/knightcrawler-migrator:2.0.18
|
||||
env_file: ../../.env
|
||||
networks:
|
||||
- knightcrawler-network
|
||||
@@ -40,7 +40,7 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
addon:
|
||||
image: gabisonfire/knightcrawler-addon:2.0.17
|
||||
image: gabisonfire/knightcrawler-addon:2.0.18
|
||||
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
||||
restart: unless-stopped
|
||||
hostname: knightcrawler-addon
|
||||
@@ -48,22 +48,22 @@ services:
|
||||
- "7000:7000"
|
||||
|
||||
consumer:
|
||||
image: gabisonfire/knightcrawler-consumer:2.0.17
|
||||
image: gabisonfire/knightcrawler-consumer:2.0.18
|
||||
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
||||
restart: unless-stopped
|
||||
|
||||
debridcollector:
|
||||
image: gabisonfire/knightcrawler-debrid-collector:2.0.17
|
||||
image: gabisonfire/knightcrawler-debrid-collector:2.0.18
|
||||
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
||||
restart: unless-stopped
|
||||
|
||||
producer:
|
||||
image: gabisonfire/knightcrawler-producer:2.0.17
|
||||
image: gabisonfire/knightcrawler-producer:2.0.18
|
||||
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
||||
restart: unless-stopped
|
||||
|
||||
qbitcollector:
|
||||
image: gabisonfire/knightcrawler-qbit-collector:2.0.17
|
||||
image: gabisonfire/knightcrawler-qbit-collector:2.0.18
|
||||
<<: [*knightcrawler-app, *knightcrawler-app-depends]
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.3" />
|
||||
<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.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
@@ -29,10 +28,30 @@
|
||||
<None Include="Configuration\logging.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</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>
|
||||
<ProjectReference Include="..\shared\SharedContracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="eng\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Remove="eng\**" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,6 +6,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedContracts", "..\share
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{2C0A0F53-28E6-404F-9EFE-DADFBEF8338B}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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
|
||||
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.19
|
||||
|
||||
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 .
|
||||
|
||||
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
|
||||
USER debrid
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD pgrep -f dotnet || exit 1
|
||||
ENV PYTHONNET_PYDLL=/usr/lib/libpython3.11.so.1.0
|
||||
ENTRYPOINT ["dotnet", "DebridCollector.dll"]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using DebridCollector.Features.Configuration;
|
||||
|
||||
namespace DebridCollector.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
@@ -17,7 +15,8 @@ public static class ServiceCollectionExtensions
|
||||
var serviceConfiguration = services.LoadConfigurationFromEnv<DebridCollectorConfiguration>();
|
||||
|
||||
services.AddRealDebridClient(serviceConfiguration);
|
||||
services.AddSingleton<IParseTorrentTitle, ParseTorrentTitle>();
|
||||
services.RegisterPythonEngine();
|
||||
services.AddSingleton<IRankTorrentName, RankTorrentName>();
|
||||
services.AddHostedService<DebridRequestProcessor>();
|
||||
|
||||
return services;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using DebridCollector.Features.Configuration;
|
||||
|
||||
namespace DebridCollector.Features.Debrid;
|
||||
namespace DebridCollector.Features.Debrid;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
|
||||
@@ -3,10 +3,11 @@ namespace DebridCollector.Features.Worker;
|
||||
public static class DebridMetaToTorrentMeta
|
||||
{
|
||||
public static IReadOnlyList<TorrentFile> MapMetadataToFilesCollection(
|
||||
IParseTorrentTitle torrentTitle,
|
||||
IRankTorrentName rankTorrentName,
|
||||
Torrent torrent,
|
||||
string ImdbId,
|
||||
FileDataDictionary Metadata)
|
||||
FileDataDictionary Metadata,
|
||||
ILogger<WriteMetadataConsumer> logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -26,23 +27,30 @@ public static class DebridMetaToTorrentMeta
|
||||
Size = metadataEntry.Value.Filesize.GetValueOrDefault(),
|
||||
};
|
||||
|
||||
var parsedTitle = torrentTitle.Parse(file.Title);
|
||||
var parsedTitle = rankTorrentName.Parse(file.Title, false);
|
||||
|
||||
file.ImdbSeason = parsedTitle.Seasons.FirstOrDefault();
|
||||
file.ImdbEpisode = parsedTitle.Episodes.FirstOrDefault();
|
||||
if (!parsedTitle.Success)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning("Failed to map metadata to files collection: {Exception}", ex.Message);
|
||||
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
|
||||
{
|
||||
@@ -74,8 +82,9 @@ public static class DebridMetaToTorrentMeta
|
||||
|
||||
return files;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning("Failed to map metadata to subtitles collection: {Exception}", ex.Message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ public class InfohashMetadataSagaStateMachine : MassTransitStateMachine<Infohash
|
||||
.Then(
|
||||
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);
|
||||
})
|
||||
.TransitionTo(Completed)
|
||||
|
||||
@@ -16,7 +16,7 @@ public record WriteMetadata(Torrent Torrent, TorrentMetadataResponse Metadata, s
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
@@ -1,25 +1,28 @@
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
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, false));
|
||||
return;
|
||||
}
|
||||
|
||||
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.Threading.Channels;
|
||||
global using DebridCollector.Extensions;
|
||||
global using DebridCollector.Features.Configuration;
|
||||
global using DebridCollector.Features.Debrid;
|
||||
global using DebridCollector.Features.Worker;
|
||||
global using MassTransit;
|
||||
global using MassTransit.Mediator;
|
||||
global using Microsoft.AspNetCore.Builder;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Polly;
|
||||
global using Polly.Extensions.Http;
|
||||
global using PromKnight.ParseTorrentTitle;
|
||||
global using SharedContracts.Configuration;
|
||||
global using SharedContracts.Dapper;
|
||||
global using SharedContracts.Extensions;
|
||||
global using SharedContracts.Models;
|
||||
global using SharedContracts.Python;
|
||||
global using SharedContracts.Python.RTN;
|
||||
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
|
||||
@@ -1 +1 @@
|
||||
rank-torrent-name==0.1.8
|
||||
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
|
||||
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.19
|
||||
|
||||
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 .
|
||||
|
||||
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
|
||||
USER qbit
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD pgrep -f dotnet || exit 1
|
||||
ENV PYTHONNET_PYDLL=/usr/lib/libpython3.11.so.1.0
|
||||
ENTRYPOINT ["dotnet", "QBitCollector.dll"]
|
||||
|
||||
@@ -13,7 +13,8 @@ public static class ServiceCollectionExtensions
|
||||
internal static IServiceCollection AddServiceConfiguration(this IServiceCollection services)
|
||||
{
|
||||
services.AddQBitTorrentClient();
|
||||
services.AddSingleton<IParseTorrentTitle, ParseTorrentTitle>();
|
||||
services.RegisterPythonEngine();
|
||||
services.AddSingleton<IRankTorrentName, RankTorrentName>();
|
||||
services.AddSingleton<QbitRequestProcessor>();
|
||||
services.AddHttpClient();
|
||||
services.AddSingleton<ITrackersService, TrackersService>();
|
||||
|
||||
@@ -3,10 +3,11 @@ namespace QBitCollector.Features.Worker;
|
||||
public static class QbitMetaToTorrentMeta
|
||||
{
|
||||
public static IReadOnlyList<TorrentFile> MapMetadataToFilesCollection(
|
||||
IParseTorrentTitle torrentTitle,
|
||||
IRankTorrentName rankTorrentName,
|
||||
Torrent torrent,
|
||||
string ImdbId,
|
||||
IReadOnlyList<TorrentContent> Metadata)
|
||||
IReadOnlyList<TorrentContent> Metadata,
|
||||
ILogger<WriteQbitMetadataConsumer> logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -24,23 +25,31 @@ public static class QbitMetaToTorrentMeta
|
||||
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.ImdbEpisode = parsedTitle.Episodes.FirstOrDefault();
|
||||
file.ImdbSeason = parsedTitle.Response?.Season?.FirstOrDefault() ?? 0;
|
||||
file.ImdbEpisode = parsedTitle.Response?.Episode?.FirstOrDefault() ?? 0;
|
||||
|
||||
files.Add(file);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning("Failed to map metadata to files collection: {Exception}", ex.Message);
|
||||
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
|
||||
{
|
||||
@@ -70,8 +79,9 @@ public static class QbitMetaToTorrentMeta
|
||||
|
||||
return files;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning("Failed to map metadata to subtitles collection: {Exception}", ex.Message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ public class QbitMetadataSagaStateMachine : MassTransitStateMachine<QbitMetadata
|
||||
.Then(
|
||||
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);
|
||||
})
|
||||
.TransitionTo(Completed)
|
||||
|
||||
@@ -16,7 +16,7 @@ public record WriteQbitMetadata(Torrent Torrent, QBitMetadataResponse Metadata,
|
||||
}
|
||||
|
||||
[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;
|
||||
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
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, false));
|
||||
return;
|
||||
}
|
||||
|
||||
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 System.Text.Json;
|
||||
global using System.Text.Json.Serialization;
|
||||
global using System.Threading.Channels;
|
||||
global using MassTransit;
|
||||
global using MassTransit.Mediator;
|
||||
global using Microsoft.AspNetCore.Builder;
|
||||
global using Microsoft.Extensions.Caching.Distributed;
|
||||
global using Microsoft.Extensions.Caching.Memory;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Polly;
|
||||
global using Polly.Extensions.Http;
|
||||
global using PromKnight.ParseTorrentTitle;
|
||||
global using QBitCollector.Extensions;
|
||||
global using QBitCollector.Features.Qbit;
|
||||
global using QBitCollector.Features.Trackers;
|
||||
@@ -21,4 +15,6 @@ global using SharedContracts.Configuration;
|
||||
global using SharedContracts.Dapper;
|
||||
global using SharedContracts.Extensions;
|
||||
global using SharedContracts.Models;
|
||||
global using SharedContracts.Python;
|
||||
global using SharedContracts.Python.RTN;
|
||||
global using SharedContracts.Requests;
|
||||
@@ -18,7 +18,6 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.3" />
|
||||
<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="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
@@ -31,10 +30,30 @@
|
||||
<None Include="Configuration\logging.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Content Remove="eng\**" />
|
||||
<None Remove="eng\**" />
|
||||
<None Update="requirements.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\shared\SharedContracts.csproj" />
|
||||
</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>
|
||||
|
||||
@@ -6,6 +6,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{2C0A0F
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QBitCollector", "QBitCollector.csproj", "{1EF124BE-6EBE-4D9E-846C-FFF814999F3B}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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
|
||||
@@ -167,12 +167,7 @@ public class DapperDataStorage(PostgresConfiguration configuration, RabbitMqConf
|
||||
INSERT INTO subtitles
|
||||
("infoHash", "fileIndex", "fileId", "title")
|
||||
VALUES
|
||||
(@InfoHash, @FileIndex, @FileId, @Title)
|
||||
ON CONFLICT
|
||||
("infoHash", "fileIndex")
|
||||
DO UPDATE SET
|
||||
"fileId" = COALESCE(subtitles."fileId", EXCLUDED."fileId"),
|
||||
"title" = COALESCE(subtitles."title", EXCLUDED."title");
|
||||
(@InfoHash, @FileIndex, @FileId, @Title);
|
||||
""";
|
||||
|
||||
await connection.ExecuteAsync(query, subtitles);
|
||||
|
||||
@@ -2,5 +2,5 @@ namespace SharedContracts.Python.RTN;
|
||||
|
||||
public interface IRankTorrentName
|
||||
{
|
||||
ParseTorrentTitleResponse Parse(string title);
|
||||
ParseTorrentTitleResponse Parse(string title, bool trashGarbage = true);
|
||||
}
|
||||
@@ -13,14 +13,14 @@ public class RankTorrentName : IRankTorrentName
|
||||
InitModules();
|
||||
}
|
||||
|
||||
public ParseTorrentTitleResponse Parse(string title) =>
|
||||
public ParseTorrentTitleResponse Parse(string title, bool trashGarbage = true) =>
|
||||
_pythonEngineService.ExecutePythonOperationWithDefault(
|
||||
() =>
|
||||
{
|
||||
var result = _rtn?.parse(title);
|
||||
var result = _rtn?.parse(title, trashGarbage);
|
||||
return ParseResult(result);
|
||||
}, new ParseTorrentTitleResponse(false, null), nameof(Parse), throwOnErrors: false, logErrors: false);
|
||||
|
||||
|
||||
private static ParseTorrentTitleResponse ParseResult(dynamic result)
|
||||
{
|
||||
if (result == null)
|
||||
@@ -34,9 +34,18 @@ public class RankTorrentName : IRankTorrentName
|
||||
{
|
||||
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);
|
||||
|
||||
|
||||
response.IsMovie = mediaType.Equals("movie", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return new(true, response);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,23 +76,8 @@ public class RtnResponse
|
||||
|
||||
[JsonPropertyName("extended")]
|
||||
public bool Extended { get; set; }
|
||||
|
||||
// [JsonPropertyName("is_show")]
|
||||
// public bool IsTvShow { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("is_movie")]
|
||||
// public bool IsMovie { get; set; }
|
||||
|
||||
public bool IsMovie { get; set; }
|
||||
|
||||
public string ToJson() => this.AsJson();
|
||||
|
||||
public bool IsMovie => !TvRegexes.Any(regex => regex.IsMatch(RawTitle)) && Season?.Count == 0 && Episode?.Count == 0;
|
||||
|
||||
private static List<Regex> TvRegexes { get; set; } =
|
||||
[
|
||||
new(@"[se]\d\d", RegexOptions.IgnoreCase),
|
||||
new(@"\b(tv|complete)\b", RegexOptions.IgnoreCase),
|
||||
new(@"\b(saisons?|stages?|seasons?).?\d", RegexOptions.IgnoreCase),
|
||||
new(@"[a-z]\s?\-\s?\d{2,4}\b", RegexOptions.IgnoreCase),
|
||||
new(@"\d{2,4}\s?\-\s?\d{2,4}\b", RegexOptions.IgnoreCase),
|
||||
];
|
||||
}
|
||||
@@ -82,11 +82,4 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
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 Microsoft.AspNetCore.Builder;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using PromKnight.ParseTorrentTitle;
|
||||
global using SharedContracts.Configuration;
|
||||
global using SharedContracts.Dapper;
|
||||
global using SharedContracts.Extensions;
|
||||
|
||||
@@ -10,7 +10,6 @@ builder.Host
|
||||
|
||||
builder.Services
|
||||
.RegisterMassTransit()
|
||||
.AddServiceConfiguration()
|
||||
.AddDatabase();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.3" />
|
||||
<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.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
|
||||
Reference in New Issue
Block a user