mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
ComicInfo Derived Reading Lists (#1929)
* Implemented the ability to generate reading lists from StoryArc and StoryArcNumber ComicInfo fields. * Refactored to add AlternativeSeries support. * Fixed up the handling when we need to update reading list where order is already present. * Refactored how skipping empty reading list pairs works
This commit is contained in:
parent
1e535d27fa
commit
7f53eadfda
@ -60,7 +60,7 @@ public class ProcessSeriesTests
|
|||||||
, Substitute.For<ICacheHelper>(), Substitute.For<IReadingItemService>(), Substitute.For<IFileService>(),
|
, Substitute.For<ICacheHelper>(), Substitute.For<IReadingItemService>(), Substitute.For<IFileService>(),
|
||||||
Substitute.For<IMetadataService>(),
|
Substitute.For<IMetadataService>(),
|
||||||
Substitute.For<IWordCountAnalyzerService>(),
|
Substitute.For<IWordCountAnalyzerService>(),
|
||||||
Substitute.For<ICollectionTagService>());
|
Substitute.For<ICollectionTagService>(), Substitute.For<IReadingListService>());
|
||||||
|
|
||||||
ps.UpdateChapterFromComicInfo(chapter, new ComicInfo()
|
ps.UpdateChapterFromComicInfo(chapter, new ComicInfo()
|
||||||
{
|
{
|
||||||
|
@ -110,6 +110,9 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||||||
builder.Entity<Library>()
|
builder.Entity<Library>()
|
||||||
.Property(b => b.ManageCollections)
|
.Property(b => b.ManageCollections)
|
||||||
.HasDefaultValue(true);
|
.HasDefaultValue(true);
|
||||||
|
builder.Entity<Library>()
|
||||||
|
.Property(b => b.ManageReadingLists)
|
||||||
|
.HasDefaultValue(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ public class ComicInfo
|
|||||||
public string SeriesGroup { get; set; } = string.Empty;
|
public string SeriesGroup { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Can contain multiple comma separated numbers that match with StoryArcNumber
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string StoryArc { get; set; } = string.Empty;
|
public string StoryArc { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
1877
API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs
generated
Normal file
1877
API/Data/Migrations/20230415123449_ManageReadingListOnLibrary.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ManageReadingListOnLibrary : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "ManageReadingLists",
|
||||||
|
table: "Library",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ManageReadingLists",
|
||||||
|
table: "Library");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
|
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
|
||||||
|
|
||||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||||
{
|
{
|
||||||
@ -649,6 +649,11 @@ namespace API.Data.Migrations
|
|||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasDefaultValue(true);
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<bool>("ManageReadingLists")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ public interface IReadingListRepository
|
|||||||
Task<IList<ReadingList>> GetAllWithNonWebPCovers();
|
Task<IList<ReadingList>> GetAllWithNonWebPCovers();
|
||||||
Task<IList<string>> GetFirstFourCoverImagesByReadingListId(int readingListId);
|
Task<IList<string>> GetFirstFourCoverImagesByReadingListId(int readingListId);
|
||||||
Task<int> RemoveReadingListsWithoutSeries();
|
Task<int> RemoveReadingListsWithoutSeries();
|
||||||
|
Task<ReadingList?> GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ReadingListRepository : IReadingListRepository
|
public class ReadingListRepository : IReadingListRepository
|
||||||
@ -145,6 +146,15 @@ public class ReadingListRepository : IReadingListRepository
|
|||||||
return await _context.SaveChangesAsync();
|
return await _context.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<ReadingList?> GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items)
|
||||||
|
{
|
||||||
|
var normalized = name.ToNormalized();
|
||||||
|
return await _context.ReadingList
|
||||||
|
.Includes(includes)
|
||||||
|
.FirstOrDefaultAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId);
|
||||||
|
}
|
||||||
|
|
||||||
public void Remove(ReadingListItem item)
|
public void Remove(ReadingListItem item)
|
||||||
{
|
{
|
||||||
_context.ReadingListItem.Remove(item);
|
_context.ReadingListItem.Remove(item);
|
||||||
|
@ -62,6 +62,7 @@ public interface IUserRepository
|
|||||||
Task<bool> HasAccessToLibrary(int libraryId, int userId);
|
Task<bool> HasAccessToLibrary(int libraryId, int userId);
|
||||||
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None);
|
Task<IEnumerable<AppUser>> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None);
|
||||||
Task<AppUser?> GetUserByConfirmationToken(string token);
|
Task<AppUser?> GetUserByConfirmationToken(string token);
|
||||||
|
Task<AppUser> GetDefaultAdminUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserRepository : IUserRepository
|
public class UserRepository : IUserRepository
|
||||||
@ -220,6 +221,17 @@ public class UserRepository : IUserRepository
|
|||||||
.SingleOrDefaultAsync(u => u.ConfirmationToken != null && u.ConfirmationToken.Equals(token));
|
.SingleOrDefaultAsync(u => u.ConfirmationToken != null && u.ConfirmationToken.Equals(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the first admin account created
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<AppUser> GetDefaultAdminUser()
|
||||||
|
{
|
||||||
|
return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole))
|
||||||
|
.OrderByDescending(u => u.Created)
|
||||||
|
.First();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||||
{
|
{
|
||||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||||
|
@ -28,9 +28,13 @@ public class Library : IEntityDate
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IncludeInSearch { get; set; } = true;
|
public bool IncludeInSearch { get; set; } = true;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should this library create and manage collections from Metadata
|
/// Should this library create collections from Metadata
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ManageCollections { get; set; } = true;
|
public bool ManageCollections { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Should this library create reading lists from Metadata
|
||||||
|
/// </summary>
|
||||||
|
public bool ManageReadingLists { get; set; } = true;
|
||||||
public DateTime Created { get; set; }
|
public DateTime Created { get; set; }
|
||||||
public DateTime LastModified { get; set; }
|
public DateTime LastModified { get; set; }
|
||||||
public DateTime CreatedUtc { get; set; }
|
public DateTime CreatedUtc { get; set; }
|
||||||
|
@ -54,4 +54,10 @@ public class ReadingListBuilder : IEntityBuilder<ReadingList>
|
|||||||
_readingList.CoverImage = coverImage;
|
_readingList.CoverImage = coverImage;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReadingListBuilder WithAppUserId(int userId)
|
||||||
|
{
|
||||||
|
_readingList.AppUserId = userId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,13 @@ public interface IReadingListService
|
|||||||
Task<CblImportSummaryDto> CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false);
|
Task<CblImportSummaryDto> CreateReadingListFromCbl(int userId, CblReadingList cblReading, bool dryRun = false);
|
||||||
Task CalculateStartAndEndDates(ReadingList readingListWithItems);
|
Task CalculateStartAndEndDates(ReadingList readingListWithItems);
|
||||||
Task<string> GenerateMergedImage(int readingListId);
|
Task<string> GenerateMergedImage(int readingListId);
|
||||||
|
/// <summary>
|
||||||
|
/// This is expected to be called from ProcessSeries and has the Full Series present. Will generate on the default admin user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="series"></param>
|
||||||
|
/// <param name="library"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task CreateReadingListsFromSeries(Series series, Library library);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -228,21 +235,26 @@ public class ReadingListService : IReadingListService
|
|||||||
public async Task<bool> UpdateReadingListItemPosition(UpdateReadingListPosition dto)
|
public async Task<bool> UpdateReadingListItemPosition(UpdateReadingListPosition dto)
|
||||||
{
|
{
|
||||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
|
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
|
||||||
var item = items.Find(r => r.Id == dto.ReadingListItemId);
|
ReorderItems(items, dto.ReadingListItemId, dto.ToPosition);
|
||||||
|
|
||||||
|
if (!_unitOfWork.HasChanges()) return true;
|
||||||
|
|
||||||
|
return await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReorderItems(List<ReadingListItem> items, int readingListItemId, int toPosition)
|
||||||
|
{
|
||||||
|
var item = items.Find(r => r.Id == readingListItemId);
|
||||||
if (item != null)
|
if (item != null)
|
||||||
{
|
{
|
||||||
items.Remove(item);
|
items.Remove(item);
|
||||||
items.Insert(dto.ToPosition, item);
|
items.Insert(toPosition, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < items.Count; i++)
|
for (var i = 0; i < items.Count; i++)
|
||||||
{
|
{
|
||||||
items[i].Order = i;
|
items[i].Order = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_unitOfWork.HasChanges()) return true;
|
|
||||||
|
|
||||||
return await _unitOfWork.CommitAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -420,6 +432,84 @@ public class ReadingListService : IReadingListService
|
|||||||
return index > lastOrder + 1;
|
return index > lastOrder + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task CreateReadingListsFromSeries(Series series, Library library)
|
||||||
|
{
|
||||||
|
if (!library.ManageReadingLists) return;
|
||||||
|
|
||||||
|
var hasReadingListMarkers = series.Volumes
|
||||||
|
.SelectMany(c => c.Chapters)
|
||||||
|
.Any(c => !string.IsNullOrEmpty(c.StoryArc) || !string.IsNullOrEmpty(c.AlternateSeries));
|
||||||
|
|
||||||
|
if (!hasReadingListMarkers) return;
|
||||||
|
|
||||||
|
_logger.LogInformation("Processing Reading Lists for {SeriesName}", series.Name);
|
||||||
|
var user = await _unitOfWork.UserRepository.GetDefaultAdminUser();
|
||||||
|
series.Metadata ??= new SeriesMetadataBuilder().Build();
|
||||||
|
foreach (var chapter in series.Volumes.SelectMany(v => v.Chapters))
|
||||||
|
{
|
||||||
|
List<Tuple<string, string>> pairs = new List<Tuple<string, string>>();
|
||||||
|
if (!string.IsNullOrEmpty(chapter.StoryArc))
|
||||||
|
{
|
||||||
|
pairs.AddRange(GeneratePairs(chapter.Files.FirstOrDefault()!.FilePath, chapter.StoryArc, chapter.StoryArcNumber));
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(chapter.AlternateSeries))
|
||||||
|
{
|
||||||
|
pairs.AddRange(GeneratePairs(chapter.Files.FirstOrDefault()!.FilePath, chapter.AlternateSeries, chapter.AlternateNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var arcPair in pairs)
|
||||||
|
{
|
||||||
|
var order = int.Parse(arcPair.Item2);
|
||||||
|
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByTitleAsync(arcPair.Item1, user.Id);
|
||||||
|
if (readingList == null)
|
||||||
|
{
|
||||||
|
readingList = new ReadingListBuilder(arcPair.Item1)
|
||||||
|
.WithAppUserId(user.Id)
|
||||||
|
.Build();
|
||||||
|
_unitOfWork.ReadingListRepository.Add(readingList);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = readingList.Items.ToList();
|
||||||
|
var readingListItem = items.FirstOrDefault(item => item.Order == order);
|
||||||
|
if (readingListItem == null)
|
||||||
|
{
|
||||||
|
items.Add(new ReadingListItemBuilder(order, series.Id, chapter.VolumeId, chapter.Id).Build());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ReorderItems(items, readingListItem.Id, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
readingList.Items = items;
|
||||||
|
await CalculateReadingListAgeRating(readingList);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IList<Tuple<string, string>> GeneratePairs(string filename, string storyArc, string storyArcNumbers)
|
||||||
|
{
|
||||||
|
var data = new List<Tuple<string, string>>();
|
||||||
|
if (string.IsNullOrEmpty(storyArc)) return data;
|
||||||
|
|
||||||
|
var arcs = storyArc.Split(",");
|
||||||
|
var arcNumbers = storyArcNumbers.Split(",");
|
||||||
|
if (arcNumbers.Length != arcs.Length)
|
||||||
|
{
|
||||||
|
_logger.LogError("There is a mismatch on StoryArc and StoryArcNumber for {FileName}", filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxPairs = Math.Min(arcs.Length, arcNumbers.Length);
|
||||||
|
for (var i = 0; i < maxPairs; i++)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(arcs[i]) || !int.TryParse(arcNumbers[i], out _)) continue;
|
||||||
|
data.Add(new Tuple<string, string>(arcs[i], arcNumbers[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check for File issues like: No entries, Reading List Name collision, Duplicate Series across Libraries
|
/// Check for File issues like: No entries, Reading List Name collision, Duplicate Series across Libraries
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -54,6 +54,7 @@ public class ProcessSeries : IProcessSeries
|
|||||||
private readonly IMetadataService _metadataService;
|
private readonly IMetadataService _metadataService;
|
||||||
private readonly IWordCountAnalyzerService _wordCountAnalyzerService;
|
private readonly IWordCountAnalyzerService _wordCountAnalyzerService;
|
||||||
private readonly ICollectionTagService _collectionTagService;
|
private readonly ICollectionTagService _collectionTagService;
|
||||||
|
private readonly IReadingListService _readingListService;
|
||||||
|
|
||||||
private Dictionary<string, Genre> _genres;
|
private Dictionary<string, Genre> _genres;
|
||||||
private IList<Person> _people;
|
private IList<Person> _people;
|
||||||
@ -66,7 +67,7 @@ public class ProcessSeries : IProcessSeries
|
|||||||
public ProcessSeries(IUnitOfWork unitOfWork, ILogger<ProcessSeries> logger, IEventHub eventHub,
|
public ProcessSeries(IUnitOfWork unitOfWork, ILogger<ProcessSeries> logger, IEventHub eventHub,
|
||||||
IDirectoryService directoryService, ICacheHelper cacheHelper, IReadingItemService readingItemService,
|
IDirectoryService directoryService, ICacheHelper cacheHelper, IReadingItemService readingItemService,
|
||||||
IFileService fileService, IMetadataService metadataService, IWordCountAnalyzerService wordCountAnalyzerService,
|
IFileService fileService, IMetadataService metadataService, IWordCountAnalyzerService wordCountAnalyzerService,
|
||||||
ICollectionTagService collectionTagService)
|
ICollectionTagService collectionTagService, IReadingListService readingListService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -78,6 +79,7 @@ public class ProcessSeries : IProcessSeries
|
|||||||
_metadataService = metadataService;
|
_metadataService = metadataService;
|
||||||
_wordCountAnalyzerService = wordCountAnalyzerService;
|
_wordCountAnalyzerService = wordCountAnalyzerService;
|
||||||
_collectionTagService = collectionTagService;
|
_collectionTagService = collectionTagService;
|
||||||
|
_readingListService = readingListService;
|
||||||
|
|
||||||
|
|
||||||
_genres = new Dictionary<string, Genre>();
|
_genres = new Dictionary<string, Genre>();
|
||||||
@ -179,8 +181,6 @@ public class ProcessSeries : IProcessSeries
|
|||||||
|
|
||||||
UpdateSeriesMetadata(series, library);
|
UpdateSeriesMetadata(series, library);
|
||||||
|
|
||||||
//CreateReadingListsFromSeries(series, library); This will be implemented later when I solution it
|
|
||||||
|
|
||||||
// Update series FolderPath here
|
// Update series FolderPath here
|
||||||
await UpdateSeriesFolderPath(parsedInfos, library, series);
|
await UpdateSeriesFolderPath(parsedInfos, library, series);
|
||||||
|
|
||||||
@ -205,6 +205,9 @@ public class ProcessSeries : IProcessSeries
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process reading list after commit as we need to commit per list
|
||||||
|
await _readingListService.CreateReadingListsFromSeries(series, library);
|
||||||
|
|
||||||
if (seriesAdded)
|
if (seriesAdded)
|
||||||
{
|
{
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.SeriesAdded,
|
await _eventHub.SendMessageAsync(MessageFactory.SeriesAdded,
|
||||||
@ -223,26 +226,6 @@ public class ProcessSeries : IProcessSeries
|
|||||||
EnqueuePostSeriesProcessTasks(series.LibraryId, series.Id);
|
EnqueuePostSeriesProcessTasks(series.LibraryId, series.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateReadingListsFromSeries(Series series, Library library)
|
|
||||||
{
|
|
||||||
//if (!library.ManageReadingLists) return;
|
|
||||||
_logger.LogInformation("Generating Reading Lists for {SeriesName}", series.Name);
|
|
||||||
|
|
||||||
series.Metadata ??= new SeriesMetadataBuilder().Build();
|
|
||||||
foreach (var chapter in series.Volumes.SelectMany(v => v.Chapters))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(chapter.StoryArc))
|
|
||||||
{
|
|
||||||
var readingLists = chapter.StoryArc.Split(',');
|
|
||||||
var readingListOrders = chapter.StoryArcNumber.Split(',');
|
|
||||||
if (readingListOrders.Length == 0)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("[ScannerService] There are no StoryArc orders listed, all reading lists fueled from StoryArc will be unordered");
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateSeriesFolderPath(IEnumerable<ParserInfo> parsedInfos, Library library, Series series)
|
private async Task UpdateSeriesFolderPath(IEnumerable<ParserInfo> parsedInfos, Library library, Series series)
|
||||||
{
|
{
|
||||||
@ -517,13 +500,10 @@ public class ProcessSeries : IProcessSeries
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (ex.Message.Equals("Sequence contains more than one matching element"))
|
if (!ex.Message.Equals("Sequence contains more than one matching element")) throw;
|
||||||
{
|
_logger.LogCritical("[ScannerService] Kavita found corrupted volume entries on {SeriesName}. Please delete the series from Kavita via UI and rescan", series.Name);
|
||||||
_logger.LogCritical("[ScannerService] Kavita found corrupted volume entries on {SeriesName}. Please delete the series from Kavita via UI and rescan", series.Name);
|
throw new KavitaException(
|
||||||
throw new KavitaException(
|
$"Kavita found corrupted volume entries on {series.Name}. Please delete the series from Kavita via UI and rescan");
|
||||||
$"Kavita found corrupted volume entries on {series.Name}. Please delete the series from Kavita via UI and rescan");
|
|
||||||
}
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
if (volume == null)
|
if (volume == null)
|
||||||
{
|
{
|
||||||
|
@ -11837,7 +11837,11 @@
|
|||||||
},
|
},
|
||||||
"manageCollections": {
|
"manageCollections": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Should this library create and manage collections from Metadata"
|
"description": "Should this library create collections from Metadata"
|
||||||
|
},
|
||||||
|
"manageReadingLists": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Should this library create reading lists from Metadata"
|
||||||
},
|
},
|
||||||
"created": {
|
"created": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user