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": [
{