From 20852dbd12967f969465cbbdf379f3e38ceb4a38 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 1 Aug 2024 20:34:25 +0300 Subject: [PATCH] Fix query plugin source capabilities --- .../exceptions/IncompatiblePluginException.kt | 6 + .../external/ExternalMangaRepository.kt | 236 ++------------ .../external/ExternalPluginContentSource.kt | 291 ++++++++++++++++++ .../core/parser/external/IndexedCursor.kt | 80 +++++ .../koitharu/kotatsu/core/util/ext/Cursor.kt | 2 + .../kotatsu/core/util/ext/Throwable.kt | 3 +- app/src/main/res/values/strings.xml | 1 + 7 files changed, 408 insertions(+), 211 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/IncompatiblePluginException.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/IncompatiblePluginException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/IncompatiblePluginException.kt new file mode 100644 index 000000000..f22b74989 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/IncompatiblePluginException.kt @@ -0,0 +1,6 @@ +package org.koitharu.kotatsu.core.exceptions + +class IncompatiblePluginException( + val name: String?, + cause: Throwable?, +) : RuntimeException(cause) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt index a047d0ebc..c888495e4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt @@ -1,19 +1,12 @@ package org.koitharu.kotatsu.core.parser.external import android.content.ContentResolver -import android.database.Cursor -import androidx.collection.ArraySet -import androidx.core.database.getStringOrNull -import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runInterruptible import org.koitharu.kotatsu.core.cache.MemoryContentCache import org.koitharu.kotatsu.core.parser.CachingMangaRepository -import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.parsers.model.ContentRating -import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaListFilter @@ -21,9 +14,6 @@ import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.parsers.util.find -import org.koitharu.kotatsu.parsers.util.mapNotNullToSet -import org.koitharu.kotatsu.parsers.util.splitTwoParts import java.util.EnumSet import java.util.Locale @@ -33,232 +23,58 @@ class ExternalMangaRepository( cache: MemoryContentCache, ) : CachingMangaRepository(cache) { - private val capabilities by lazy { queryCapabilities() } + private val contentSource = ExternalPluginContentSource(contentResolver, source) + + private val capabilities by lazy { + runCatching { + contentSource.getCapabilities() + }.onFailure { + it.printStackTraceDebug() + }.getOrNull() + } override val sortOrders: Set get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.ALPHABETICAL) + override val states: Set get() = capabilities?.availableStates.orEmpty() + override val contentRatings: Set get() = capabilities?.availableContentRating.orEmpty() + override var defaultSortOrder: SortOrder get() = capabilities?.defaultSortOrder ?: SortOrder.ALPHABETICAL set(value) = Unit + override val isMultipleTagsSupported: Boolean get() = capabilities?.isMultipleTagsSupported ?: true + override val isTagsExclusionSupported: Boolean get() = capabilities?.isTagsExclusionSupported ?: false + override val isSearchSupported: Boolean get() = capabilities?.isSearchSupported ?: true override suspend fun getList(offset: Int, filter: MangaListFilter?): List = - runInterruptible(Dispatchers.Default) { - val uri = "content://${source.authority}/manga".toUri().buildUpon() - uri.appendQueryParameter("offset", offset.toString()) - when (filter) { - is MangaListFilter.Advanced -> { - filter.tags.forEach { uri.appendQueryParameter("tag_include", it.key) } - filter.tagsExclude.forEach { uri.appendQueryParameter("tag_exclude", it.key) } - filter.states.forEach { uri.appendQueryParameter("state", it.name) } - filter.locale?.let { uri.appendQueryParameter("locale", it.language) } - filter.contentRating.forEach { uri.appendQueryParameter("content_rating", it.name) } - } - - is MangaListFilter.Search -> { - uri.appendQueryParameter("query", filter.query) - } - - null -> Unit - } - contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name)?.use { cursor -> - val result = ArrayList(cursor.count) - if (cursor.moveToFirst()) { - do { - result += cursor.getManga() - } while (cursor.moveToNext()) - } - result - }.orEmpty() + runInterruptible(Dispatchers.IO) { + contentSource.getList(offset, filter) } - override suspend fun getDetailsImpl(manga: Manga): Manga = coroutineScope { - val chapters = async { queryChapters(manga.url) } - val details = queryDetails(manga.url) - Manga( - id = manga.id, - title = details.title.ifBlank { manga.title }, - altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle }, - url = details.url.ifEmpty { manga.url }, - publicUrl = details.publicUrl.ifEmpty { manga.publicUrl }, - rating = maxOf(details.rating, manga.rating), - isNsfw = details.isNsfw, - coverUrl = details.coverUrl.ifEmpty { manga.coverUrl }, - tags = details.tags + manga.tags, - state = details.state ?: manga.state, - author = details.author.ifNullOrEmpty { manga.author }, - largeCoverUrl = details.largeCoverUrl.ifNullOrEmpty { manga.largeCoverUrl }, - description = details.description.ifNullOrEmpty { manga.description }, - chapters = chapters.await(), - source = source, - ) + override suspend fun getDetailsImpl(manga: Manga): Manga = runInterruptible(Dispatchers.IO) { + contentSource.getDetails(manga) } - override suspend fun getPagesImpl(chapter: MangaChapter): List = runInterruptible(Dispatchers.Default) { - val uri = "content://${source.authority}/chapters".toUri() - .buildUpon() - .appendPath(chapter.url) - .build() - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val result = ArrayList(cursor.count) - if (cursor.moveToFirst()) { - do { - result += MangaPage( - id = cursor.getLong(0), - url = cursor.getString(1), - preview = cursor.getStringOrNull(2), - source = source, - ) - } while (cursor.moveToNext()) - } - result - }.orEmpty() + override suspend fun getPagesImpl(chapter: MangaChapter): List = runInterruptible(Dispatchers.IO) { + contentSource.getPages(chapter) } - override suspend fun getPageUrl(page: MangaPage): String = page.url + override suspend fun getPageUrl(page: MangaPage): String = page.url // TODO - override suspend fun getTags(): Set = runInterruptible(Dispatchers.Default) { - val uri = "content://${source.authority}/tags".toUri() - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val result = ArraySet(cursor.count) - if (cursor.moveToFirst()) { - do { - result += MangaTag( - key = cursor.getString(0), - title = cursor.getString(1), - source = source, - ) - } while (cursor.moveToNext()) - } - result - }.orEmpty() + override suspend fun getTags(): Set = runInterruptible(Dispatchers.IO) { + contentSource.getTags() } - override suspend fun getLocales(): Set = emptySet() + override suspend fun getLocales(): Set = emptySet() // TODO override suspend fun getRelatedMangaImpl(seed: Manga): List = emptyList() // TODO - - private suspend fun queryDetails(url: String): Manga = runInterruptible(Dispatchers.Default) { - val uri = "content://${source.authority}/manga".toUri() - .buildUpon() - .appendPath(url) - .build() - checkNotNull( - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - cursor.moveToFirst() - cursor.getManga() - }, - ) - } - - private suspend fun queryChapters(url: String): List? = runInterruptible(Dispatchers.Default) { - val uri = "content://${source.authority}/manga/chapters".toUri() - .buildUpon() - .appendPath(url) - .build() - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val result = ArrayList(cursor.count) - if (cursor.moveToFirst()) { - do { - result += MangaChapter( - id = cursor.getLong(0), - name = cursor.getString(1), - number = cursor.getFloat(2), - volume = cursor.getInt(3), - url = cursor.getString(4), - scanlator = cursor.getStringOrNull(5), - uploadDate = cursor.getLong(6), - branch = cursor.getStringOrNull(7), - source = source, - ) - } while (cursor.moveToNext()) - } - result - } - } - - private fun Cursor.getManga() = Manga( - id = getLong(0), - title = getString(1), - altTitle = getStringOrNull(2), - url = getString(3), - publicUrl = getString(4), - rating = getFloat(5), - isNsfw = getInt(6) > 1, - coverUrl = getString(7), - tags = getStringOrNull(8)?.split(':')?.mapNotNullToSet { - val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null - MangaTag(key = parts.first, title = parts.second, source = source) - }.orEmpty(), - state = getStringOrNull(9)?.let { MangaState.entries.find(it) }, - author = optString(10), - largeCoverUrl = optString(11), - description = optString(12), - chapters = emptyList(), - source = source, - ) - - private fun Cursor.optString(columnIndex: Int): String? { - return if (isNull(columnIndex)) { - null - } else { - getString(columnIndex) - } - } - - private fun queryCapabilities(): MangaSourceCapabilities? { - val uri = "content://${source.authority}/capabilities".toUri() - return contentResolver.query(uri, null, null, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - MangaSourceCapabilities( - availableSortOrders = cursor.getStringOrNull(0) - ?.split(',') - ?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) { - SortOrder.entries.find(it) - }.orEmpty(), - availableStates = cursor.getStringOrNull(1) - ?.split(',') - ?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) { - MangaState.entries.find(it) - }.orEmpty(), - availableContentRating = cursor.getStringOrNull(2) - ?.split(',') - ?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) { - ContentRating.entries.find(it) - }.orEmpty(), - isMultipleTagsSupported = cursor.getInt(3) > 1, - isTagsExclusionSupported = cursor.getInt(4) > 1, - isSearchSupported = cursor.getInt(5) > 1, - contentType = ContentType.entries.find(cursor.getString(6)) ?: ContentType.OTHER, - defaultSortOrder = cursor.getStringOrNull(7)?.let { - SortOrder.entries.find(it) - } ?: SortOrder.ALPHABETICAL, - sourceLocale = cursor.getStringOrNull(8)?.let { Locale(it) } ?: Locale.ROOT, - ) - } else { - null - } - } - } - - private class MangaSourceCapabilities( - val availableSortOrders: Set, - val availableStates: Set, - val availableContentRating: Set, - val isMultipleTagsSupported: Boolean, - val isTagsExclusionSupported: Boolean, - val isSearchSupported: Boolean, - val contentType: ContentType, - val defaultSortOrder: SortOrder, - val sourceLocale: Locale, - ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt new file mode 100644 index 000000000..5e7d9a800 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt @@ -0,0 +1,291 @@ +package org.koitharu.kotatsu.core.parser.external + +import android.content.ContentResolver +import android.database.Cursor +import androidx.annotation.WorkerThread +import androidx.collection.ArraySet +import androidx.core.net.toUri +import org.jetbrains.annotations.Blocking +import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.core.util.ext.toLocale +import org.koitharu.kotatsu.parsers.model.ContentRating +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaListFilter +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.find +import org.koitharu.kotatsu.parsers.util.mapNotNullToSet +import org.koitharu.kotatsu.parsers.util.splitTwoParts +import java.util.EnumSet +import java.util.Locale + +class ExternalPluginContentSource( + private val contentResolver: ContentResolver, + private val source: ExternalMangaSource, +) { + + @Blocking + @WorkerThread + fun getList(offset: Int, filter: MangaListFilter?): List = runCatchingCompatibility { + val uri = "content://${source.authority}/manga".toUri().buildUpon() + uri.appendQueryParameter("offset", offset.toString()) + when (filter) { + is MangaListFilter.Advanced -> { + filter.tags.forEach { uri.appendQueryParameter("tag_include", it.key) } + filter.tagsExclude.forEach { uri.appendQueryParameter("tag_exclude", it.key) } + filter.states.forEach { uri.appendQueryParameter("state", it.name) } + filter.locale?.let { uri.appendQueryParameter("locale", it.language) } + filter.contentRating.forEach { uri.appendQueryParameter("content_rating", it.name) } + } + + is MangaListFilter.Search -> { + uri.appendQueryParameter("query", filter.query) + } + + null -> Unit + } + contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name) + .indexed() + .use { cursor -> + val result = ArrayList(cursor.count) + if (cursor.moveToFirst()) { + do { + result += cursor.getManga() + } while (cursor.moveToNext()) + } + result + } + } + + @Blocking + @WorkerThread + fun getDetails(manga: Manga) = runCatchingCompatibility { + val chapters = queryChapters(manga.url) + val details = queryDetails(manga.url) + Manga( + id = manga.id, + title = details.title.ifBlank { manga.title }, + altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle }, + url = details.url.ifEmpty { manga.url }, + publicUrl = details.publicUrl.ifEmpty { manga.publicUrl }, + rating = maxOf(details.rating, manga.rating), + isNsfw = details.isNsfw, + coverUrl = details.coverUrl.ifEmpty { manga.coverUrl }, + tags = details.tags + manga.tags, + state = details.state ?: manga.state, + author = details.author.ifNullOrEmpty { manga.author }, + largeCoverUrl = details.largeCoverUrl.ifNullOrEmpty { manga.largeCoverUrl }, + description = details.description.ifNullOrEmpty { manga.description }, + chapters = chapters, + source = source, + ) + } + + @Blocking + @WorkerThread + fun getPages(chapter: MangaChapter): List = runCatchingCompatibility { + val uri = "content://${source.authority}/chapters".toUri() + .buildUpon() + .appendPath(chapter.url) + .build() + contentResolver.query(uri, null, null, null, null) + .indexed() + .use { cursor -> + val result = ArrayList(cursor.count) + if (cursor.moveToFirst()) { + do { + result += MangaPage( + id = cursor.getLong(COLUMN_ID), + url = cursor.getString(COLUMN_URL), + preview = cursor.getStringOrNull(COLUMN_PREVIEW), + source = source, + ) + } while (cursor.moveToNext()) + } + result + } + } + + @Blocking + @WorkerThread + fun getTags(): Set = runCatchingCompatibility { + val uri = "content://${source.authority}/tags".toUri() + contentResolver.query(uri, null, null, null, null) + .indexed() + .use { cursor -> + val result = ArraySet(cursor.count) + if (cursor.moveToFirst()) { + do { + result += MangaTag( + key = cursor.getString(COLUMN_KEY), + title = cursor.getString(COLUMN_TITLE), + source = source, + ) + } while (cursor.moveToNext()) + } + result + } + } + + fun getCapabilities(): MangaSourceCapabilities? { + val uri = "content://${source.authority}/capabilities".toUri() + return contentResolver.query(uri, null, null, null, null) + .indexed() + .use { cursor -> + if (cursor.moveToFirst()) { + MangaSourceCapabilities( + availableSortOrders = cursor.getStringOrNull(COLUMN_SORT_ORDERS) + ?.split(',') + ?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) { + SortOrder.entries.find(it) + }.orEmpty(), + availableStates = cursor.getStringOrNull(COLUMN_STATES) + ?.split(',') + ?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) { + MangaState.entries.find(it) + }.orEmpty(), + availableContentRating = cursor.getStringOrNull(COLUMN_CONTENT_RATING) + ?.split(',') + ?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) { + ContentRating.entries.find(it) + }.orEmpty(), + isMultipleTagsSupported = cursor.getBooleanOrDefault(COLUMN_MULTIPLE_TAGS_SUPPORTED, true), + isTagsExclusionSupported = cursor.getBooleanOrDefault(COLUMN_TAGS_EXCLUSION_SUPPORTED, false), + isSearchSupported = cursor.getBooleanOrDefault(COLUMN_SEARCH_SUPPORTED, true), + contentType = cursor.getStringOrNull(COLUMN_CONTENT_TYPE)?.let { + ContentType.entries.find(it) + } ?: ContentType.OTHER, + defaultSortOrder = cursor.getStringOrNull(COLUMN_DEFAULT_SORT_ORDER)?.let { + SortOrder.entries.find(it) + } ?: SortOrder.ALPHABETICAL, + sourceLocale = cursor.getStringOrNull(COLUMN_LOCALE)?.toLocale() ?: Locale.ROOT, + ) + } else { + null + } + } + } + + private fun queryDetails(url: String): Manga { + val uri = "content://${source.authority}/manga".toUri() + .buildUpon() + .appendPath(url) + .build() + return contentResolver.query(uri, null, null, null, null) + .indexed() + .use { cursor -> + cursor.moveToFirst() + cursor.getManga() + } + } + + private fun queryChapters(url: String): List { + val uri = "content://${source.authority}/manga/chapters".toUri() + .buildUpon() + .appendPath(url) + .build() + return contentResolver.query(uri, null, null, null, null) + .indexed() + .use { cursor -> + val result = ArrayList(cursor.count) + if (cursor.moveToFirst()) { + do { + result += MangaChapter( + id = cursor.getLong(COLUMN_ID), + name = cursor.getString(COLUMN_NAME), + number = cursor.getFloatOrDefault(COLUMN_NUMBER, 0f), + volume = cursor.getIntOrDefault(COLUMN_VOLUME, 0), + url = cursor.getString(COLUMN_URL), + scanlator = cursor.getStringOrNull(COLUMN_SCANLATOR), + uploadDate = cursor.getLongOrDefault(COLUMN_UPLOAD_DATE, 0L), + branch = cursor.getStringOrNull(COLUMN_BRANCH), + source = source, + ) + } while (cursor.moveToNext()) + } + result + } + } + + private fun IndexedCursor.getManga() = Manga( + id = getLong(COLUMN_ID), + title = getString(COLUMN_TITLE), + altTitle = getStringOrNull(COLUMN_ALT_TITLE), + url = getString(COLUMN_URL), + publicUrl = getString(COLUMN_PUBLIC_URL), + rating = getFloat(COLUMN_RATING), + isNsfw = getBooleanOrDefault(COLUMN_IS_NSFW, false), + coverUrl = getString(COLUMN_COVER_URL), + tags = getStringOrNull(COLUMN_TAGS)?.split(':')?.mapNotNullToSet { + val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null + MangaTag(key = parts.first, title = parts.second, source = source) + }.orEmpty(), + state = getStringOrNull(COLUMN_STATE)?.let { MangaState.entries.find(it) }, + author = getStringOrNull(COLUMN_AUTHOR), + largeCoverUrl = getStringOrNull(COLUMN_LARGE_COVER_URL), + description = getStringOrNull(COLUMN_DESCRIPTION), + chapters = emptyList(), + source = source, + ) + + private inline fun runCatchingCompatibility(block: () -> R): R = try { + block() + } catch (e: NoSuchElementException) { // unknown column name + throw IncompatiblePluginException(source.name, e) + } catch (e: IllegalArgumentException) { + throw IncompatiblePluginException(source.name, e) + } + + private fun Cursor?.indexed() = IndexedCursor(this ?: throw IncompatiblePluginException(source.name, null)) + + class MangaSourceCapabilities( + val availableSortOrders: Set, + val availableStates: Set, + val availableContentRating: Set, + val isMultipleTagsSupported: Boolean, + val isTagsExclusionSupported: Boolean, + val isSearchSupported: Boolean, + val contentType: ContentType, + val defaultSortOrder: SortOrder, + val sourceLocale: Locale, + ) + + private companion object { + + const val COLUMN_SORT_ORDERS = "sort_orders" + const val COLUMN_STATES = "states" + const val COLUMN_CONTENT_RATING = "content_rating" + const val COLUMN_MULTIPLE_TAGS_SUPPORTED = "multiple_tags_supported" + const val COLUMN_TAGS_EXCLUSION_SUPPORTED = "tags_exclusion_supported" + const val COLUMN_SEARCH_SUPPORTED = "search_supported" + const val COLUMN_CONTENT_TYPE = "content_type" + const val COLUMN_DEFAULT_SORT_ORDER = "default_sort_order" + const val COLUMN_LOCALE = "locale" + const val COLUMN_ID = "id" + const val COLUMN_NAME = "name" + const val COLUMN_NUMBER = "number" + const val COLUMN_VOLUME = "volume" + const val COLUMN_URL = "url" + const val COLUMN_SCANLATOR = "scanlator" + const val COLUMN_UPLOAD_DATE = "upload_date" + const val COLUMN_BRANCH = "branch" + const val COLUMN_TITLE = "title" + const val COLUMN_ALT_TITLE = "alt_title" + const val COLUMN_PUBLIC_URL = "public_url" + const val COLUMN_RATING = "rating" + const val COLUMN_IS_NSFW = "is_nsfw" + const val COLUMN_COVER_URL = "cover_url" + const val COLUMN_TAGS = "tags" + const val COLUMN_STATE = "state" + const val COLUMN_AUTHOR = "author" + const val COLUMN_LARGE_COVER_URL = "large_cover_url" + const val COLUMN_DESCRIPTION = "description" + const val COLUMN_PREVIEW = "preview" + const val COLUMN_KEY = "key" + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt new file mode 100644 index 000000000..0ebe258dd --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/IndexedCursor.kt @@ -0,0 +1,80 @@ +package org.koitharu.kotatsu.core.parser.external + +import android.database.Cursor +import android.database.CursorWrapper +import androidx.collection.MutableObjectIntMap +import androidx.collection.ObjectIntMap +import org.koitharu.kotatsu.core.util.ext.getBoolean + +class IndexedCursor(cursor: Cursor) : CursorWrapper(cursor) { + + private val columns: ObjectIntMap = MutableObjectIntMap(columnCount).also { result -> + val names = columnNames + names.forEachIndexed { index, s -> result.put(s, index) } + } + + fun getString(columnName: String): String { + return getString(columns[columnName]) + } + + fun getStringOrNull(columnName: String): String? { + val columnIndex = columns.getOrDefault(columnName, -1) + return when { + columnIndex == -1 -> null + isNull(columnIndex) -> null + else -> getString(columnIndex) + } + } + + fun getBoolean(columnName: String): Boolean { + return getBoolean(columns[columnName]) + } + + fun getBooleanOrDefault(columnName: String, defaultValue: Boolean): Boolean { + val columnIndex = columns.getOrDefault(columnName, -1) + return when { + columnIndex == -1 -> defaultValue + isNull(columnIndex) -> defaultValue + else -> getBoolean(columnIndex) + } + } + + fun getInt(columnName: String): Int { + return getInt(columns[columnName]) + } + + fun getIntOrDefault(columnName: String, defaultValue: Int): Int { + val columnIndex = columns.getOrDefault(columnName, -1) + return when { + columnIndex == -1 -> defaultValue + isNull(columnIndex) -> defaultValue + else -> getInt(columnIndex) + } + } + + fun getLong(columnName: String): Long { + return getLong(columns[columnName]) + } + + fun getLongOrDefault(columnName: String, defaultValue: Long): Long { + val columnIndex = columns.getOrDefault(columnName, -1) + return when { + columnIndex == -1 -> defaultValue + isNull(columnIndex) -> defaultValue + else -> getLong(columnIndex) + } + } + + fun getFloat(columnName: String): Float { + return getFloat(columns[columnName]) + } + + fun getFloatOrDefault(columnName: String, defaultValue: Float): Float { + val columnIndex = columns.getOrDefault(columnName, -1) + return when { + columnIndex == -1 -> defaultValue + isNull(columnIndex) -> defaultValue + else -> getFloat(columnIndex) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt index 3cec3da3b..c7c665d93 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Cursor.kt @@ -37,3 +37,5 @@ fun JSONObject.toContentValues(): ContentValues { } private fun String.escapeName() = "`$this`" + +fun Cursor.getBoolean(columnIndex: Int) = getInt(columnIndex) > 0 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt index 485d2a6ac..dd38ab04d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.exceptions.CaughtException import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException +import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException import org.koitharu.kotatsu.core.exceptions.SyncApiException import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions @@ -60,7 +61,7 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { -> resources.getString(R.string.network_error) is NoDataReceivedException -> resources.getString(R.string.error_no_data_received) - + is IncompatiblePluginException -> resources.getString(R.string.plugin_incompatible) is WrongPasswordException -> resources.getString(R.string.wrong_password) is NotFoundException -> resources.getString(R.string.not_found_404) is UnsupportedSourceException -> resources.getString(R.string.unsupported_source) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4424582d6..227506ecd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -669,4 +669,5 @@ Chapters read Chapters left External/plugin + Incompatible plugin or internal error. Make sure you are using the latest version of the plugin and Kotatsu