diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 70244a8f16..c25a7012dd 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -34,6 +34,7 @@ public class ScannerServiceTests : IDisposable private readonly IImageService _imageService = Substitute.For(); private readonly ILogger _metadataLogger = Substitute.For>(); private readonly IDirectoryService _directoryService = Substitute.For(); + private readonly ICacheService _cacheService = Substitute.For(); private readonly DbConnection _connection; private readonly DataContext _context; @@ -53,7 +54,7 @@ public ScannerServiceTests() IMetadataService metadataService = Substitute.For(unitOfWork, _metadataLogger, _archiveService, _bookService, _imageService); - _scannerService = new ScannerService(unitOfWork, _logger, _archiveService, metadataService, _bookService); + _scannerService = new ScannerService(unitOfWork, _logger, _archiveService, metadataService, _bookService, _cacheService); } private async Task SeedDb() diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 2fd460f797..929f0d0aec 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -22,15 +22,17 @@ public class ServerController : BaseApiController private readonly IConfiguration _config; private readonly IBackupService _backupService; private readonly IArchiveService _archiveService; + private readonly ICacheService _cacheService; public ServerController(IHostApplicationLifetime applicationLifetime, ILogger logger, IConfiguration config, - IBackupService backupService, IArchiveService archiveService) + IBackupService backupService, IArchiveService archiveService, ICacheService cacheService) { _applicationLifetime = applicationLifetime; _logger = logger; _config = config; _backupService = backupService; _archiveService = archiveService; + _cacheService = cacheService; } [HttpPost("restart")] @@ -42,6 +44,15 @@ public ActionResult RestartServer() return Ok(); } + [HttpPost("clear-cache")] + public ActionResult ClearCache() + { + _logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", User.GetUsername()); + _cacheService.Cleanup(); + + return Ok(); + } + /// /// Returns non-sensitive information about the current system /// diff --git a/API/Extensions/DirectoryInfoExtensions.cs b/API/Extensions/DirectoryInfoExtensions.cs index e5518438fd..a922e73135 100644 --- a/API/Extensions/DirectoryInfoExtensions.cs +++ b/API/Extensions/DirectoryInfoExtensions.cs @@ -79,7 +79,6 @@ private static void FlattenDirectory(DirectoryInfo root, DirectoryInfo directory foreach (var subDirectory in directory.EnumerateDirectories()) { - Console.WriteLine($"Flattening {subDirectory}"); FlattenDirectory(root, subDirectory, ref directoryIndex); } } diff --git a/API/Interfaces/Services/ICacheService.cs b/API/Interfaces/Services/ICacheService.cs index 830b6bb42b..f2d6cfa714 100644 --- a/API/Interfaces/Services/ICacheService.cs +++ b/API/Interfaces/Services/ICacheService.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using API.Entities; namespace API.Interfaces.Services @@ -22,11 +23,11 @@ public interface ICacheService /// Clears cache directory of all volumes. This can be invoked from deleting a library or a series. /// /// Volumes that belong to that library. Assume the library might have been deleted before this invocation. - void CleanupChapters(int[] chapterIds); - + void CleanupChapters(IEnumerable chapterIds); + /// - /// Returns the absolute path of a cached page. + /// Returns the absolute path of a cached page. /// /// Chapter entity with Files populated. /// Page number to look for @@ -35,4 +36,4 @@ public interface ICacheService void EnsureCacheDirectory(); } -} \ No newline at end of file +} diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index c0c1b2c780..914508f9fc 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -11,7 +11,7 @@ public static class Parser { public const string DefaultChapter = "0"; public const string DefaultVolume = "0"; - private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(250); + private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg)"; public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; @@ -507,7 +507,8 @@ public static class Parser // If SP\d+ is in the filename, we force treat it as a special regardless if volume or chapter might have been found. private static readonly Regex SpecialMarkerRegex = new Regex( @"(?SP\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled + RegexOptions.IgnoreCase | RegexOptions.Compiled, + RegexTimeout ); diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index 66a5e2618d..a03188a5d8 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -301,7 +301,6 @@ private static ComicInfo FindComicInfoXml(IEnumerable entries) entry.WriteTo(ms); ms.Position = 0; - var serializer = new XmlSerializer(typeof(ComicInfo)); var info = (ComicInfo) serializer.Deserialize(ms); return info; diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 0fe910fdf9..ea8fb10aa3 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -379,7 +379,6 @@ public void ExtractPdfImages(string fileFilePath, string targetDirectory) using var stream = StreamManager.GetStream("BookService.GetPdfPage"); for (var pageNumber = 0; pageNumber < pages; pageNumber++) { - // IDEA! Move stream out and use the same stream GetPdfPage(docReader, pageNumber, stream); File.WriteAllBytes(Path.Combine(targetDirectory, "Page-" + pageNumber + ".png"), stream.ToArray()); } diff --git a/API/Services/CacheService.cs b/API/Services/CacheService.cs index 1744190902..3d557ed7f1 100644 --- a/API/Services/CacheService.cs +++ b/API/Services/CacheService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -113,7 +114,7 @@ public void Cleanup() _logger.LogInformation("Cache directory purged"); } - public void CleanupChapters(int[] chapterIds) + public void CleanupChapters(IEnumerable chapterIds) { _logger.LogInformation("Running Cache cleanup on Volumes"); diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index a4d683db3a..33d2d8ea96 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -26,16 +26,18 @@ public class ScannerService : IScannerService private readonly IArchiveService _archiveService; private readonly IMetadataService _metadataService; private readonly IBookService _bookService; + private readonly ICacheService _cacheService; private readonly NaturalSortComparer _naturalSort = new (); public ScannerService(IUnitOfWork unitOfWork, ILogger logger, IArchiveService archiveService, - IMetadataService metadataService, IBookService bookService) + IMetadataService metadataService, IBookService bookService, ICacheService cacheService) { _unitOfWork = unitOfWork; _logger = logger; _archiveService = archiveService; _metadataService = metadataService; _bookService = bookService; + _cacheService = cacheService; } [DisableConcurrentExecution(timeoutInSeconds: 360)] @@ -46,6 +48,7 @@ public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate, Canc var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); var library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId, seriesId); var dirs = FindHighestDirectoriesFromFiles(library, files); + var chapterIds = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new []{ seriesId }); _logger.LogInformation("Beginning file scan on {SeriesName}", series.Name); var scanner = new ParseScannedFiles(_bookService, _logger); @@ -71,6 +74,7 @@ public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate, Canc totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name); CleanupUserProgress(); BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate)); + BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); } else { diff --git a/API/Startup.cs b/API/Startup.cs index f5cbfc7346..3741399f9a 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -101,7 +101,11 @@ public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJo // Ordering is important. Cors, authentication, authorization if (env.IsDevelopment()) { - app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200").WithExposedHeaders("Content-Disposition")); + app.UseCors(policy => policy + .AllowAnyHeader() + .AllowAnyMethod() + .WithOrigins("http://localhost:4200") + .WithExposedHeaders("Content-Disposition", "Pagination")); } app.UseResponseCaching(); diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 1837b9fe0e..3abec515b9 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -19,4 +19,8 @@ export class ServerService { getServerInfo() { return this.httpClient.get(this.baseUrl + 'server/server-info'); } + + clearCache() { + return this.httpClient.post(this.baseUrl + 'server/clear-cache', {}); + } } diff --git a/UI/Web/src/app/admin/dashboard/dashboard.component.html b/UI/Web/src/app/admin/dashboard/dashboard.component.html index c206f7c34a..2830cbf4d5 100644 --- a/UI/Web/src/app/admin/dashboard/dashboard.component.html +++ b/UI/Web/src/app/admin/dashboard/dashboard.component.html @@ -1,10 +1,6 @@

Admin Dashboard

-
- -
-
- {{utilityService.mangaFormat(series.format)}} + {{utilityService.mangaFormat(series.format)}}
diff --git a/UI/Web/src/app/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/series-detail.component.ts index 4d07be6bc4..1eb89bda89 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -6,6 +6,7 @@ import { ToastrService } from 'ngx-toastr'; import { take } from 'rxjs/operators'; import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config'; import { ConfirmService } from '../shared/confirm.service'; +import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component'; import { CardDetailsModalComponent } from '../shared/_modals/card-details-modal/card-details-modal.component'; import { DownloadService } from '../shared/_services/download.service'; import { UtilityService } from '../shared/_services/utility.service'; @@ -74,6 +75,10 @@ export class SeriesDetailComponent implements OnInit { return MangaFormat; } + get TagBadgeCursor(): typeof TagBadgeCursor { + return TagBadgeCursor; + } + constructor(private route: ActivatedRoute, private seriesService: SeriesService, private ratingConfig: NgbRatingConfig, private router: Router, private modalService: NgbModal, public readerService: ReaderService, diff --git a/UI/Web/src/app/shared/tag-badge/tag-badge.component.html b/UI/Web/src/app/shared/tag-badge/tag-badge.component.html index 9054ff7059..b1e1b9a277 100644 --- a/UI/Web/src/app/shared/tag-badge/tag-badge.component.html +++ b/UI/Web/src/app/shared/tag-badge/tag-badge.component.html @@ -1,3 +1,3 @@ -
+
\ No newline at end of file diff --git a/UI/Web/src/app/shared/tag-badge/tag-badge.component.scss b/UI/Web/src/app/shared/tag-badge/tag-badge.component.scss index 5a05bed5c9..8c9d013a24 100644 --- a/UI/Web/src/app/shared/tag-badge/tag-badge.component.scss +++ b/UI/Web/src/app/shared/tag-badge/tag-badge.component.scss @@ -20,4 +20,28 @@ $bdr-color: #f2f2f2; margin-right: 0px; cursor: pointer; } +} + +::ng-deep .selectable-cursor { + cursor: default !important; + + i { + cursor: default !important; + } +} + +::ng-deep .not-allowed-cursor { + cursor: not-allowed !important; + + i { + cursor: not-allowed !important; + } +} + +::ng-deep .clickable-cursor { + cursor: pointer !important; + + i { + cursor: pointer !important; + } } \ No newline at end of file diff --git a/UI/Web/src/app/shared/tag-badge/tag-badge.component.ts b/UI/Web/src/app/shared/tag-badge/tag-badge.component.ts index 6d7fb06155..58e9e93e57 100644 --- a/UI/Web/src/app/shared/tag-badge/tag-badge.component.ts +++ b/UI/Web/src/app/shared/tag-badge/tag-badge.component.ts @@ -1,4 +1,25 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; + +/** + * What type of cursor to apply to the tag badge + */ +export enum TagBadgeCursor { + /** + * Allows the user to select text + * cursor: default + */ + Selectable, + /** + * Informs the user they can click and interact with badge + * cursor: pointer + */ + Clickable, + /** + * Informs the user they cannot click or interact with badge + * cursor: not-allowed + */ + NotAllowed, +} @Component({ selector: 'app-tag-badge', @@ -7,9 +28,24 @@ import { Component, OnInit } from '@angular/core'; }) export class TagBadgeComponent implements OnInit { + @Input() selectionMode: TagBadgeCursor = TagBadgeCursor.Selectable; + + cursor: string = 'default'; + constructor() { } ngOnInit(): void { + switch (this.selectionMode) { + case TagBadgeCursor.Selectable: + this.cursor = 'selectable-cursor'; + break; + case TagBadgeCursor.NotAllowed: + this.cursor = 'not-allowed-cursor'; + break; + case TagBadgeCursor.Clickable: + this.cursor = 'clickable-cursor'; + break; + } } }