Migrate external sources to new filter

This commit is contained in:
Koitharu
2024-09-25 12:28:10 +03:00
parent b8be2f7158
commit 9a444cf965
3 changed files with 106 additions and 67 deletions

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import java.util.EnumSet import java.util.EnumSet
class ExternalMangaRepository( class ExternalMangaRepository(
@@ -31,38 +32,19 @@ class ExternalMangaRepository(
}.getOrNull() }.getOrNull()
} }
private val filterOptions = SuspendLazy(contentSource::getListFilterOptions)
override val sortOrders: Set<SortOrder> override val sortOrders: Set<SortOrder>
get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.ALPHABETICAL) get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.POPULARITY)
override val filterCapabilities: MangaListFilterCapabilities override val filterCapabilities: MangaListFilterCapabilities
get() = capabilities.let { get() = capabilities?.listFilterCapabilities ?: MangaListFilterCapabilities()
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
)
}
override var defaultSortOrder: SortOrder override var defaultSortOrder: SortOrder
get() = capabilities?.defaultSortOrder ?: SortOrder.ALPHABETICAL get() = capabilities?.availableSortOrders?.firstOrNull() ?: SortOrder.ALPHABETICAL
set(value) = Unit set(value) = Unit
override suspend fun getFilterOptions(): MangaListFilterOptions = capabilities.let { override suspend fun getFilterOptions(): MangaListFilterOptions = filterOptions.get()
MangaListFilterOptions(
availableTags = runInterruptible(Dispatchers.IO) {
contentSource.getTags()
},
availableStates = it?.availableStates.orEmpty(),
availableContentRating = it?.availableContentRating.orEmpty(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
)
}
override suspend fun getList(offset: Int, order: SortOrder?, filter: MangaListFilter?): List<Manga> = override suspend fun getList(offset: Int, order: SortOrder?, filter: MangaListFilter?): List<Manga> =
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
@@ -77,7 +59,9 @@ class ExternalMangaRepository(
contentSource.getPages(chapter) 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<Manga> = emptyList() // TODO override suspend fun getRelatedMangaImpl(seed: Manga): List<Manga> = emptyList() // TODO
} }

View File

@@ -8,12 +8,14 @@ import androidx.core.net.toUri
import org.jetbrains.annotations.Blocking import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty 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.ContentRating
import org.koitharu.kotatsu.parsers.model.ContentType 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter 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.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -29,6 +31,17 @@ class ExternalPluginContentSource(
private val source: ExternalMangaSource, 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 @Blocking
@WorkerThread @WorkerThread
fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
@@ -106,8 +119,8 @@ class ExternalPluginContentSource(
@Blocking @Blocking
@WorkerThread @WorkerThread
fun getTags(): Set<MangaTag> { private fun fetchTags(): Set<MangaTag> {
val uri = "content://${source.authority}/tags".toUri() val uri = "content://${source.authority}/filter/tags".toUri()
return contentResolver.query(uri, null, null, null, null) return contentResolver.query(uri, null, null, null, null)
.safe() .safe()
.use { cursor -> .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<Locale> {
val uri = "content://${source.authority}/filter/locales".toUri()
return contentResolver.query(uri, null, null, null, null)
.safe()
.use { cursor ->
val result = ArraySet<Locale>(cursor.count)
if (cursor.moveToFirst()) {
do {
result += Locale(cursor.getString(COLUMN_NAME))
} while (cursor.moveToNext())
}
result
}
}
fun getCapabilities(): MangaSourceCapabilities? { fun getCapabilities(): MangaSourceCapabilities? {
val uri = "content://${source.authority}/capabilities".toUri() val uri = "content://${source.authority}/capabilities".toUri()
return contentResolver.query(uri, null, null, null, null) return contentResolver.query(uri, null, null, null, null)
@@ -137,26 +184,18 @@ class ExternalPluginContentSource(
?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) { ?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) {
SortOrder.entries.find(it) SortOrder.entries.find(it)
}.orEmpty(), }.orEmpty(),
availableStates = cursor.getStringOrNull(COLUMN_STATES) listFilterCapabilities = MangaListFilterCapabilities(
?.split(',') isMultipleTagsSupported = cursor.getBooleanOrDefault(COLUMN_MULTIPLE_TAGS, false),
?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) { isTagsExclusionSupported = cursor.getBooleanOrDefault(COLUMN_TAGS_EXCLUSION, false),
MangaState.entries.find(it) isSearchSupported = cursor.getBooleanOrDefault(COLUMN_SEARCH, false),
}.orEmpty(), isSearchWithFiltersSupported = cursor.getBooleanOrDefault(
availableContentRating = cursor.getStringOrNull(COLUMN_CONTENT_RATING) COLUMN_SEARCH_WITH_FILTERS,
?.split(',') false,
?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) { ),
ContentRating.entries.find(it) isYearSupported = cursor.getBooleanOrDefault(COLUMN_YEAR, false),
}.orEmpty(), isYearRangeSupported = cursor.getBooleanOrDefault(COLUMN_YEAR_RANGE, false),
isMultipleTagsSupported = cursor.getBooleanOrDefault(COLUMN_MULTIPLE_TAGS_SUPPORTED, true), isOriginalLocaleSupported = cursor.getBooleanOrDefault(COLUMN_ORIGINAL_LOCALE, false),
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 { } else {
null null
@@ -226,6 +265,26 @@ class ExternalPluginContentSource(
source = source, source = source,
) )
private fun <E : Enum<E>> fetchEnumSet(cls: Class<E>, path: String): EnumSet<E> {
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( private fun Cursor?.safe() = ExternalPluginCursor(
source = source, source = source,
cursor = this ?: throw IncompatiblePluginException(source.name, null), cursor = this ?: throw IncompatiblePluginException(source.name, null),
@@ -233,27 +292,19 @@ class ExternalPluginContentSource(
class MangaSourceCapabilities( class MangaSourceCapabilities(
val availableSortOrders: Set<SortOrder>, val availableSortOrders: Set<SortOrder>,
val availableStates: Set<MangaState>, val listFilterCapabilities: MangaListFilterCapabilities,
val availableContentRating: Set<ContentRating>,
val isMultipleTagsSupported: Boolean,
val isTagsExclusionSupported: Boolean,
val isSearchSupported: Boolean,
val contentType: ContentType,
val defaultSortOrder: SortOrder,
val sourceLocale: Locale,
) )
private companion object { private companion object {
const val COLUMN_SORT_ORDERS = "sort_orders" const val COLUMN_SORT_ORDERS = "sort_orders"
const val COLUMN_STATES = "states" const val COLUMN_MULTIPLE_TAGS = "multiple_tags"
const val COLUMN_CONTENT_RATING = "content_rating" const val COLUMN_TAGS_EXCLUSION = "tags_exclusion"
const val COLUMN_MULTIPLE_TAGS_SUPPORTED = "multiple_tags_supported" const val COLUMN_SEARCH = "search"
const val COLUMN_TAGS_EXCLUSION_SUPPORTED = "tags_exclusion_supported" const val COLUMN_SEARCH_WITH_FILTERS = "search_with_filters"
const val COLUMN_SEARCH_SUPPORTED = "search_supported" const val COLUMN_YEAR = "year"
const val COLUMN_CONTENT_TYPE = "content_type" const val COLUMN_YEAR_RANGE = "year_range"
const val COLUMN_DEFAULT_SORT_ORDER = "default_sort_order" const val COLUMN_ORIGINAL_LOCALE = "original_locale"
const val COLUMN_LOCALE = "locale"
const val COLUMN_ID = "id" const val COLUMN_ID = "id"
const val COLUMN_NAME = "name" const val COLUMN_NAME = "name"
const val COLUMN_NUMBER = "number" const val COLUMN_NUMBER = "number"
@@ -275,5 +326,6 @@ class ExternalPluginContentSource(
const val COLUMN_DESCRIPTION = "description" const val COLUMN_DESCRIPTION = "description"
const val COLUMN_PREVIEW = "preview" const val COLUMN_PREVIEW = "preview"
const val COLUMN_KEY = "key" const val COLUMN_KEY = "key"
const val COLUMN_VALUE = "value"
} }
} }

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.reader.domain package org.koitharu.kotatsu.reader.domain
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
@@ -140,7 +141,7 @@ class PageLoader @Inject constructor(
val entry = zip.getEntry(uri.fragment) val entry = zip.getEntry(uri.fragment)
context.ensureRamAtLeast(entry.size * 2) context.ensureRamAtLeast(entry.size * 2)
zip.getInputStream(zip.getEntry(uri.fragment)).use { 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() val file = uri.toFile()
context.ensureRamAtLeast(file.length() * 2) context.ensureRamAtLeast(file.length() * 2)
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
BitmapFactory.decodeFile(file.absolutePath) checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath))
}.use { image -> }.use { image ->
image.compressToPNG(file) image.compressToPNG(file)
} }
@@ -245,6 +246,8 @@ class PageLoader @Inject constructor(
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES) 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<Uri>.isValid(): Boolean { private fun Deferred<Uri>.isValid(): Boolean {
return getCompletionResultOrNull()?.map { uri -> return getCompletionResultOrNull()?.map { uri ->
uri.exists() && uri.isTargetNotEmpty() uri.exists() && uri.isTargetNotEmpty()