Improve/Refactor overall code + Fixed Filters

This commit is contained in:
Mio.
2025-07-12 19:03:35 +02:00
parent d3695482f2
commit ffbfdbc0cc
4 changed files with 320 additions and 216 deletions

View File

@@ -103,8 +103,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
override val name = "${KavitaInt.KAVITA_NAME} (${preferences.getString(KavitaConstants.customSourceNamePref, suffix)})" override val name = "${KavitaInt.KAVITA_NAME} (${preferences.getString(KavitaConstants.customSourceNamePref, suffix)})"
override val lang = "all" override val lang = "all"
override val supportsLatest = true override val supportsLatest = true
private val apiUrl: String by lazy { getPrefApiUrl().ifEmpty { "" } } private val apiUrl: String by lazy { getPrefApiUrl() }
private val apiKey: String by lazy { getPrefApiKey().ifEmpty { "" } } private val apiKey: String by lazy { getPrefApiKey() }
override val baseUrl by lazy { getPrefBaseUrl() } override val baseUrl by lazy { getPrefBaseUrl() }
private val address by lazy { getPrefAddress() } // Address for the Kavita OPDS url. Should be http(s)://host:(port)/api/opds/api-key private val address by lazy { getPrefAddress() } // Address for the Kavita OPDS url. Should be http(s)://host:(port)/api/opds/api-key
private var jwtToken = "" // * JWT Token for authentication with the server. Stored in memory. private var jwtToken = "" // * JWT Token for authentication with the server. Stored in memory.
@@ -112,7 +112,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
private var isLogged = false // Used to know if login was correct and not send login requests anymore private var isLogged = false // Used to know if login was correct and not send login requests anymore
private val json: Json by injectLazy() private val json: Json by injectLazy()
private var series = emptyList<SeriesDto>() // Acts as a cache // Act as a cache
private var series = emptyList<SeriesDto>()
private val libraryTypeCache = mutableMapOf<Int, LibraryTypeEnum>()
/** /**
* Extension function to parse a network [Response] as a given type [T]. * Extension function to parse a network [Response] as a given type [T].
@@ -276,103 +278,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} }
} }
private fun readingListRequest(): Request {
return POST(
"$apiUrl/ReadingList/lists?includePromoted=true&sortByLastModified=true",
headersBuilder().build(),
"{}".toRequestBody(JSON_MEDIA_TYPE),
)
}
private fun readingListParse(response: Response): MangasPage {
return try {
val readingLists = response.parseAs<List<ReadingListDto>>()
cachedReadingLists = readingLists
val now = java.util.Calendar.getInstance()
val mangaList = readingLists.map { list ->
val hasDates = list.startingYear > 0 && list.endingYear > 0
val status = if (hasDates) {
val endCal = java.util.Calendar.getInstance().apply {
set(list.endingYear, list.endingMonth - 1, 1)
}
if (now.after(endCal)) SManga.PUBLISHING_FINISHED else SManga.ONGOING
} else {
SManga.COMPLETED
}
SManga.create().apply {
title = (if (list.promoted) "🔺 " else "") + list.title
artist = "${list.itemCount} items"
author = list.ownerUserName
thumbnail_url = if (!list.coverImage.isNullOrBlank()) {
"$apiUrl/image/${list.coverImage}?apiKey=$apiKey"
} else {
"$apiUrl/Image/readinglist-cover?readingListId=${list.id}&apiKey=$apiKey"
}
description = list.summary ?: "Reading List"
url = "$baseUrl/ReadingList/items?readingListId=${list.id}&source=readinglist"
initialized = true
this.status = status
}
}
MangasPage(mangaList, false)
} catch (e: Exception) {
Log.e(LOG_TAG, "Error parsing reading lists", e)
MangasPage(emptyList(), false)
}
}
private fun fetchReadingListItems(manga: SManga): Observable<List<SChapter>> {
val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&")
val request = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build())
return client.newCall(request)
.asObservableSuccess()
.map { parseReadingListItems(it) }
}
private fun parseReadingListItems(response: Response): List<SChapter> {
val items = response.parseAs<List<ReadingListItemDto>>()
val readingListId = response.request.url.queryParameter("readingListId")
return items.map { item ->
SChapter.create().apply {
url = when {
item.chapterId != null && item.chapterId > 0 ->
"/Chapter/${item.chapterId}?readingListId=$readingListId&seriesId=${item.seriesId}&chapterId=${item.chapterId}"
item.volumeId != null && item.volumeId > 0 ->
"/Volume/${item.volumeId}?readingListId=$readingListId&seriesId=${item.seriesId}&volumeId=${item.volumeId}"
else ->
"/Series/${item.seriesId}?readingListId=$readingListId&seriesId=${item.seriesId}&order=${item.order}"
}
name = buildString {
append("${item.order + 1}. ")
when {
item.volumeNumber != null && item.volumeNumber != KavitaConstants.UNNUMBERED_VOLUME_STR -> {
append("Volume ${item.volumeNumber}")
}
item.chapterNumber != null && item.chapterNumber != KavitaConstants.UNNUMBERED_VOLUME_STR -> {
val libraryType = getLibraryType(item.seriesId)
when (libraryType) {
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> append("Issue #${item.chapterNumber}")
else -> append("Chapter ${item.chapterNumber}")
}
}
else -> {
append("Item ${item.order + 1}")
}
}
}
date_upload = if (preferences.RdDate && !item.releaseDate.isNullOrBlank()) {
parseDateSafe(item.releaseDate)
} else {
0L
}
chapter_number = item.order.toFloat()
scanlator = item.seriesName
}
}.sortedBy { it.chapter_number }
}
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
try { try {
val result = response.parseAs<List<SeriesDto>>() val result = response.parseAs<List<SeriesDto>>()
@@ -401,8 +306,17 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
sortOptions = SortOptions(SortFieldEnum.AverageRating.type, false), sortOptions = SortOptions(SortFieldEnum.AverageRating.type, false),
statements = mutableListOf(), statements = mutableListOf(),
) )
val payload = json.encodeToJsonElement(filter).toString()
// Always exclude EPUBs unless explicitly included
if (!preferences.getBoolean(SHOW_EPUB_PREF, SHOW_EPUB_DEFAULT)) {
filter.addStatement(
FilterComparison.NotContains,
FilterField.Formats,
"3",
)
}
val payload = json.encodeToJsonElement(filter).toString()
return POST( return POST(
"$apiUrl/Series/all-v2?pageNumber=$page&pageSize=20", "$apiUrl/Series/all-v2?pageNumber=$page&pageSize=20",
headersBuilder().build(), headersBuilder().build(),
@@ -413,8 +327,18 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val filter = FilterV2Dto( val filter = FilterV2Dto(
sortOptions = SortOptions(SortFieldEnum.LastChapterAdded.type, false), sortOptions = SortOptions(SortFieldEnum.LastChapterAdded.type, false),
statements = mutableListOf(), // optionally, add statements to filter out EPUB, etc. statements = mutableListOf(),
) )
// Always exclude EPUBs unless explicitly included
if (!preferences.getBoolean(SHOW_EPUB_PREF, SHOW_EPUB_DEFAULT)) {
filter.addStatement(
FilterComparison.NotContains,
FilterField.Formats,
"3",
)
}
val payload = json.encodeToJsonElement(filter).toString() val payload = json.encodeToJsonElement(filter).toString()
return prepareRequest(page, payload) return prepareRequest(page, payload)
} }
@@ -433,7 +357,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
// If a SmartFilter selected, apply its filter and return that // If a SmartFilter selected, apply its filter and return that
if (smartFilterFilter?.state != 0 && smartFilterFilter != null) { if (smartFilterFilter?.state != 0 && smartFilterFilter != null) {
val index = try { val index = try {
smartFilterFilter?.state as Int - 1 smartFilterFilter.state as Int - 1
} catch (e: Exception) { } catch (e: Exception) {
Log.e(LOG_TAG, e.toString(), e) Log.e(LOG_TAG, e.toString(), e)
0 0
@@ -604,6 +528,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
"Image" -> 0 "Image" -> 0
"Archive" -> 1 "Archive" -> 1
"Pdf" -> 4 "Pdf" -> 4
"Epub" -> 3
"Unknown" -> 2 "Unknown" -> 2
else -> null else -> null
} }
@@ -648,12 +573,12 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
val included = filter.state val included = filter.state
.filter { it.state == STATE_INCLUDE } .filter { it.state == STATE_INCLUDE }
.mapNotNull { languageFilter -> .mapNotNull { languageFilter ->
languagesListMeta?.find { it.title == languageFilter.name }?.isoCode languagesListMeta.find { it.title == languageFilter.name }?.isoCode
} }
val excluded = filter.state val excluded = filter.state
.filter { it.state == STATE_EXCLUDE } .filter { it.state == STATE_EXCLUDE }
.mapNotNull { languageFilter -> .mapNotNull { languageFilter ->
languagesListMeta?.find { it.title == languageFilter.name }?.isoCode languagesListMeta.find { it.title == languageFilter.name }?.isoCode
} }
if (included.isNotEmpty()) { if (included.isNotEmpty()) {
@@ -693,11 +618,21 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
is PubStatusFilterGroup -> { is PubStatusFilterGroup -> {
filter.state.forEach { pubStatusFilter -> filter.state.forEach { pubStatusFilter ->
if ((pubStatusFilter as Filter.CheckBox).state) { if ((pubStatusFilter as Filter.CheckBox).state) {
filterV2.addStatement( val statusValue = when (pubStatusFilter.name) {
FilterComparison.Equal, "Ongoing" -> 0
FilterField.PublicationStatus, "Hiatus" -> 1
pubStatusFilter.name, "Completed" -> 2
) "Cancelled" -> 3
"Ended" -> 4
else -> null
}
statusValue?.let {
filterV2.addStatement(
FilterComparison.Equal,
FilterField.PublicationStatus,
it.toString(),
)
}
} }
} }
} }
@@ -964,8 +899,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
), ),
).execute() ).execute()
val rawResponse = responsePlus.body?.string() val rawResponse = responsePlus.body.string()
if (rawResponse.isNullOrBlank() || rawResponse.trim().startsWith("<")) { if (rawResponse.isBlank() || rawResponse.trim().startsWith("<")) {
Log.e(LOG_TAG, "Invalid response for series-detail-plus: $rawResponse") Log.e(LOG_TAG, "Invalid response for series-detail-plus: $rawResponse")
// Optionally, show a default or skip averageScore // Optionally, show a default or skip averageScore
"" ""
@@ -1037,7 +972,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
for (volume in volumes) { for (volume in volumes) {
// Issue: use chapter covers // Issue: use chapter covers
if (isComicLibrary && volume.number == KavitaConstants.UNNUMBERED_VOLUME) { if (isComicLibrary && volume.minNumber == KavitaConstants.UNNUMBERED_VOLUME) {
coverCandidates += volume.chapters coverCandidates += volume.chapters
.filterNot { it.coverImage.isNullOrBlank() } .filterNot { it.coverImage.isNullOrBlank() }
.map { chapter -> .map { chapter ->
@@ -1054,7 +989,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} }
if (hasSingleFile && !volume.coverImage.isNullOrBlank()) { if (hasSingleFile && !volume.coverImage.isNullOrBlank()) {
val isUnread = volume.pagesRead < volume.pages val isUnread = volume.pagesRead < volume.pages
val volumeNumber = volume.number.toFloat() val volumeNumber = volume.minNumber.toFloat()
coverCandidates.add( coverCandidates.add(
Triple( Triple(
"$apiUrl/Image/volume-cover?volumeId=${volume.id}&apiKey=$apiKey", "$apiUrl/Image/volume-cover?volumeId=${volume.id}&apiKey=$apiKey",
@@ -1144,10 +1079,167 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
1 -> SManga.ON_HIATUS 1 -> SManga.ON_HIATUS
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
Log.d(LOG_TAG, "Publication status received: ${result.publicationStatus}") // Log.d(LOG_TAG, "Publication status received: ${result.publicationStatus}")
} }
} }
/**
** Reading Lists
**/
private fun readingListRequest(): Request {
return POST(
"$apiUrl/ReadingList/lists?includePromoted=true&sortByLastModified=true",
headersBuilder().build(),
"{}".toRequestBody(JSON_MEDIA_TYPE),
)
}
private fun readingListParse(response: Response): MangasPage {
return try {
val readingLists = response.parseAs<List<ReadingListDto>>()
cachedReadingLists = readingLists
val now = java.util.Calendar.getInstance()
val mangaList = readingLists.map { list ->
val hasDates = list.startingYear > 0 && list.endingYear > 0
val status = if (hasDates) {
val endCal = java.util.Calendar.getInstance().apply {
set(list.endingYear, list.endingMonth - 1, 1)
}
if (now.after(endCal)) SManga.PUBLISHING_FINISHED else SManga.ONGOING
} else {
SManga.COMPLETED
}
SManga.create().apply {
title = (if (list.promoted) "🔺 " else "") + list.title
artist = "${list.itemCount} items"
author = list.ownerUserName
thumbnail_url = if (!list.coverImage.isNullOrBlank()) {
"$apiUrl/image/${list.coverImage}?apiKey=$apiKey"
} else {
"$apiUrl/Image/readinglist-cover?readingListId=${list.id}&apiKey=$apiKey"
}
description = list.summary ?: "Reading List"
url = "$baseUrl/ReadingList/items?readingListId=${list.id}&source=readinglist"
initialized = true
this.status = status
}
}
MangasPage(mangaList, false)
} catch (e: Exception) {
Log.e(LOG_TAG, "Error parsing reading lists", e)
MangasPage(emptyList(), false)
}
}
private fun fetchReadingListItems(manga: SManga): Observable<List<SChapter>> {
val readingListId = manga.url.substringAfter("readingListId=").substringBefore("&")
val request = GET("$apiUrl/ReadingList/items?readingListId=$readingListId", headersBuilder().build())
return client.newCall(request)
.asObservableSuccess()
.map { parseReadingListItems(it) }
}
private fun parseReadingListItems(response: Response): List<SChapter> {
val items = response.parseAs<List<ReadingListItemDto>>()
val readingListId = response.request.url.queryParameter("readingListId")
return items.map { item ->
SChapter.create().apply {
url = when {
item.chapterId != null && item.chapterId > 0 ->
"/Chapter/${item.chapterId}?readingListId=$readingListId&seriesId=${item.seriesId}&chapterId=${item.chapterId}"
item.volumeId != null && item.volumeId > 0 ->
"/Volume/${item.volumeId}?readingListId=$readingListId&seriesId=${item.seriesId}&volumeId=${item.volumeId}"
else ->
"/Series/${item.seriesId}?readingListId=$readingListId&seriesId=${item.seriesId}&order=${item.order}"
}
val type = when {
item.volumeId != null && item.volumeNumber != null -> {
when {
item.volumeNumber == KavitaConstants.UNNUMBERED_VOLUME_STR -> {
val libraryType = getLibraryType(item.seriesId)
when (libraryType) {
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> ChapterType.Issue
else -> ChapterType.Chapter
}
}
item.chapterNumber == KavitaConstants.UNNUMBERED_VOLUME_STR -> ChapterType.SingleFileVolume
else -> ChapterType.Regular
}
}
item.chapterId != null && item.chapterNumber != null -> {
val libraryType = getLibraryType(item.seriesId)
when (libraryType) {
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> ChapterType.Issue
else -> ChapterType.Chapter
}
}
else -> ChapterType.Special
}
name = buildString {
append("${item.order + 1}. ")
when (type) {
ChapterType.Regular -> {
val chapterNum = item.chapterNumber?.toIntOrNull()?.let {
it.toString().padStart(2, '0')
} ?: item.chapterNumber ?: ""
when {
item.chapterTitleName?.isNotBlank() == true ->
append("$chapterNum - ${item.chapterTitleName}")
else ->
append("Vol. ${item.volumeNumber} Ch. $chapterNum")
}
}
ChapterType.SingleFileVolume -> {
when {
item.chapterTitleName?.isNotBlank() == true ->
append("v${item.volumeNumber} - ${item.chapterTitleName}")
else ->
append("Volume ${item.volumeNumber}")
}
}
ChapterType.Issue -> {
val issueNum = item.chapterNumber?.toIntOrNull()?.let {
it.toString().padStart(3, '0')
} ?: item.chapterNumber ?: ""
when {
item.chapterTitleName?.isNotBlank() == true ->
append("${item.chapterTitleName} (#$issueNum)")
else ->
append("Issue #$issueNum")
}
}
ChapterType.Chapter -> {
val chapterNum = item.chapterNumber?.toIntOrNull()?.let {
it.toString().padStart(2, '0')
} ?: item.chapterNumber ?: ""
when {
item.chapterTitleName?.isNotBlank() == true ->
append("$chapterNum - ${item.chapterTitleName}")
else ->
append("Chapter $chapterNum")
}
}
ChapterType.Special -> {
append(item.chapterTitleName ?: "Item ${item.order + 1}")
}
}
}
date_upload = if (preferences.RdDate && !item.releaseDate.isNullOrBlank()) {
parseDateSafe(item.releaseDate)
} else {
0L
}
chapter_number = item.order.toFloat()
scanlator = item.seriesName
}
}.sortedBy { it.chapter_number }
}
/** /**
* Related Titles / Suggestions * Related Titles / Suggestions
* This only works on Komikku * This only works on Komikku
@@ -1181,8 +1273,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} }
} }
} else { } else {
val rawResponse = response.body?.string() val rawResponse = response.body.string()
if (rawResponse.isNullOrEmpty() || rawResponse.startsWith("<")) { if (rawResponse.isEmpty() || rawResponse.startsWith("<")) {
Log.e(LOG_TAG, "Invalid response for related manga: $rawResponse") Log.e(LOG_TAG, "Invalid response for related manga: $rawResponse")
return emptyList() return emptyList()
} }
@@ -1237,24 +1329,27 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} }
private fun getLibraryType(seriesId: Int): LibraryTypeEnum? { private fun getLibraryType(seriesId: Int): LibraryTypeEnum? {
return try { return libraryTypeCache[seriesId] ?: run {
val seriesDto = client.newCall(GET("$apiUrl/Series/$seriesId", headersBuilder().build())) try {
.execute() val seriesDto = client.newCall(GET("$apiUrl/Series/$seriesId", headersBuilder().build()))
.parseAs<SeriesDto>()
// First try to get library type from library metadata
libraryListMeta.find { it.id == seriesDto.libraryId }?.type?.let { typeInt ->
LibraryTypeEnum.fromInt(typeInt)
} ?: run {
// Fallback: fetch library directly
val library = client.newCall(GET("$apiUrl/Library/${seriesDto.libraryId}", headersBuilder().build()))
.execute() .execute()
.parseAs<LibraryDto>() .parseAs<SeriesDto>()
LibraryTypeEnum.fromInt(library.type)
val type = libraryListMeta.find { it.id == seriesDto.libraryId }?.type?.let { typeInt ->
LibraryTypeEnum.fromInt(typeInt)
} ?: run {
val library = client.newCall(GET("$apiUrl/Library/${seriesDto.libraryId}", headersBuilder().build()))
.execute()
.parseAs<LibraryDto>()
LibraryTypeEnum.fromInt(library.type)
}
type?.let { libraryTypeCache[seriesId] = it }
type
} catch (e: Exception) {
Log.e(LOG_TAG, "Error getting library type for series $seriesId", e)
null
} }
} catch (e: Exception) {
Log.e(LOG_TAG, "Error getting library type for series $seriesId", e)
null
} }
} }
@@ -1267,36 +1362,53 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
val volumeItems = mutableListOf<SChapter>() val volumeItems = mutableListOf<SChapter>()
volumes.forEach { volume -> volumes.forEach { volume ->
volume.chapters.forEach { chapter -> // This fixes volumes that are recognized as chapters in Kavita because they have numbers in their titles
val sChapter = helper.chapterFromVolume(chapter, volume, libraryType = libraryType) if (volume.chapters.size == 1 && volume.name.isNotBlank()) {
when (ChapterType.of(chapter, volume, libraryType)) { val chapter = volume.chapters.first()
ChapterType.SingleFileVolume -> { val sChapter = helper.chapterFromVolume(
// For Case 2, use positive volume numbers chapter,
// For Case 3, use negative numbers volume,
sChapter.chapter_number = volume.number.toFloat() singleFileVolumeNumber = volume.minNumber,
libraryType = libraryType,
)
// Force SingleFileVolume type
sChapter.name = when {
volume.name.any { it.isLetter() } -> "v${volume.minNumber} - ${volume.name}"
else -> "Volume ${volume.minNumber}"
}
sChapter.chapter_number = volume.minNumber.toFloat()
sChapter.scanlator = "Volume"
volumeItems.add(sChapter)
} else {
volume.chapters.forEach { chapter ->
val type = ChapterType.of(chapter, volume, libraryType)
val sChapter = helper.chapterFromVolume(chapter, volume, libraryType = libraryType)
if (type == ChapterType.SingleFileVolume) {
volumeItems.add(sChapter) volumeItems.add(sChapter)
} else {
chapters.add(sChapter)
} }
else -> chapters.add(sChapter)
} }
} }
} }
return when { return when {
// Case 1: Only chapters // Case 1: Only chapters exist
chapters.isNotEmpty() && volumeItems.isEmpty() -> chapters.isNotEmpty() && volumeItems.isEmpty() ->
chapters.sortedByDescending { it.chapter_number } chapters.sortedByDescending { it.chapter_number }
// Case 2: Only volumes - treat as chapters with positive numbers // Case 2: Only volumes exist
volumeItems.isNotEmpty() && chapters.isEmpty() -> volumeItems.isNotEmpty() && chapters.isEmpty() ->
volumeItems.sortedByDescending { it.chapter_number } volumeItems.sortedByDescending { it.chapter_number }
// Case 3: Mixed content - chapters first, then volumes (as negative numbers) // Case 3: Mixed content
else -> { else -> {
// Convert volume numbers to negative for proper sorting // Convert volume numbers to negative for proper sorting
volumeItems.forEach { it.chapter_number = -it.chapter_number } volumeItems.forEach { it.chapter_number = -it.chapter_number }
val sortedChapters = chapters.sortedByDescending { it.chapter_number } (
val sortedVolumes = volumeItems.sortedBy { it.chapter_number } chapters.sortedByDescending { it.chapter_number } +
sortedChapters + sortedVolumes volumeItems.sortedBy { it.chapter_number }
)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -1471,21 +1583,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
private var collectionsListMeta = emptyList<MetadataCollections>() private var collectionsListMeta = emptyList<MetadataCollections>()
private var smartFilters = emptyList<SmartFilter>() private var smartFilters = emptyList<SmartFilter>()
private var cachedReadingLists: List<ReadingListDto> = emptyList() private var cachedReadingLists: List<ReadingListDto> = emptyList()
private val personRoles = listOf(
"Writer",
"Penciller",
"Inker",
"Colorist",
"Letterer",
"CoverArtist",
"Editor",
"Publisher",
"Character",
"Translator",
)
/** /**
* 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 Mihon can show them to the user
*/ */
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
val toggledFilters = getToggledFilters() val toggledFilters = getToggledFilters()
@@ -1542,6 +1642,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
"Image", "Image",
"Archive", "Archive",
"Pdf", "Pdf",
"Epub",
"Unknown", "Unknown",
).map { FormatFilter(it) }, ).map { FormatFilter(it) },
), ),
@@ -1564,10 +1665,18 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
} }
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(
listOf(
PubStatusFilter("Ongoing"),
PubStatusFilter("Hiatus"),
PubStatusFilter("Completed"),
PubStatusFilter("Cancelled"),
PubStatusFilter("Ended"),
),
),
) )
} }
if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Rating")) { if (ageRatingsListMeta.isNotEmpty() and toggledFilters.contains("Rating")) {
filtersLoaded.add( filtersLoaded.add(
UserRating(), UserRating(),
) )
@@ -1795,14 +1904,11 @@ 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
get() = getBoolean(SCORE_PREF, SCORE_DEFAULT)
private val SharedPreferences.groupTags: Boolean private val SharedPreferences.groupTags: Boolean
get() = getBoolean(GROUP_TAGS_PREF, GROUP_TAGS_DEFAULT) get() = getBoolean(GROUP_TAGS_PREF, GROUP_TAGS_DEFAULT)
@@ -1829,7 +1935,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
// Clear all preferences // Clear all preferences
preferences.edit().clear().commit() preferences.edit().clear().commit()
// Reset all in-memory state // Reset all in-memory state (for debug)
// jwtToken = "" // jwtToken = ""
// isLogged = false // isLogged = false
// genresListMeta = emptyList() // genresListMeta = emptyList()
@@ -1852,16 +1958,18 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
private fun getPrefApiKey(): String { private fun getPrefApiKey(): String {
// http(s)://host:(port)/api/opds/api-key // http(s)://host:(port)/api/opds/api-key
val existingKey = preferences.getString("APIKEY", "") val existingKey = preferences.getString(APIKEY, "")
return existingKey!!.ifEmpty { preferences.getString(ADDRESS_TITLE, "")!!.split("/opds/")[1] } if (!existingKey.isNullOrEmpty()) return existingKey
val address = preferences.getString(ADDRESS_TITLE, "") ?: ""
val parts = address.split("/opds/")
return if (parts.size > 1) parts[1] else ""
} }
companion object { companion object {
private const val ADDRESS_TITLE = "Address" private const val ADDRESS_TITLE = "Address"
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull() private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
private const val SCORE_PREF = "Score" private const val APIKEY = "APIKEY"
const val SCORE_DEFAULT = true
private const val LAST_VOLUME_COVER_PREF = "LastVolumeCover" private const val LAST_VOLUME_COVER_PREF = "LastVolumeCover"
const val LAST_VOLUME_COVER_DEFAULT = false const val LAST_VOLUME_COVER_DEFAULT = false
@@ -1876,9 +1984,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
const val RD_DATE_DEFAULT = false const val RD_DATE_DEFAULT = false
} }
/*
* LOGIN
**/
/** /**
* Used to check if a url is configured already in any of the sources * Used to check if a url is configured already in any of the sources
* This is a limitation needed for tracking. * This is a limitation needed for tracking.
@@ -1909,6 +2014,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
return "" return ""
} }
/**
* LOGIN
* **/
private fun setupLogin(addressFromPreference: String = "") { private fun setupLogin(addressFromPreference: String = "") {
Log.v(LOG_TAG, "[Setup Login] Starting setup") Log.v(LOG_TAG, "[Setup Login] Starting setup")
val validAddress = address.ifEmpty { addressFromPreference } val validAddress = address.ifEmpty { addressFromPreference }
@@ -1969,7 +2077,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
init { init {
if (apiUrl.isNotBlank()) { if (apiUrl.isNotBlank()) {
Single.fromCallable { Single.fromCallable {
// Login
doLogin() doLogin()
try { // Get current version try { // Get current version
val requestUrl = "$apiUrl/Server/server-info-slim" val requestUrl = "$apiUrl/Server/server-info-slim"

View File

@@ -20,6 +20,8 @@ class KavitaHelper {
val json = Json { val json = Json {
isLenient = true isLenient = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
coerceInputValues = true
explicitNulls = false
allowSpecialFloatingPointValues = true allowSpecialFloatingPointValues = true
useArrayPolymorphism = true useArrayPolymorphism = true
prettyPrint = true prettyPrint = true
@@ -37,9 +39,9 @@ class KavitaHelper {
var hasNextPage = false var hasNextPage = false
if (!paginationHeader.isNullOrEmpty()) { if (!paginationHeader.isNullOrEmpty()) {
val paginationInfo = json.decodeFromString<PaginationInfo>(paginationHeader) val paginationInfo = json.decodeFromString<PaginationInfo>(paginationHeader)
hasNextPage = paginationInfo.currentPage + 1 > paginationInfo.totalPages hasNextPage = paginationInfo.currentPage + 1 < paginationInfo.totalPages
} }
return !hasNextPage return hasNextPage
} }
fun getIdFromUrl(url: String): Int { fun getIdFromUrl(url: String): Int {
@@ -73,6 +75,9 @@ class KavitaHelper {
fun chapterFromVolume(chapter: ChapterDto, volume: VolumeDto, singleFileVolumeNumber: Int? = null, libraryType: LibraryTypeEnum? = null): SChapter = fun chapterFromVolume(chapter: ChapterDto, volume: VolumeDto, singleFileVolumeNumber: Int? = null, libraryType: LibraryTypeEnum? = null): SChapter =
SChapter.create().apply { SChapter.create().apply {
val type = ChapterType.of(chapter, volume, libraryType) val type = ChapterType.of(chapter, volume, libraryType)
val titleName = chapter.titleName ?: ""
val title = chapter.title ?: ""
val range = chapter.range ?: ""
name = when (type) { name = when (type) {
ChapterType.Regular -> { ChapterType.Regular -> {
@@ -80,23 +85,20 @@ class KavitaHelper {
it.toString().padStart(2, '0') it.toString().padStart(2, '0')
} ?: chapter.number } ?: chapter.number
when { when {
chapter.titleName.isBlank() -> "Vol.${volume.number} Ch.$chapterNum" titleName.any { it.isLetter() } -> "$chapterNum - $titleName"
chapter.titleName.trim().matches(Regex("^\\d+$")) -> else -> "Vol.${volume.minNumber} Ch.$chapterNum"
"Vol.${volume.number} Ch.${chapter.titleName.trim().padStart(2, '0')}"
else -> "Vol.${volume.number} Ch.$chapterNum - ${chapter.titleName}"
} }
} }
ChapterType.SingleFileVolume -> { ChapterType.SingleFileVolume -> {
when { when {
volume.name.isBlank() -> "Volume ${volume.number}" volume.name.any { it.isLetter() } -> "v${volume.minNumber} - ${volume.name}"
volume.name.trim().matches(Regex("^\\d+$")) -> "Volume ${volume.name.trim().toIntOrNull()?.toString() ?: volume.name.trim()}" else -> "Volume ${volume.minNumber}"
else -> "${volume.number} - ${volume.name}"
} }
} }
ChapterType.Special -> { ChapterType.Special -> {
when { when {
chapter.title.isNotBlank() -> chapter.title title.isNotBlank() -> title
chapter.range.isNotBlank() -> chapter.range range.isNotBlank() -> range
else -> "Special" else -> "Special"
} }
} }
@@ -105,9 +107,8 @@ class KavitaHelper {
it.toString().padStart(2, '0') it.toString().padStart(2, '0')
} ?: chapter.number } ?: chapter.number
when { when {
chapter.titleName.isBlank() -> "Chapter $chapterNum" titleName.any { it.isLetter() } -> "$chapterNum - $titleName"
chapter.titleName.trim().matches(Regex("^\\d+$")) -> "Chapter ${chapter.titleName.trim().padStart(2, '0')}" else -> "Chapter $chapterNum"
else -> "$chapterNum - ${chapter.titleName}"
} }
} }
ChapterType.Issue -> { ChapterType.Issue -> {
@@ -115,10 +116,7 @@ class KavitaHelper {
it.toString().padStart(3, '0') it.toString().padStart(3, '0')
} ?: chapter.number } ?: chapter.number
when { when {
chapter.titleName.isNotBlank() && !chapter.titleName.trim().matches(Regex("^\\d+$")) -> titleName.any { it.isLetter() } -> "$titleName (#$issueNum)"
"#$issueNum ${chapter.titleName}"
chapter.title.isNotBlank() && !chapter.title.trim().matches(Regex("^\\d+$")) ->
"#$issueNum ${chapter.title}"
else -> "Issue #$issueNum" else -> "Issue #$issueNum"
} }
} }
@@ -128,13 +126,19 @@ class KavitaHelper {
type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null -> type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null ->
singleFileVolumeNumber.toFloat() singleFileVolumeNumber.toFloat()
type == ChapterType.SingleFileVolume -> {
// For standalone volumes, use positive numbers
volume.minNumber.toFloat()
}
// If this is a regular chapter (not a volume)
type != ChapterType.SingleFileVolume -> type != ChapterType.SingleFileVolume ->
chapter.number.toFloatOrNull() ?: 0f chapter.number.toFloatOrNull() ?: 0f
// For volumes in mixed content, place them below chapter 0 // For volumes in mixed content, place them below chapter 0
else -> { else -> {
val volumeNum = try { val volumeNum = try {
volume.number.toString().toFloatOrNull() ?: 0f volume.minNumber.toString().toFloatOrNull() ?: 0f
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
0f 0f
} }
@@ -149,7 +153,6 @@ class KavitaHelper {
} }
if (chapter.fileCount > 1) { if (chapter.fileCount > 1) {
// 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
url = "${url}_${chapter.fileCount}" url = "${url}_${chapter.fileCount}"
} }
@@ -161,7 +164,6 @@ class KavitaHelper {
ChapterType.Issue -> "Issue" ChapterType.Issue -> "Issue"
ChapterType.Chapter -> "Chapter" ChapterType.Chapter -> "Chapter"
ChapterType.Regular -> "Chapter" ChapterType.Regular -> "Chapter"
else -> "Chapter"
} }
} }

View File

@@ -10,11 +10,6 @@ import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
val formatterDate = SimpleDateFormat("yyyy-MM-dd", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
val formatterDateTime = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
fun parseDateSafe(date: String?): Long { fun parseDateSafe(date: String?): Long {
return date?.let { return date?.let {
try { try {

View File

@@ -9,7 +9,7 @@ interface ConvertibleToSManga {
fun toSManga(baseUrl: String, apiUrl: String, apiKey: String): SManga fun toSManga(baseUrl: String, apiUrl: String, apiKey: String): SManga
} }
@Serializable @Serializable // https://github.com/Kareadita/Kavita/blob/develop/API/Entities/Enums/MangaFormat.cs
enum class MangaFormat(val format: Int) { enum class MangaFormat(val format: Int) {
Image(0), Image(0),
Archive(1), Archive(1),
@@ -190,8 +190,8 @@ data class AuthorDto(
@Serializable @Serializable
data class VolumeDto( data class VolumeDto(
val id: Int, val id: Int,
val number: Int, val minNumber: Int,
val name: String, val name: String = "",
val pages: Int, val pages: Int,
val pagesRead: Int, val pagesRead: Int,
val lastModified: String, val lastModified: String,
@@ -217,16 +217,16 @@ enum class ChapterType {
fun of(chapter: ChapterDto, volume: VolumeDto, libraryType: LibraryTypeEnum? = null): ChapterType = fun of(chapter: ChapterDto, volume: VolumeDto, libraryType: LibraryTypeEnum? = null): ChapterType =
when { when {
// Special // Special
volume.number == SPECIAL_NUMBER -> Special volume.minNumber == SPECIAL_NUMBER -> Special
// Issue // Issue
volume.number == UNNUMBERED_VOLUME_NUMBER -> when (libraryType) { volume.minNumber == UNNUMBERED_VOLUME_NUMBER -> when (libraryType) {
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
else -> Chapter else -> Chapter
} }
// SingleFileVolume // SingleFileVolume
chapter.number == KavitaConstants.UNNUMBERED_VOLUME_STR -> SingleFileVolume chapter.number == KavitaConstants.UNNUMBERED_VOLUME_STR -> SingleFileVolume
// Regular // Regular
volume.number > 0 -> Regular volume.minNumber > 0 -> Regular
// Everything else depends on library type // Everything else depends on library type
else -> when (libraryType) { else -> when (libraryType) {
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue
@@ -244,7 +244,7 @@ data class ChapterDto(
val pages: Int, val pages: Int,
val isSpecial: Boolean, val isSpecial: Boolean,
val title: String, val title: String,
val titleName: String, val titleName: String = "",
val pagesRead: Int, val pagesRead: Int,
val coverImageLocked: Boolean, val coverImageLocked: Boolean,
val coverImage: String, val coverImage: String,
@@ -268,13 +268,13 @@ data class ReadingListDto(
val title: String, val title: String,
val coverImage: String? = null, val coverImage: String? = null,
val promoted: Boolean = false, val promoted: Boolean = false,
val summary: String?, val summary: String? = null,
val itemCount: Int, val itemCount: Int,
val startingYear: Int, val startingYear: Int,
val startingMonth: Int, val startingMonth: Int,
val endingYear: Int, val endingYear: Int,
val endingMonth: Int, val endingMonth: Int,
val ownerUserName: String?, val ownerUserName: String? = null,
@SerialName("items") val items: List<ReadingListItemDto> = emptyList(), @SerialName("items") val items: List<ReadingListItemDto> = emptyList(),
) )
@@ -287,11 +287,11 @@ data class ReadingListItemDto(
val seriesName: String, val seriesName: String,
val chapterNumber: String?, val chapterNumber: String?,
val volumeNumber: String?, val volumeNumber: String?,
val chapterTitleName: String?, val chapterTitleName: String? = null,
val volumeId: Int?, val volumeId: Int?,
val title: String?, val title: String? = null,
val summary: String?, val summary: String? = null,
val releaseDate: String?, val releaseDate: String? = null,
val libraryName: String?, val libraryName: String?,
@SerialName("readingListId") val readingListId: Int, @SerialName("readingListId") val readingListId: Int,
) )