consistency for volumes

This commit is contained in:
Mio.
2025-07-15 02:36:10 +02:00
parent 94cbfbd394
commit 67704dce6d
3 changed files with 62 additions and 38 deletions

View File

@@ -145,7 +145,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
when { when {
// Handle both API-style and web-style URLs // Handle both API-style and web-style URLs
manga.url.contains("/series/") || manga.url.contains("/Series/") -> { manga.url.contains("/series/") || manga.url.contains("/Series/") -> {
// Extract series ID from URL (works for both formats)
val seriesId = manga.url val seriesId = manga.url
.substringAfterLast("/series/") .substringAfterLast("/series/")
.substringAfterLast("/Series/") .substringAfterLast("/Series/")
@@ -166,7 +165,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
// Use web-friendly URL format // Use web-friendly URL format
"$baseUrl/library/$libraryId/series/$seriesId" "$baseUrl/library/$libraryId/series/$seriesId"
} else { } else {
// Fallback to original URL format if we don't have libraryId
"$baseUrl/library/1/series/$seriesId" "$baseUrl/library/1/series/$seriesId"
} }
} }
@@ -1439,7 +1437,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} ?: false } ?: false
volumes.forEach { volume -> volumes.forEach { volume ->
// This fixes specials being parsed as volumes // This fixes specials being parsed as volumes sometimes
if (volume.chapters.size == 1 && volume.minNumber.toInt() != KavitaConstants.SPECIAL_NUMBER) { if (volume.chapters.size == 1 && volume.minNumber.toInt() != KavitaConstants.SPECIAL_NUMBER) {
val chapter = volume.chapters.first() val chapter = volume.chapters.first()
val sChapter = helper.chapterFromVolume( val sChapter = helper.chapterFromVolume(
@@ -1450,11 +1448,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
isWebtoon = isWebtoon, isWebtoon = isWebtoon,
mangaTitle = seriesName, mangaTitle = seriesName,
) )
sChapter.url = "/Chapter/${chapter.id}" sChapter.url = "/Chapter/${chapter.id}" // Needed to read volumes as chapters
sChapter.name = when {
volume.name.any { it.isLetter() } -> "v${helper.formatVolumeNumber(volume)} - ${volume.name}"
else -> "Volume ${helper.formatVolumeNumber(volume)}"
}
sChapter.chapter_number = volume.minNumber.toFloat() sChapter.chapter_number = volume.minNumber.toFloat()
sChapter.scanlator = if (isWebtoon) "Season" else "Volume" sChapter.scanlator = if (isWebtoon) "Season" else "Volume"
volumeItems.add(sChapter) volumeItems.add(sChapter)
@@ -1469,6 +1463,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
mangaTitle = seriesName, mangaTitle = seriesName,
) )
if (type == ChapterType.SingleFileVolume) { if (type == ChapterType.SingleFileVolume) {
sChapter.url = "/Chapter/${chapter.id}" // Needed to read volumes as chapters
sChapter.chapter_number = volume.minNumber.toFloat()
volumeItems.add(sChapter) volumeItems.add(sChapter)
} else { } else {
chapters.add(sChapter) chapters.add(sChapter)
@@ -1488,11 +1484,14 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
// Case 3: Mixed content - chapters first, then volumes // Case 3: Mixed content - chapters first, then volumes
else -> { else -> {
// Convert volume numbers to negative for proper sorting volumeItems.forEachIndexed { index, it ->
volumeItems.forEach { it.chapter_number = -it.chapter_number } if (it.chapter_number <= 0f) {
it.chapter_number = chapters.size + index + 1f
}
}
( (
chapters.sortedByDescending { it.chapter_number } + chapters.sortedByDescending { it.chapter_number } +
volumeItems.sortedBy { it.chapter_number } volumeItems.sortedByDescending { it.chapter_number }
) )
} }
} }
@@ -1521,6 +1520,12 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
return GET("$apiUrl/${chapterItem.chapterId}", headersBuilder().build()) return GET("$apiUrl/${chapterItem.chapterId}", headersBuilder().build())
} }
// Handle volume URLs
if (chapter.url.startsWith("Volume/")) {
val volumeId = chapter.url.substringAfter("Volume/").substringBefore("_")
return GET("$apiUrl/Volume/$volumeId", headersBuilder().build())
}
// Original handling for regular chapters // Original handling for regular chapters
val chapterId = chapter.url.substringBefore("_") val chapterId = chapter.url.substringBefore("_")
return GET("$apiUrl/$chapterId", headersBuilder().build()) return GET("$apiUrl/$chapterId", headersBuilder().build())
@@ -1581,10 +1586,13 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
// For normal titles // For normal titles
return Observable.fromCallable { return Observable.fromCallable {
// Check if this is a volume URL (contains "Volume/") // Check if this is a volume URL
if (chapter.url.contains("/Volume/")) { if (chapter.url.contains("/Volume/") || chapter.url.startsWith("volume_")) {
val volumeId = chapter.url.substringAfter("/Volume/").substringBefore("?").toIntOrNull() val volumeId = when {
?: throw IOException("Invalid volume ID") chapter.url.startsWith("volume_") -> chapter.url.substringAfter("volume_").substringBefore("_")
chapter.url.contains("/Volume/") -> chapter.url.substringAfter("/Volume/").substringBefore("_")
else -> throw IOException("Invalid volume URL format")
}
// Get all pages in this volume // Get all pages in this volume
val volumeRequest = GET("$apiUrl/Volume/$volumeId", headersBuilder().build()) val volumeRequest = GET("$apiUrl/Volume/$volumeId", headersBuilder().build())

View File

@@ -5,7 +5,7 @@ object KavitaConstants {
const val UNNUMBERED_VOLUME = -100000 const val UNNUMBERED_VOLUME = -100000
const val UNNUMBERED_VOLUME_STR = "-100000" const val UNNUMBERED_VOLUME_STR = "-100000"
const val SPECIAL_NUMBER = 100_000 const val SPECIAL_NUMBER = 100_000
const val VOLUME_NUMBER_OFFSET = 100000f const val VOLUME_NUMBER_OFFSET = 10000f
val PERSON_ROLES = listOf( val PERSON_ROLES = listOf(
"Writer", "Penciller", "Inker", "Colorist", "Writer", "Penciller", "Inker", "Colorist",

View File

@@ -104,8 +104,8 @@ class KavitaHelper {
ChapterType.SingleFileVolume -> { ChapterType.SingleFileVolume -> {
cleanChapterTitle( cleanChapterTitle(
when { when {
volume.name.any { it.isLetter() } -> "v${volume.minNumber} - ${volume.name}" volume.name.any { it.isLetter() } -> "v${formatVolumeNumber(volume)} - ${volume.name}"
else -> "Volume ${volume.minNumber}" else -> "Volume ${formatVolumeNumber(volume)}"
}, },
ChapterTitleContext( ChapterTitleContext(
mangaTitle = mangaTitle, mangaTitle = mangaTitle,
@@ -164,7 +164,6 @@ class KavitaHelper {
} }
// @todo Keeping this in case we're switching the logic here this after the Mihon PR // @todo Keeping this in case we're switching the logic here this after the Mihon PR
// Handle decimal chapter numbers properly
// chapter_number = when { // chapter_number = when {
// type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null -> // type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
// singleFileVolumeNumber.toFloat() // singleFileVolumeNumber.toFloat()
@@ -206,16 +205,38 @@ class KavitaHelper {
// } // }
// Handle decimal chapter numbers properly // Handle decimal chapter numbers properly
chapter_number = if (type == ChapterType.SingleFileVolume) { chapter_number = when {
if (volume.minNumber % 1 != 0.0) { type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
volume.minNumber.toFloat()
type == ChapterType.SingleFileVolume -> {
// For standalone volumes, use positive numbers
volume.minNumber.toFloat() volume.minNumber.toFloat()
} else {
volume.minNumber.toInt().toFloat()
} }
} else if (chapter.minNumber % 1 != 0.0) {
chapter.minNumber.toFloat() // For regular chapters
} else { type != ChapterType.SingleFileVolume && type != ChapterType.Special -> {
chapter.minNumber.toInt().toFloat() // Handle decimal chapter numbers
if (chapter.minNumber % 1 != 0.0) {
chapter.minNumber.toFloat()
} else {
chapter.minNumber.toInt().toFloat()
}
}
// For volumes/specials in mixed content, place them below chapter 0
else -> {
val volumeNum = try {
if (volume.minNumber % 1 != 0.0) {
volume.minNumber.toFloat()
} else {
volume.minNumber.toInt().toFloat()
}
} catch (e: NumberFormatException) {
0f
}
volumeNum / KavitaConstants.VOLUME_NUMBER_OFFSET
}
} }
url = when { url = when {
@@ -224,11 +245,9 @@ class KavitaHelper {
else -> "chapter_${chapter.id}" else -> "chapter_${chapter.id}"
} }
// Handle multi-file chapters
if (chapter.fileCount > 1) { if (chapter.fileCount > 1) {
// Add small offset to maintain order while keeping the main chapter number // salt/offset to recognize chapters with new merged part-chapters as new and hence unread
chapter_number += 0.001f * chapter.fileCount chapter_number += 0.001f * chapter.fileCount
// Append file count to URL for reference
url = "${url}_${chapter.fileCount}" url = "${url}_${chapter.fileCount}"
} }
@@ -242,7 +261,7 @@ class KavitaHelper {
} }
} }
fun formatVolumeNumber(volume: VolumeDto): String { private fun formatVolumeNumber(volume: VolumeDto): String {
return when { return when {
volume.maxNumber > volume.minNumber -> volume.maxNumber > volume.minNumber ->
"${removeTrailingZero(volume.minNumber)}-${removeTrailingZero(volume.maxNumber)}" "${removeTrailingZero(volume.minNumber)}-${removeTrailingZero(volume.maxNumber)}"
@@ -268,14 +287,11 @@ class KavitaHelper {
// Helper function to remove .0 from whole numbers while preserving actual decimals // Helper function to remove .0 from whole numbers while preserving actual decimals
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
private fun removeTrailingZero(number: Double): String { private fun removeTrailingZero(number: Double): String {
return if (number.isInfinite() || number.isNaN()) { return if (number % 1 == 0.0) {
number.toString() number.toInt().toString()
} else { } else {
val formatted = String.format("%.10f", number) // Use String.format to avoid scientific notation for very small decimals
.replace(",", ".") String.format("%.10f", number).replace(",", ".").trimEnd('0').trimEnd('.')
.trimEnd('0')
.trimEnd('.')
formatted.ifEmpty { "0" }
} }
} }