diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index a56794992..8c0e81b03 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -631,4 +631,50 @@ public class ScannerServiceTests : AbstractDbTest Assert.Contains(postLib.Series, s => s.Name == "Plush"); } + [Fact] + public async Task ScanLibrary_DeleteSeriesInUI_ComeBack() + { + const string testcase = "Delete Series In UI - Manga.json"; + + // Setup: Generate test library + var infos = new Dictionary(); + var library = await _scannerHelper.GenerateScannerData(testcase, infos); + + var testDirectoryPath = Path.Combine(Directory.GetCurrentDirectory(), + "../../../Services/Test Data/ScannerService/ScanTests", + testcase.Replace(".json", string.Empty)); + + library.Folders = + [ + new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 1") }, + new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 2") } + ]; + + _unitOfWork.LibraryRepository.Update(library); + await _unitOfWork.CommitAsync(); + + var scanner = _scannerHelper.CreateServices(); + + // First Scan: Everything should be added + await scanner.ScanLibrary(library.Id); + var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + + Assert.NotNull(postLib); + Assert.Contains(postLib.Series, s => s.Name == "Accel"); + Assert.Contains(postLib.Series, s => s.Name == "Plush"); + + // Second Scan: Delete the Series + library.Series = []; + await _unitOfWork.CommitAsync(); + + postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + Assert.NotNull(postLib); + Assert.Empty(postLib.Series); + + await scanner.ScanLibrary(library.Id); + postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + + Assert.Contains(postLib.Series, s => s.Name == "Accel"); // Ensure Accel is gone + Assert.Contains(postLib.Series, s => s.Name == "Plush"); + } } diff --git a/API.Tests/Services/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json b/API.Tests/Services/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json new file mode 100644 index 000000000..791dcdc44 --- /dev/null +++ b/API.Tests/Services/Test Data/ScannerService/TestCases/Delete Series In UI - Manga.json @@ -0,0 +1,5 @@ +[ + "Root 1/Antarctic Press/Plush/Plush v01.cbz", + "Root 1/Antarctic Press/Plush/Plush v02.cbz", + "Root 2/Accel/Accel v01.cbz" +] diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index fbf0dea89..d3d6b8375 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -646,6 +646,7 @@ public class LibraryController : BaseApiController library.ManageCollections = dto.ManageCollections; library.ManageReadingLists = dto.ManageReadingLists; library.AllowScrobbling = dto.AllowScrobbling; + library.AllowMetadataMatching = dto.AllowMetadataMatching; library.LibraryFileTypes = dto.FileGroupTypes .Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id}) .Distinct() diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index 7a406fe48..c14f4409a 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -28,7 +28,7 @@ public interface IMetadataService [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false, bool forceColorScape = false); /// - /// Performs a forced refresh of cover images just for a series and it's nested entities + /// Performs a forced refresh of cover images just for a series, and it's nested entities /// /// /// @@ -75,12 +75,12 @@ public class MetadataService : IMetadataService /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image /// Convert image to Encoding Format when extracting the cover /// Force colorscape gen - private Task UpdateChapterCoverImage(Chapter? chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceColorScape = false) + private bool UpdateChapterCoverImage(Chapter? chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceColorScape = false) { - if (chapter == null) return Task.FromResult(false); + if (chapter == null) return false; var firstFile = chapter.Files.MinBy(x => x.Chapter); - if (firstFile == null) return Task.FromResult(false); + if (firstFile == null) return false; if (!_cacheHelper.ShouldUpdateCoverImage( _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), @@ -93,7 +93,7 @@ public class MetadataService : IMetadataService _updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter)); } - return Task.FromResult(false); + return false; } @@ -107,7 +107,7 @@ public class MetadataService : IMetadataService _unitOfWork.ChapterRepository.Update(chapter); _updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter)); - return Task.FromResult(true); + return true; } private void UpdateChapterLastModified(Chapter chapter, bool forceUpdate) @@ -135,10 +135,10 @@ public class MetadataService : IMetadataService /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image /// Force updating colorscape - private Task UpdateVolumeCoverImage(Volume? volume, bool forceUpdate, bool forceColorScape = false) + private bool UpdateVolumeCoverImage(Volume? volume, bool forceUpdate, bool forceColorScape = false) { // We need to check if Volume coverImage matches first chapters if forceUpdate is false - if (volume == null) return Task.FromResult(false); + if (volume == null) return false; if (!_cacheHelper.ShouldUpdateCoverImage( _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, volume.CoverImage), @@ -150,7 +150,7 @@ public class MetadataService : IMetadataService _unitOfWork.VolumeRepository.Update(volume); _updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume)); } - return Task.FromResult(false); + return false; } if (!volume.CoverImageLocked) @@ -162,7 +162,7 @@ public class MetadataService : IMetadataService if (firstChapter == null) { firstChapter = volume.Chapters.MinBy(x => x.SortOrder, ChapterSortComparerDefaultFirst.Default); - if (firstChapter == null) return Task.FromResult(false); + if (firstChapter == null) return false; } volume.CoverImage = firstChapter.CoverImage; @@ -171,7 +171,7 @@ public class MetadataService : IMetadataService _updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume)); - return Task.FromResult(true); + return true; } /// @@ -179,9 +179,9 @@ public class MetadataService : IMetadataService /// /// /// Force updating cover image even if underlying file has not been modified or chapter already has a cover image - private Task UpdateSeriesCoverImage(Series? series, bool forceUpdate, bool forceColorScape = false) + private void UpdateSeriesCoverImage(Series? series, bool forceUpdate, bool forceColorScape = false) { - if (series == null) return Task.CompletedTask; + if (series == null) return; if (!_cacheHelper.ShouldUpdateCoverImage( _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, series.CoverImage), @@ -194,7 +194,7 @@ public class MetadataService : IMetadataService _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); } - return Task.CompletedTask; + return; } series.Volumes ??= []; @@ -203,7 +203,6 @@ public class MetadataService : IMetadataService _imageService.UpdateColorScape(series); _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); - return Task.CompletedTask; } @@ -213,7 +212,7 @@ public class MetadataService : IMetadataService /// /// /// - private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceColorScape = false) + private void ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceColorScape = false) { _logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName); try @@ -226,8 +225,8 @@ public class MetadataService : IMetadataService var index = 0; foreach (var chapter in volume.Chapters) { - var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat, coverImageSize, forceColorScape); - // If cover was update, either the file has changed or first scan and we should force a metadata update + var chapterUpdated = UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat, coverImageSize, forceColorScape); + // If cover was update, either the file has changed or first scan, and we should force a metadata update UpdateChapterLastModified(chapter, forceUpdate || chapterUpdated); if (index == 0 && chapterUpdated) { @@ -237,7 +236,7 @@ public class MetadataService : IMetadataService index++; } - var volumeUpdated = await UpdateVolumeCoverImage(volume, firstChapterUpdated || forceUpdate, forceColorScape); + var volumeUpdated = UpdateVolumeCoverImage(volume, firstChapterUpdated || forceUpdate, forceColorScape); if (volumeIndex == 0 && volumeUpdated) { firstVolumeUpdated = true; @@ -245,7 +244,7 @@ public class MetadataService : IMetadataService volumeIndex++; } - await UpdateSeriesCoverImage(series, firstVolumeUpdated || forceUpdate, forceColorScape); + UpdateSeriesCoverImage(series, firstVolumeUpdated || forceUpdate, forceColorScape); } catch (Exception ex) { @@ -311,7 +310,7 @@ public class MetadataService : IMetadataService try { - await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize, forceColorScape); + ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize, forceColorScape); } catch (Exception ex) { @@ -383,7 +382,7 @@ public class MetadataService : IMetadataService await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name)); - await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize, forceColorScape); + ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize, forceColorScape); if (_unitOfWork.HasChanges()) diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/API/Services/Tasks/Metadata/CoverDbService.cs index a75c17b76..6dfb414df 100644 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ b/API/Services/Tasks/Metadata/CoverDbService.cs @@ -537,8 +537,16 @@ public class CoverDbService : ICoverDbService // Additional check to see if downloaded image is similar and we have a higher resolution if (chooseBetterImage) { - var betterImage = Path.Join(_directoryService.CoverImageDirectory, series.CoverImage).GetBetterImage(Path.Join(_directoryService.CoverImageDirectory, filePath))!; - filePath = Path.GetFileName(betterImage); + try + { + var betterImage = Path.Join(_directoryService.CoverImageDirectory, series.CoverImage) + .GetBetterImage(Path.Join(_directoryService.CoverImageDirectory, filePath))!; + filePath = Path.GetFileName(betterImage); + } + catch (Exception ex) + { + _logger.LogError(ex, "There was an issue trying to choose a better cover image for Series: {SeriesName} ({SeriesId})", series.Name, series.Id); + } } series.CoverImage = filePath; diff --git a/API/Services/Tasks/SiteThemeService.cs b/API/Services/Tasks/SiteThemeService.cs index aaa06a846..a9736854d 100644 --- a/API/Services/Tasks/SiteThemeService.cs +++ b/API/Services/Tasks/SiteThemeService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json.Serialization; using System.Threading.Tasks; using API.Data; using API.DTOs.Theme; @@ -32,6 +33,7 @@ internal class GitHubContent [JsonProperty("type")] public string Type { get; set; } + [JsonPropertyName("download_url")] [JsonProperty("download_url")] public string DownloadUrl { get; set; } @@ -151,6 +153,7 @@ public class ThemeService : IThemeService // Fetch contents of the theme directory var themeContents = await GetDirectoryContent(themeDir.Path); + // Find css and preview files var cssFile = themeContents.FirstOrDefault(c => c.Name.EndsWith(".css")); var previewUrls = GetPreviewUrls(themeContents); @@ -187,7 +190,7 @@ public class ThemeService : IThemeService return themeDtos; } - private static IList GetPreviewUrls(IEnumerable themeContents) + private static List GetPreviewUrls(IEnumerable themeContents) { return themeContents.Where(c => c.Name.ToLower().EndsWith(".jpg") || c.Name.ToLower().EndsWith(".png") ) .Select(p => p.DownloadUrl) @@ -196,10 +199,12 @@ public class ThemeService : IThemeService private static async Task> GetDirectoryContent(string path) { - return await $"{GithubBaseUrl}/repos/Kareadita/Themes/contents/{path}" + var json = await $"{GithubBaseUrl}/repos/Kareadita/Themes/contents/{path}" .WithHeader("Accept", "application/vnd.github+json") .WithHeader("User-Agent", "Kavita") - .GetJsonAsync>(); + .GetStringAsync(); + + return string.IsNullOrEmpty(json) ? [] : JsonConvert.DeserializeObject>(json); } /// diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 9b3d9b1e4..0eb179779 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -54,7 +54,7 @@ export class ServerService { } checkHowOutOfDate() { - return this.http.get(this.baseUrl + 'server/checkHowOutOfDate', TextResonse) + return this.http.get(this.baseUrl + 'server/check-out-of-date', TextResonse) .pipe(map(r => parseInt(r, 10))); } diff --git a/UI/Web/src/app/admin/license/license.component.html b/UI/Web/src/app/admin/license/license.component.html index 99b594261..d26e1bb61 100644 --- a/UI/Web/src/app/admin/license/license.component.html +++ b/UI/Web/src/app/admin/license/license.component.html @@ -9,7 +9,7 @@
- +
- - - - +