diff --git a/API.Tests/Helpers/KoreaderHelperTests.cs b/API.Tests/Helpers/KoreaderHelperTests.cs index 24eb7cfe1..ec35dcccb 100644 --- a/API.Tests/Helpers/KoreaderHelperTests.cs +++ b/API.Tests/Helpers/KoreaderHelperTests.cs @@ -14,7 +14,7 @@ public class KoreaderHelperTests public void GetEpubPositionDto(string koreaderPosition, int page, int? pNumber) { var expected = EmptyProgressDto(); - expected.BookScrollId = pNumber.HasValue ? $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/DIV/P[{pNumber}]" : null; + expected.BookScrollId = pNumber.HasValue ? $"//BODY/DIV/P[{pNumber}]" : null; expected.PageNum = page; var actual = EmptyProgressDto(); @@ -28,7 +28,7 @@ public class KoreaderHelperTests public void GetEpubPositionDtoWithExtraXpath(string koreaderPosition, int page, int? pNumber) { var expected = EmptyProgressDto(); - expected.BookScrollId = pNumber.HasValue ? $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/DIV/P[{pNumber}]/text().264" : null; + expected.BookScrollId = pNumber.HasValue ? $"//BODY/DIV/P[{pNumber}]" : null; expected.PageNum = page; var actual = EmptyProgressDto(); @@ -40,8 +40,8 @@ public class KoreaderHelperTests [Theory] - [InlineData("//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[20]", 5, "/body/DocFragment[6]/body/p[20]")] - [InlineData(null, 10, "/body/DocFragment[11]/body/a")] // I've not seen a null/just an a from Koreader in testing + [InlineData("//body/p[20]", 5, "/body/DocFragment[5]/body/p[20]")] + [InlineData(null, 10, "/body/DocFragment[10]/body/p[1]")] // I've not seen a null/just an a from Koreader in testing public void GetKoreaderPosition(string scrollId, int page, string koreaderPosition) { var given = EmptyProgressDto(); diff --git a/API/Controllers/KoreaderController.cs b/API/Controllers/KoreaderController.cs index 7e87b7aa5..dd0c38ebb 100644 --- a/API/Controllers/KoreaderController.cs +++ b/API/Controllers/KoreaderController.cs @@ -19,10 +19,8 @@ namespace API.Controllers; /// Koreader uses a different form of authentication. It stores the username and password in headers. /// https://github.com/koreader/koreader/blob/master/plugins/kosync.koplugin/KOSyncClient.lua /// -[AllowAnonymous] public class KoreaderController : BaseApiController { - private readonly IUnitOfWork _unitOfWork; private readonly ILocalizationService _localizationService; private readonly IKoreaderService _koreaderService; @@ -37,23 +35,10 @@ public class KoreaderController : BaseApiController _logger = logger; } - // We won't allow users to be created from Koreader. Rather, they - // must already have an account. - /* - [HttpPost("/users/create")] - public IActionResult CreateUser(CreateUserRequest request) - { - } - */ - [HttpGet("{apiKey}/users/auth")] - public async Task Authenticate(string apiKey) + public IActionResult Authenticate(string apiKey) { - var userId = await GetUserId(apiKey); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId); - if (user == null) return Unauthorized(); - - return Ok(new { username = user.UserName }); + return Ok(new { username = Username }); } /// @@ -67,8 +52,7 @@ public class KoreaderController : BaseApiController { try { - var userId = await GetUserId(apiKey); - await _koreaderService.SaveProgress(request, userId); + await _koreaderService.SaveProgress(request, UserId); return Ok(new KoreaderProgressUpdateDto{ Document = request.document, Timestamp = DateTime.UtcNow }); } @@ -89,9 +73,8 @@ public class KoreaderController : BaseApiController { try { - var userId = await GetUserId(apiKey); - var response = await _koreaderService.GetProgress(ebookHash, userId); - _logger.LogDebug("Koreader response progress for User ({UserId}): {Progress}", userId, response.progress.Sanitize()); + var response = await _koreaderService.GetProgress(ebookHash, UserId); + _logger.LogDebug("Koreader response progress for User ({UserName}): {Progress}", Username, response.progress.Sanitize()); // We must pack this manually for Koreader due to a bug in their code: https://github.com/koreader/koreader/issues/13629 @@ -109,16 +92,4 @@ public class KoreaderController : BaseApiController return BadRequest(ex.Message); } } - - private async Task GetUserId(string apiKey) - { - try - { - return await _unitOfWork.UserRepository.GetUserIdByAuthKeyAsync(apiKey); - } - catch - { - throw new KavitaException(await _localizationService.Get("en", "user-doesnt-exist")); - } - } } diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 906179faa..b074dc15f 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -8,11 +8,8 @@ using API.DTOs.OPDS.Requests; using API.DTOs.Progress; using API.Entities.Enums; using API.Exceptions; -using API.Extensions; -using API.Middleware; using API.Services; using API.Services.Reading; -using API.Services.Store; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -25,7 +22,6 @@ namespace API.Controllers; public class OpdsController : BaseApiController { private readonly IOpdsService _opdsService; - private readonly IUserContext _userContext; private readonly IUnitOfWork _unitOfWork; private readonly IDownloadService _downloadService; private readonly IDirectoryService _directoryService; @@ -38,7 +34,7 @@ public class OpdsController : BaseApiController public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService, IDirectoryService directoryService, ICacheService cacheService, IReaderService readerService, IAccountService accountService, - ILocalizationService localizationService, IOpdsService opdsService, IUserContext userContext) + ILocalizationService localizationService, IOpdsService opdsService) { _unitOfWork = unitOfWork; _downloadService = downloadService; @@ -48,7 +44,6 @@ public class OpdsController : BaseApiController _accountService = accountService; _localizationService = localizationService; _opdsService = opdsService; - _userContext = userContext; _xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription)); } @@ -751,7 +746,6 @@ public class OpdsController : BaseApiController [ResponseCache(Duration = 60 * 60, Location = ResponseCacheLocation.Client, NoStore = false)] public async Task GetFavicon(string apiKey) { - var userId = UserId; var files = _directoryService.GetFilesWithExtension(Path.Join(Directory.GetCurrentDirectory(), ".."), @"\.ico"); if (files.Length == 0) return BadRequest(await _localizationService.Translate(UserId, "favicon-doesnt-exist")); diff --git a/API/Helpers/KoreaderHelper.cs b/API/Helpers/KoreaderHelper.cs index 30cab0082..b69a8c241 100644 --- a/API/Helpers/KoreaderHelper.cs +++ b/API/Helpers/KoreaderHelper.cs @@ -92,7 +92,8 @@ public static class KoreaderHelper var lastPart = koreaderPosition.Split("/body/")[^1]; var lastTag = path[5].ToUpper(); - // TODO: Enhance this code: /body/DocFragment[27]/body/section/p[3]/text().229 -> p[3] but we probably can get more + // If lastPart ends in a .Decimal, remove it as it's not a valid xpath + lastPart = lastPart.Split("/text()")[0]; if (lastTag == "A") { @@ -101,28 +102,27 @@ public static class KoreaderHelper else { // The format that Kavita accepts as a progress string. It tells Kavita where Koreader last left off. - progress.BookScrollId = $"//html[1]/{BookService.BookReaderBodyScope[2..].ToLowerInvariant()}/{lastPart}"; + progress.BookScrollId = $"//body/{lastPart}"; } } - + /// + /// The format that Koreader accepts as a progress string. It tells Koreader where Kavita last left off. + /// + /// + /// Koreader stores the format as: + /// /body/DocFragment[fragment_index]/body/[xpath_to_element] + /// fragment_index is the page number for the xhtml files + /// + /// + /// public static string GetKoreaderPosition(ProgressDto progressDto) { - string nonBodyTag; - var koreaderPageNumber = progressDto.PageNum + 1; - if (string.IsNullOrEmpty(progressDto.BookScrollId)) - { - nonBodyTag = "a"; - } - else - { - // What we Store: //html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/section/p[62]/text().0 - // What we Need to send back: section/p[62]/text().0 - nonBodyTag = progressDto.BookScrollId.Replace("//html[1]/", "//", StringComparison.InvariantCultureIgnoreCase).Replace(BookService.BookReaderBodyScope + "/", string.Empty, StringComparison.InvariantCultureIgnoreCase); - } + var targetPath = !string.IsNullOrEmpty(progressDto.BookScrollId) + ? progressDto.BookScrollId.Replace("//body/", string.Empty, StringComparison.InvariantCultureIgnoreCase) + : "p[1]"; // Default to first paragraph if unknown - // The format that Koreader accepts as a progress string. It tells Koreader where Kavita last left off. - return $"/body/DocFragment[{koreaderPageNumber}]/body/{nonBodyTag}"; + return $"/body/DocFragment[{progressDto.PageNum}]/body/{targetPath}"; } } diff --git a/API/Services/KoreaderService.cs b/API/Services/KoreaderService.cs index 1362fecbb..209157113 100644 --- a/API/Services/KoreaderService.cs +++ b/API/Services/KoreaderService.cs @@ -64,8 +64,10 @@ public class KoreaderService : IKoreaderService // Update the bookScrollId if possible var reportedProgress = koreaderBookDto.progress; KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.progress); - _logger.LogDebug("Converting KOReader progress from {ReportedProgress} to {ScopedProgress}", reportedProgress.Sanitize(), userProgressDto.BookScrollId?.Sanitize()); + _logger.LogDebug("Converting KOReader progress from {ReportedProgress} to {ScopedProgress}", + reportedProgress.Sanitize(), userProgressDto.BookScrollId?.Sanitize()); + // Normal saving from kavita will be //body/h2[1] await _readerService.SaveReadingProgress(userProgressDto, userId); } @@ -89,7 +91,8 @@ public class KoreaderService : IKoreaderService _logger.LogDebug("Converting KOReader progress from {KavitaProgress} to {KOReaderProgress}", originalScrollId?.Sanitize() ?? string.Empty, progressDto?.BookScrollId?.Sanitize() ?? string.Empty); - return new KoreaderBookDtoBuilder(bookHash).WithProgress(koreaderProgress) + return new KoreaderBookDtoBuilder(bookHash) + .WithProgress(koreaderProgress) .WithPercentage(progressDto?.PageNum, file.Pages) .WithDeviceId(settingsDto.InstallId, userId) .WithTimestamp(progressDto?.LastModifiedUtc) diff --git a/API/Services/Plus/SmartCollectionSyncService.cs b/API/Services/Plus/SmartCollectionSyncService.cs index b75958335..c56054d3d 100644 --- a/API/Services/Plus/SmartCollectionSyncService.cs +++ b/API/Services/Plus/SmartCollectionSyncService.cs @@ -213,7 +213,6 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService } // At this point, all series in the info have been checked and added if necessary - // You may want to commit changes to the database if needed collection.LastSyncUtc = DateTime.UtcNow.Truncate(TimeSpan.TicksPerHour); collection.TotalSourceCount = info.TotalItems; collection.Summary = info.Summary; diff --git a/API/Services/Reading/ReaderService.cs b/API/Services/Reading/ReaderService.cs index d8da8bac9..4a3c297c1 100644 --- a/API/Services/Reading/ReaderService.cs +++ b/API/Services/Reading/ReaderService.cs @@ -273,7 +273,7 @@ public class ReaderService(IUnitOfWork unitOfWork, ILogger logger } logger.LogDebug("Saving Progress on Series {SeriesId}, Chapter {ChapterId} to Page {PageNum}", progressDto.SeriesId, progressDto.ChapterId, progressDto.PageNum); - userProgress?.MarkModified(); + userProgress.MarkModified(); if (!unitOfWork.HasChanges() || await unitOfWork.CommitAsync()) {