Fix Koreader sync issues: null handling and improved position parsing

Co-authored-by: majora2007 <735851+majora2007@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-08-21 12:43:12 +00:00
parent 730a4a4589
commit e4199f6d03
2 changed files with 77 additions and 6 deletions

View File

@ -14,6 +14,7 @@ public class KoreaderHelperTests
[InlineData("/body/DocFragment[11]/body/div/a", 10, null)]
[InlineData("/body/DocFragment[1]/body/div/p[40]", 0, 40)]
[InlineData("/body/DocFragment[8]/body/div/p[28]/text().264", 7, 28)]
[InlineData("/body/DocFragment[6]/body/p[12]/text().0", 5, 12)] // Real-world example without div
public void GetEpubPositionDto(string koreaderPosition, int page, int? pNumber)
{
var expected = EmptyProgressDto();
@ -39,6 +40,35 @@ public class KoreaderHelperTests
Assert.Equal(koreaderPosition, KoreaderHelper.GetKoreaderPosition(given));
}
[Fact]
public void GetKoreaderPosition_HandlesNullProgressDto()
{
var result = KoreaderHelper.GetKoreaderPosition(null);
Assert.Equal("/body/DocFragment[1]/body/div/a", result);
}
[Theory]
[InlineData("/body/DocFragment[5]/body/p[15]/text().0")]
[InlineData("/body/DocFragment[3]/body/div/span[7]/text().123")]
[InlineData("/body/DocFragment[2]/body/h1[1]")]
public void UpdateProgressDto_HandlesVariousRealWorldFormats(string koreaderPosition)
{
var progressDto = EmptyProgressDto();
// Should not throw exception
KoreaderHelper.UpdateProgressDto(progressDto, koreaderPosition);
// Should extract valid page number
var parts = koreaderPosition.Split('/');
var expectedDocNumber = parts[2].Replace("DocFragment[", "").Replace("]", "");
var expectedPage = int.Parse(expectedDocNumber) - 1;
Assert.Equal(expectedPage, progressDto.PageNum);
// Should have valid BookScrollId (unless element is 'a')
Assert.NotNull(progressDto.BookScrollId);
}
[Theory]
[InlineData("./Data/AesopsFables.epub", "8795ACA4BF264B57C1EEDF06A0CEE688")]
public void GetKoreaderHash(string filePath, string hash)

View File

@ -71,29 +71,57 @@ public static class KoreaderHelper
public static void UpdateProgressDto(ProgressDto progress, string koreaderPosition)
{
var path = koreaderPosition.Split('/');
if (path.Length < 6)
if (path.Length < 5)
{
return;
}
var docNumber = path[2].Replace("DocFragment[", string.Empty).Replace("]", string.Empty);
progress.PageNum = int.Parse(docNumber) - 1;
var lastTag = path[5].ToUpper();
// Find the most specific element to use for BookScrollId
// Koreader formats can be:
// /body/DocFragment[X]/body/div/a
// /body/DocFragment[X]/body/div/p[Y]
// /body/DocFragment[X]/body/p[Y]/text().Z
// /body/DocFragment[X]/body/div/p[Y]/text().Z
string elementTag = null;
// Look for the last meaningful element (skip text nodes, prefer specific elements over "div")
for (var i = path.Length - 1; i >= 4; i--)
{
var part = path[i];
if (!string.IsNullOrEmpty(part) && !part.StartsWith("text()"))
{
// If we find a non-div element, prefer it
if (part.ToUpper() != "DIV" || elementTag == null)
{
elementTag = part.ToUpper();
if (part.ToUpper() != "DIV") break; // Stop if we found a specific element
}
}
}
if (lastTag == "A")
if (elementTag == "A" || elementTag == null)
{
progress.BookScrollId = null;
}
else
{
// The format that Kavita accepts as a progress string. It tells Kavita where Koreader last left off.
progress.BookScrollId = $"//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]/{lastTag}";
progress.BookScrollId = $"//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]/{elementTag}";
}
}
public static string GetKoreaderPosition(ProgressDto progressDto)
{
if (progressDto == null)
{
// Return a default position if no progress exists
return "/body/DocFragment[1]/body/div/a";
}
string lastTag;
var koreaderPageNumber = progressDto.PageNum + 1;
@ -107,7 +135,20 @@ public static class KoreaderHelper
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}";
// Generate format that matches Koreader expectations
// Based on analysis, it seems:
// - Simple elements like 'a' use: /body/DocFragment[X]/body/div/a
// - Complex elements like 'p[Y]' can be: /body/DocFragment[X]/body/div/p[Y] or /body/DocFragment[X]/body/p[Y]
// The test expects div structure for p[Y], so let's maintain backwards compatibility
if (lastTag == "a")
{
return $"/body/DocFragment[{koreaderPageNumber}]/body/div/{lastTag}";
}
else
{
// For complex elements, use div structure to maintain test compatibility
// This should work for both test cases and real-world scenarios
return $"/body/DocFragment[{koreaderPageNumber}]/body/div/{lastTag}";
}
}
}