Drawers, Estimated Reading Time, Korean Parsing Support (#1297)
* Started building out idea around detail drawer. Need code from word count to continue * Fixed the logic for caluclating time to read on comics * Adding styles * more styling fixes * Cleaned up the styles a bit more so it's at least functional. Not sure on the feature, might abandon. * Pulled Robbie's changes in and partially migrated them to the drawer. * Add offset overrides for offcanvas so it takes our header into account * Implemented a basic time left to finish the series (or at least what's in Kavita). Rough around the edges. * Cleaned up the drawer code. * Added Quick Catch ups to recommended page. Updated the timeout for scan tasks to ensure we don't run 2 at the same time. * Quick catchups implemented * Added preliminary support for Korean filename parsing. Reduced an array alloc that is called many thousands of times per scan. * Fixing drawer overflow * Fixed a calculation bug with average reading time. * Small spacing changes to drawer * Don't show estimated reading time if the user hasn't read anything * Bump eventsource from 1.1.1 to 2.0.2 in /UI/Web Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.1 to 2.0.2. - [Release notes](https://github.com/EventSource/eventsource/releases) - [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md) - [Commits](https://github.com/EventSource/eventsource/compare/v1.1.1...v2.0.2) --- updated-dependencies: - dependency-name: eventsource dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> * Added image to series detail drawer Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
parent
d796bcdc0a
commit
63475722ea
40 changed files with 883 additions and 144 deletions
|
|
@ -149,4 +149,18 @@ public class MetadataController : BaseApiController
|
|||
IsoCode = c.IetfLanguageTag
|
||||
}).Where(l => !string.IsNullOrEmpty(l.IsoCode));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns summary for the chapter
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("chapter-summary")]
|
||||
public async Task<ActionResult<string>> GetChapterSummary(int chapterId)
|
||||
{
|
||||
if (chapterId <= 0) return BadRequest("Chapter does not exist");
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
if (chapter == null) return BadRequest("Chapter does not exist");
|
||||
return Ok(chapter.Summary);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using API.Data.Repositories;
|
|||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
|
|
@ -627,5 +628,46 @@ namespace API.Controllers
|
|||
return await _readerService.GetPrevChapterIdAsync(seriesId, volumeId, currentChapterId, userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For the current user, returns an estimate on how long it would take to finish reading the series.
|
||||
/// </summary>
|
||||
/// <remarks>For Epubs, this does not check words inside a chapter due to overhead so may not work in all cases.</remarks>
|
||||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("time-left")]
|
||||
public async Task<ActionResult<HourEstimateRangeDto>> GetEstimateToCompletion(int seriesId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
|
||||
// Get all sum of all chapters with progress that is complete then subtract from series. Multiply by modifiers
|
||||
var progress = await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(seriesId, userId);
|
||||
if (series.Format == MangaFormat.Epub)
|
||||
{
|
||||
var chapters =
|
||||
await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(progress.Select(p => p.ChapterId).ToList());
|
||||
// Word count
|
||||
var progressCount = chapters.Sum(c => c.WordCount);
|
||||
var wordsLeft = series.WordCount - progressCount;
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((wordsLeft / ReaderService.MinWordsPerHour)),
|
||||
MaxHours = (int) Math.Round((wordsLeft / ReaderService.MaxWordsPerHour)),
|
||||
AvgHours = (int) Math.Round((wordsLeft / ReaderService.AvgWordsPerHour)),
|
||||
HasProgress = progressCount > 0
|
||||
});
|
||||
}
|
||||
|
||||
var progressPageCount = progress.Sum(p => p.PagesRead);
|
||||
var pagesLeft = series.Pages - progressPageCount;
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((pagesLeft / ReaderService.MinPagesPerMinute / 60F)),
|
||||
MaxHours = (int) Math.Round((pagesLeft / ReaderService.MaxPagesPerMinute / 60F)),
|
||||
AvgHours = (int) Math.Round((pagesLeft / ReaderService.AvgPagesPerMinute / 60F)),
|
||||
HasProgress = progressPageCount > 0
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public class RecommendedController : BaseApiController
|
|||
|
||||
|
||||
/// <summary>
|
||||
/// Quick Reads are series that are less than 2K pages in total.
|
||||
/// Quick Reads are series that should be readable in less than 10 in total and are not Ongoing in release.
|
||||
/// </summary>
|
||||
/// <param name="libraryId">Library to restrict series to</param>
|
||||
/// <returns></returns>
|
||||
|
|
@ -35,6 +35,24 @@ public class RecommendedController : BaseApiController
|
|||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quick Catchup Reads are series that should be readable in less than 10 in total and are Ongoing in release.
|
||||
/// </summary>
|
||||
/// <param name="libraryId">Library to restrict series to</param>
|
||||
/// <param name="userParams"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("quick-catchup-reads")]
|
||||
public async Task<ActionResult<PagedList<SeriesDto>>> GetQuickCatchupReads(int libraryId, [FromQuery] UserParams userParams)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
|
||||
userParams ??= new UserParams();
|
||||
var series = await _unitOfWork.SeriesRepository.GetQuickCatchupReads(user.Id, libraryId, userParams);
|
||||
|
||||
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highly Rated based on other users ratings. Will pull series with ratings > 4.0, weighted by count of other users.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -394,6 +394,8 @@ namespace API.Controllers
|
|||
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Authorize(Policy="RequireAdminRole")]
|
||||
[HttpPost("update-related")]
|
||||
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using API.DTOs.Metadata;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs
|
||||
{
|
||||
|
|
@ -61,9 +62,5 @@ namespace API.DTOs
|
|||
/// </summary>
|
||||
/// <remarks>Metadata field</remarks>
|
||||
public string TitleName { get; set; }
|
||||
/// <summary>
|
||||
/// Number of Words for this chapter. Only applies to Epub
|
||||
/// </summary>
|
||||
public long WordCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ namespace API.DTOs.Metadata
|
|||
/// Total number of issues for the series
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
/// <summary>
|
||||
/// Number of Words for this chapter. Only applies to Epub
|
||||
/// </summary>
|
||||
public long WordCount { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
API/DTOs/Reader/HourEstimateRangeDto.cs
Normal file
24
API/DTOs/Reader/HourEstimateRangeDto.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
namespace API.DTOs.Reader;
|
||||
|
||||
/// <summary>
|
||||
/// A range of time to read a selection (series, chapter, etc)
|
||||
/// </summary>
|
||||
public class HourEstimateRangeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Min hours to read the selection
|
||||
/// </summary>
|
||||
public int MinHours { get; set; } = 1;
|
||||
/// <summary>
|
||||
/// Max hours to read the selection
|
||||
/// </summary>
|
||||
public int MaxHours { get; set; } = 1;
|
||||
/// <summary>
|
||||
/// Estimated average hours to read the selection
|
||||
/// </summary>
|
||||
public int AvgHours { get; set; } = 1;
|
||||
/// <summary>
|
||||
/// Does the user have progress on the range this represents
|
||||
/// </summary>
|
||||
public bool HasProgress { get; set; } = false;
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ using API.Entities.Enums;
|
|||
using API.Entities.Metadata;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
using API.Services.Tasks;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
|
|
@ -113,6 +114,7 @@ public interface ISeriesRepository
|
|||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
|
||||
Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams);
|
||||
Task<PagedList<SeriesDto>> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams);
|
||||
Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams);
|
||||
Task<PagedList<SeriesDto>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams);
|
||||
Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams);
|
||||
|
|
@ -1131,8 +1133,11 @@ public class SeriesRepository : ISeriesRepository
|
|||
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => s.Pages < 2000 && !distinctSeriesIdsWithProgress.Contains(s.Id) &&
|
||||
usersSeriesIds.Contains(s.Id))
|
||||
.Where(s => (
|
||||
(s.Pages / ReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub)
|
||||
|| (s.WordCount * ReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub))
|
||||
&& !distinctSeriesIdsWithProgress.Contains(s.Id) &&
|
||||
usersSeriesIds.Contains(s.Id))
|
||||
.Where(s => s.Metadata.PublicationStatus != PublicationStatus.OnGoing)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||
|
|
@ -1141,6 +1146,30 @@ public class SeriesRepository : ISeriesRepository
|
|||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetQuickCatchupReads(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||
.Select(p => p.SeriesId)
|
||||
.Distinct();
|
||||
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => (
|
||||
(s.Pages / ReaderService.AvgPagesPerMinute / 60 < 10 && s.Format != MangaFormat.Epub)
|
||||
|| (s.WordCount * ReaderService.AvgWordsPerHour < 10 && s.Format == MangaFormat.Epub))
|
||||
&& !distinctSeriesIdsWithProgress.Contains(s.Id) &&
|
||||
usersSeriesIds.Contains(s.Id))
|
||||
.Where(s => s.Metadata.PublicationStatus == PublicationStatus.OnGoing)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider);
|
||||
|
||||
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all library ids for a user
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -110,6 +110,22 @@ namespace API.Parser
|
|||
new Regex(
|
||||
@"(卷|册)(?<Volume>\d+)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Korean Volume: 제n권 -> Volume n, n권 -> Volume n, 63권#200.zip -> Volume 63 (no chapter, #200 is just files inside)
|
||||
new Regex(
|
||||
@"제?(?<Volume>\d+)권",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Korean Season: 시즌n -> Season n,
|
||||
new Regex(
|
||||
@"시즌(?<Volume>\d+\-?\d+)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Korean Season: 시즌n -> Season n, n시즌 -> season n
|
||||
new Regex(
|
||||
@"(?<Volume>\d+(\-|~)?\d+?)시즌",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Korean Season: 시즌n -> Season n, n시즌 -> season n
|
||||
new Regex(
|
||||
@"시즌(?<Volume>\d+(\-|~)?\d+?)",
|
||||
MatchOptions, RegexTimeout),
|
||||
};
|
||||
|
||||
private static readonly Regex[] MangaSeriesRegex = new[]
|
||||
|
|
@ -340,6 +356,18 @@ namespace API.Parser
|
|||
new Regex(
|
||||
@"^(?<Series>.+?)(?:\s|_)(v|vol|tome|t)\.?(\s|_)?(?<Volume>\d+)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Chinese Volume: 第n卷 -> Volume n, 第n册 -> Volume n, 幽游白书完全版 第03卷 天下 or 阿衰online 第1册
|
||||
new Regex(
|
||||
@"第(?<Volume>\d+)(卷|册)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Chinese Volume: 卷n -> Volume n, 册n -> Volume n
|
||||
new Regex(
|
||||
@"(卷|册)(?<Volume>\d+)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Korean Volume: 제n권 -> Volume n, n권 -> Volume n, 63권#200.zip
|
||||
new Regex(
|
||||
@"제?(?<Volume>\d+)권",
|
||||
MatchOptions, RegexTimeout),
|
||||
};
|
||||
|
||||
private static readonly Regex[] ComicChapterRegex = new[]
|
||||
|
|
@ -398,11 +426,7 @@ namespace API.Parser
|
|||
new Regex(
|
||||
@"^(?<Series>.+?)-(chapter-)?(?<Chapter>\d+)",
|
||||
MatchOptions, RegexTimeout),
|
||||
// Cyberpunk 2077 - Your Voice 01
|
||||
// new Regex(
|
||||
// @"^(?<Series>.+?\s?-\s?(?:.+?))(?<Chapter>(\d+(\.\d)?)-?(\d+(\.\d)?)?)$",
|
||||
// MatchOptions,
|
||||
// RegexTimeout),
|
||||
|
||||
};
|
||||
|
||||
private static readonly Regex[] ReleaseGroupRegex = new[]
|
||||
|
|
@ -461,7 +485,10 @@ namespace API.Parser
|
|||
new Regex(
|
||||
@"第(?<Chapter>\d+)话",
|
||||
MatchOptions, RegexTimeout),
|
||||
|
||||
// Korean Chapter: 제n화 -> Chapter n, 가디언즈 오브 갤럭시 죽음의 보석.E0008.7화#44
|
||||
new Regex(
|
||||
@"제?(?<Chapter>\d+\.?\d+)(화|장)",
|
||||
MatchOptions, RegexTimeout),
|
||||
};
|
||||
private static readonly Regex[] MangaEditionRegex = {
|
||||
// Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz
|
||||
|
|
@ -525,11 +552,13 @@ namespace API.Parser
|
|||
MatchOptions, RegexTimeout
|
||||
);
|
||||
|
||||
private static readonly ImmutableArray<string> FormatTagSpecialKeyowrds = ImmutableArray.Create(
|
||||
private static readonly ImmutableArray<string> FormatTagSpecialKeywords = ImmutableArray.Create(
|
||||
"Special", "Reference", "Director's Cut", "Box Set", "Box-Set", "Annual", "Anthology", "Epilogue",
|
||||
"One Shot", "One-Shot", "Prologue", "TPB", "Trade Paper Back", "Omnibus", "Compendium", "Absolute", "Graphic Novel",
|
||||
"GN", "FCBD");
|
||||
|
||||
private static readonly char[] LeadingZeroesTrimChars = new[] { '0' };
|
||||
|
||||
public static MangaFormat ParseFormat(string filePath)
|
||||
{
|
||||
if (IsArchive(filePath)) return MangaFormat.Archive;
|
||||
|
|
@ -916,8 +945,8 @@ namespace API.Parser
|
|||
|
||||
public static string RemoveLeadingZeroes(string title)
|
||||
{
|
||||
var ret = title.TrimStart(new[] { '0' });
|
||||
return ret == string.Empty ? "0" : ret;
|
||||
var ret = title.TrimStart(LeadingZeroesTrimChars);
|
||||
return string.IsNullOrEmpty(ret) ? "0" : ret;
|
||||
}
|
||||
|
||||
public static bool IsArchive(string filePath)
|
||||
|
|
@ -1060,7 +1089,7 @@ namespace API.Parser
|
|||
/// <returns></returns>
|
||||
public static bool HasComicInfoSpecial(string comicInfoFormat)
|
||||
{
|
||||
return FormatTagSpecialKeyowrds.Contains(comicInfoFormat);
|
||||
return FormatTagSpecialKeywords.Contains(comicInfoFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ public class MetadataService : IMetadataService
|
|||
/// <remarks>This can be heavy on memory first run</remarks>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 360)]
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task RefreshMetadata(int libraryId, bool forceUpdate = false)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ public class ReaderService : IReaderService
|
|||
private readonly ChapterSortComparer _chapterSortComparer = new ChapterSortComparer();
|
||||
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
|
||||
|
||||
public const float MinWordsPerHour = 10260F;
|
||||
public const float MaxWordsPerHour = 30000F;
|
||||
public const float AvgWordsPerHour = (MaxWordsPerHour + MinWordsPerHour) / 2F;
|
||||
public const float MinPagesPerMinute = 3.33F;
|
||||
public const float MaxPagesPerMinute = 2.75F;
|
||||
public const float AvgPagesPerMinute = (MaxPagesPerMinute + MinPagesPerMinute) / 2F;
|
||||
|
||||
|
||||
public ReaderService(IUnitOfWork unitOfWork, ILogger<ReaderService> logger, IEventHub eventHub)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||
_cacheHelper = cacheHelper;
|
||||
}
|
||||
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 360)]
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanLibrary(int libraryId, bool forceUpdate = false)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class ScannerService : IScannerService
|
|||
_wordCountAnalyzerService = wordCountAnalyzerService;
|
||||
}
|
||||
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 360)]
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanSeries(int libraryId, int seriesId, CancellationToken token)
|
||||
{
|
||||
|
|
@ -247,7 +247,7 @@ public class ScannerService : IScannerService
|
|||
}
|
||||
|
||||
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 360)]
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60 * 4)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanLibraries()
|
||||
{
|
||||
|
|
@ -267,7 +267,7 @@ public class ScannerService : IScannerService
|
|||
/// ie) all entities will be rechecked for new cover images and comicInfo.xml changes
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
[DisableConcurrentExecution(360)]
|
||||
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanLibrary(int libraryId)
|
||||
{
|
||||
|
|
@ -470,6 +470,7 @@ public class ScannerService : IScannerService
|
|||
foreach (var series in duplicateSeries)
|
||||
{
|
||||
_logger.LogCritical("[ScannerService] Duplicate Series Found: {Key} maps with {Series}", key.Name, series.OriginalName);
|
||||
|
||||
}
|
||||
|
||||
continue;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue