More cleanup, handling edge cases, and todos for original creator.
This commit is contained in:
parent
9893c9f473
commit
231db28a5e
7 changed files with 117 additions and 40 deletions
|
@ -21,7 +21,7 @@ public class KoreaderHelperTests
|
||||||
expected.PageNum = page;
|
expected.PageNum = page;
|
||||||
var actual = EmptyProgressDto();
|
var actual = EmptyProgressDto();
|
||||||
|
|
||||||
KoreaderHelper.UpdateProgressDto(koreaderPosition, actual);
|
KoreaderHelper.UpdateProgressDto(actual, koreaderPosition);
|
||||||
Assert.Equal(expected.BookScrollId, actual.BookScrollId);
|
Assert.Equal(expected.BookScrollId, actual.BookScrollId);
|
||||||
Assert.Equal(expected.PageNum, actual.PageNum);
|
Assert.Equal(expected.PageNum, actual.PageNum);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,15 +13,15 @@ using Microsoft.Extensions.Logging;
|
||||||
using static System.Net.WebRequestMethods;
|
using static System.Net.WebRequestMethods;
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The endpoint to interface with Koreader's Progress Sync plugin.
|
/// The endpoint to interface with Koreader's Progress Sync plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Koreader uses a different form of authentication. It stores the username and password in headers.
|
/// 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
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <see cref="https://github.com/koreader/koreader/blob/master/plugins/kosync.koplugin/KOSyncClient.lua"/>
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public class KoreaderController : BaseApiController
|
public class KoreaderController : BaseApiController
|
||||||
{
|
{
|
||||||
|
@ -59,17 +59,22 @@ public class KoreaderController : BaseApiController
|
||||||
return Ok(new { username = user.UserName });
|
return Ok(new { username = user.UserName });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Syncs book progress with Kavita. Will attempt to save the underlying reader position if possible.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiKey"></param>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPut("{apiKey}/syncs/progress")]
|
[HttpPut("{apiKey}/syncs/progress")]
|
||||||
public async Task<IActionResult> UpdateProgress(string apiKey, KoreaderBookDto request)
|
public async Task<ActionResult<KoreaderProgressUpdateDto>> UpdateProgress(string apiKey, KoreaderBookDto request)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Koreader sync progress: {Progress}", request.Progress);
|
|
||||||
var userId = await GetUserId(apiKey);
|
var userId = await GetUserId(apiKey);
|
||||||
await _koreaderService.SaveProgress(request, userId);
|
await _koreaderService.SaveProgress(request, userId);
|
||||||
|
|
||||||
return Ok(new { document = request.Document, timestamp = DateTime.UtcNow });
|
return Ok(new KoreaderProgressUpdateDto{ Document = request.Document, Timestamp = DateTime.UtcNow });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("{apiKey}/syncs/progress/{ebookHash}")]
|
[HttpGet("{apiKey}/syncs/progress/{ebookHash}")]
|
||||||
public async Task<ActionResult<KoreaderBookDto>> GetProgress(string apiKey, string ebookHash)
|
public async Task<ActionResult<KoreaderBookDto>> GetProgress(string apiKey, string ebookHash)
|
||||||
{
|
{
|
||||||
|
@ -80,11 +85,6 @@ public class KoreaderController : BaseApiController
|
||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user from the API key
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The user's Id</returns>
|
|
||||||
private async Task<int> GetUserId(string apiKey)
|
private async Task<int> GetUserId(string apiKey)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
9
API/DTOs/Koreader/KoreaderProgressUpdateDto.cs
Normal file
9
API/DTOs/Koreader/KoreaderProgressUpdateDto.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace API.DTOs.Koreader;
|
||||||
|
|
||||||
|
public class KoreaderProgressUpdateDto
|
||||||
|
{
|
||||||
|
public string Document { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
@ -11,7 +10,7 @@ public interface IMangaFileRepository
|
||||||
{
|
{
|
||||||
void Update(MangaFile file);
|
void Update(MangaFile file);
|
||||||
Task<IList<MangaFile>> GetAllWithMissingExtension();
|
Task<IList<MangaFile>> GetAllWithMissingExtension();
|
||||||
Task<MangaFile> GetByKoreaderHash(string hash);
|
Task<MangaFile?> GetByKoreaderHash(string hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MangaFileRepository : IMangaFileRepository
|
public class MangaFileRepository : IMangaFileRepository
|
||||||
|
@ -35,9 +34,12 @@ public class MangaFileRepository : IMangaFileRepository
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<MangaFile> GetByKoreaderHash(string hash)
|
public async Task<MangaFile?> GetByKoreaderHash(string hash)
|
||||||
{
|
{
|
||||||
return _context.MangaFile
|
if (string.IsNullOrEmpty(hash)) return null;
|
||||||
.FirstOrDefaultAsync(f => f.KoreaderHash == hash.ToUpper());
|
|
||||||
|
return await _context.MangaFile
|
||||||
|
.FirstOrDefaultAsync(f => !string.IsNullOrEmpty(f.KoreaderHash)
|
||||||
|
&& f.KoreaderHash.Equals(hash, System.StringComparison.CurrentCultureIgnoreCase));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
42
API/Helpers/Builders/KoreaderBookDtoBuilder.cs
Normal file
42
API/Helpers/Builders/KoreaderBookDtoBuilder.cs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
using API.DTOs.Koreader;
|
||||||
|
|
||||||
|
namespace API.Helpers.Builders;
|
||||||
|
|
||||||
|
public class KoreaderBookDtoBuilder : IEntityBuilder<KoreaderBookDto>
|
||||||
|
{
|
||||||
|
private readonly KoreaderBookDto _dto;
|
||||||
|
public KoreaderBookDto Build() => _dto;
|
||||||
|
|
||||||
|
public KoreaderBookDtoBuilder(string documentHash)
|
||||||
|
{
|
||||||
|
_dto = new KoreaderBookDto()
|
||||||
|
{
|
||||||
|
Document = documentHash,
|
||||||
|
Device = "Kavita"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public KoreaderBookDtoBuilder WithDocument(string documentHash)
|
||||||
|
{
|
||||||
|
_dto.Document = documentHash;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KoreaderBookDtoBuilder WithProgress(string progress)
|
||||||
|
{
|
||||||
|
_dto.Progress = progress;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KoreaderBookDtoBuilder WithPercentage(int? pageNum, int pages)
|
||||||
|
{
|
||||||
|
_dto.Percentage = (pageNum ?? 0) / (float) pages;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KoreaderBookDtoBuilder WithDeviceId(string installId, int userId)
|
||||||
|
{
|
||||||
|
_dto.Device_id = installId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,7 +66,7 @@ public static class KoreaderHelper
|
||||||
return BitConverter.ToString(bytes).Replace("-", string.Empty);
|
return BitConverter.ToString(bytes).Replace("-", string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void UpdateProgressDto(string koreaderPosition, ProgressDto progress)
|
public static void UpdateProgressDto(ProgressDto progress, string koreaderPosition)
|
||||||
{
|
{
|
||||||
var path = koreaderPosition.Split('/');
|
var path = koreaderPosition.Split('/');
|
||||||
if (path.Length < 6)
|
if (path.Length < 6)
|
||||||
|
@ -94,6 +94,7 @@ public static class KoreaderHelper
|
||||||
{
|
{
|
||||||
string lastTag;
|
string lastTag;
|
||||||
var koreaderPageNumber = progressDto.PageNum + 1;
|
var koreaderPageNumber = progressDto.PageNum + 1;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(progressDto.BookScrollId))
|
if (string.IsNullOrEmpty(progressDto.BookScrollId))
|
||||||
{
|
{
|
||||||
lastTag = "a";
|
lastTag = "a";
|
||||||
|
@ -103,6 +104,7 @@ public static class KoreaderHelper
|
||||||
var tokens = progressDto.BookScrollId.Split('/');
|
var tokens = progressDto.BookScrollId.Split('/');
|
||||||
lastTag = tokens[^1].ToLower();
|
lastTag = tokens[^1].ToLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The format that Koreader accepts as a progress string. It tells Koreader where Kavita last left off.
|
// The format that Koreader accepts as a progress string. It tells Koreader where Kavita last left off.
|
||||||
return $"/body/DocFragment[{koreaderPageNumber}]/body/div/{lastTag}";
|
return $"/body/DocFragment[{koreaderPageNumber}]/body/div/{lastTag}";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs.Koreader;
|
using API.DTOs.Koreader;
|
||||||
|
using API.DTOs.Progress;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
|
using API.Helpers.Builders;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Services;
|
namespace API.Services;
|
||||||
|
@ -16,45 +18,65 @@ public interface IKoreaderService
|
||||||
|
|
||||||
public class KoreaderService : IKoreaderService
|
public class KoreaderService : IKoreaderService
|
||||||
{
|
{
|
||||||
private IReaderService _readerService;
|
private readonly IReaderService _readerService;
|
||||||
private IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private ILogger<KoreaderService> _logger;
|
private readonly ILogger<KoreaderService> _logger;
|
||||||
|
|
||||||
public KoreaderService(IReaderService readerService, IUnitOfWork unitOfWork,
|
public KoreaderService(IReaderService readerService, IUnitOfWork unitOfWork, ILogger<KoreaderService> logger)
|
||||||
ILogger<KoreaderService> logger)
|
|
||||||
{
|
{
|
||||||
_readerService = readerService;
|
_readerService = readerService;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given a Koreader hash, locate the underlying file and generate/update a progress event.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="koreaderBookDto"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId)
|
public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Saving Koreader progress for {UserId}: {KoreaderProgress}", userId, koreaderBookDto.Progress);
|
||||||
var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.Document);
|
var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.Document);
|
||||||
|
if (file == null) return;
|
||||||
|
|
||||||
var userProgressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId);
|
var userProgressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId);
|
||||||
|
if (userProgressDto == null)
|
||||||
|
{
|
||||||
|
// TODO: Handle this case
|
||||||
|
userProgressDto = new ProgressDto()
|
||||||
|
{
|
||||||
|
ChapterId = file.ChapterId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Update the bookScrollId if possible
|
||||||
|
KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.Progress);
|
||||||
|
|
||||||
_logger.LogInformation("Saving Koreader progress to Kavita: {KoreaderProgress}", koreaderBookDto.Progress);
|
|
||||||
KoreaderHelper.UpdateProgressDto(koreaderBookDto.Progress, userProgressDto);
|
|
||||||
await _readerService.SaveReadingProgress(userProgressDto, userId);
|
await _readerService.SaveReadingProgress(userProgressDto, userId);
|
||||||
|
|
||||||
await _unitOfWork.CommitAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a Koreader Dto representing current book and the progress within
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bookHash"></param>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public async Task<KoreaderBookDto> GetProgress(string bookHash, int userId)
|
public async Task<KoreaderBookDto> GetProgress(string bookHash, int userId)
|
||||||
{
|
{
|
||||||
var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(bookHash);
|
|
||||||
var progressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId);
|
|
||||||
_logger.LogInformation("Transmitting Kavita progress to Koreader: {KoreaderProgress}", progressDto.BookScrollId);
|
|
||||||
var koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto);
|
|
||||||
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
|
var builder = new KoreaderBookDtoBuilder(bookHash);
|
||||||
|
|
||||||
return new KoreaderBookDto
|
var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(bookHash);
|
||||||
{
|
|
||||||
Document = bookHash,
|
// TODO: How do we handle when file isn't found by hash?
|
||||||
Device_id = settingsDto.InstallId,
|
if (file == null) return builder.Build();
|
||||||
Device = "Kavita",
|
|
||||||
Progress = koreaderProgress,
|
var progressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId);
|
||||||
Percentage = progressDto.PageNum / (float) file.Pages
|
var koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto);
|
||||||
};
|
|
||||||
|
return builder.WithProgress(koreaderProgress)
|
||||||
|
.WithPercentage(progressDto?.PageNum, file.Pages)
|
||||||
|
.WithDeviceId(settingsDto.InstallId, userId) // TODO: Should we generate a hash for UserId + InstallId so that this DeviceId is unique to the user on the server?
|
||||||
|
.Build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue