diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 2f2fd6d02..4755690a2 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -1366,6 +1366,45 @@ public class ReaderServiceTests Assert.Equal("1", nextChapter.Range); } + [Fact] + public async Task GetContinuePoint_ShouldReturnFirstVolume_WhenFirstVolumeIsAlsoTaggedAsChapter1Through11_WithProgress() + { + await ResetDb(); + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("1") + .WithChapter(new ChapterBuilder("1", "1-11").WithPages(3).Build()) + .Build()) + .WithVolume(new VolumeBuilder("2") + .WithChapter(new ChapterBuilder("0").WithPages(1).Build()) + .Build()) + .WithPages(4) + .Build(); + series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); + + _context.Series.Add(series); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + + + + await _readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 2, + ChapterId = 1, + SeriesId = 1, + VolumeId = 1 + }, 1); + var nextChapter = await _readerService.GetContinuePoint(1, 1); + + Assert.Equal("1-11", nextChapter.Range); + } + [Fact] public async Task GetContinuePoint_ShouldReturnFirstNonSpecial() { diff --git a/API/API.csproj b/API/API.csproj index 46d180742..b14e6855c 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -78,6 +78,7 @@ + diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs index 29f827dfb..4dd538c14 100644 --- a/API/Controllers/ImageController.cs +++ b/API/Controllers/ImageController.cs @@ -8,6 +8,7 @@ using API.Extensions; using API.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using MimeTypes; namespace API.Controllers; @@ -38,9 +39,9 @@ public class ImageController : BaseApiController { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId)); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image"); - var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty); + var format = _directoryService.FileSystem.Path.GetExtension(path); - return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path)); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); } /// @@ -54,9 +55,9 @@ public class ImageController : BaseApiController { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.LibraryRepository.GetLibraryCoverImageAsync(libraryId)); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image"); - var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty); + var format = _directoryService.FileSystem.Path.GetExtension(path); - return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path)); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); } /// @@ -70,9 +71,9 @@ public class ImageController : BaseApiController { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId)); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image"); - var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty); + var format = _directoryService.FileSystem.Path.GetExtension(path); - return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path)); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); } /// @@ -86,11 +87,11 @@ public class ImageController : BaseApiController { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId)); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image"); - var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty); + var format = _directoryService.FileSystem.Path.GetExtension(path); Response.AddCacheHeader(path); - return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path)); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); } /// @@ -104,9 +105,9 @@ public class ImageController : BaseApiController { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId)); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image"); - var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty); + var format = _directoryService.FileSystem.Path.GetExtension(path); - return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path)); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); } /// @@ -119,13 +120,10 @@ public class ImageController : BaseApiController public async Task GetReadingListCoverImage(int readingListId) { var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId)); - if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) - { - return BadRequest($"No cover image"); - } - var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty); + if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image"); + var format = _directoryService.FileSystem.Path.GetExtension(path); - return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path)); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); } /// @@ -147,9 +145,9 @@ public class ImageController : BaseApiController var bookmarkDirectory = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value; var file = new FileInfo(Path.Join(bookmarkDirectory, bookmark.FileName)); - var format = Path.GetExtension(file.FullName).Replace(".", string.Empty); + var format = Path.GetExtension(file.FullName); - return PhysicalFile(file.FullName, "image/" + format, Path.GetFileName(file.FullName)); + return PhysicalFile(file.FullName, MimeTypeMap.GetMimeType(format), Path.GetFileName(file.FullName)); } /// @@ -166,8 +164,8 @@ public class ImageController : BaseApiController var path = Path.Join(_directoryService.TempDirectory, filename); if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist"); - var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", string.Empty); + var format = _directoryService.FileSystem.Path.GetExtension(path); - return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path)); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), _directoryService.FileSystem.Path.GetFileName(path)); } } diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 2a49c5a29..1564a26a0 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -20,6 +20,7 @@ using API.Services; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using MimeTypes; namespace API.Controllers; @@ -529,29 +530,39 @@ public class OpdsController : BaseApiController var seriesDetail = await _seriesService.GetSeriesDetail(seriesId, userId); foreach (var volume in seriesDetail.Volumes) { - // If there is only one chapter to the Volume, we will emulate a volume to flatten the amount of hops a user must go through - if (volume.Chapters.Count == 1) + var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volume.Id)).OrderBy(x => double.Parse(x.Number), + _chapterSortComparer); + + foreach (var chapter in chapters) { - var firstChapter = volume.Chapters.First(); - var chapter = CreateChapter(apiKey, volume.Name, firstChapter.Summary, firstChapter.Id, volume.Id, seriesId); - chapter.Id = firstChapter.Id.ToString(); - feed.Entries.Add(chapter); - } - else - { - feed.Entries.Add(CreateVolume(volume, seriesId, apiKey)); + var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id); + var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id); + foreach (var mangaFile in files) + { + feed.Entries.Add(await CreateChapterWithFile(seriesId, volume.Id, chapter.Id, mangaFile, series, chapterTest, apiKey)); + } } } foreach (var storylineChapter in seriesDetail.StorylineChapters.Where(c => !c.IsSpecial)) { - feed.Entries.Add(CreateChapter(apiKey, storylineChapter.Title, storylineChapter.Summary, storylineChapter.Id, storylineChapter.VolumeId, seriesId)); + var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(storylineChapter.Id); + var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(storylineChapter.Id); + foreach (var mangaFile in files) + { + feed.Entries.Add(await CreateChapterWithFile(seriesId, storylineChapter.VolumeId, storylineChapter.Id, mangaFile, series, chapterTest, apiKey)); + } } foreach (var special in seriesDetail.Specials) { - feed.Entries.Add(CreateChapter(apiKey, special.Title, special.Summary, special.Id, special.VolumeId, seriesId)); + var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(special.Id); + var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(special.Id); + foreach (var mangaFile in files) + { + feed.Entries.Add(await CreateChapterWithFile(seriesId, special.VolumeId, special.Id, mangaFile, series, chapterTest, apiKey)); + } } return CreateXmlResult(SerializeXml(feed)); @@ -570,21 +581,17 @@ public class OpdsController : BaseApiController var chapters = (await _unitOfWork.ChapterRepository.GetChaptersAsync(volumeId)).OrderBy(x => double.Parse(x.Number), _chapterSortComparer); - - var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s ", $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey); + var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s ", + $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey); SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{SeriesService.FormatChapterName(libraryType)}s"); foreach (var chapter in chapters) { - feed.Entries.Add(new FeedEntry() + var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapter.Id); + var chapterTest = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id); + foreach (var mangaFile in files) { - Id = chapter.Id.ToString(), - Title = SeriesService.FormatChapterTitle(chapter, libraryType), - Links = new List() - { - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, Prefix + $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapter.Id}"), - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapter.Id}") - } - }); + feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapter.Id, mangaFile, series, chapterTest, apiKey)); + } } return CreateXmlResult(SerializeXml(feed)); @@ -604,7 +611,8 @@ public class OpdsController : BaseApiController var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId); var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId); - var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s", $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey); + var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {SeriesService.FormatChapterName(libraryType)}s", + $"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey); SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{SeriesService.FormatChapterName(libraryType)}-{chapterId}-files"); foreach (var mangaFile in files) { @@ -727,25 +735,6 @@ public class OpdsController : BaseApiController }; } - private static FeedEntry CreateVolume(VolumeDto volumeDto, int seriesId, string apiKey) - { - return new FeedEntry() - { - Id = volumeDto.Id.ToString(), - Title = volumeDto.Name, - Summary = volumeDto.Chapters.First().Summary ?? string.Empty, - Links = new List() - { - CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, - Prefix + $"{apiKey}/series/{seriesId}/volume/{volumeDto.Id}"), - CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, - $"/api/image/volume-cover?volumeId={volumeDto.Id}"), - CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, - $"/api/image/volume-cover?volumeId={volumeDto.Id}") - } - }; - } - private static FeedEntry CreateChapter(string apiKey, string title, string summary, int chapterId, int volumeId, int seriesId) { return new FeedEntry() @@ -768,6 +757,7 @@ public class OpdsController : BaseApiController private async Task CreateChapterWithFile(int seriesId, int volumeId, int chapterId, MangaFile mangaFile, SeriesDto series, ChapterDto chapter, string apiKey) { var fileSize = + mangaFile.Bytes > 0 ? DirectoryService.GetHumanReadableBytes(mangaFile.Bytes) : DirectoryService.GetHumanReadableBytes(_directoryService.GetTotalSize(new List() {mangaFile.FilePath})); var fileType = _downloadService.GetContentTypeFromFile(mangaFile.FilePath); @@ -775,15 +765,23 @@ public class OpdsController : BaseApiController var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId); var volume = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, await GetUser(apiKey)); - var title = $"{series.Name} - "; + var title = $"{series.Name}"; + if (volume.Chapters.Count == 1) { SeriesService.RenameVolumeName(volume.Chapters.First(), volume, libraryType); - title += $"{volume.Name}"; + if (volume.Name != "0") + { + title += $" - {volume.Name}"; + } + } + else if (volume.Number != 0) + { + title = $" - {series.Name} - Volume {volume.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}"; } else { - title = $"{series.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}"; + title = $" - {series.Name} - {SeriesService.FormatChapterTitle(chapter, libraryType)}"; } // Chunky requires a file at the end. Our API ignores this @@ -841,7 +839,7 @@ public class OpdsController : BaseApiController if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {pageNumber}"); var content = await _directoryService.ReadFileAsync(path); - var format = Path.GetExtension(path).Replace(".", string.Empty); + var format = Path.GetExtension(path); // Calculates SHA1 Hash for byte[] Response.AddCacheHeader(content); @@ -856,7 +854,7 @@ public class OpdsController : BaseApiController LibraryId =libraryId }, await GetUser(apiKey)); - return File(content, "image/" + format); + return File(content, MimeTypeMap.GetMimeType(format)); } catch (Exception) { @@ -873,9 +871,9 @@ public class OpdsController : BaseApiController if (files.Length == 0) return BadRequest("Cannot find icon"); var path = files[0]; var content = await _directoryService.ReadFileAsync(path); - var format = Path.GetExtension(path).Replace(".", string.Empty); + var format = Path.GetExtension(path); - return File(content, "image/" + format); + return File(content, MimeTypeMap.GetMimeType(format)); } /// diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 6ba025003..ded9345b0 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; +using MimeTypes; namespace API.Controllers; @@ -103,9 +104,9 @@ public class ReaderController : BaseApiController { var path = _cacheService.GetCachedPagePath(chapter.Id, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache."); - var format = Path.GetExtension(path).Replace(".", string.Empty); + var format = Path.GetExtension(path); - return PhysicalFile(path, "image/" + format, Path.GetFileName(path), true); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true); } catch (Exception) { @@ -124,8 +125,8 @@ public class ReaderController : BaseApiController var images = _cacheService.GetCachedPages(chapterId); var path = await _readerService.GetThumbnail(chapter, pageNum, images); - var format = Path.GetExtension(path).Replace(".", string.Empty); // TODO: Make this an extension - return PhysicalFile(path, "image/" + format, Path.GetFileName(path), true); + var format = Path.GetExtension(path); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path), true); } /// @@ -154,9 +155,9 @@ public class ReaderController : BaseApiController { var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}"); - var format = Path.GetExtension(path).Replace(".", string.Empty); + var format = Path.GetExtension(path); - return PhysicalFile(path, "image/" + format, Path.GetFileName(path)); + return PhysicalFile(path, MimeTypeMap.GetMimeType(format), Path.GetFileName(path)); } catch (Exception) { diff --git a/API/Services/DownloadService.cs b/API/Services/DownloadService.cs index 6ff152df9..a8dfd5d50 100644 --- a/API/Services/DownloadService.cs +++ b/API/Services/DownloadService.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using API.Entities; using Microsoft.AspNetCore.StaticFiles; +using MimeTypes; namespace API.Services; @@ -47,7 +48,7 @@ public class DownloadService : IDownloadService ".zip" => "application/zip", ".tar.gz" => "application/gzip", ".pdf" => "application/pdf", - _ => contentType + _ => MimeTypeMap.GetMimeType(contentType) }; } diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index fb1a91709..953f42b93 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -495,7 +495,7 @@ public class ReaderService : IReaderService // NOTE: If volume 1 has chapter 1 and volume 2 is just chapter 0 due to being a full volume file, then this fails // If there are any volumes that have progress, return those. If not, move on. var currentlyReadingChapter = volumeChapters - .OrderBy(c => double.Parse(c.Range), _chapterSortComparer) + .OrderBy(c => double.Parse(c.Number), _chapterSortComparer) // BUG: This is throwing an exception when Range is 1-11 .FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages && chapter.PagesRead > 0); if (currentlyReadingChapter != null) return currentlyReadingChapter; diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index b9605821d..bbec61d7f 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -466,10 +466,14 @@ public class SeriesService : ISeriesService if (string.IsNullOrEmpty(title)) return; volume.Name += $" - {title}"; } - else + else if (volume.Name != "0") { volume.Name += $" - {firstChapter.TitleName}"; } + else + { + volume.Name += $""; + } return; } diff --git a/openapi.json b/openapi.json index 7cd43ca42..304d3ffdc 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.1.27" + "version": "0.7.1.28" }, "servers": [ {