diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 4c3bd4442..c9e441cdb 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -30,44 +30,34 @@ namespace API.Controllers } [HttpGet("image")] - public async Task> GetImage(int chapterId, int page) + public async Task GetImage(int chapterId, int page) { - // Temp let's iterate the directory each call to get next image var chapter = await _cacheService.Ensure(chapterId); - if (chapter == null) return BadRequest("There was an issue finding image file for reading"); var (path, mangaFile) = await _cacheService.GetCachedPagePath(chapter, page); - if (string.IsNullOrEmpty(path)) return BadRequest($"No such image for page {page}"); - var file = await _directoryService.ReadImageAsync(path); - file.Page = page; - file.MangaFileName = mangaFile.FilePath; - file.NeedsSplitting = file.Width > file.Height; - - // TODO: Validate if sending page whole (not base64 encoded) fixes Tablet issue - //Response.Headers.Add("Transfer-Encoding", "gzip"); + if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}"); - return Ok(file); + var content = await _directoryService.ReadFileAsync(path); + var format = Path.GetExtension(path).Replace(".", ""); + + // Look into HttpContext.Cache so we can utilize a memorystream for Zip entries (want to limit response time by 300ms) + // Calculates SHA1 Hash for byte[] + using var sha1 = new System.Security.Cryptography.SHA1CryptoServiceProvider(); + Response.Headers.Add("ETag", string.Concat(sha1.ComputeHash(content).Select(x => x.ToString("X2")))); + Response.Headers.Add("Cache-Control", "private"); + + return File(content, "image/" + format); } - - [HttpGet("image2")] - public async Task GetImage2(int chapterId, int page) - { - // Temp let's iterate the directory each call to get next image - var chapter = await _cacheService.Ensure(chapterId); + [HttpGet("chapter-path")] + public async Task> GetImagePath(int chapterId) + { + var chapter = await _cacheService.Ensure(chapterId); if (chapter == null) return BadRequest("There was an issue finding image file for reading"); - var (path, mangaFile) = await _cacheService.GetCachedPagePath(chapter, page); - if (string.IsNullOrEmpty(path)) return BadRequest($"No such image for page {page}"); - var file = await _directoryService.ReadImageAsync(path); - file.Page = page; - file.MangaFileName = mangaFile.FilePath; - file.NeedsSplitting = file.Width > file.Height; - - // TODO: Validate if sending page whole (not base64 encoded) fixes Tablet issue - - return File(file.Content, "image/jpeg", mangaFile.FilePath); + var (path, mangaFile) = await _cacheService.GetCachedPagePath(chapter, 0); + return Ok(mangaFile.FilePath); } [HttpGet("get-bookmark")] @@ -162,6 +152,46 @@ namespace API.Controllers return BadRequest("There was an issue saving progress"); } + [HttpPost("mark-volume-read")] + public async Task MarkVolumeAsRead(MarkVolumeReadDto markVolumeReadDto) + { + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + _logger.LogDebug("Saving {UserName} progress for Volume {VolumeID} to read", user.UserName, markVolumeReadDto.VolumeId); + + var chapters = await _unitOfWork.VolumeRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); + foreach (var chapter in chapters) + { + user.Progresses ??= new List(); + var userProgress = user.Progresses.SingleOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id); + + if (userProgress == null) + { + user.Progresses.Add(new AppUserProgress + { + PagesRead = chapter.Pages, + VolumeId = markVolumeReadDto.VolumeId, + SeriesId = markVolumeReadDto.SeriesId, + ChapterId = chapter.Id + }); + } + else + { + userProgress.PagesRead = chapter.Pages; + userProgress.SeriesId = markVolumeReadDto.SeriesId; + userProgress.VolumeId = markVolumeReadDto.VolumeId; + } + } + + _unitOfWork.UserRepository.Update(user); + + if (await _unitOfWork.Complete()) + { + return Ok(); + } + + return BadRequest("Could not save progress"); + } + [HttpPost("bookmark")] public async Task Bookmark(BookmarkDto bookmarkDto) { diff --git a/API/DTOs/MarkVolumeReadDto.cs b/API/DTOs/MarkVolumeReadDto.cs new file mode 100644 index 000000000..ffae155a2 --- /dev/null +++ b/API/DTOs/MarkVolumeReadDto.cs @@ -0,0 +1,8 @@ +namespace API.DTOs +{ + public class MarkVolumeReadDto + { + public int SeriesId { get; init; } + public int VolumeId { get; init; } + } +} \ No newline at end of file diff --git a/API/Data/VolumeRepository.cs b/API/Data/VolumeRepository.cs index ce8dd0eea..abfb672a8 100644 --- a/API/Data/VolumeRepository.cs +++ b/API/Data/VolumeRepository.cs @@ -35,9 +35,22 @@ namespace API.Data { return await _context.Chapter .Include(c => c.Files) - .AsNoTracking() .SingleOrDefaultAsync(c => c.Id == chapterId); } + + + /// + /// Returns Chapters for a volume id. + /// + /// + /// + public async Task> GetChaptersAsync(int volumeId) + { + return await _context.Chapter + .Where(c => c.VolumeId == volumeId) + .ToListAsync(); + } + public async Task GetChapterDtoAsync(int chapterId) { diff --git a/API/Extensions/HttpExtensions.cs b/API/Extensions/HttpExtensions.cs index 468bac728..3d08cc94a 100644 --- a/API/Extensions/HttpExtensions.cs +++ b/API/Extensions/HttpExtensions.cs @@ -18,5 +18,6 @@ namespace API.Extensions response.Headers.Add("Pagination", JsonSerializer.Serialize(paginationHeader, options)); response.Headers.Add("Access-Control-Expose-Headers", "Pagination"); } + } } \ No newline at end of file diff --git a/API/Interfaces/IVolumeRepository.cs b/API/Interfaces/IVolumeRepository.cs index 0bc28253b..727133d80 100644 --- a/API/Interfaces/IVolumeRepository.cs +++ b/API/Interfaces/IVolumeRepository.cs @@ -11,5 +11,6 @@ namespace API.Interfaces Task GetChapterAsync(int chapterId); Task GetChapterDtoAsync(int chapterId); Task> GetFilesForChapter(int chapterId); + Task> GetChaptersAsync(int volumeId); } } \ No newline at end of file diff --git a/API/Interfaces/Services/IDirectoryService.cs b/API/Interfaces/Services/IDirectoryService.cs index 93fbfd64f..c4b15b94b 100644 --- a/API/Interfaces/Services/IDirectoryService.cs +++ b/API/Interfaces/Services/IDirectoryService.cs @@ -13,8 +13,6 @@ namespace API.Interfaces.Services /// Absolute path of directory to scan. /// List of folder names IEnumerable ListDirectory(string rootPath); - - Task ReadImageAsync(string imagePath); /// /// Gets files in a directory. If searchPatternExpression is passed, will match the regex against for filtering. /// diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index cd1dead46..f1f4f5eb1 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -145,26 +145,6 @@ namespace API.Services return dirs; } - - public async Task ReadImageAsync(string imagePath) - { - if (!File.Exists(imagePath)) - { - _logger.LogError("Image does not exist on disk"); - return null; - } - using var image = Image.NewFromFile(imagePath); - - return new ImageDto - { - Content = await ReadFileAsync(imagePath), - Filename = Path.GetFileNameWithoutExtension(imagePath), - FullPath = Path.GetFullPath(imagePath), - Width = image.Width, - Height = image.Height, - Format = image.Format, - }; - } public async Task ReadFileAsync(string path) { diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index dd3f21150..42c45d465 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -78,6 +78,8 @@ namespace API.Services { _logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId); BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, forceUpdate)); + BackgroundJob.Enqueue(() => _cleanupService.Cleanup()); // When we do a scan, force cache to re-unpack in case page numbers change + } public void CleanupChapters(int[] chapterIds) diff --git a/API/Startup.cs b/API/Startup.cs index 8b59f2f12..029fe5f5c 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -1,9 +1,12 @@ +using System.IO.Compression; +using System.Linq; using API.Extensions; using API.Middleware; using Hangfire; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -37,8 +40,22 @@ namespace API { c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" }); }); - - + // This doesn't seem to work. + services.AddResponseCompression(options => + { + options.Providers.Add(); + options.Providers.Add(); + options.MimeTypes = + ResponseCompressionDefaults.MimeTypes.Concat( + new[] { "image/jpeg", "image/jpg" }); + options.EnableForHttps = true; + }); + services.Configure(options => + { + options.Level = CompressionLevel.Fastest; + }); + + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -52,6 +69,7 @@ namespace API app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1")); app.UseHangfireDashboard(); } + app.UseResponseCompression(); app.UseForwardedHeaders(); @@ -62,13 +80,15 @@ namespace API { app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("http://localhost:4200")); } + + //app.UseResponseCaching(); app.UseAuthentication(); app.UseAuthorization(); app.UseDefaultFiles(); - + app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = new FileExtensionContentTypeProvider() diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json index 67e0049ba..4dd015431 100644 --- a/API/appsettings.Development.json +++ b/API/appsettings.Development.json @@ -8,7 +8,8 @@ "Default": "Debug", "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Error", - "Hangfire": "Information" + "Hangfire": "Information", + "Microsoft.AspNetCore.Hosting.Internal.WebHost": "Information" }, "File": { "Path": "kavita.log",