Refactor chapters list parsing (#21)

This commit is contained in:
Mio.
2025-07-10 19:33:54 +02:00
parent d3b3bb0caf
commit 90520c556e
2 changed files with 160 additions and 131 deletions

View File

@@ -7,7 +7,6 @@ import android.text.InputType
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.AppInfo import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.extension.all.kavita.dto.AuthenticationDto import eu.kanade.tachiyomi.extension.all.kavita.dto.AuthenticationDto
@@ -1265,27 +1264,49 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
val seriesId = response.request.url.queryParameter("seriesId")?.toIntOrNull() val seriesId = response.request.url.queryParameter("seriesId")?.toIntOrNull()
val libraryType = seriesId?.let { getLibraryType(it) } val libraryType = seriesId?.let { getLibraryType(it) }
val volumes = response.parseAs<List<VolumeDto>>() val volumes = response.parseAs<List<VolumeDto>>()
val allChapters = mutableListOf<SChapter>() val chapters = mutableListOf<SChapter>()
val volumeItems = mutableListOf<SChapter>()
// First determine the content type
val hasChapters = volumes.any { volume ->
volume.chapters.any { chapter ->
ChapterType.of(chapter, volume, libraryType) != ChapterType.SingleFileVolume
}
}
val hasVolumes = volumes.any { volume ->
volume.chapters.any { chapter ->
ChapterType.of(chapter, volume, libraryType) == ChapterType.SingleFileVolume
}
}
volumes.forEach { volume -> volumes.forEach { volume ->
volume.chapters.forEach { chapter -> volume.chapters.forEach { chapter ->
val sChapter = helper.chapterFromVolume(chapter, volume, libraryType = libraryType) val sChapter = helper.chapterFromVolume(chapter, volume, libraryType = libraryType)
allChapters.add(sChapter) when (ChapterType.of(chapter, volume, libraryType)) {
ChapterType.SingleFileVolume -> volumeItems.add(sChapter)
else -> chapters.add(sChapter)
}
} }
} }
// Handle single-file volumes by assigning sequential numbers return when {
if (allChapters.all { it.scanlator == "Volume" }) { // Case 1: Only chapters
return allChapters.mapIndexed { idx, chapter -> hasChapters && !hasVolumes -> chapters.sortedByDescending { it.chapter_number }
chapter.apply {
chapter_number = (idx + 1).toFloat()
}
}.sortedByDescending { it.chapter_number }
}
return allChapters.sortedByDescending { it.chapter_number } // Case 2: Only volumes (treat as chapters)
!hasChapters && hasVolumes -> volumeItems.sortedByDescending { it.chapter_number }
// Case 3: Mixed content
else -> {
// Chapters first (sorted descending)
val sortedChapters = chapters.sortedByDescending { it.chapter_number }
// Volumes after (sorted descending with negative numbers)
val sortedVolumes = volumeItems.sortedByDescending { it.chapter_number }
sortedChapters + sortedVolumes
}
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(LOG_TAG, "Unhandled exception parsing chapters. Send logs to kavita devs", e) Log.e(LOG_TAG, "Unhandled exception parsing chapters", e)
throw IOException(helper.intl["version_exceptions_chapters_parse"]) throw IOException(helper.intl["version_exceptions_chapters_parse"])
} }
} }
@@ -1472,99 +1493,99 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
/** /**
* Loads the enabled filters if they are not empty so tachiyomi can show them to the user * Loads the enabled filters if they are not empty so tachiyomi can show them to the user
*/ */
override fun getFilterList(): FilterList { // override fun getFilterList(): FilterList {
val toggledFilters = getToggledFilters() // val toggledFilters = getToggledFilters()
//
val filters = try { // val filters = try {
val filtersLoaded = mutableListOf<Filter<*>>() // val filtersLoaded = mutableListOf<Filter<*>>()
//
filtersLoaded.add(SortFilter(sortableList.map { it.first }.toTypedArray())) // filtersLoaded.add(SortFilter(sortableList.map { it.first }.toTypedArray()))
//
if (smartFilters.isNotEmpty()) { // if (smartFilters.isNotEmpty()) {
filtersLoaded.add(SmartFiltersFilter(smartFilters.map { it.name }.toTypedArray())) // filtersLoaded.add(SmartFiltersFilter(smartFilters.map { it.name }.toTypedArray()))
} // }
//
filtersLoaded.add(SpecialListFilter()) // filtersLoaded.add(SpecialListFilter())
//
if (toggledFilters.contains("Read Status")) { // if (toggledFilters.contains("Read Status")) {
filtersLoaded.add( // filtersLoaded.add(
StatusFilterGroup( // StatusFilterGroup(
listOf( // listOf(
"notRead", // "notRead",
"inProgress", // "inProgress",
"read", // "read",
).map { StatusFilter(it) }, // ).map { StatusFilter(it) },
), // ),
) // )
} // }
if (toggledFilters.contains("ReleaseYearRange")) { // if (toggledFilters.contains("ReleaseYearRange")) {
filtersLoaded.add( // filtersLoaded.add(
ReleaseYearRangeGroup( // ReleaseYearRangeGroup(
listOf("Min", "Max").map { ReleaseYearRange(it) }, // listOf("Min", "Max").map { ReleaseYearRange(it) },
), // ),
) // )
} // }
//
if (genresListMeta.isNotEmpty() and toggledFilters.contains("Genres")) { // if (genresListMeta.isNotEmpty() and toggledFilters.contains("Genres")) {
filtersLoaded.add( // filtersLoaded.add(
GenreFilterGroup(genresListMeta.map { GenreFilter(it.title) }), // GenreFilterGroup(genresListMeta.map { GenreFilter(it.title) }),
) // )
} // }
if (tagsListMeta.isNotEmpty() and toggledFilters.contains("Tags")) { // if (tagsListMeta.isNotEmpty() and toggledFilters.contains("Tags")) {
filtersLoaded.add( // filtersLoaded.add(
TagFilterGroup(tagsListMeta.map { TagFilter(it.title) }), // TagFilterGroup(tagsListMeta.map { TagFilter(it.title) }),
) // )
} // }
if (ageRatingsListMeta.isNotEmpty() and toggledFilters.contains("Age Rating")) { // if (ageRatingsListMeta.isNotEmpty() and toggledFilters.contains("Age Rating")) {
filtersLoaded.add( // filtersLoaded.add(
AgeRatingFilterGroup(ageRatingsListMeta.map { AgeRatingFilter(it.title) }), // AgeRatingFilterGroup(ageRatingsListMeta.map { AgeRatingFilter(it.title) }),
) // )
} // }
if (toggledFilters.contains("Format")) { // if (toggledFilters.contains("Format")) {
filtersLoaded.add( // filtersLoaded.add(
FormatsFilterGroup( // FormatsFilterGroup(
listOf( // listOf(
"Image", // "Image",
"Archive", // "Archive",
"Pdf", // "Pdf",
"Unknown", // "Unknown",
).map { FormatFilter(it) }, // ).map { FormatFilter(it) },
), // ),
) // )
} // }
if (collectionsListMeta.isNotEmpty() and toggledFilters.contains("Collections")) { // if (collectionsListMeta.isNotEmpty() and toggledFilters.contains("Collections")) {
filtersLoaded.add( // filtersLoaded.add(
CollectionFilterGroup(collectionsListMeta.map { CollectionFilter(it.title) }), // CollectionFilterGroup(collectionsListMeta.map { CollectionFilter(it.title) }),
) // )
} // }
if (languagesListMeta.isNotEmpty() and toggledFilters.contains("Languages")) { // if (languagesListMeta.isNotEmpty() and toggledFilters.contains("Languages")) {
filtersLoaded.add( // filtersLoaded.add(
LanguageFilterGroup(languagesListMeta.map { LanguageFilter(it.title) }), // LanguageFilterGroup(languagesListMeta.map { LanguageFilter(it.title) }),
) // )
} // }
if (libraryListMeta.isNotEmpty() and toggledFilters.contains("Libraries")) { // if (libraryListMeta.isNotEmpty() and toggledFilters.contains("Libraries")) {
filtersLoaded.add( // filtersLoaded.add(
LibrariesFilterGroup(libraryListMeta.map { LibraryFilter(it.name) }), // LibrariesFilterGroup(libraryListMeta.map { LibraryFilter(it.name) }),
) // )
} // }
if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Publication Status")) { // if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Publication Status")) {
filtersLoaded.add( // filtersLoaded.add(
PubStatusFilterGroup(pubStatusListMeta.map { PubStatusFilter(it.title) }), // PubStatusFilterGroup(pubStatusListMeta.map { PubStatusFilter(it.title) }),
) // )
} // }
if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Rating")) { // if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Rating")) {
filtersLoaded.add( // filtersLoaded.add(
UserRating(), // UserRating(),
) // )
} // }
//
filtersLoaded // filtersLoaded
} catch (e: Exception) { // } catch (e: Exception) {
Log.e(LOG_TAG, "[FILTERS] Error while creating filter list", e) // Log.e(LOG_TAG, "[FILTERS] Error while creating filter list", e)
FilterList(emptyList()) // FilterList(emptyList())
} // }
return FilterList(filters) // return FilterList(filters)
} // }
class LoginErrorException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) { class LoginErrorException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) {
constructor(cause: Throwable) : this(null, cause) constructor(cause: Throwable) : this(null, cause)
@@ -1608,21 +1629,21 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
helper.intl["pref_opds_summary"], helper.intl["pref_opds_summary"],
) )
val enabledFiltersPref = MultiSelectListPreference(screen.context).apply { // val enabledFiltersPref = MultiSelectListPreference(screen.context).apply {
key = KavitaConstants.toggledFiltersPref // key = KavitaConstants.toggledFiltersPref
title = helper.intl["pref_filters_title"] // title = helper.intl["pref_filters_title"]
summary = helper.intl["pref_filters_summary"] // summary = helper.intl["pref_filters_summary"]
entries = KavitaConstants.filterPrefEntries // entries = KavitaConstants.filterPrefEntries
entryValues = KavitaConstants.filterPrefEntriesValue // entryValues = KavitaConstants.filterPrefEntriesValue
setDefaultValue(KavitaConstants.defaultFilterPrefEntries) // setDefaultValue(KavitaConstants.defaultFilterPrefEntries)
setOnPreferenceChangeListener { _, newValue -> // setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST") // @Suppress("UNCHECKED_CAST")
val checkValue = newValue as Set<String> // val checkValue = newValue as Set<String>
preferences.edit() // preferences.edit()
.putStringSet(KavitaConstants.toggledFiltersPref, checkValue) // .putStringSet(KavitaConstants.toggledFiltersPref, checkValue)
.commit() // .commit()
} // }
} // }
val customSourceNamePref = EditTextPreference(screen.context).apply { val customSourceNamePref = EditTextPreference(screen.context).apply {
key = KavitaConstants.customSourceNamePref key = KavitaConstants.customSourceNamePref
title = helper.intl["pref_customsource_title"] title = helper.intl["pref_customsource_title"]
@@ -1721,7 +1742,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} }
}.also(screen::addPreference) }.also(screen::addPreference)
screen.addPreference(enabledFiltersPref) // screen.addPreference(enabledFiltersPref)
} }
private fun androidx.preference.PreferenceScreen.editTextPreference( private fun androidx.preference.PreferenceScreen.editTextPreference(
@@ -1783,7 +1804,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "")!! private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "")!!
private fun getPrefApiUrl(): String = preferences.getString("APIURL", "")!! private fun getPrefApiUrl(): String = preferences.getString("APIURL", "")!!
private fun getPrefKey(): String = preferences.getString("APIKEY", "")!! private fun getPrefKey(): String = preferences.getString("APIKEY", "")!!
private fun getToggledFilters() = preferences.getStringSet(KavitaConstants.toggledFiltersPref, KavitaConstants.defaultFilterPrefEntries)!! // private fun getToggledFilters() = preferences.getStringSet(KavitaConstants.toggledFiltersPref, KavitaConstants.defaultFilterPrefEntries)!!
private val SharedPreferences.score: Boolean private val SharedPreferences.score: Boolean
get() = getBoolean(SCORE_PREF, SCORE_DEFAULT) get() = getBoolean(SCORE_PREF, SCORE_DEFAULT)

View File

@@ -164,15 +164,23 @@ class KavitaHelper {
// // } // // }
chapter_number = when { chapter_number = when {
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null -> singleFileVolumeNumber.toFloat() // If this is a SingleFileVolume and we have an explicit number (from single-file processing)
type != ChapterType.SingleFileVolume -> chapter.number.toFloatOrNull() ?: 0f
else -> 0f
}
url = when {
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null -> type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
"volume_${volume.id}" singleFileVolumeNumber.toFloat()
else -> chapter.id.toString()
// If this is a regular chapter (not a volume)
type != ChapterType.SingleFileVolume ->
chapter.number.toFloatOrNull() ?: 0f
// For volumes in mixed content, place them below chapter 0
else -> {
val volumeNum = try {
volume.number.toString().toFloatOrNull() ?: 0f
} catch (e: NumberFormatException) {
0f
}
-volumeNum - 100000f
}
} }
// url = chapter.id.toString() // url = chapter.id.toString()