diff --git a/Wasari.Anime4k/AppExtensions.cs b/Wasari.Anime4k/AppExtensions.cs index 714cadd..b571571 100644 --- a/Wasari.Anime4k/AppExtensions.cs +++ b/Wasari.Anime4k/AppExtensions.cs @@ -9,7 +9,7 @@ public static void AddAnime4KShader(this IServiceCollection serviceCollection) { serviceCollection.Configure(c => { - c.ShadersFactory.Add("anime4k", s => new Anime4KShader()); + c.ShadersFactory.Add("anime4k", _ => new Anime4KShader()); }); } } \ No newline at end of file diff --git a/Wasari.App/DownloadOptions.cs b/Wasari.App/DownloadOptions.cs index 46f7b1c..316cc88 100644 --- a/Wasari.App/DownloadOptions.cs +++ b/Wasari.App/DownloadOptions.cs @@ -21,6 +21,8 @@ public record DownloadOptions public bool CreateSeasonFolder { get; set; } + public bool TryEnrichEpisodes { get; set; } = true; + private Dictionary HostDownloadService { get; } = new(); public DownloadOptions AddHostDownloader(string host) where T : IDownloadService diff --git a/Wasari.Cli/Commands/DownloadCommand.cs b/Wasari.Cli/Commands/DownloadCommand.cs index 9dd3d25..53af186 100644 --- a/Wasari.Cli/Commands/DownloadCommand.cs +++ b/Wasari.Cli/Commands/DownloadCommand.cs @@ -12,6 +12,7 @@ using Wasari.Cli.Converters; using Wasari.Crunchyroll; using Wasari.FFmpeg; +using Wasari.Tvdb.Api.Client; using Wasari.YoutubeDlp; using WasariEnvironment; @@ -90,6 +91,9 @@ public DownloadCommand(EnvironmentService environmentService, ILogger Logger { get; } @@ -172,6 +176,7 @@ public async ValueTask ExecuteAsync(IConsole console) serviceCollection.AddDownloadServices(); serviceCollection.AddCrunchyrollServices(); serviceCollection.AddMemoryCache(); + serviceCollection.AddWasariTvdbApi(); serviceCollection.Configure(o => { o.OutputDirectory = OutputDirectory; @@ -182,6 +187,7 @@ public async ValueTask ExecuteAsync(IConsole console) o.SeasonsRange = ParseRange(SeasonsRange); o.CreateSeriesFolder = CreateSeriesFolder; o.CreateSeasonFolder = CreateSeasonFolder; + o.TryEnrichEpisodes = EnrichEpisodes; }); serviceCollection.Configure(o => { diff --git a/Wasari.Cli/Dockerfile b/Wasari.Cli/Dockerfile index a9b6e99..060196b 100644 --- a/Wasari.Cli/Dockerfile +++ b/Wasari.Cli/Dockerfile @@ -1,16 +1,16 @@ -FROM alpine:3.15 AS ffmpeg-base +FROM alpine:3.15 AS ffmpeg-base RUN apk update && apk add wget RUN wget https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz RUN mkdir /ffmpeg RUN tar -C /ffmpeg -xvf ffmpeg-master-latest-linux64-gpl.tar.xz --strip-components=1 -FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base +FROM mcr.microsoft.com/dotnet/runtime:7.0 AS base RUN apt update && apt install curl python3 -y RUN curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp RUN chmod a+rx /usr/local/bin/yt-dlp WORKDIR /app -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build WORKDIR /src COPY ["Wasari.Cli/Wasari.Cli.csproj", "Wasari.Cli/"] COPY ["Wasari.App/Wasari.App.csproj", "Wasari.App/"] diff --git a/Wasari.Cli/Program.cs b/Wasari.Cli/Program.cs index f127ac7..81856af 100644 --- a/Wasari.Cli/Program.cs +++ b/Wasari.Cli/Program.cs @@ -6,6 +6,7 @@ using Wasari.Cli.Commands; using Wasari.Cli.Converters; using Wasari.FFmpeg; +using Wasari.Tvdb.Api.Client; using WasariEnvironment; namespace Wasari.Cli; diff --git a/Wasari.Cli/Wasari.Cli.csproj b/Wasari.Cli/Wasari.Cli.csproj index 5b28a88..f070519 100644 --- a/Wasari.Cli/Wasari.Cli.csproj +++ b/Wasari.Cli/Wasari.Cli.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 enable enable Linux diff --git a/Wasari.Crunchyroll/CrunchyrollApiService.cs b/Wasari.Crunchyroll/CrunchyrollApiService.cs index d67cceb..dbcfc6b 100644 --- a/Wasari.Crunchyroll/CrunchyrollApiService.cs +++ b/Wasari.Crunchyroll/CrunchyrollApiService.cs @@ -57,14 +57,14 @@ private async Task BuildUrlFromSignature(string endpoint) .SetQueryParam("Key-Pair-Id", signature.KeyPairId) .SetQueryParam("locale", "en-US"); } - + public IAsyncEnumerable GetAllEpisodes(string seriesId) { return GetSeasons(seriesId) .SelectMany(season => GetEpisodes(season.Id)); } - public async IAsyncEnumerable GetEpisodes(string seasonId) + private async IAsyncEnumerable GetEpisodes(string seasonId) { var url = await BuildUrlFromSignature("episodes"); url = url.SetQueryParam("season_id", seasonId); @@ -94,12 +94,12 @@ public async IAsyncEnumerable GetSeasons(string seriesId) url = url.SetQueryParam("series_id", seriesId); var responseJson = await HttpClient.GetJsonAsync(url); - + var seasons = responseJson.GetProperty("items").EnumerateArray() .Select(i => i.Deserialize()) .Where(i => i != null) .ToArray(); - + var lastNumber = seasons.Length > 0 ? seasons.Min(o => o.Number) : 1; foreach (var apiSeason in seasons) @@ -107,11 +107,11 @@ public async IAsyncEnumerable GetSeasons(string seriesId) if (apiSeason != null) { apiSeason.Number = lastNumber; - + if (apiSeason.Number > 0 && !apiSeason.IsDubbed) lastNumber++; } - + yield return apiSeason; } } diff --git a/Wasari.Crunchyroll/CrunchyrollDownloadService.cs b/Wasari.Crunchyroll/CrunchyrollDownloadService.cs index d37175d..9770e00 100644 --- a/Wasari.Crunchyroll/CrunchyrollDownloadService.cs +++ b/Wasari.Crunchyroll/CrunchyrollDownloadService.cs @@ -10,21 +10,108 @@ using Wasari.App; using Wasari.App.Abstractions; using Wasari.FFmpeg; +using Wasari.Tvdb.Api.Client; using Wasari.YoutubeDlp; namespace Wasari.Crunchyroll; +internal static class EpisodeExtensions +{ + public static async IAsyncEnumerable EnrichWithWasariApi(this IAsyncEnumerable episodes, IServiceProvider serviceProvider, IOptions downloadOptions) + { + var wasariTvdbApi = downloadOptions.Value.TryEnrichEpisodes ? serviceProvider.GetService() : null; + + if (wasariTvdbApi != null) + { + var logger = serviceProvider.GetRequiredService>(); + logger.LogInformation("Trying to enrich episodes with Wasari.Tvdb"); + + var episodesArray = await episodes.ToArrayAsync(); + + var seriesName = episodesArray.Select(o => o.SeriesTitle) + .Distinct() + .ToArray(); + + if (seriesName.Length == 1) + { + var wasariApiEpisodes = await wasariTvdbApi.GetEpisodesAsync(seriesName.Single()) + .ContinueWith(t => + { + if (t.IsCompletedSuccessfully) + { + return t.Result; + } + + logger.LogError(t.Exception, "Error while getting episodes from Wasari.Tvdb"); + return null; + }); + + if (wasariApiEpisodes != null) + { + var episodesLookup = wasariApiEpisodes + .Where(i => !i.IsMovie) + .ToLookup(i => i.Name); + + foreach (var episode in episodesArray) + { + var wasariEpisode = episodesLookup[episode.Title].SingleOrDefault(); + + if (wasariEpisode == null) + { + wasariEpisode = wasariApiEpisodes + .Where(i => !i.IsMovie) + .SingleOrDefault(o => o.Name.StartsWith(episode.Title, StringComparison.InvariantCultureIgnoreCase)); + + if (wasariEpisode == null) + { + logger.LogWarning("Skipping episode {EpisodeTitle} because it could not be found in Wasari.Tvdb", episode.Title); + } + } + + if (wasariEpisode != null) + yield return episode with + { + SeasonNumber = wasariEpisode?.SeasonNumber ?? episode.SeasonNumber, + EpisodeNumber = wasariEpisode?.Number ?? episode.EpisodeNumber + }; + } + + yield break; + } + } + + foreach (var episode in episodesArray) + { + yield return episode; + } + + yield break; + } + + + await foreach (var episode in episodes) + { + yield return episode; + } + } +} + internal class CrunchyrollDownloadService : GenericDownloadService { private CrunchyrollApiService CrunchyrollApiService { get; } - + private IOptions DownloadOptions { get; } - public CrunchyrollDownloadService(ILogger logger, FFmpegService fFmpegService, IOptions options, YoutubeDlpService youtubeDlpService, CrunchyrollApiService crunchyrollApiService, IOptions downloadOptions) : base(logger, fFmpegService, options, + private IServiceProvider ServiceProvider { get; } + + public CrunchyrollDownloadService(ILogger logger, FFmpegService fFmpegService, IOptions options, YoutubeDlpService youtubeDlpService, CrunchyrollApiService crunchyrollApiService, IOptions downloadOptions, + IServiceProvider serviceProvider) : base(logger, fFmpegService, + options, youtubeDlpService) { CrunchyrollApiService = crunchyrollApiService; DownloadOptions = downloadOptions; + ServiceProvider = serviceProvider; } public override async Task DownloadEpisodes(string url, int levelOfParallelism) @@ -34,8 +121,10 @@ public override async Task DownloadEpisodes(string url, int if (match.Groups["seriesId"].Success) { var seriesId = match.Groups["seriesId"].Value; + var episodes = CrunchyrollApiService.GetAllEpisodes(seriesId) .Where(i => i.EpisodeNumber.HasValue && (DownloadOptions.Value.IncludeDubs || !i.IsDubbed)) + .EnrichWithWasariApi(ServiceProvider, DownloadOptions) .GroupBy(i => new { i.EpisodeNumber, i.SeasonNumber, i.SeriesTitle }) .SelectAwait(async groupedEpisodes => { @@ -73,7 +162,7 @@ private async IAsyncEnumerable ProcessEpisode(ApiEpisode ep Logger.LogWarning("Episode found with no stream options: {@Episode}", episode); yield break; } - + var stream = episode.ApiEpisodeStreams.Streams.Single(o => o.Type == "adaptive_hls" && string.IsNullOrEmpty(o.Locale)); var mediaInfo = await FFProbe.AnalyseAsync(new Uri(stream.Url)); var bestVideo = mediaInfo.VideoStreams.OrderBy(vStream => vStream.Height + vStream.Width).Last(); diff --git a/Wasari.Crunchyroll/Wasari.Crunchyroll.csproj b/Wasari.Crunchyroll/Wasari.Crunchyroll.csproj index 9287969..e22e9a9 100644 --- a/Wasari.Crunchyroll/Wasari.Crunchyroll.csproj +++ b/Wasari.Crunchyroll/Wasari.Crunchyroll.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 @@ -10,15 +10,16 @@ - + - + +