Migrate external sources to new filter

This commit is contained in:
Koitharu
2024-09-25 12:28:10 +03:00
committed by Mac135135
parent 956831f9d7
commit c8d04e4eb7
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.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<SortOrder>
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<Manga> =
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<Manga> = emptyList() // TODO
}

View File

@@ -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<Manga> {
@@ -106,8 +119,8 @@ class ExternalPluginContentSource(
@Blocking
@WorkerThread
fun getTags(): Set<MangaTag> {
val uri = "content://${source.authority}/tags".toUri()
private fun fetchTags(): Set<MangaTag> {
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<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? {
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 <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(
source = source,
cursor = this ?: throw IncompatiblePluginException(source.name, null),
@@ -233,27 +292,19 @@ class ExternalPluginContentSource(
class MangaSourceCapabilities(
val availableSortOrders: Set<SortOrder>,
val availableStates: Set<MangaState>,
val availableContentRating: Set<ContentRating>,
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"
}
}

View File

@@ -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<Uri>.isValid(): Boolean {
return getCompletionResultOrNull()?.map { uri ->
uri.exists() && uri.isTargetNotEmpty()