Migrate external sources to new filter
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user