Merge pull request #431 from KotatsuApp/ExcludeTags

pull/421/head
Koitharu 2 years ago committed by GitHub
commit efb5d34279
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,6 +4,8 @@ object ErrorMessages {
const val FILTER_MULTIPLE_STATES_NOT_SUPPORTED = "Multiple states are not supported by this source"
const val FILTER_MULTIPLE_GENRES_NOT_SUPPORTED = "Multiple genres are not supported by this source"
const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED =
"Multiple Content Rating are not supported by this source"
const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED =
"Filtering by both genres and locale is not supported by this source"
const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED =

@ -33,11 +33,20 @@ abstract class MangaParser @InternalParsersApi constructor(
open val availableStates: Set<MangaState>
get() = emptySet()
open val availableContentRating: Set<ContentRating>
get() = emptySet()
/**
* Whether parser supports filtering by more than one tag
*/
open val isMultipleTagsSupported: Boolean = true
/**
* Whether parser supports tagsExclude field in filter
*/
open val isTagsExclusionSupported: Boolean = false
@Deprecated(
message = "Use availableSortOrders instead",
replaceWith = ReplaceWith("availableSortOrders"),
@ -95,6 +104,7 @@ abstract class MangaParser @InternalParsersApi constructor(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = throw NotImplementedError("Please implement getList(offset, filter) instead")
@ -130,18 +140,30 @@ abstract class MangaParser @InternalParsersApi constructor(
"org.koitharu.kotatsu.parsers.model.MangaListFilter",
),
)
open suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
open suspend fun getList(
offset: Int,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder?,
): List<Manga> {
return getList(
offset,
MangaListFilter.Advanced(sortOrder ?: defaultSortOrder, tags.orEmpty(), null, emptySet()),
MangaListFilter.Advanced(
sortOrder ?: defaultSortOrder,
tags.orEmpty(),
tagsExclude.orEmpty(),
null,
emptySet(),
emptySet(),
),
)
}
open suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) {
is MangaListFilter.Advanced -> getList(offset, null, filter.tags, filter.sortOrder)
is MangaListFilter.Search -> getList(offset, filter.query, null, defaultSortOrder)
null -> getList(offset, null, null, defaultSortOrder)
is MangaListFilter.Advanced -> getList(offset, null, filter.tags, filter.tagsExclude, filter.sortOrder)
is MangaListFilter.Search -> getList(offset, filter.query, null, null, defaultSortOrder)
null -> getList(offset, null, null, null, defaultSortOrder)
}
}

@ -36,6 +36,7 @@ abstract class PagedMangaParser(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = throw UnsupportedOperationException("You should use getListPage for PagedMangaParser")
@ -43,14 +44,15 @@ abstract class PagedMangaParser(
page: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = throw NotImplementedError("Please implement getListPage(page, filter) instead")
open suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) {
is MangaListFilter.Advanced -> getListPage(page, null, filter.tags, filter.sortOrder)
is MangaListFilter.Search -> getListPage(page, filter.query, null, defaultSortOrder)
null -> getListPage(page, null, null, defaultSortOrder)
is MangaListFilter.Advanced -> getListPage(page, null, filter.tags, filter.tagsExclude, filter.sortOrder)
is MangaListFilter.Search -> getListPage(page, filter.query, null, null, defaultSortOrder)
null -> getListPage(page, null, null, null, defaultSortOrder)
}
}

@ -0,0 +1,7 @@
package org.koitharu.kotatsu.parsers.model
enum class ContentRating {
SAFE,
SUGGESTIVE,
ADULT
}

@ -20,10 +20,12 @@ sealed interface MangaListFilter {
data class Advanced(
override val sortOrder: SortOrder,
@JvmField val tags: Set<MangaTag>,
@JvmField val tagsExclude: Set<MangaTag>,
@JvmField val locale: Locale?,
@JvmField val states: Set<MangaState>,
@JvmField val contentRating: Set<ContentRating>,
) : MangaListFilter {
override fun isEmpty(): Boolean = tags.isEmpty() && locale == null && states.isEmpty()
override fun isEmpty(): Boolean = tags.isEmpty() && tagsExclude.isEmpty() && locale == null && states.isEmpty() && contentRating.isEmpty()
}
}

@ -21,8 +21,6 @@ private const val CHAPTERS_FIRST_PAGE_SIZE = 120
private const val CHAPTERS_MAX_PAGE_SIZE = 500
private const val CHAPTERS_PARALLELISM = 3
private const val CHAPTERS_MAX_COUNT = 10_000 // strange api behavior, looks like a bug
private const val CONTENT_RATING =
"contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic"
private const val LOCALE_FALLBACK = "en"
@MangaSourceParser("MANGADEX", "MangaDex")
@ -32,6 +30,8 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
override val availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.allOf(ContentRating::class.java)
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
@ -45,22 +45,30 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
append(PAGE_SIZE)
append("&offset=")
append(offset)
append("&includes[]=cover_art&includes[]=author&includes[]=artist&")
append("&includes[]=cover_art&includes[]=author&includes[]=artist")
when (filter) {
is MangaListFilter.Search -> {
append("title=")
append("&title=")
append(filter.query)
append('&')
}
is MangaListFilter.Advanced -> {
filter.tags.forEach { tag ->
append("includedTags[]=")
append("&includedTags[]=")
append(tag.key)
append('&')
}
append(CONTENT_RATING)
if (filter.contentRating.isNotEmpty()) {
filter.contentRating.forEach {
when (it) {
ContentRating.SAFE -> append("&contentRating[]=safe")
ContentRating.SUGGESTIVE -> append("&contentRating[]=suggestive&contentRating[]=erotica")
ContentRating.ADULT -> append("&contentRating[]=pornographic")
}
}
}
append("&order")
append(
when (filter.sortOrder) {
@ -251,8 +259,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
append(limitedLimit)
append("&includes[]=scanlation_group&order[volume]=asc&order[chapter]=asc&offset=")
append(offset)
append('&')
append(CONTENT_RATING)
append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic")
}
val json = webClient.httpGet(url).parseJson()
if (json.getString("result") == "ok") {

@ -31,6 +31,7 @@ internal class AnibelParser(context: MangaLoaderContext) : MangaParser(context,
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {

@ -32,6 +32,8 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
override val isTagsExclusionSupported = true
init {
paginator.firstPage = 0
searchPaginator.firstPage = 0
@ -63,6 +65,10 @@ internal class BentomangaParser(context: MangaLoaderContext) : PagedMangaParser(
url.addQueryParameter("withCategories", filter.tags.joinToString(",") { it.key })
}
if (filter.tagsExclude.isNotEmpty()) {
url.addQueryParameter("withoutCategories", filter.tagsExclude.joinToString(",") { it.key })
}
filter.states.oneOrThrowIfMany()?.let {
url.addQueryParameter(
"state",

@ -37,6 +37,7 @@ internal class DesuMeParser(context: MangaLoaderContext) : PagedMangaParser(cont
page: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (query != null && page != searchPaginator.firstPage) {

@ -48,6 +48,7 @@ internal class NudeMoonParser(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val domain = domain

@ -77,6 +77,7 @@ internal class RemangaParser(
page: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
copyCookies()

@ -32,6 +32,7 @@ internal abstract class ChanParser(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val domain = domain

@ -44,6 +44,7 @@ internal open class MangaLibParser(
page: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
if (!query.isNullOrEmpty()) {

@ -67,7 +67,13 @@ class HentaiUkrParser(context: MangaLoaderContext) : MangaParser(context, MangaS
)
}
override suspend fun getList(offset: Int, query: String?, tags: Set<MangaTag>?, sortOrder: SortOrder): List<Manga> {
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
// Get all manga
val json = allManga.get().toMutableList()

@ -80,6 +80,7 @@ class HoneyMangaParser(context: MangaLoaderContext) : PagedMangaParser(context,
page: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val body = JSONObject()

@ -34,6 +34,7 @@ class MangaInUaParser(context: MangaLoaderContext) : PagedMangaParser(
page: Int,
query: String?,
tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
val url = when {

@ -68,6 +68,15 @@ fun Set<MangaState>?.oneOrThrowIfMany(): MangaState? {
}
}
@InternalParsersApi
fun Set<ContentRating>?.oneOrThrowIfMany(): ContentRating? {
return when {
isNullOrEmpty() -> null
size == 1 -> first()
else -> throw IllegalArgumentException(ErrorMessages.FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED)
}
}
val MangaParser.domain: String
get() {
return config[configKeyDomain]

@ -28,7 +28,7 @@ internal class MangaParserTest {
@MangaSources
fun list(source: MangaSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source)
val list = parser.getList(0, sortOrder = SortOrder.POPULARITY, tags = null)
val list = parser.getList(0, sortOrder = SortOrder.POPULARITY, tags = null, tagsExclude = null)
checkMangaList(list, "list")
assert(list.all { it.source == source })
}
@ -61,7 +61,7 @@ internal class MangaParserTest {
offset = 0,
filter = MangaListFilter.Advanced(
sortOrder = SortOrder.POPULARITY,
tags = emptySet(), locale = null, states = emptySet(),
tags = emptySet(), locale = null, states = emptySet(), tagsExclude = emptySet(), contentRating = emptySet()
),
).minByOrNull {
it.title.length
@ -92,7 +92,7 @@ internal class MangaParserTest {
assert(tags.all { it.source == source })
val tag = tags.last()
val list = parser.getList(offset = 0, tags = setOf(tag), sortOrder = null)
val list = parser.getList(offset = 0, tags = setOf(tag), tagsExclude = setOf(tag), sortOrder = null)
checkMangaList(list, "${tag.title} (${tag.key})")
assert(list.all { it.source == source })
}
@ -104,7 +104,7 @@ internal class MangaParserTest {
val tags = parser.getAvailableTags().shuffled().take(2).toSet()
val list = try {
parser.getList(offset = 0, tags = tags, sortOrder = null)
parser.getList(offset = 0, tags = tags, tagsExclude = tags, sortOrder = null)
} catch (e: IllegalArgumentException) {
if (e.message == "Multiple genres are not supported by this source") {
return@runTest
@ -127,8 +127,10 @@ internal class MangaParserTest {
val filter = MangaListFilter.Advanced(
sortOrder = parser.availableSortOrders.first(),
tags = setOf(),
tagsExclude = setOf(),
locale = locales.random(),
states = setOf(),
contentRating = setOf(),
)
val list = parser.getList(offset = 0, filter)
checkMangaList(list, filter.locale.toString())
@ -140,7 +142,7 @@ internal class MangaParserTest {
@MangaSources
fun details(source: MangaSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source)
val list = parser.getList(0, sortOrder = SortOrder.POPULARITY, tags = null)
val list = parser.getList(0, sortOrder = SortOrder.POPULARITY, tags = null, tagsExclude = null)
val manga = list[3]
parser.getDetails(manga).apply {
assert(!chapters.isNullOrEmpty()) { "Chapters are null or empty" }
@ -169,7 +171,7 @@ internal class MangaParserTest {
@MangaSources
fun pages(source: MangaSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source)
val list = parser.getList(0, sortOrder = SortOrder.UPDATED, tags = null)
val list = parser.getList(0, sortOrder = SortOrder.UPDATED, tags = null, tagsExclude = null)
val manga = list.first()
val chapter = parser.getDetails(manga).chapters?.firstOrNull() ?: error("Chapter is null at ${manga.publicUrl}")
val pages = parser.getPages(chapter)

Loading…
Cancel
Save