mirror of
				https://github.com/Kareadita/Kavita.git
				synced 2025-11-03 19:17:05 -05:00 
			
		
		
		
	* Moved logic from TachiyomiController.cs to TachiyomiService.cs * Added GetLatestChapter Unit Tests * Tachiyomi more tests. Implemented test for yearly volumes * MarkVolumesUntilAsRead unit test * Registered tachiyomi service. Added new test * Fixed test pages * Added missing check if its single-file volume * Removed dead code * Added method documentation and breaked thousands with `_` * Review details and renamed test method to be more descriptive * Review changes - Removed automapper - Added spaces after commas - Added class documentation (copied from controller) - Made Culture static - Added 'R' doc linking to docs.ms - Added trycatch to service when saving progress and logged - Removed redundant qualifiers * finishing touches Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
		
			
				
	
	
		
			159 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			159 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using System;
 | 
						|
using API.DTOs;
 | 
						|
using System.Threading.Tasks;
 | 
						|
using API.Data;
 | 
						|
using System.Collections.Immutable;
 | 
						|
using System.Collections.Generic;
 | 
						|
using System.Globalization;
 | 
						|
using System.Linq;
 | 
						|
using API.Comparators;
 | 
						|
using API.Entities;
 | 
						|
using AutoMapper;
 | 
						|
using Microsoft.Extensions.Logging;
 | 
						|
 | 
						|
namespace API.Services;
 | 
						|
 | 
						|
public interface ITachiyomiService
 | 
						|
{
 | 
						|
    Task<ChapterDto> GetLatestChapter(int seriesId, int userId);
 | 
						|
    Task<bool> MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber);
 | 
						|
}
 | 
						|
 | 
						|
/// <summary>
 | 
						|
/// All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any
 | 
						|
/// other purposes.
 | 
						|
/// </summary>
 | 
						|
public class TachiyomiService : ITachiyomiService
 | 
						|
{
 | 
						|
    private readonly IUnitOfWork _unitOfWork;
 | 
						|
    private readonly IMapper _mapper;
 | 
						|
    private readonly ILogger<ReaderService> _logger;
 | 
						|
    private readonly IReaderService _readerService;
 | 
						|
 | 
						|
    private static readonly CultureInfo EnglishCulture = CultureInfo.CreateSpecificCulture("en-US");
 | 
						|
 | 
						|
    public TachiyomiService(IUnitOfWork unitOfWork, IMapper mapper, ILogger<ReaderService> logger, IReaderService readerService)
 | 
						|
    {
 | 
						|
        _unitOfWork = unitOfWork;
 | 
						|
        _readerService = readerService;
 | 
						|
        _mapper = mapper;
 | 
						|
        _logger = logger;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Gets the latest chapter/volume read.
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="seriesId"></param>
 | 
						|
    /// <param name="userId"></param>
 | 
						|
    /// <returns>Due to how Tachiyomi works we need a hack to properly return both chapters and volumes.
 | 
						|
    /// If its a chapter, return the chapterDto as is.
 | 
						|
    /// If it's a volume, the volume number gets returned in the 'Number' attribute of a chapterDto encoded.
 | 
						|
    /// The volume number gets divided by 10,000 because that's how Tachiyomi interprets volumes</returns>
 | 
						|
    public async Task<ChapterDto> GetLatestChapter(int seriesId, int userId)
 | 
						|
    {
 | 
						|
 | 
						|
 | 
						|
        var currentChapter = await _readerService.GetContinuePoint(seriesId, userId);
 | 
						|
 | 
						|
        var prevChapterId =
 | 
						|
            await _readerService.GetPrevChapterIdAsync(seriesId, currentChapter.VolumeId, currentChapter.Id, userId);
 | 
						|
 | 
						|
        // If prevChapterId is -1, this means either nothing is read or everything is read.
 | 
						|
        if (prevChapterId == -1)
 | 
						|
        {
 | 
						|
            var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
 | 
						|
            var userHasProgress = series.PagesRead != 0 && series.PagesRead <= series.Pages;
 | 
						|
 | 
						|
            // If the user doesn't have progress, then return null, which the extension will catch as 204 (no content) and report nothing as read
 | 
						|
            if (!userHasProgress) return null;
 | 
						|
 | 
						|
            // Else return the max chapter to Tachiyomi so it can consider everything read
 | 
						|
            var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(seriesId)).ToImmutableList();
 | 
						|
            var looseLeafChapterVolume = volumes.FirstOrDefault(v => v.Number == 0);
 | 
						|
            if (looseLeafChapterVolume == null)
 | 
						|
            {
 | 
						|
                var volumeChapter = _mapper.Map<ChapterDto>(volumes.Last().Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparerZeroFirst.Default).Last());
 | 
						|
                if (volumeChapter.Number == "0")
 | 
						|
                {
 | 
						|
                    var volume = volumes.First(v => v.Id == volumeChapter.VolumeId);
 | 
						|
                    return new ChapterDto()
 | 
						|
                    {
 | 
						|
                        // Use R to ensure that localization of underlying system doesn't affect the stringification
 | 
						|
                        // https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework
 | 
						|
                        Number = (volume.Number / 10_000f).ToString("R", EnglishCulture)
 | 
						|
                    };
 | 
						|
                }
 | 
						|
 | 
						|
                return new ChapterDto()
 | 
						|
                {
 | 
						|
                    Number = (int.Parse(volumeChapter.Number) / 10_000f).ToString("R", EnglishCulture)
 | 
						|
                };
 | 
						|
            }
 | 
						|
 | 
						|
            var lastChapter = looseLeafChapterVolume.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default).Last();
 | 
						|
            return _mapper.Map<ChapterDto>(lastChapter);
 | 
						|
        }
 | 
						|
 | 
						|
        // There is progress, we now need to figure out the highest volume or chapter and return that.
 | 
						|
        var prevChapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId);
 | 
						|
        var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId);
 | 
						|
        // We only encode for single-file volumes
 | 
						|
        if (volumeWithProgress.Number != 0 && volumeWithProgress.Chapters.Count == 1)
 | 
						|
        {
 | 
						|
            // The progress is on a volume, encode it as a fake chapterDTO
 | 
						|
            return new ChapterDto()
 | 
						|
            {
 | 
						|
                // Use R to ensure that localization of underlying system doesn't affect the stringification
 | 
						|
                // https://docs.microsoft.com/en-us/globalization/locale/number-formatting-in-dotnet-framework
 | 
						|
                Number = (volumeWithProgress.Number / 10_000f).ToString("R", EnglishCulture)
 | 
						|
 | 
						|
            };
 | 
						|
        }
 | 
						|
 | 
						|
        // Progress is just on a chapter, return as is
 | 
						|
        return prevChapter;
 | 
						|
    }
 | 
						|
 | 
						|
    /// <summary>
 | 
						|
    /// Marks every chapter and volume that is sorted below the passed number as Read. This will not mark any specials as read.
 | 
						|
    /// Passed number will also be marked as read
 | 
						|
    /// </summary>
 | 
						|
    /// <param name="userWithProgress"></param>
 | 
						|
    /// <param name="seriesId"></param>
 | 
						|
    /// <param name="chapterNumber">Can also be a Tachiyomi encoded volume number</param>
 | 
						|
    public async Task<bool> MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber)
 | 
						|
    {
 | 
						|
        userWithProgress.Progresses ??= new List<AppUserProgress>();
 | 
						|
 | 
						|
        switch (chapterNumber)
 | 
						|
        {
 | 
						|
            // When Tachiyomi sync's progress, if there is no current progress in Tachiyomi, 0.0f is sent.
 | 
						|
            // Due to the encoding for volumes, this marks all chapters in volume 0 (loose chapters) as read.
 | 
						|
            // Hence we catch and return early, so we ignore the request.
 | 
						|
            case 0.0f:
 | 
						|
                return true;
 | 
						|
            case < 1.0f:
 | 
						|
            {
 | 
						|
                // This is a hack to track volume number. We need to map it back by x10,000
 | 
						|
                var volumeNumber = int.Parse($"{(int)(chapterNumber * 10_000)}", EnglishCulture);
 | 
						|
                await _readerService.MarkVolumesUntilAsRead(userWithProgress, seriesId, volumeNumber);
 | 
						|
                break;
 | 
						|
            }
 | 
						|
            default:
 | 
						|
                await _readerService.MarkChaptersUntilAsRead(userWithProgress, seriesId, chapterNumber);
 | 
						|
                break;
 | 
						|
        }
 | 
						|
 | 
						|
        try {
 | 
						|
            _unitOfWork.UserRepository.Update(userWithProgress);
 | 
						|
 | 
						|
            if (!_unitOfWork.HasChanges()) return true;
 | 
						|
            if (await _unitOfWork.CommitAsync()) return true;
 | 
						|
        } catch (Exception ex) {
 | 
						|
            _logger.LogError(ex, "There was an error saving progress from tachiyomi");
 | 
						|
            await _unitOfWork.RollbackAsync();
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
}
 |