diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt index 25964ea3..9eb70be0 100644 --- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/Kavita.kt @@ -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 lang = "all" override val supportsLatest = true - private val apiUrl: String by lazy { getPrefApiUrl().ifEmpty { "" } } - private val apiKey: String by lazy { getPrefApiKey().ifEmpty { "" } } + private val apiUrl: String by lazy { getPrefApiUrl() } + private val apiKey: String by lazy { getPrefApiKey() } 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 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 val json: Json by injectLazy() - private var series = emptyList() // Acts as a cache + // Act as a cache + private var series = emptyList() + private val libraryTypeCache = mutableMapOf() /** * 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>() - 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> { - 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 { - val items = response.parseAs>() - 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 { try { val result = response.parseAs>() @@ -401,8 +306,17 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou sortOptions = SortOptions(SortFieldEnum.AverageRating.type, false), 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( "$apiUrl/Series/all-v2?pageNumber=$page&pageSize=20", headersBuilder().build(), @@ -413,8 +327,18 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou override fun latestUpdatesRequest(page: Int): Request { val filter = FilterV2Dto( 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() 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 (smartFilterFilter?.state != 0 && smartFilterFilter != null) { val index = try { - smartFilterFilter?.state as Int - 1 + smartFilterFilter.state as Int - 1 } catch (e: Exception) { Log.e(LOG_TAG, e.toString(), e) 0 @@ -604,6 +528,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou "Image" -> 0 "Archive" -> 1 "Pdf" -> 4 + "Epub" -> 3 "Unknown" -> 2 else -> null } @@ -648,12 +573,12 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou val included = filter.state .filter { it.state == STATE_INCLUDE } .mapNotNull { languageFilter -> - languagesListMeta?.find { it.title == languageFilter.name }?.isoCode + languagesListMeta.find { it.title == languageFilter.name }?.isoCode } val excluded = filter.state .filter { it.state == STATE_EXCLUDE } .mapNotNull { languageFilter -> - languagesListMeta?.find { it.title == languageFilter.name }?.isoCode + languagesListMeta.find { it.title == languageFilter.name }?.isoCode } if (included.isNotEmpty()) { @@ -693,11 +618,21 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou is PubStatusFilterGroup -> { filter.state.forEach { pubStatusFilter -> if ((pubStatusFilter as Filter.CheckBox).state) { - filterV2.addStatement( - FilterComparison.Equal, - FilterField.PublicationStatus, - pubStatusFilter.name, - ) + val statusValue = when (pubStatusFilter.name) { + "Ongoing" -> 0 + "Hiatus" -> 1 + "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() - val rawResponse = responsePlus.body?.string() - if (rawResponse.isNullOrBlank() || rawResponse.trim().startsWith("<")) { + val rawResponse = responsePlus.body.string() + if (rawResponse.isBlank() || rawResponse.trim().startsWith("<")) { Log.e(LOG_TAG, "Invalid response for series-detail-plus: $rawResponse") // Optionally, show a default or skip averageScore "" @@ -1037,7 +972,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou for (volume in volumes) { // Issue: use chapter covers - if (isComicLibrary && volume.number == KavitaConstants.UNNUMBERED_VOLUME) { + if (isComicLibrary && volume.minNumber == KavitaConstants.UNNUMBERED_VOLUME) { coverCandidates += volume.chapters .filterNot { it.coverImage.isNullOrBlank() } .map { chapter -> @@ -1054,7 +989,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou } if (hasSingleFile && !volume.coverImage.isNullOrBlank()) { val isUnread = volume.pagesRead < volume.pages - val volumeNumber = volume.number.toFloat() + val volumeNumber = volume.minNumber.toFloat() coverCandidates.add( Triple( "$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 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>() + 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> { + 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 { + val items = response.parseAs>() + 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 * This only works on Komikku @@ -1181,8 +1273,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou } } } else { - val rawResponse = response.body?.string() - if (rawResponse.isNullOrEmpty() || rawResponse.startsWith("<")) { + val rawResponse = response.body.string() + if (rawResponse.isEmpty() || rawResponse.startsWith("<")) { Log.e(LOG_TAG, "Invalid response for related manga: $rawResponse") return emptyList() } @@ -1237,24 +1329,27 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou } private fun getLibraryType(seriesId: Int): LibraryTypeEnum? { - return try { - val seriesDto = client.newCall(GET("$apiUrl/Series/$seriesId", headersBuilder().build())) - .execute() - .parseAs() - - // 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())) + return libraryTypeCache[seriesId] ?: run { + try { + val seriesDto = client.newCall(GET("$apiUrl/Series/$seriesId", headersBuilder().build())) .execute() - .parseAs() - LibraryTypeEnum.fromInt(library.type) + .parseAs() + + 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() + 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() volumes.forEach { volume -> - volume.chapters.forEach { chapter -> - val sChapter = helper.chapterFromVolume(chapter, volume, libraryType = libraryType) - when (ChapterType.of(chapter, volume, libraryType)) { - ChapterType.SingleFileVolume -> { - // For Case 2, use positive volume numbers - // For Case 3, use negative numbers - sChapter.chapter_number = volume.number.toFloat() + // This fixes volumes that are recognized as chapters in Kavita because they have numbers in their titles + if (volume.chapters.size == 1 && volume.name.isNotBlank()) { + val chapter = volume.chapters.first() + val sChapter = helper.chapterFromVolume( + chapter, + volume, + 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) + } else { + chapters.add(sChapter) } - else -> chapters.add(sChapter) } } } return when { - // Case 1: Only chapters + // Case 1: Only chapters exist chapters.isNotEmpty() && volumeItems.isEmpty() -> 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.sortedByDescending { it.chapter_number } - // Case 3: Mixed content - chapters first, then volumes (as negative numbers) + // Case 3: Mixed content else -> { // Convert volume numbers to negative for proper sorting volumeItems.forEach { it.chapter_number = -it.chapter_number } - val sortedChapters = chapters.sortedByDescending { it.chapter_number } - val sortedVolumes = volumeItems.sortedBy { it.chapter_number } - sortedChapters + sortedVolumes + ( + chapters.sortedByDescending { it.chapter_number } + + volumeItems.sortedBy { it.chapter_number } + ) } } } catch (e: Exception) { @@ -1471,21 +1583,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou private var collectionsListMeta = emptyList() private var smartFilters = emptyList() private var cachedReadingLists: List = 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 { val toggledFilters = getToggledFilters() @@ -1542,6 +1642,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou "Image", "Archive", "Pdf", + "Epub", "Unknown", ).map { FormatFilter(it) }, ), @@ -1564,10 +1665,18 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou } if (pubStatusListMeta.isNotEmpty() and toggledFilters.contains("Publication Status")) { 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( UserRating(), ) @@ -1795,14 +1904,11 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou } } - private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "")!! - private fun getPrefApiUrl(): String = preferences.getString("APIURL", "")!! - private fun getPrefKey(): String = preferences.getString("APIKEY", "")!! + private fun getPrefBaseUrl(): String = preferences.getString("BASEURL", "") ?: "" + private fun getPrefApiUrl(): String = preferences.getString("APIURL", "") ?: "" + private fun getPrefKey(): String = preferences.getString("APIKEY", "") ?: "" 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 get() = getBoolean(GROUP_TAGS_PREF, GROUP_TAGS_DEFAULT) @@ -1829,7 +1935,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou // Clear all preferences preferences.edit().clear().commit() - // Reset all in-memory state + // Reset all in-memory state (for debug) // jwtToken = "" // isLogged = false // genresListMeta = emptyList() @@ -1852,16 +1958,18 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou private fun getPrefApiKey(): String { // http(s)://host:(port)/api/opds/api-key - val existingKey = preferences.getString("APIKEY", "") - return existingKey!!.ifEmpty { preferences.getString(ADDRESS_TITLE, "")!!.split("/opds/")[1] } + val existingKey = preferences.getString(APIKEY, "") + 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 { private const val ADDRESS_TITLE = "Address" private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull() - private const val SCORE_PREF = "Score" - const val SCORE_DEFAULT = true + private const val APIKEY = "APIKEY" private const val LAST_VOLUME_COVER_PREF = "LastVolumeCover" 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 } - /* - * LOGIN - **/ /** * Used to check if a url is configured already in any of the sources * This is a limitation needed for tracking. @@ -1909,6 +2014,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou return "" } + /** + * LOGIN + * **/ private fun setupLogin(addressFromPreference: String = "") { Log.v(LOG_TAG, "[Setup Login] Starting setup") val validAddress = address.ifEmpty { addressFromPreference } @@ -1969,7 +2077,6 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou init { if (apiUrl.isNotBlank()) { Single.fromCallable { - // Login doLogin() try { // Get current version val requestUrl = "$apiUrl/Server/server-info-slim" diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt index 7ad246fe..ed6c6b7c 100644 --- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaHelper.kt @@ -20,6 +20,8 @@ class KavitaHelper { val json = Json { isLenient = true ignoreUnknownKeys = true + coerceInputValues = true + explicitNulls = false allowSpecialFloatingPointValues = true useArrayPolymorphism = true prettyPrint = true @@ -37,9 +39,9 @@ class KavitaHelper { var hasNextPage = false if (!paginationHeader.isNullOrEmpty()) { val paginationInfo = json.decodeFromString(paginationHeader) - hasNextPage = paginationInfo.currentPage + 1 > paginationInfo.totalPages + hasNextPage = paginationInfo.currentPage + 1 < paginationInfo.totalPages } - return !hasNextPage + return hasNextPage } fun getIdFromUrl(url: String): Int { @@ -73,6 +75,9 @@ class KavitaHelper { fun chapterFromVolume(chapter: ChapterDto, volume: VolumeDto, singleFileVolumeNumber: Int? = null, libraryType: LibraryTypeEnum? = null): SChapter = SChapter.create().apply { val type = ChapterType.of(chapter, volume, libraryType) + val titleName = chapter.titleName ?: "" + val title = chapter.title ?: "" + val range = chapter.range ?: "" name = when (type) { ChapterType.Regular -> { @@ -80,23 +85,20 @@ class KavitaHelper { it.toString().padStart(2, '0') } ?: chapter.number when { - chapter.titleName.isBlank() -> "Vol.${volume.number} Ch.$chapterNum" - chapter.titleName.trim().matches(Regex("^\\d+$")) -> - "Vol.${volume.number} Ch.${chapter.titleName.trim().padStart(2, '0')}" - else -> "Vol.${volume.number} Ch.$chapterNum - ${chapter.titleName}" + titleName.any { it.isLetter() } -> "$chapterNum - $titleName" + else -> "Vol.${volume.minNumber} Ch.$chapterNum" } } ChapterType.SingleFileVolume -> { when { - volume.name.isBlank() -> "Volume ${volume.number}" - volume.name.trim().matches(Regex("^\\d+$")) -> "Volume ${volume.name.trim().toIntOrNull()?.toString() ?: volume.name.trim()}" - else -> "${volume.number} - ${volume.name}" + volume.name.any { it.isLetter() } -> "v${volume.minNumber} - ${volume.name}" + else -> "Volume ${volume.minNumber}" } } ChapterType.Special -> { when { - chapter.title.isNotBlank() -> chapter.title - chapter.range.isNotBlank() -> chapter.range + title.isNotBlank() -> title + range.isNotBlank() -> range else -> "Special" } } @@ -105,9 +107,8 @@ class KavitaHelper { it.toString().padStart(2, '0') } ?: chapter.number when { - chapter.titleName.isBlank() -> "Chapter $chapterNum" - chapter.titleName.trim().matches(Regex("^\\d+$")) -> "Chapter ${chapter.titleName.trim().padStart(2, '0')}" - else -> "$chapterNum - ${chapter.titleName}" + titleName.any { it.isLetter() } -> "$chapterNum - $titleName" + else -> "Chapter $chapterNum" } } ChapterType.Issue -> { @@ -115,10 +116,7 @@ class KavitaHelper { it.toString().padStart(3, '0') } ?: chapter.number when { - chapter.titleName.isNotBlank() && !chapter.titleName.trim().matches(Regex("^\\d+$")) -> - "#$issueNum ${chapter.titleName}" - chapter.title.isNotBlank() && !chapter.title.trim().matches(Regex("^\\d+$")) -> - "#$issueNum ${chapter.title}" + titleName.any { it.isLetter() } -> "$titleName (#$issueNum)" else -> "Issue #$issueNum" } } @@ -128,13 +126,19 @@ class KavitaHelper { type == ChapterType.SingleFileVolume && singleFileVolumeNumber != null -> 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 -> chapter.number.toFloatOrNull() ?: 0f // For volumes in mixed content, place them below chapter 0 else -> { val volumeNum = try { - volume.number.toString().toFloatOrNull() ?: 0f + volume.minNumber.toString().toFloatOrNull() ?: 0f } catch (e: NumberFormatException) { 0f } @@ -149,7 +153,6 @@ class KavitaHelper { } 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 url = "${url}_${chapter.fileCount}" } @@ -161,7 +164,6 @@ class KavitaHelper { ChapterType.Issue -> "Issue" ChapterType.Chapter -> "Chapter" ChapterType.Regular -> "Chapter" - else -> "Chapter" } } diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaUtils.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaUtils.kt index 76f28b77..711195d2 100644 --- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaUtils.kt +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/KavitaUtils.kt @@ -10,11 +10,6 @@ import java.text.SimpleDateFormat import java.util.Locale 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 { return date?.let { try { diff --git a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt index d2e53694..4814b9fd 100644 --- a/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt +++ b/src/all/kavita/src/eu/kanade/tachiyomi/extension/all/kavita/dto/MangaDto.kt @@ -9,7 +9,7 @@ interface ConvertibleToSManga { 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) { Image(0), Archive(1), @@ -190,8 +190,8 @@ data class AuthorDto( @Serializable data class VolumeDto( val id: Int, - val number: Int, - val name: String, + val minNumber: Int, + val name: String = "", val pages: Int, val pagesRead: Int, val lastModified: String, @@ -217,16 +217,16 @@ enum class ChapterType { fun of(chapter: ChapterDto, volume: VolumeDto, libraryType: LibraryTypeEnum? = null): ChapterType = when { // Special - volume.number == SPECIAL_NUMBER -> Special + volume.minNumber == SPECIAL_NUMBER -> Special // Issue - volume.number == UNNUMBERED_VOLUME_NUMBER -> when (libraryType) { + volume.minNumber == UNNUMBERED_VOLUME_NUMBER -> when (libraryType) { LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue else -> Chapter } // SingleFileVolume chapter.number == KavitaConstants.UNNUMBERED_VOLUME_STR -> SingleFileVolume // Regular - volume.number > 0 -> Regular + volume.minNumber > 0 -> Regular // Everything else depends on library type else -> when (libraryType) { LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> Issue @@ -244,7 +244,7 @@ data class ChapterDto( val pages: Int, val isSpecial: Boolean, val title: String, - val titleName: String, + val titleName: String = "", val pagesRead: Int, val coverImageLocked: Boolean, val coverImage: String, @@ -268,13 +268,13 @@ data class ReadingListDto( val title: String, val coverImage: String? = null, val promoted: Boolean = false, - val summary: String?, + val summary: String? = null, val itemCount: Int, val startingYear: Int, val startingMonth: Int, val endingYear: Int, val endingMonth: Int, - val ownerUserName: String?, + val ownerUserName: String? = null, @SerialName("items") val items: List = emptyList(), ) @@ -287,11 +287,11 @@ data class ReadingListItemDto( val seriesName: String, val chapterNumber: String?, val volumeNumber: String?, - val chapterTitleName: String?, + val chapterTitleName: String? = null, val volumeId: Int?, - val title: String?, - val summary: String?, - val releaseDate: String?, + val title: String? = null, + val summary: String? = null, + val releaseDate: String? = null, val libraryName: String?, @SerialName("readingListId") val readingListId: Int, )