Skip to content

Commit

Permalink
More cleanup, handling edge cases, and todos for original creator.
Browse files Browse the repository at this point in the history
  • Loading branch information
majora2007 committed Oct 26, 2024
1 parent 9893c9f commit 231db28
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 38 deletions.
2 changes: 1 addition & 1 deletion API.Tests/Helpers/KoreaderHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public void GetEpubPositionDto(string koreaderPosition, int page, int? pNumber)
expected.PageNum = page;
var actual = EmptyProgressDto();

KoreaderHelper.UpdateProgressDto(koreaderPosition, actual);
KoreaderHelper.UpdateProgressDto(actual, koreaderPosition);
Assert.Equal(expected.BookScrollId, actual.BookScrollId);
Assert.Equal(expected.PageNum, actual.PageNum);
}
Expand Down
22 changes: 11 additions & 11 deletions API/Controllers/KoreaderController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
using static System.Net.WebRequestMethods;

namespace API.Controllers;

#nullable enable

/// <summary>
/// The endpoint to interface with Koreader's Progress Sync plugin.
/// </summary>
/// <remarks>
/// 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>
/// <see cref="https://github.com/koreader/koreader/blob/master/plugins/kosync.koplugin/KOSyncClient.lua"/>
[AllowAnonymous]
public class KoreaderController : BaseApiController
{
Expand Down Expand Up @@ -59,17 +59,22 @@ public async Task<IActionResult> Authenticate(string apiKey)
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")]
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);
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}")]
public async Task<ActionResult<KoreaderBookDto>> GetProgress(string apiKey, string ebookHash)
{
Expand All @@ -80,11 +85,6 @@ public async Task<ActionResult<KoreaderBookDto>> GetProgress(string apiKey, stri
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)
{
try
Expand Down
9 changes: 9 additions & 0 deletions API/DTOs/Koreader/KoreaderProgressUpdateDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace API.DTOs.Koreader;

public class KoreaderProgressUpdateDto
{
public string Document { get; set; }
public DateTime Timestamp { get; set; }
}
14 changes: 8 additions & 6 deletions API/Data/Repositories/MangaFileRepository.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Entities;
Expand All @@ -11,7 +10,7 @@ public interface IMangaFileRepository
{
void Update(MangaFile file);
Task<IList<MangaFile>> GetAllWithMissingExtension();
Task<MangaFile> GetByKoreaderHash(string hash);
Task<MangaFile?> GetByKoreaderHash(string hash);
}

public class MangaFileRepository : IMangaFileRepository
Expand All @@ -35,9 +34,12 @@ public async Task<IList<MangaFile>> GetAllWithMissingExtension()
.ToListAsync();
}

public Task<MangaFile> GetByKoreaderHash(string hash)
public async Task<MangaFile?> GetByKoreaderHash(string hash)
{
return _context.MangaFile
.FirstOrDefaultAsync(f => f.KoreaderHash == hash.ToUpper());
if (string.IsNullOrEmpty(hash)) return null;

return await _context.MangaFile
.FirstOrDefaultAsync(f => !string.IsNullOrEmpty(f.KoreaderHash)
&& f.KoreaderHash.Equals(hash, System.StringComparison.CurrentCultureIgnoreCase));
}
}
42 changes: 42 additions & 0 deletions API/Helpers/Builders/KoreaderBookDtoBuilder.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 3 additions & 1 deletion API/Helpers/KoreaderHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public static string HashTitle(string filePath)
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('/');
if (path.Length < 6)
Expand Down Expand Up @@ -94,6 +94,7 @@ public static string GetKoreaderPosition(ProgressDto progressDto)
{
string lastTag;
var koreaderPageNumber = progressDto.PageNum + 1;

if (string.IsNullOrEmpty(progressDto.BookScrollId))
{
lastTag = "a";
Expand All @@ -103,6 +104,7 @@ public static string GetKoreaderPosition(ProgressDto progressDto)
var tokens = progressDto.BookScrollId.Split('/');
lastTag = tokens[^1].ToLower();
}

// The format that Koreader accepts as a progress string. It tells Koreader where Kavita last left off.
return $"/body/DocFragment[{koreaderPageNumber}]/body/div/{lastTag}";
}
Expand Down
60 changes: 41 additions & 19 deletions API/Services/KoreaderService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Threading.Tasks;
using API.Data;
using API.DTOs.Koreader;
using API.DTOs.Progress;
using API.Helpers;
using API.Helpers.Builders;
using Microsoft.Extensions.Logging;

namespace API.Services;
Expand All @@ -16,45 +18,65 @@ public interface IKoreaderService

public class KoreaderService : IKoreaderService
{
private IReaderService _readerService;
private IUnitOfWork _unitOfWork;
private ILogger<KoreaderService> _logger;
private readonly IReaderService _readerService;
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<KoreaderService> _logger;

public KoreaderService(IReaderService readerService, IUnitOfWork unitOfWork,
ILogger<KoreaderService> logger)
public KoreaderService(IReaderService readerService, IUnitOfWork unitOfWork, ILogger<KoreaderService> logger)
{
_readerService = readerService;
_unitOfWork = unitOfWork;
_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)
{
_logger.LogDebug("Saving Koreader progress for {UserId}: {KoreaderProgress}", userId, koreaderBookDto.Progress);
var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.Document);
if (file == null) return;

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 _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)
{
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var builder = new KoreaderBookDtoBuilder(bookHash);

var file = await _unitOfWork.MangaFileRepository.GetByKoreaderHash(bookHash);

// TODO: How do we handle when file isn't found by hash?
if (file == null) return builder.Build();

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();

return new KoreaderBookDto
{
Document = bookHash,
Device_id = settingsDto.InstallId,
Device = "Kavita",
Progress = koreaderProgress,
Percentage = progressDto.PageNum / (float) file.Pages
};
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();
}
}

0 comments on commit 231db28

Please sign in to comment.