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 148e5abff..2f4e9db24 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 @@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.SuspendLazy import java.util.EnumSet class ExternalMangaRepository( @@ -31,38 +32,19 @@ class ExternalMangaRepository( }.getOrNull() } + private val filterOptions = SuspendLazy(contentSource::getListFilterOptions) + override val sortOrders: Set - get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.ALPHABETICAL) + get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.POPULARITY) override val filterCapabilities: MangaListFilterCapabilities - get() = capabilities.let { - MangaListFilterCapabilities( - isMultipleTagsSupported = it?.isMultipleTagsSupported == true, - isTagsExclusionSupported = it?.isTagsExclusionSupported == true, - isSearchSupported = it?.isSearchSupported == true, - isSearchWithFiltersSupported = false, // TODO - isYearSupported = false, // TODO - isYearRangeSupported = false, // TODO - isOriginalLocaleSupported = false, // TODO - ) - } + get() = capabilities?.listFilterCapabilities ?: MangaListFilterCapabilities() override var defaultSortOrder: SortOrder - get() = capabilities?.defaultSortOrder ?: SortOrder.ALPHABETICAL + get() = capabilities?.availableSortOrders?.firstOrNull() ?: SortOrder.ALPHABETICAL set(value) = Unit - override suspend fun getFilterOptions(): MangaListFilterOptions = capabilities.let { - MangaListFilterOptions( - availableTags = runInterruptible(Dispatchers.IO) { - contentSource.getTags() - }, - availableStates = it?.availableStates.orEmpty(), - availableContentRating = it?.availableContentRating.orEmpty(), - availableContentTypes = emptySet(), - availableDemographics = emptySet(), - availableLocales = emptySet(), - ) - } + override suspend fun getFilterOptions(): MangaListFilterOptions = filterOptions.get() override suspend fun getList(offset: Int, order: SortOrder?, filter: MangaListFilter?): List = runInterruptible(Dispatchers.IO) { @@ -77,7 +59,9 @@ class ExternalMangaRepository( contentSource.getPages(chapter) } - override suspend fun getPageUrl(page: MangaPage): String = page.url // TODO + override suspend fun getPageUrl(page: MangaPage): String = runInterruptible(Dispatchers.IO) { + contentSource.getPageUrl(page.url) + } override suspend fun getRelatedMangaImpl(seed: Manga): List = emptyList() // TODO } 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 index c032b05a4..103b40d66 100644 --- 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 @@ -8,12 +8,14 @@ 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.Demographic 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.MangaListFilterCapabilities +import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag @@ -29,6 +31,17 @@ class ExternalPluginContentSource( private val source: ExternalMangaSource, ) { + @Blocking + @WorkerThread + fun getListFilterOptions() = MangaListFilterOptions( + availableTags = fetchTags(), + availableStates = fetchEnumSet(MangaState::class.java, "filter/states"), + availableContentRating = fetchEnumSet(ContentRating::class.java, "filter/content_ratings"), + availableContentTypes = fetchEnumSet(ContentType::class.java, "filter/content_types"), + availableDemographics = fetchEnumSet(Demographic::class.java, "filter/demographics"), + availableLocales = fetchLocales(), + ) + @Blocking @WorkerThread fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List { @@ -106,8 +119,8 @@ class ExternalPluginContentSource( @Blocking @WorkerThread - fun getTags(): Set { - val uri = "content://${source.authority}/tags".toUri() + private fun fetchTags(): Set { + val uri = "content://${source.authority}/filter/tags".toUri() return contentResolver.query(uri, null, null, null, null) .safe() .use { cursor -> @@ -125,6 +138,40 @@ class ExternalPluginContentSource( } } + @Blocking + @WorkerThread + fun getPageUrl(url: String): String { + val uri = "content://${source.authority}/pages/0".toUri().buildUpon() + .appendQueryParameter("url", url) + .build() + return contentResolver.query(uri, null, null, null, null) + .safe() + .use { cursor -> + if (cursor.moveToFirst()) { + cursor.getString(COLUMN_VALUE) + } else { + url + } + } + } + + @Blocking + @WorkerThread + private fun fetchLocales(): Set { + val uri = "content://${source.authority}/filter/locales".toUri() + return contentResolver.query(uri, null, null, null, null) + .safe() + .use { cursor -> + val result = ArraySet(cursor.count) + if (cursor.moveToFirst()) { + do { + result += Locale(cursor.getString(COLUMN_NAME)) + } while (cursor.moveToNext()) + } + result + } + } + fun getCapabilities(): MangaSourceCapabilities? { val uri = "content://${source.authority}/capabilities".toUri() return contentResolver.query(uri, null, null, null, null) @@ -137,26 +184,18 @@ class ExternalPluginContentSource( ?.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, + listFilterCapabilities = MangaListFilterCapabilities( + isMultipleTagsSupported = cursor.getBooleanOrDefault(COLUMN_MULTIPLE_TAGS, false), + isTagsExclusionSupported = cursor.getBooleanOrDefault(COLUMN_TAGS_EXCLUSION, false), + isSearchSupported = cursor.getBooleanOrDefault(COLUMN_SEARCH, false), + isSearchWithFiltersSupported = cursor.getBooleanOrDefault( + COLUMN_SEARCH_WITH_FILTERS, + false, + ), + isYearSupported = cursor.getBooleanOrDefault(COLUMN_YEAR, false), + isYearRangeSupported = cursor.getBooleanOrDefault(COLUMN_YEAR_RANGE, false), + isOriginalLocaleSupported = cursor.getBooleanOrDefault(COLUMN_ORIGINAL_LOCALE, false), + ), ) } else { null @@ -226,6 +265,26 @@ class ExternalPluginContentSource( source = source, ) + private fun > fetchEnumSet(cls: Class, path: String): EnumSet { + val uri = "content://${source.authority}/$path".toUri() + return contentResolver.query(uri, null, null, null, null) + .safe() + .use { cursor -> + val result = EnumSet.noneOf(cls) + val enumConstants = cls.enumConstants ?: return@use result + if (cursor.moveToFirst()) { + do { + val name = cursor.getString(COLUMN_NAME) + val enumValue = enumConstants.find { it.name == name } + if (enumValue != null) { + result.add(enumValue) + } + } while (cursor.moveToNext()) + } + result + } + } + private fun Cursor?.safe() = ExternalPluginCursor( source = source, cursor = this ?: throw IncompatiblePluginException(source.name, null), @@ -233,27 +292,19 @@ class ExternalPluginContentSource( 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, + val listFilterCapabilities: MangaListFilterCapabilities, ) 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_MULTIPLE_TAGS = "multiple_tags" + const val COLUMN_TAGS_EXCLUSION = "tags_exclusion" + const val COLUMN_SEARCH = "search" + const val COLUMN_SEARCH_WITH_FILTERS = "search_with_filters" + const val COLUMN_YEAR = "year" + const val COLUMN_YEAR_RANGE = "year_range" + const val COLUMN_ORIGINAL_LOCALE = "original_locale" const val COLUMN_ID = "id" const val COLUMN_NAME = "name" const val COLUMN_NUMBER = "number" @@ -275,5 +326,6 @@ class ExternalPluginContentSource( const val COLUMN_DESCRIPTION = "description" const val COLUMN_PREVIEW = "preview" const val COLUMN_KEY = "key" + const val COLUMN_VALUE = "value" } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 216804742..14e7cccc2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.reader.domain import android.content.Context +import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Rect import android.net.Uri @@ -140,7 +141,7 @@ class PageLoader @Inject constructor( val entry = zip.getEntry(uri.fragment) context.ensureRamAtLeast(entry.size * 2) zip.getInputStream(zip.getEntry(uri.fragment)).use { - BitmapFactory.decodeStream(it) + checkBitmapNotNull(BitmapFactory.decodeStream(it)) } } } @@ -149,7 +150,7 @@ class PageLoader @Inject constructor( val file = uri.toFile() context.ensureRamAtLeast(file.length() * 2) runInterruptible(Dispatchers.IO) { - BitmapFactory.decodeFile(file.absolutePath) + checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath)) }.use { image -> image.compressToPNG(file) } @@ -245,6 +246,8 @@ class PageLoader @Inject constructor( return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES) } + private fun checkBitmapNotNull(bitmap: Bitmap?): Bitmap = checkNotNull(bitmap) { "Cannot decode bitmap" } + private fun Deferred.isValid(): Boolean { return getCompletionResultOrNull()?.map { uri -> uri.exists() && uri.isTargetNotEmpty()