Koreader Sync Fix and More (#4006)

Co-authored-by: Joe Milazzo <josephmajora@gmail.com>
This commit is contained in:
Fesaa
2025-08-28 01:44:30 +02:00
committed by GitHub
parent aa268c2dca
commit 6137d2a30e
30 changed files with 556 additions and 310 deletions
+110 -71
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
@@ -28,6 +29,7 @@ using AutoMapper;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using MimeTypes;
@@ -35,7 +37,59 @@ namespace API.Controllers;
#nullable enable
/**
* Middleware that checks if Opds has been enabled for this server
*/
[AttributeUsage(AttributeTargets.Class)]
public class OpdsActionFilterAttribute(IUnitOfWork unitOfWork, ILocalizationService localizationService, ILogger<OpdsController> logger): ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
int userId;
try
{
if (!context.ActionArguments.TryGetValue("apiKey", out var apiKeyObj) ||
apiKeyObj is not string apiKey || context.Controller is not OpdsController controller)
{
context.Result = new BadRequestResult();
return;
}
userId = await controller.GetUser(apiKey);
if (userId == null || userId == 0)
{
context.Result = new UnauthorizedResult();
return;
}
var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
if (!settings.EnableOpds)
{
context.Result = new ContentResult
{
Content = await localizationService.Translate(userId, "opds-disabled"),
ContentType = "text/plain",
StatusCode = (int)HttpStatusCode.BadRequest,
};
return;
}
}
catch (Exception ex)
{
logger.LogError(ex, "failed to handle OPDS request");
context.Result = new BadRequestResult();
return;
}
context.HttpContext.Items.Add(OpdsController.UserId, userId);
await next();
}
}
[AllowAnonymous]
[ServiceFilter(typeof(OpdsActionFilterAttribute))]
public class OpdsController : BaseApiController
{
private readonly ILogger<OpdsController> _logger;
@@ -80,6 +134,7 @@ public class OpdsController : BaseApiController
private readonly FilterV2Dto _filterV2Dto = new FilterV2Dto();
private readonly ChapterSortComparerDefaultLast _chapterSortComparerDefaultLast = ChapterSortComparerDefaultLast.Default;
private const int PageSize = 20;
public const string UserId = nameof(UserId);
public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService,
IDirectoryService directoryService, ICacheService cacheService,
@@ -102,15 +157,17 @@ public class OpdsController : BaseApiController
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
}
private int GetUserIdFromContext()
{
return (int) HttpContext.Items[UserId]!;
}
[HttpPost("{apiKey}")]
[HttpGet("{apiKey}")]
[Produces("application/xml")]
public async Task<IActionResult> Get(string apiKey)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (_, prefix) = await GetPrefix();
var feed = CreateFeed("Kavita", string.Empty, apiKey, prefix);
@@ -316,12 +373,9 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetSmartFilter(string apiKey, int filterId, [FromQuery] int pageNumber = 0)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(filterId);
if (filter == null) return BadRequest(_localizationService.Translate(userId, "smart-filter-doesnt-exist"));
var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters-" + filter.Id), $"{apiKey}/smart-filters/{filter.Id}/", apiKey, prefix);
@@ -345,9 +399,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetSmartFilters(string apiKey)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (_, prefix) = await GetPrefix();
var filters = _unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(userId);
@@ -376,9 +428,7 @@ public class OpdsController : BaseApiController
public async Task<IActionResult> GetExternalSources(string apiKey)
{
// NOTE: This doesn't seem possible in OPDS v2.1 due to the resulting stream using relative links and most apps resolve against source url. Even using full paths doesn't work
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (_, prefix) = await GetPrefix();
var externalSources = await _unitOfWork.AppUserExternalSourceRepository.GetExternalSources(userId);
@@ -408,9 +458,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetLibraries(string apiKey)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var feed = CreateFeed(await _localizationService.Translate(userId, "libraries"), $"{apiKey}/libraries", apiKey, prefix);
SetFeedId(feed, "libraries");
@@ -442,9 +490,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetWantToRead(string apiKey, [FromQuery] int pageNumber = 0)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var wantToReadSeries = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(userId, GetUserParams(pageNumber), _filterV2Dto);
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(wantToReadSeries.Select(s => s.Id));
@@ -463,9 +509,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetCollections(string apiKey)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return Unauthorized();
@@ -501,9 +545,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetCollection(int collectionId, string apiKey, [FromQuery] int pageNumber = 0)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (user == null) return Unauthorized();
@@ -534,9 +576,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetReadingLists(string apiKey, [FromQuery] int pageNumber = 0)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId,
@@ -583,7 +623,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetReadingListItems(int readingListId, string apiKey, [FromQuery] int pageNumber = 0)
{
var userId = await GetUser(apiKey);
var userId = GetUserIdFromContext();
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
{
@@ -633,9 +673,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetSeriesForLibrary(int libraryId, string apiKey, [FromQuery] int pageNumber = 0)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var library =
(await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(userId)).SingleOrDefault(l =>
@@ -674,9 +712,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetRecentlyAdded(string apiKey, [FromQuery] int pageNumber = 1)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var recentlyAdded = await _unitOfWork.SeriesRepository.GetRecentlyAddedV2(userId, GetUserParams(pageNumber), _filterV2Dto);
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(recentlyAdded.Select(s => s.Id));
@@ -697,9 +733,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetMoreInGenre(string apiKey, [FromQuery] int genreId, [FromQuery] int pageNumber = 1)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var genre = await _unitOfWork.GenreRepository.GetGenreById(genreId);
var seriesDtos = await _unitOfWork.SeriesRepository.GetMoreIn(userId, 0, genreId, GetUserParams(pageNumber));
@@ -721,13 +755,21 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetRecentlyUpdated(string apiKey, [FromQuery] int pageNumber = 1)
{
var userId = await GetUser(apiKey);
var userId = GetUserIdFromContext();
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
{
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var (baseUrl, prefix) = await GetPrefix();
var seriesDtos = (await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId, PageSize)).ToList();
}
var userParams = new UserParams
{
PageNumber = pageNumber,
PageSize = PageSize,
};
var seriesDtos = (await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId, userParams)).ToList();
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(seriesDtos.Select(s => s.SeriesId));
var (baseUrl, prefix) = await GetPrefix();
var feed = CreateFeed(await _localizationService.Translate(userId, "recently-updated"), $"{apiKey}/recently-updated", apiKey, prefix);
SetFeedId(feed, "recently-updated");
@@ -751,10 +793,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetOnDeck(string apiKey, [FromQuery] int pageNumber = 1)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var userParams = GetUserParams(pageNumber);
@@ -785,9 +824,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> SearchSeries(string apiKey, [FromQuery] string query)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
@@ -859,9 +896,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetSearchDescriptor(string apiKey)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (_, prefix) = await GetPrefix();
var feed = new OpenSearchDescription()
{
@@ -884,9 +919,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetSeries(string apiKey, int seriesId)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
@@ -958,24 +991,34 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetVolume(string apiKey, int seriesId, int volumeId)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
if (series == null)
{
return NotFound();
}
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId, VolumeIncludes.Chapters);
if (volume == null)
{
return NotFound();
}
var feed = CreateFeed(series.Name + " - Volume " + volume!.Name + $" - {_seriesService.FormatChapterName(userId, libraryType)}s ",
$"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix);
SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}-{_seriesService.FormatChapterName(userId, libraryType)}s");
foreach (var chapter in volume.Chapters)
foreach (var chapterId in volume.Chapters.Select(c => c.Id))
{
var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapter.Id, ChapterIncludes.Files | ChapterIncludes.People);
var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId, ChapterIncludes.Files | ChapterIncludes.People);
if (chapterDto == null) continue;
foreach (var mangaFile in chapterDto.Files)
{
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapter.Id, mangaFile, series, chapterDto!, apiKey, prefix, baseUrl));
feed.Entries.Add(await CreateChapterWithFile(userId, seriesId, volumeId, chapterId, mangaFile, series, chapterDto!, apiKey, prefix, baseUrl));
}
}
@@ -986,9 +1029,7 @@ public class OpdsController : BaseApiController
[Produces("application/xml")]
public async Task<IActionResult> GetChapter(string apiKey, int seriesId, int volumeId, int chapterId)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var userId = GetUserIdFromContext();
var (baseUrl, prefix) = await GetPrefix();
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
@@ -1023,10 +1064,8 @@ public class OpdsController : BaseApiController
[HttpGet("{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}/download/{filename}")]
public async Task<ActionResult> DownloadFile(string apiKey, int seriesId, int volumeId, int chapterId, string filename)
{
var userId = await GetUser(apiKey);
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
var userId = GetUserIdFromContext();
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
if (!await _accountService.HasDownloadPermission(user))
{
return Forbid("User does not have download permissions");
@@ -1249,7 +1288,7 @@ public class OpdsController : BaseApiController
public async Task<ActionResult> GetPageStreamedImage(string apiKey, [FromQuery] int libraryId, [FromQuery] int seriesId,
[FromQuery] int volumeId,[FromQuery] int chapterId, [FromQuery] int pageNumber, [FromQuery] bool saveProgress = true)
{
var userId = await GetUser(apiKey);
var userId = GetUserIdFromContext();
if (pageNumber < 0) return BadRequest(await _localizationService.Translate(userId, "greater-0", "Page"));
var chapter = await _cacheService.Ensure(chapterId, true);
if (chapter == null) return BadRequest(await _localizationService.Translate(userId, "cache-file-find"));
@@ -1293,7 +1332,7 @@ public class OpdsController : BaseApiController
[ResponseCache(Duration = 60 * 60, Location = ResponseCacheLocation.Client, NoStore = false)]
public async Task<ActionResult> GetFavicon(string apiKey)
{
var userId = await GetUser(apiKey);
var userId = GetUserIdFromContext();
var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico");
if (files.Length == 0) return BadRequest(await _localizationService.Translate(userId, "favicon-doesnt-exist"));
var path = files[0];
@@ -1307,7 +1346,7 @@ public class OpdsController : BaseApiController
/// Gets the user from the API key
/// </summary>
/// <returns></returns>
private async Task<int> GetUser(string apiKey)
public async Task<int> GetUser(string apiKey)
{
try
{