Merge pull request #431 from KotatsuApp/ExcludeTags

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_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_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 = const val FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED =
"Filtering by both genres and locale is not supported by this source" "Filtering by both genres and locale is not supported by this source"
const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED = const val FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED =

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

@ -36,6 +36,7 @@ abstract class PagedMangaParser(
offset: Int, offset: Int,
query: String?, query: String?,
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder, sortOrder: SortOrder,
): List<Manga> = throw UnsupportedOperationException("You should use getListPage for PagedMangaParser") ): List<Manga> = throw UnsupportedOperationException("You should use getListPage for PagedMangaParser")
@ -43,14 +44,15 @@ abstract class PagedMangaParser(
page: Int, page: Int,
query: String?, query: String?,
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder, sortOrder: SortOrder,
): List<Manga> = throw NotImplementedError("Please implement getListPage(page, filter) instead") ): List<Manga> = throw NotImplementedError("Please implement getListPage(page, filter) instead")
open suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> { open suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
return when (filter) { return when (filter) {
is MangaListFilter.Advanced -> getListPage(page, null, filter.tags, filter.sortOrder) is MangaListFilter.Advanced -> getListPage(page, null, filter.tags, filter.tagsExclude, filter.sortOrder)
is MangaListFilter.Search -> getListPage(page, filter.query, null, defaultSortOrder) is MangaListFilter.Search -> getListPage(page, filter.query, null, null, defaultSortOrder)
null -> getListPage(page, 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( data class Advanced(
override val sortOrder: SortOrder, override val sortOrder: SortOrder,
@JvmField val tags: Set<MangaTag>, @JvmField val tags: Set<MangaTag>,
@JvmField val tagsExclude: Set<MangaTag>,
@JvmField val locale: Locale?, @JvmField val locale: Locale?,
@JvmField val states: Set<MangaState>, @JvmField val states: Set<MangaState>,
@JvmField val contentRating: Set<ContentRating>,
) : MangaListFilter { ) : 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_MAX_PAGE_SIZE = 500
private const val CHAPTERS_PARALLELISM = 3 private const val CHAPTERS_PARALLELISM = 3
private const val CHAPTERS_MAX_COUNT = 10_000 // strange api behavior, looks like a bug 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" private const val LOCALE_FALLBACK = "en"
@MangaSourceParser("MANGADEX", "MangaDex") @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 availableSortOrders: EnumSet<SortOrder> = EnumSet.allOf(SortOrder::class.java)
override val availableContentRating: Set<ContentRating> = EnumSet.allOf(ContentRating::class.java)
override val availableStates: Set<MangaState> = override val availableStates: Set<MangaState> =
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED) 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(PAGE_SIZE)
append("&offset=") append("&offset=")
append(offset) append(offset)
append("&includes[]=cover_art&includes[]=author&includes[]=artist&") append("&includes[]=cover_art&includes[]=author&includes[]=artist")
when (filter) { when (filter) {
is MangaListFilter.Search -> { is MangaListFilter.Search -> {
append("title=") append("&title=")
append(filter.query) append(filter.query)
append('&')
} }
is MangaListFilter.Advanced -> { is MangaListFilter.Advanced -> {
filter.tags.forEach { tag -> filter.tags.forEach { tag ->
append("includedTags[]=") append("&includedTags[]=")
append(tag.key) 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("&order")
append( append(
when (filter.sortOrder) { when (filter.sortOrder) {
@ -251,8 +259,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
append(limitedLimit) append(limitedLimit)
append("&includes[]=scanlation_group&order[volume]=asc&order[chapter]=asc&offset=") append("&includes[]=scanlation_group&order[volume]=asc&order[chapter]=asc&offset=")
append(offset) append(offset)
append('&') append("&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic")
append(CONTENT_RATING)
} }
val json = webClient.httpGet(url).parseJson() val json = webClient.httpGet(url).parseJson()
if (json.getString("result") == "ok") { if (json.getString("result") == "ok") {

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

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

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

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

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

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

@ -44,6 +44,7 @@ internal open class MangaLibParser(
page: Int, page: Int,
query: String?, query: String?,
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder, sortOrder: SortOrder,
): List<Manga> { ): List<Manga> {
if (!query.isNullOrEmpty()) { 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 // Get all manga
val json = allManga.get().toMutableList() val json = allManga.get().toMutableList()

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

@ -34,6 +34,7 @@ class MangaInUaParser(context: MangaLoaderContext) : PagedMangaParser(
page: Int, page: Int,
query: String?, query: String?,
tags: Set<MangaTag>?, tags: Set<MangaTag>?,
tagsExclude: Set<MangaTag>?,
sortOrder: SortOrder, sortOrder: SortOrder,
): List<Manga> { ): List<Manga> {
val url = when { 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 val MangaParser.domain: String
get() { get() {
return config[configKeyDomain] return config[configKeyDomain]

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

Loading…
Cancel
Save