Reading List Detail Overhaul + More Bugfixes and Polish (#3687)
Co-authored-by: Yongun Seong <yseong.p@gmail.com>
This commit is contained in:
parent
b2ee651fb8
commit
dad212bfb9
71 changed files with 5056 additions and 729 deletions
|
|
@ -213,7 +213,6 @@ public class LibraryController : BaseApiController
|
|||
|
||||
var ret = _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username);
|
||||
await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24));
|
||||
_logger.LogDebug("Caching libraries for {Key}", cacheKey);
|
||||
|
||||
return Ok(ret);
|
||||
}
|
||||
|
|
@ -419,8 +418,7 @@ public class LibraryController : BaseApiController
|
|||
.Distinct()
|
||||
.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
|
||||
|
||||
var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder,
|
||||
new List<string>() {dto.FolderPath});
|
||||
var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder, [dto.FolderPath]);
|
||||
|
||||
_taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath);
|
||||
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ public class LocaleController : BaseApiController
|
|||
}
|
||||
|
||||
var ret = _localizationService.GetLocales().Where(l => l.TranslationCompletion > 0f);
|
||||
await _localeCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromDays(7));
|
||||
await _localeCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromDays(1));
|
||||
|
||||
return Ok();
|
||||
return Ok(ret);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -803,7 +803,7 @@ public class ReaderController : BaseApiController
|
|||
/// <param name="seriesId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("time-left")]
|
||||
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = ["seriesId"])]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId"])]
|
||||
public async Task<ActionResult<HourEstimateRangeDto>> GetEstimateToCompletion(int seriesId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using API.Data;
|
|||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services;
|
||||
|
|
@ -23,13 +24,15 @@ public class ReadingListController : BaseApiController
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IReadingListService _readingListService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IReaderService _readerService;
|
||||
|
||||
public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService,
|
||||
ILocalizationService localizationService)
|
||||
ILocalizationService localizationService, IReaderService readerService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_readingListService = readingListService;
|
||||
_localizationService = localizationService;
|
||||
_readerService = readerService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -128,7 +131,7 @@ public class ReadingListController : BaseApiController
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a list item from the list. Will reorder all item positions afterwards
|
||||
/// Deletes a list item from the list. Item orders will update as a result.
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
|
|
@ -452,26 +455,38 @@ public class ReadingListController : BaseApiController
|
|||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of characters associated with the reading list
|
||||
/// Returns a list of a given role associated with the reading list
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <param name="role">PersonRole</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("people")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId", "role"])]
|
||||
public ActionResult<IEnumerable<PersonDto>> GetPeopleByRoleForList(int readingListId, PersonRole role)
|
||||
{
|
||||
return Ok(_unitOfWork.ReadingListRepository.GetReadingListPeopleAsync(readingListId, role));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all people in given roles for a reading list
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("characters")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute)]
|
||||
public ActionResult<IEnumerable<PersonDto>> GetCharactersForList(int readingListId)
|
||||
[HttpGet("all-people")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId"])]
|
||||
public async Task<ActionResult<IEnumerable<PersonDto>>> GetAllPeopleForList(int readingListId)
|
||||
{
|
||||
return Ok(_unitOfWork.ReadingListRepository.GetReadingListCharactersAsync(readingListId));
|
||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListAllPeopleAsync(readingListId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next chapter within the reading list
|
||||
/// </summary>
|
||||
/// <param name="currentChapterId"></param>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
|
||||
/// <returns>Chapter ID for next item, -1 if nothing exists</returns>
|
||||
[HttpGet("next-chapter")]
|
||||
public async Task<ActionResult<int>> GetNextChapter(int currentChapterId, int readingListId)
|
||||
{
|
||||
|
|
@ -577,4 +592,26 @@ public class ReadingListController : BaseApiController
|
|||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns random information about a Reading List
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("info")]
|
||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["readingListId"])]
|
||||
public async Task<ActionResult<ReadingListInfoDto?>> GetReadingListInfo(int readingListId)
|
||||
{
|
||||
var result = await _unitOfWork.ReadingListRepository.GetReadingListInfoAsync(readingListId);
|
||||
|
||||
if (result == null) return Ok(null);
|
||||
|
||||
var timeEstimate = _readerService.GetTimeEstimate(result.WordCount, result.Pages, result.IsAllEpub);
|
||||
|
||||
result.MinHoursToRead = timeEstimate.MinHours;
|
||||
result.AvgHoursToRead = timeEstimate.AvgHours;
|
||||
result.MaxHoursToRead = timeEstimate.MaxHours;
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,8 @@ public class SeriesController : BaseApiController
|
|||
// Trigger a refresh when we are moving from a locked image to a non-locked
|
||||
needsRefreshMetadata = true;
|
||||
series.CoverImage = null;
|
||||
series.CoverImageLocked = updateSeries.CoverImageLocked;
|
||||
series.CoverImageLocked = false;
|
||||
_logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id);
|
||||
series.ResetColorScape();
|
||||
|
||||
}
|
||||
|
|
|
|||
20
API/DTOs/ReadingLists/ReadingListCast.cs
Normal file
20
API/DTOs/ReadingLists/ReadingListCast.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs.ReadingLists;
|
||||
|
||||
public class ReadingListCast
|
||||
{
|
||||
public ICollection<PersonDto> Writers { get; set; } = [];
|
||||
public ICollection<PersonDto> CoverArtists { get; set; } = [];
|
||||
public ICollection<PersonDto> Publishers { get; set; } = [];
|
||||
public ICollection<PersonDto> Characters { get; set; } = [];
|
||||
public ICollection<PersonDto> Pencillers { get; set; } = [];
|
||||
public ICollection<PersonDto> Inkers { get; set; } = [];
|
||||
public ICollection<PersonDto> Imprints { get; set; } = [];
|
||||
public ICollection<PersonDto> Colorists { get; set; } = [];
|
||||
public ICollection<PersonDto> Letterers { get; set; } = [];
|
||||
public ICollection<PersonDto> Editors { get; set; } = [];
|
||||
public ICollection<PersonDto> Translators { get; set; } = [];
|
||||
public ICollection<PersonDto> Teams { get; set; } = [];
|
||||
public ICollection<PersonDto> Locations { get; set; } = [];
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Interfaces;
|
||||
|
||||
namespace API.DTOs.ReadingLists;
|
||||
|
|
@ -43,6 +44,10 @@ public class ReadingListDto : IHasCoverImage
|
|||
/// Maximum Month the Reading List starts
|
||||
/// </summary>
|
||||
public int EndingMonth { get; set; }
|
||||
/// <summary>
|
||||
/// The highest age rating from all Series within the reading list
|
||||
/// </summary>
|
||||
public required AgeRating AgeRating { get; set; } = AgeRating.Unknown;
|
||||
|
||||
public void ResetColorScape()
|
||||
{
|
||||
|
|
|
|||
26
API/DTOs/ReadingLists/ReadingListInfoDto.cs
Normal file
26
API/DTOs/ReadingLists/ReadingListInfoDto.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using API.DTOs.Reader;
|
||||
using API.Entities.Interfaces;
|
||||
|
||||
namespace API.DTOs.ReadingLists;
|
||||
|
||||
public class ReadingListInfoDto : IHasReadTimeEstimate
|
||||
{
|
||||
/// <summary>
|
||||
/// Total Pages across all Reading List Items
|
||||
/// </summary>
|
||||
public int Pages { get; set; }
|
||||
/// <summary>
|
||||
/// Total Word count across all Reading List Items
|
||||
/// </summary>
|
||||
public long WordCount { get; set; }
|
||||
/// <summary>
|
||||
/// Are ALL Reading List Items epub
|
||||
/// </summary>
|
||||
public bool IsAllEpub { get; set; }
|
||||
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
|
||||
public int MinHoursToRead { get; set; }
|
||||
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
|
||||
public int MaxHoursToRead { get; set; }
|
||||
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
|
||||
public float AvgHoursToRead { get; set; }
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ public class ReadingListItemDto
|
|||
/// <summary>
|
||||
/// Release Date from Chapter
|
||||
/// </summary>
|
||||
public DateTime ReleaseDate { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
/// <summary>
|
||||
/// Used internally only
|
||||
/// </summary>
|
||||
|
|
@ -33,7 +33,7 @@ public class ReadingListItemDto
|
|||
/// <summary>
|
||||
/// The last time a reading list item (underlying chapter) was read by current authenticated user
|
||||
/// </summary>
|
||||
public DateTime LastReadingProgressUtc { get; set; }
|
||||
public DateTime? LastReadingProgressUtc { get; set; }
|
||||
/// <summary>
|
||||
/// File size of underlying item
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,13 @@ public class UserPreferencesDto
|
|||
/// </summary>
|
||||
[Required]
|
||||
public bool ShowScreenHints { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Allow Automatic Webtoon detection
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool AllowAutomaticWebtoonReaderDetection { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Book Reader Option: Override extra Margin
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -133,6 +133,9 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.WantToReadSync)
|
||||
.HasDefaultValue(true);
|
||||
builder.Entity<AppUserPreferences>()
|
||||
.Property(b => b.AllowAutomaticWebtoonReaderDetection)
|
||||
.HasDefaultValue(true);
|
||||
|
||||
builder.Entity<Library>()
|
||||
.Property(b => b.AllowScrobbling)
|
||||
|
|
|
|||
3403
API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.Designer.cs
generated
Normal file
3403
API/Data/Migrations/20250328125012_AutomaticWebtoonReaderMode.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 AutomaticWebtoonReaderMode : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowAutomaticWebtoonReaderDetection",
|
||||
table: "AppUserPreferences",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowAutomaticWebtoonReaderDetection",
|
||||
table: "AppUserPreferences");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
|
|
@ -353,6 +353,11 @@ namespace API.Data.Migrations
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AllowAutomaticWebtoonReaderDetection")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("AniListScrobblingEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
|
|
@ -911,24 +916,6 @@ namespace API.Data.Migrations
|
|||
b.ToTable("Chapter");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ChapterPeople", b =>
|
||||
{
|
||||
b.Property<int>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PersonId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ChapterId", "PersonId", "Role");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("ChapterPeople");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.CollectionTag", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -1640,7 +1627,7 @@ namespace API.Data.Migrations
|
|||
b.ToTable("MetadataFieldMapping");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MetadataSettings", b =>
|
||||
modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
|
|
@ -1703,7 +1690,25 @@ namespace API.Data.Migrations
|
|||
b.ToTable("MetadataSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person", b =>
|
||||
modelBuilder.Entity("API.Entities.Person.ChapterPeople", b =>
|
||||
{
|
||||
b.Property<int>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PersonId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ChapterId", "PersonId", "Role");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("ChapterPeople");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person.Person", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
|
|
@ -1747,6 +1752,32 @@ namespace API.Data.Migrations
|
|||
b.ToTable("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b =>
|
||||
{
|
||||
b.Property<int>("SeriesMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PersonId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("KavitaPlusConnection")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("OrderWeight")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.HasKey("SeriesMetadataId", "PersonId", "Role");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("SeriesMetadataPeople");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
|
@ -2111,32 +2142,6 @@ namespace API.Data.Migrations
|
|||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b =>
|
||||
{
|
||||
b.Property<int>("SeriesMetadataId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PersonId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("KavitaPlusConnection")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("OrderWeight")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.HasKey("SeriesMetadataId", "PersonId", "Role");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("SeriesMetadataPeople");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ServerSetting", b =>
|
||||
{
|
||||
b.Property<int>("Key")
|
||||
|
|
@ -2804,25 +2809,6 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Volume");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ChapterPeople", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||
.WithMany("People")
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Person", "Person")
|
||||
.WithMany("ChapterPeople")
|
||||
.HasForeignKey("PersonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Chapter");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Device", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
|
|
@ -2943,7 +2929,7 @@ namespace API.Data.Migrations
|
|||
|
||||
modelBuilder.Entity("API.Entities.MetadataFieldMapping", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.MetadataSettings", "MetadataSettings")
|
||||
b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings")
|
||||
.WithMany("FieldMappings")
|
||||
.HasForeignKey("MetadataSettingsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
|
|
@ -2952,6 +2938,44 @@ namespace API.Data.Migrations
|
|||
b.Navigation("MetadataSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person.ChapterPeople", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||
.WithMany("People")
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Person.Person", "Person")
|
||||
.WithMany("ChapterPeople")
|
||||
.HasForeignKey("PersonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Chapter");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Person.Person", "Person")
|
||||
.WithMany("SeriesMetadataPeople")
|
||||
.HasForeignKey("PersonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata")
|
||||
.WithMany("People")
|
||||
.HasForeignKey("SeriesMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Person");
|
||||
|
||||
b.Navigation("SeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.ReadingList", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
|
|
@ -3072,25 +3096,6 @@ namespace API.Data.Migrations
|
|||
b.Navigation("Library");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.SeriesMetadataPeople", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Person", "Person")
|
||||
.WithMany("SeriesMetadataPeople")
|
||||
.HasForeignKey("PersonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata")
|
||||
.WithMany("People")
|
||||
.HasForeignKey("SeriesMetadataId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Person");
|
||||
|
||||
b.Navigation("SeriesMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Volume", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
|
|
@ -3351,12 +3356,12 @@ namespace API.Data.Migrations
|
|||
b.Navigation("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MetadataSettings", b =>
|
||||
modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b =>
|
||||
{
|
||||
b.Navigation("FieldMappings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Person", b =>
|
||||
modelBuilder.Entity("API.Entities.Person.Person", b =>
|
||||
{
|
||||
b.Navigation("ChapterPeople");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
using System.Collections;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Person;
|
||||
using API.Extensions;
|
||||
|
|
@ -31,15 +28,13 @@ public interface IPersonRepository
|
|||
Task<IList<PersonDto>> GetAllPersonDtosByRoleAsync(int userId, PersonRole role);
|
||||
Task RemoveAllPeopleNoLongerAssociated();
|
||||
Task<IList<PersonDto>> GetAllPeopleDtosForLibrariesAsync(int userId, List<int>? libraryIds = null);
|
||||
Task<int> GetCountAsync();
|
||||
|
||||
Task<string> GetCoverImageAsync(int personId);
|
||||
Task<string?> GetCoverImageAsync(int personId);
|
||||
Task<string?> GetCoverImageByNameAsync(string name);
|
||||
Task<IEnumerable<PersonRole>> GetRolesForPersonByName(int personId, int userId);
|
||||
Task<PagedList<BrowsePersonDto>> GetAllWritersAndSeriesCount(int userId, UserParams userParams);
|
||||
Task<Person?> GetPersonById(int personId);
|
||||
Task<PersonDto?> GetPersonDtoByName(string name, int userId);
|
||||
Task<Person> GetPersonByName(string name);
|
||||
Task<bool> IsNameUnique(string name);
|
||||
|
||||
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
|
||||
|
|
@ -126,12 +121,8 @@ public class PersonRepository : IPersonRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetCountAsync()
|
||||
{
|
||||
return await _context.Person.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetCoverImageAsync(int personId)
|
||||
public async Task<string?> GetCoverImageAsync(int personId)
|
||||
{
|
||||
return await _context.Person
|
||||
.Where(c => c.Id == personId)
|
||||
|
|
@ -139,7 +130,7 @@ public class PersonRepository : IPersonRepository
|
|||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<string> GetCoverImageByNameAsync(string name)
|
||||
public async Task<string?> GetCoverImageByNameAsync(string name)
|
||||
{
|
||||
var normalized = name.ToNormalized();
|
||||
return await _context.Person
|
||||
|
|
@ -208,7 +199,7 @@ public class PersonRepository : IPersonRepository
|
|||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<PersonDto> GetPersonDtoByName(string name, int userId)
|
||||
public async Task<PersonDto?> GetPersonDtoByName(string name, int userId)
|
||||
{
|
||||
var normalized = name.ToNormalized();
|
||||
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||
|
|
@ -220,11 +211,6 @@ public class PersonRepository : IPersonRepository
|
|||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<Person> GetPersonByName(string name)
|
||||
{
|
||||
return await _context.Person.FirstOrDefaultAsync(p => p.NormalizedName == name.ToNormalized());
|
||||
}
|
||||
|
||||
public async Task<bool> IsNameUnique(string name)
|
||||
{
|
||||
return !(await _context.Person.AnyAsync(p => p.Name == name));
|
||||
|
|
|
|||
|
|
@ -49,12 +49,14 @@ public interface IReadingListRepository
|
|||
Task<IList<string>> GetRandomCoverImagesAsync(int readingListId);
|
||||
Task<IList<string>> GetAllCoverImagesAsync();
|
||||
Task<bool> ReadingListExists(string name);
|
||||
IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId);
|
||||
IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role);
|
||||
Task<ReadingListCast> GetReadingListAllPeopleAsync(int readingListId);
|
||||
Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
|
||||
Task<int> RemoveReadingListsWithoutSeries();
|
||||
Task<ReadingList?> GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items);
|
||||
Task<IEnumerable<ReadingList>> GetReadingListsByIds(IList<int> ids, ReadingListIncludes includes = ReadingListIncludes.Items);
|
||||
Task<IEnumerable<ReadingList>> GetReadingListsBySeriesId(int seriesId, ReadingListIncludes includes = ReadingListIncludes.Items);
|
||||
Task<ReadingListInfoDto?> GetReadingListInfoAsync(int readingListId);
|
||||
}
|
||||
|
||||
public class ReadingListRepository : IReadingListRepository
|
||||
|
|
@ -121,12 +123,12 @@ public class ReadingListRepository : IReadingListRepository
|
|||
.AnyAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized));
|
||||
}
|
||||
|
||||
public IEnumerable<PersonDto> GetReadingListCharactersAsync(int readingListId)
|
||||
public IEnumerable<PersonDto> GetReadingListPeopleAsync(int readingListId, PersonRole role)
|
||||
{
|
||||
return _context.ReadingListItem
|
||||
.Where(item => item.ReadingListId == readingListId)
|
||||
.SelectMany(item => item.Chapter.People)
|
||||
.Where(p => p.Role == PersonRole.Character)
|
||||
.Where(p => p.Role == role)
|
||||
.OrderBy(p => p.Person.NormalizedName)
|
||||
.Select(p => p.Person)
|
||||
.Distinct()
|
||||
|
|
@ -134,6 +136,77 @@ public class ReadingListRepository : IReadingListRepository
|
|||
.AsEnumerable();
|
||||
}
|
||||
|
||||
public async Task<ReadingListCast> GetReadingListAllPeopleAsync(int readingListId)
|
||||
{
|
||||
var allPeople = await _context.ReadingListItem
|
||||
.Where(item => item.ReadingListId == readingListId)
|
||||
.SelectMany(item => item.Chapter.People)
|
||||
.OrderBy(p => p.Person.NormalizedName)
|
||||
.Select(p => new
|
||||
{
|
||||
Role = p.Role,
|
||||
Person = _mapper.Map<PersonDto>(p.Person)
|
||||
})
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
// Create the ReadingListCast object
|
||||
var cast = new ReadingListCast();
|
||||
|
||||
// Group people by role and populate the appropriate collections
|
||||
foreach (var personGroup in allPeople.GroupBy(p => p.Role))
|
||||
{
|
||||
var people = personGroup.Select(pg => pg.Person).ToList();
|
||||
|
||||
switch (personGroup.Key)
|
||||
{
|
||||
case PersonRole.Writer:
|
||||
cast.Writers = people;
|
||||
break;
|
||||
case PersonRole.CoverArtist:
|
||||
cast.CoverArtists = people;
|
||||
break;
|
||||
case PersonRole.Publisher:
|
||||
cast.Publishers = people;
|
||||
break;
|
||||
case PersonRole.Character:
|
||||
cast.Characters = people;
|
||||
break;
|
||||
case PersonRole.Penciller:
|
||||
cast.Pencillers = people;
|
||||
break;
|
||||
case PersonRole.Inker:
|
||||
cast.Inkers = people;
|
||||
break;
|
||||
case PersonRole.Imprint:
|
||||
cast.Imprints = people;
|
||||
break;
|
||||
case PersonRole.Colorist:
|
||||
cast.Colorists = people;
|
||||
break;
|
||||
case PersonRole.Letterer:
|
||||
cast.Letterers = people;
|
||||
break;
|
||||
case PersonRole.Editor:
|
||||
cast.Editors = people;
|
||||
break;
|
||||
case PersonRole.Translator:
|
||||
cast.Translators = people;
|
||||
break;
|
||||
case PersonRole.Team:
|
||||
cast.Teams = people;
|
||||
break;
|
||||
case PersonRole.Location:
|
||||
cast.Locations = people;
|
||||
break;
|
||||
case PersonRole.Other:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return cast;
|
||||
}
|
||||
|
||||
public async Task<IList<ReadingList>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat)
|
||||
{
|
||||
var extension = encodeFormat.GetExtension();
|
||||
|
|
@ -181,6 +254,33 @@ public class ReadingListRepository : IReadingListRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Partial ReadingListInfoDto. The HourEstimate needs to be calculated outside the repo
|
||||
/// </summary>
|
||||
/// <param name="readingListId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<ReadingListInfoDto?> GetReadingListInfoAsync(int readingListId)
|
||||
{
|
||||
// Get sum of these across all ReadingListItems: long wordCount, int pageCount, bool isEpub (assume false if any ReadingListeItem.Series.Format is non-epub)
|
||||
var readingList = await _context.ReadingList
|
||||
.Where(rl => rl.Id == readingListId)
|
||||
.Include(rl => rl.Items)
|
||||
.ThenInclude(item => item.Series)
|
||||
.Include(rl => rl.Items)
|
||||
.ThenInclude(item => item.Volume)
|
||||
.Include(rl => rl.Items)
|
||||
.ThenInclude(item => item.Chapter)
|
||||
.Select(rl => new ReadingListInfoDto()
|
||||
{
|
||||
WordCount = rl.Items.Sum(item => item.Chapter.WordCount),
|
||||
Pages = rl.Items.Sum(item => item.Chapter.Pages),
|
||||
IsAllEpub = rl.Items.All(item => item.Series.Format == MangaFormat.Epub),
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return readingList;
|
||||
}
|
||||
|
||||
|
||||
public void Remove(ReadingListItem item)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -167,11 +167,11 @@ public class ScrobbleRepository : IScrobbleRepository
|
|||
var query = _context.ScrobbleEvent
|
||||
.Where(e => e.AppUserId == userId)
|
||||
.Include(e => e.Series)
|
||||
.SortBy(filter.Field, filter.IsDescending)
|
||||
.WhereIf(!string.IsNullOrEmpty(filter.Query), s =>
|
||||
EF.Functions.Like(s.Series.Name, $"%{filter.Query}%")
|
||||
)
|
||||
.WhereIf(!filter.IncludeReviews, e => e.ScrobbleEventType != ScrobbleEventType.Review)
|
||||
.SortBy(filter.Field, filter.IsDescending)
|
||||
.AsSplitQuery()
|
||||
.ProjectTo<ScrobbleEventDto>(_mapper.ConfigurationProvider);
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ public class AppUserPreferences
|
|||
/// Manga Reader Option: Should swiping trigger pagination
|
||||
/// </summary>
|
||||
public bool SwipeToPaginate { get; set; }
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Allow Automatic Webtoon detection
|
||||
/// </summary>
|
||||
public bool AllowAutomaticWebtoonReaderDetection { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -7,5 +7,6 @@ public enum ScrobbleEventSortField
|
|||
LastModified = 2,
|
||||
Type= 3,
|
||||
Series = 4,
|
||||
IsProcessed = 5
|
||||
IsProcessed = 5,
|
||||
ScrobbleEventFilter = 6
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,6 +255,7 @@ public static class QueryableExtensions
|
|||
ScrobbleEventSortField.Type => query.OrderByDescending(s => s.ScrobbleEventType),
|
||||
ScrobbleEventSortField.Series => query.OrderByDescending(s => s.Series.NormalizedName),
|
||||
ScrobbleEventSortField.IsProcessed => query.OrderByDescending(s => s.IsProcessed),
|
||||
ScrobbleEventSortField.ScrobbleEventFilter => query.OrderByDescending(s => s.ScrobbleEventType),
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
|
|
@ -267,6 +268,7 @@ public static class QueryableExtensions
|
|||
ScrobbleEventSortField.Type => query.OrderBy(s => s.ScrobbleEventType),
|
||||
ScrobbleEventSortField.Series => query.OrderBy(s => s.Series.NormalizedName),
|
||||
ScrobbleEventSortField.IsProcessed => query.OrderBy(s => s.IsProcessed),
|
||||
ScrobbleEventSortField.ScrobbleEventFilter => query.OrderBy(s => s.ScrobbleEventType),
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ public static partial class StringHelper
|
|||
private static partial Regex BrMultipleRegex();
|
||||
[GeneratedRegex(@"\s+")]
|
||||
private static partial Regex WhiteSpaceRegex();
|
||||
[GeneratedRegex("&#64;")]
|
||||
private static partial Regex HtmlEncodedAtSymbolRegex();
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -52,4 +54,16 @@ public static partial class StringHelper
|
|||
|
||||
return SourceRegex().Replace(description, string.Empty).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces some HTML encoded characters in urls with the proper symbol. This is common in People Description's
|
||||
/// </summary>
|
||||
/// <param name="description"></param>
|
||||
/// <returns></returns>
|
||||
public static string? CorrectUrls(string? description)
|
||||
{
|
||||
if (string.IsNullOrEmpty(description)) return description;
|
||||
|
||||
return HtmlEncodedAtSymbolRegex().Replace(description, "@");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -222,6 +222,10 @@ public class MediaConversionService : IMediaConversionService
|
|||
{
|
||||
if (string.IsNullOrEmpty(series.CoverImage)) continue;
|
||||
series.CoverImage = series.GetCoverImage();
|
||||
if (series.CoverImage == null)
|
||||
{
|
||||
_logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id);
|
||||
}
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,6 +199,10 @@ public class MetadataService : IMetadataService
|
|||
|
||||
series.Volumes ??= [];
|
||||
series.CoverImage = series.GetCoverImage();
|
||||
if (series.CoverImage == null)
|
||||
{
|
||||
_logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id);
|
||||
}
|
||||
|
||||
_imageService.UpdateColorScape(series);
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
};
|
||||
// Allow 50 requests per 24 hours
|
||||
private static readonly RateLimiter RateLimiter = new RateLimiter(50, TimeSpan.FromHours(24), false);
|
||||
static bool IsRomanCharacters(string input) => Regex.IsMatch(input, @"^[\p{IsBasicLatin}\p{IsLatin-1Supplement}]+$");
|
||||
private static bool IsRomanCharacters(string input) => Regex.IsMatch(input, @"^[\p{IsBasicLatin}\p{IsLatin-1Supplement}]+$");
|
||||
|
||||
public ExternalMetadataService(IUnitOfWork unitOfWork, ILogger<ExternalMetadataService> logger, IMapper mapper,
|
||||
ILicenseService licenseService, IScrobblingService scrobblingService, IEventHub eventHub, ICoverDbService coverDbService)
|
||||
|
|
@ -115,18 +115,24 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
// Find all Series that are eligible and limit
|
||||
var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, false);
|
||||
if (ids.Count == 0) return;
|
||||
ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, true);
|
||||
|
||||
_logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+", ids.Count);
|
||||
_logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+: {Ids}", ids.Count, string.Join(',', ids));
|
||||
var count = 0;
|
||||
var successfulMatches = new List<int>();
|
||||
var libTypes = await _unitOfWork.LibraryRepository.GetLibraryTypesBySeriesIdsAsync(ids);
|
||||
foreach (var seriesId in ids)
|
||||
{
|
||||
var libraryType = libTypes[seriesId];
|
||||
var success = await FetchSeriesMetadata(seriesId, libraryType);
|
||||
if (success) count++;
|
||||
if (success)
|
||||
{
|
||||
count++;
|
||||
successfulMatches.Add(seriesId);
|
||||
}
|
||||
await Task.Delay(6000); // Currently AL is degraded and has 30 requests/min, give a little padding since this is a background request
|
||||
}
|
||||
_logger.LogInformation("[Kavita+ Data Refresh] Finished Refreshing {Count} series data from Kavita+", count);
|
||||
_logger.LogInformation("[Kavita+ Data Refresh] Finished Refreshing {Count} / {Total} series data from Kavita+: {Ids}", count, ids.Count, string.Join(',', successfulMatches));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -146,7 +152,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
if (!RateLimiter.TryAcquire(string.Empty))
|
||||
{
|
||||
// Request not allowed due to rate limit
|
||||
_logger.LogDebug("Rate Limit hit for Kavita+ prefetch");
|
||||
_logger.LogInformation("Rate Limit hit for Kavita+ prefetch");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -731,7 +737,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
{
|
||||
Name = w.Name,
|
||||
AniListId = ScrobblingService.ExtractId<int>(w.Url, ScrobblingService.AniListCharacterWebsite),
|
||||
Description = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description)),
|
||||
Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))),
|
||||
})
|
||||
.Concat(series.Metadata.People
|
||||
.Where(p => p.Role == PersonRole.Character)
|
||||
|
|
@ -743,7 +749,9 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
.ToList();
|
||||
|
||||
if (characters.Count == 0) return false;
|
||||
|
||||
await SeriesService.HandlePeopleUpdateAsync(series.Metadata, characters, PersonRole.Character, _unitOfWork);
|
||||
|
||||
foreach (var spPerson in series.Metadata.People.Where(p => p.Role == PersonRole.Character))
|
||||
{
|
||||
// Set a sort order based on their role
|
||||
|
|
@ -810,7 +818,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
{
|
||||
Name = w.Name,
|
||||
AniListId = ScrobblingService.ExtractId<int>(w.Url, ScrobblingService.AniListStaffWebsite),
|
||||
Description = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description)),
|
||||
Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))),
|
||||
})
|
||||
.Concat(series.Metadata.People
|
||||
.Where(p => p.Role == PersonRole.CoverArtist)
|
||||
|
|
@ -867,7 +875,7 @@ public class ExternalMetadataService : IExternalMetadataService
|
|||
{
|
||||
Name = w.Name,
|
||||
AniListId = ScrobblingService.ExtractId<int>(w.Url, ScrobblingService.AniListStaffWebsite),
|
||||
Description = StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description)),
|
||||
Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))),
|
||||
})
|
||||
.Concat(series.Metadata.People
|
||||
.Where(p => p.Role == PersonRole.Writer)
|
||||
|
|
|
|||
|
|
@ -552,14 +552,22 @@ public class CoverDbService : ICoverDbService
|
|||
|
||||
series.CoverImage = filePath;
|
||||
series.CoverImageLocked = true;
|
||||
if (series.CoverImage == null)
|
||||
{
|
||||
_logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null");
|
||||
}
|
||||
_imageService.UpdateColorScape(series);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
series.CoverImage = string.Empty;
|
||||
series.CoverImage = null;
|
||||
series.CoverImageLocked = false;
|
||||
if (series.CoverImage == null)
|
||||
{
|
||||
_logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null");
|
||||
}
|
||||
_imageService.UpdateColorScape(series);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -278,7 +278,8 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
{
|
||||
// Attempt to fetch from cache
|
||||
var cachedReleases = await TryGetCachedReleases();
|
||||
if (cachedReleases != null)
|
||||
// If there is a cached release and the current version is within it, use it, otherwise regenerate
|
||||
if (cachedReleases != null && cachedReleases.Any(r => IsVersionEqual(r.UpdateVersion, BuildInfo.Version.ToString())))
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
|
|
@ -338,6 +339,29 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
return updateDtos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares 2 versions and ensures that the minor is always there
|
||||
/// </summary>
|
||||
/// <param name="v1"></param>
|
||||
/// <param name="v2"></param>
|
||||
/// <returns></returns>
|
||||
private static bool IsVersionEqual(string v1, string v2)
|
||||
{
|
||||
var versionParts = v1.Split('.');
|
||||
if (versionParts.Length < 4)
|
||||
{
|
||||
v1 += ".0"; // Append missing parts
|
||||
}
|
||||
|
||||
versionParts = v2.Split('.');
|
||||
if (versionParts.Length < 4)
|
||||
{
|
||||
v2 += ".0"; // Append missing parts
|
||||
}
|
||||
|
||||
return string.Equals(v2, v2, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task<IList<UpdateNotificationDto>?> TryGetCachedReleases()
|
||||
{
|
||||
if (!File.Exists(_cacheFilePath)) return null;
|
||||
|
|
@ -370,7 +394,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService
|
|||
{
|
||||
try
|
||||
{
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(updates, JsonOptions);
|
||||
var json = JsonSerializer.Serialize(updates, JsonOptions);
|
||||
await File.WriteAllTextAsync(_cacheFilePath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue