Merge branch 'filter-type-and-demographic' into feature/advanced_filter

master
Koitharu 2 years ago
commit 821e51ff7d
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -5,7 +5,11 @@ 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 = const val FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED =
"Multiple Content Rating are not supported by this source" "Multiple Content ratings are not supported by this source"
const val FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED =
"Multiple Content types are not supported by this source"
const val FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED =
"Multiple Demographics 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 =

@ -34,9 +34,30 @@ abstract class MangaParser @InternalParsersApi constructor(
get() = emptySet() get() = emptySet()
/**
* Supported [ContentRating] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
open val availableContentRating: Set<ContentRating> open val availableContentRating: Set<ContentRating>
get() = emptySet() get() = emptySet()
/**
* Supported [ContentType] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
open val availableContentTypes: Set<ContentType>
get() = emptySet()
/**
* Supported [Demographic] variants for filtering. May be empty.
*
* For better performance use [EnumSet] for more than one item.
*/
open val availableDemographics: Set<Demographic>
get() = emptySet()
/** /**
* Whether parser supports filtering by more than one tag * Whether parser supports filtering by more than one tag
*/ */
@ -191,6 +212,8 @@ abstract class MangaParser @InternalParsersApi constructor(
year = null, year = null,
yearFrom = null, yearFrom = null,
yearTo = null, yearTo = null,
types = emptySet(),
demographics = emptySet(),
), ),
) )
} }

@ -7,6 +7,10 @@ enum class ContentType {
*/ */
MANGA, MANGA,
MANHWA,
MANHUA,
/** /**
* Use this if the source provides mostly nsfw content. * Use this if the source provides mostly nsfw content.
*/ */
@ -17,6 +21,8 @@ enum class ContentType {
*/ */
COMICS, COMICS,
NOVEL,
/** /**
* Use this type if no other suits your needs. For example, for an indie manga * Use this type if no other suits your needs. For example, for an indie manga
*/ */

@ -0,0 +1,9 @@
package org.koitharu.kotatsu.parsers.model
enum class Demographic {
SHOUNEN,
SHOUJO,
SEINEN,
JOSEI,
NONE,
}

@ -15,8 +15,10 @@ sealed interface MangaListFilter {
(tagsExclude.isEmpty() || parser.isTagsExclusionSupported) && (tagsExclude.isEmpty() || parser.isTagsExclusionSupported) &&
(contentRating.isEmpty() || parser.availableContentRating.containsAll(contentRating)) && (contentRating.isEmpty() || parser.availableContentRating.containsAll(contentRating)) &&
(states.isEmpty() || parser.availableStates.containsAll(states) && (states.isEmpty() || parser.availableStates.containsAll(states) &&
(parser.searchSupportedWithMultipleFilters) &&(parser.isSearchOriginalLanguages) && (parser.searchSupportedWithMultipleFilters) && (parser.isSearchOriginalLanguages) &&
(parser.isSearchYearSupported) && (parser.isSearchYearRangeSupported)) (parser.isSearchYearSupported) && (parser.isSearchYearRangeSupported)) &&
(types.isEmpty() || parser.availableContentTypes.containsAll(types)) &&
(demographics.isEmpty() || parser.availableDemographics.containsAll(demographics))
is Search -> parser.isSearchSupported is Search -> parser.isSearchSupported
} }
@ -42,10 +44,12 @@ sealed interface MangaListFilter {
@JvmField val year: Int?, @JvmField val year: Int?,
@JvmField val yearFrom: Int?, @JvmField val yearFrom: Int?,
@JvmField val yearTo: Int?, @JvmField val yearTo: Int?,
@JvmField val types: Set<ContentType>,
@JvmField val demographics: Set<Demographic>,
) : MangaListFilter { ) : MangaListFilter {
override fun isEmpty(): Boolean = override fun isEmpty(): Boolean =
tags.isEmpty() && tagsExclude.isEmpty() && locale == null && localeMangas == null && states.isEmpty() && contentRating.isEmpty() && query == null && year == null && yearFrom == null && yearTo == null tags.isEmpty() && tagsExclude.isEmpty() && locale == null && localeMangas == null && states.isEmpty() && contentRating.isEmpty() && query == null && year == null && yearFrom == null && yearTo == null && types.isEmpty() && demographics.isEmpty()
fun newBuilder() = Builder(sortOrder) fun newBuilder() = Builder(sortOrder)
.tags(tags) .tags(tags)
@ -58,6 +62,8 @@ sealed interface MangaListFilter {
.year(year) .year(year)
.yearFrom(yearFrom) .yearFrom(yearFrom)
.yearTo(yearTo) .yearTo(yearTo)
.type(types)
.demographic(demographics)
class Builder(sortOrder: SortOrder) { class Builder(sortOrder: SortOrder) {
@ -72,6 +78,8 @@ sealed interface MangaListFilter {
private var _year: Int? = null private var _year: Int? = null
private var _yearFrom: Int? = null private var _yearFrom: Int? = null
private var _yearTo: Int? = null private var _yearTo: Int? = null
private var _types: Set<ContentType>? = null
private var _demographic: Set<Demographic>? = null
fun sortOrder(order: SortOrder) = apply { fun sortOrder(order: SortOrder) = apply {
_sortOrder = order _sortOrder = order
@ -117,7 +125,13 @@ sealed interface MangaListFilter {
_yearTo = yearTo _yearTo = yearTo
} }
fun type(type: Set<ContentType>?) = apply {
_types = type
}
fun demographic(demographic: Set<Demographic>?) = apply {
_demographic = demographic
}
fun build() = Advanced( fun build() = Advanced(
sortOrder = _sortOrder, sortOrder = _sortOrder,
@ -131,7 +145,9 @@ sealed interface MangaListFilter {
year = _year, year = _year,
yearFrom = _yearFrom, yearFrom = _yearFrom,
yearTo = _yearTo, yearTo = _yearTo,
) types = _types.orEmpty(),
demographics = _demographic.orEmpty(),
)
} }
} }
} }

@ -38,6 +38,15 @@ internal class ComickFunParser(context: MangaLoaderContext) :
SortOrder.NEWEST, SortOrder.NEWEST,
) )
override val availableContentTypes: Set<ContentType> = EnumSet.of(
ContentType.MANGA,
ContentType.MANHWA,
ContentType.MANHUA,
ContentType.OTHER,
)
override val availableDemographics: Set<Demographic> = EnumSet.allOf(Demographic::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)
@ -58,10 +67,6 @@ internal class ComickFunParser(context: MangaLoaderContext) :
url.addQueryParameter("q", filter.query) url.addQueryParameter("q", filter.query)
} }
null -> {
url.addQueryParameter("sort", "view")
}
is MangaListFilter.Advanced -> { is MangaListFilter.Advanced -> {
filter.tags.forEach { filter.tags.forEach {
@ -97,12 +102,42 @@ internal class ComickFunParser(context: MangaLoaderContext) :
} }
filter.yearFrom?.let { filter.yearFrom?.let {
url.addQueryParameter("from", filter.yearFrom.toString() ) url.addQueryParameter("from", filter.yearFrom.toString())
} }
filter.yearTo?.let { filter.yearTo?.let {
url.addQueryParameter("to", filter.yearTo.toString() ) url.addQueryParameter("to", filter.yearTo.toString())
}
filter.types.forEach {
url.addQueryParameter(
"country",
when (it) {
ContentType.MANGA -> "jp"
ContentType.MANHWA -> "kr"
ContentType.MANHUA -> "cn"
ContentType.OTHER -> "others"
else -> ""
},
)
} }
filter.demographics.forEach {
url.addQueryParameter(
"demographic",
when (it) {
Demographic.SHOUNEN -> "1"
Demographic.SHOUJO -> "2"
Demographic.SEINEN -> "3"
Demographic.JOSEI -> "4"
Demographic.NONE -> "5"
},
)
}
}
null -> {
url.addQueryParameter("sort", "uploaded")
} }
} }
val ja = webClient.httpGet(url.build()).parseJsonArray() val ja = webClient.httpGet(url.build()).parseJsonArray()

@ -37,6 +37,8 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
override val availableContentRating: Set<ContentRating> = EnumSet.allOf(ContentRating::class.java) override val availableContentRating: Set<ContentRating> = EnumSet.allOf(ContentRating::class.java)
override val availableDemographics: Set<Demographic> = EnumSet.allOf(Demographic::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)
@ -108,6 +110,7 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
SortOrder.RELEVANCE -> "&order[relevance]=desc" SortOrder.RELEVANCE -> "&order[relevance]=desc"
}, },
) )
filter.states.forEach { filter.states.forEach {
append("&status[]=") append("&status[]=")
when (it) { when (it) {
@ -118,6 +121,20 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
else -> append("") else -> append("")
} }
} }
filter.demographics.forEach {
append("&publicationDemographic[]=")
append(
when (it) {
Demographic.SHOUNEN -> "shounen"
Demographic.SHOUJO -> "shoujo"
Demographic.SEINEN -> "seinen"
Demographic.JOSEI -> "josei"
Demographic.NONE -> "none"
},
)
}
filter.locale?.let { filter.locale?.let {
append("&availableTranslatedLanguage[]=") append("&availableTranslatedLanguage[]=")
append(it.language) append(it.language)

@ -46,6 +46,16 @@ internal abstract class MangaReaderParser(
override val availableStates: Set<MangaState> override val availableStates: Set<MangaState>
get() = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED) get() = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED)
override val availableContentTypes: Set<ContentType>
get() = EnumSet.of(
ContentType.MANGA,
ContentType.MANHWA,
ContentType.MANHUA,
ContentType.COMICS,
ContentType.NOVEL,
)
override val isTagsExclusionSupported = true override val isTagsExclusionSupported = true
protected open val listUrl = "/manga" protected open val listUrl = "/manga"
@ -110,6 +120,20 @@ internal abstract class MangaReaderParser(
} }
} }
filter.types.oneOrThrowIfMany()?.let {
append("&type=")
append(
when (it) {
ContentType.MANGA -> "manga"
ContentType.MANHWA -> "manhwa"
ContentType.MANHUA -> "manhua"
ContentType.COMICS -> "comic"
ContentType.NOVEL -> "novel"
else -> ""
},
)
}
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
@ -223,18 +247,18 @@ internal abstract class MangaReaderParser(
"En cours de publication", "Đang tiến hành", "Em lançamento", "em lançamento", "Em Lançamento", "Онгоінг", "Publishing", "En cours de publication", "Đang tiến hành", "Em lançamento", "em lançamento", "Em Lançamento", "Онгоінг", "Publishing",
"Devam Ediyor", "Em Andamento", "In Corso", "Güncel", "Berjalan", "Продолжается", "Updating", "Lançando", "In Arrivo", "Emision", "Devam Ediyor", "Em Andamento", "In Corso", "Güncel", "Berjalan", "Продолжается", "Updating", "Lançando", "In Arrivo", "Emision",
"En emision", "مستمر", "Curso", "En marcha", "Publicandose", "Publicando", "连载中", "Devam ediyor", "Devam Etmekte", "En emision", "مستمر", "Curso", "En marcha", "Publicandose", "Publicando", "连载中", "Devam ediyor", "Devam Etmekte",
-> MangaState.ONGOING -> MangaState.ONGOING
"Completed", "Completo", "Complété", "Fini", "Achevé", "Terminé", "Terminé ⚫", "Tamamlandı", "Đã hoàn thành", "Hoàn Thành", "Completed", "Completo", "Complété", "Fini", "Achevé", "Terminé", "Terminé ⚫", "Tamamlandı", "Đã hoàn thành", "Hoàn Thành",
"مكتملة", "Завершено", "Finished", "Finalizado", "Completata", "One-Shot", "Bitti", "Tamat", "Completado", "Concluído", "مكتملة", "Завершено", "Finished", "Finalizado", "Completata", "One-Shot", "Bitti", "Tamat", "Completado", "Concluído",
"Concluido", "已完结", "Bitmiş", "Concluido", "已完结", "Bitmiş",
-> MangaState.FINISHED -> MangaState.FINISHED
"Canceled", "Cancelled", "Cancelado", "cancellato", "Cancelados", "Dropped", "Discontinued", "abandonné", "Abandonné", "Canceled", "Cancelled", "Cancelado", "cancellato", "Cancelados", "Dropped", "Discontinued", "abandonné", "Abandonné",
-> MangaState.ABANDONED -> MangaState.ABANDONED
"Hiatus", "On Hold", "Pausado", "En espera", "En pause", "En Pause", "En attente", "Hiatus", "On Hold", "Pausado", "En espera", "En pause", "En Pause", "En attente",
-> MangaState.PAUSED -> MangaState.PAUSED
else -> null else -> null
} }

@ -51,30 +51,34 @@ fun Element.parseFailed(message: String? = null): Nothing {
} }
@InternalParsersApi @InternalParsersApi
fun Set<MangaTag>?.oneOrThrowIfMany(): MangaTag? { fun Set<MangaTag>?.oneOrThrowIfMany(): MangaTag? = oneOrThrowIfMany(
return when { ErrorMessages.FILTER_MULTIPLE_GENRES_NOT_SUPPORTED,
isNullOrEmpty() -> null )
size == 1 -> first()
else -> throw IllegalArgumentException(ErrorMessages.FILTER_MULTIPLE_GENRES_NOT_SUPPORTED)
}
}
@InternalParsersApi @InternalParsersApi
fun Set<MangaState>?.oneOrThrowIfMany(): MangaState? { fun Set<MangaState>?.oneOrThrowIfMany(): MangaState? = oneOrThrowIfMany(
return when { ErrorMessages.FILTER_MULTIPLE_STATES_NOT_SUPPORTED,
isNullOrEmpty() -> null )
size == 1 -> first()
else -> throw IllegalArgumentException(ErrorMessages.FILTER_MULTIPLE_STATES_NOT_SUPPORTED)
}
}
@InternalParsersApi @InternalParsersApi
fun Set<ContentRating>?.oneOrThrowIfMany(): ContentRating? { fun Set<ContentType>?.oneOrThrowIfMany(): ContentType? = oneOrThrowIfMany(
return when { ErrorMessages.FILTER_MULTIPLE_CONTENT_TYPES_NOT_SUPPORTED,
isNullOrEmpty() -> null )
size == 1 -> first()
else -> throw IllegalArgumentException(ErrorMessages.FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED) @InternalParsersApi
} fun Set<Demographic>?.oneOrThrowIfMany(): Demographic? = oneOrThrowIfMany(
ErrorMessages.FILTER_MULTIPLE_DEMOGRAPHICS_NOT_SUPPORTED,
)
@InternalParsersApi
fun Set<ContentRating>?.oneOrThrowIfMany(): ContentRating? = oneOrThrowIfMany(
ErrorMessages.FILTER_MULTIPLE_CONTENT_RATING_NOT_SUPPORTED,
)
private fun <T> Set<T>?.oneOrThrowIfMany(msg: String): T? = when {
isNullOrEmpty() -> null
size == 1 -> first()
else -> throw IllegalArgumentException(msg)
} }
val MangaParser.domain: String val MangaParser.domain: String

@ -19,7 +19,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|list|{0}") @ParameterizedTest(name = "{index}|list|{0}")
@MangaSources @MangaSources
fun list(source: MangaParserSource) = runTest(timeout = timeout) { fun list(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
val list = parser.getList(0, null) val list = parser.getList(0, null)
checkMangaList(list, "list") checkMangaList(list, "list")
@ -28,7 +28,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|pagination|{0}") @ParameterizedTest(name = "{index}|pagination|{0}")
@MangaSources @MangaSources
fun pagination(source: MangaParserSource) = runTest(timeout = timeout) { fun pagination(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
val page1 = parser.getList(0, filter = null) val page1 = parser.getList(0, filter = null)
val page2 = parser.getList(page1.size, filter = null) val page2 = parser.getList(page1.size, filter = null)
@ -48,7 +48,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|search|{0}") @ParameterizedTest(name = "{index}|search|{0}")
@MangaSources @MangaSources
fun search(source: MangaParserSource) = runTest(timeout = timeout) { fun search(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
val subject = parser.getList( val subject = parser.getList(
offset = 0, offset = 0,
@ -64,6 +64,8 @@ internal class MangaParserTest {
year = null, year = null,
yearFrom = null, yearFrom = null,
yearTo = null, yearTo = null,
types = emptySet(),
demographics = emptySet(),
), ),
).minByOrNull { ).minByOrNull {
it.title.length it.title.length
@ -81,7 +83,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|tags|{0}") @ParameterizedTest(name = "{index}|tags|{0}")
@MangaSources @MangaSources
fun tags(source: MangaParserSource) = runTest(timeout = timeout) { fun tags(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
val tags = parser.getAvailableTags() val tags = parser.getAvailableTags()
assert(tags.isNotEmpty()) { "No tags found" } assert(tags.isNotEmpty()) { "No tags found" }
@ -110,7 +112,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|tags_multiple|{0}") @ParameterizedTest(name = "{index}|tags_multiple|{0}")
@MangaSources @MangaSources
fun tagsMultiple(source: MangaParserSource) = runTest(timeout = timeout) { fun tagsMultiple(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
if (!parser.isMultipleTagsSupported) return@runTest if (!parser.isMultipleTagsSupported) return@runTest
val tags = parser.getAvailableTags().shuffled().take(2).toSet() val tags = parser.getAvailableTags().shuffled().take(2).toSet()
@ -125,7 +127,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|locale|{0}") @ParameterizedTest(name = "{index}|locale|{0}")
@MangaSources @MangaSources
fun locale(source: MangaParserSource) = runTest(timeout = timeout) { fun locale(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
val locales = parser.getAvailableLocales() val locales = parser.getAvailableLocales()
if (locales.isEmpty()) { if (locales.isEmpty()) {
@ -143,6 +145,8 @@ internal class MangaParserTest {
year = null, year = null,
yearFrom = null, yearFrom = null,
yearTo = null, yearTo = null,
types = emptySet(),
demographics = emptySet(),
) )
val list = parser.getList(offset = 0, filter) val list = parser.getList(offset = 0, filter)
checkMangaList(list, filter.locale.toString()) checkMangaList(list, filter.locale.toString())
@ -152,7 +156,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|details|{0}") @ParameterizedTest(name = "{index}|details|{0}")
@MangaSources @MangaSources
fun details(source: MangaParserSource) = runTest(timeout = timeout) { fun details(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
val list = parser.getList(0, null) val list = parser.getList(0, null)
val manga = list[3] val manga = list[3]
@ -182,7 +186,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|pages|{0}") @ParameterizedTest(name = "{index}|pages|{0}")
@MangaSources @MangaSources
fun pages(source: MangaParserSource) = runTest(timeout = timeout) { fun pages(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
val list = parser.getList(0, null) val list = parser.getList(0, null)
val manga = list.first() val manga = list.first()
@ -206,7 +210,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|favicon|{0}") @ParameterizedTest(name = "{index}|favicon|{0}")
@MangaSources @MangaSources
fun favicon(source: MangaParserSource) = runTest(timeout = timeout) { fun favicon(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
val favicons = parser.getFavicons() val favicons = parser.getFavicons()
val types = setOf("png", "svg", "ico", "gif", "jpg", "jpeg") val types = setOf("png", "svg", "ico", "gif", "jpg", "jpeg")
@ -222,7 +226,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|domain|{0}") @ParameterizedTest(name = "{index}|domain|{0}")
@MangaSources @MangaSources
fun domain(source: MangaParserSource) = runTest(timeout = timeout) { fun domain(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
val defaultDomain = parser.domain val defaultDomain = parser.domain
val url = HttpUrl.Builder().host(defaultDomain).scheme("https").toString() val url = HttpUrl.Builder().host(defaultDomain).scheme("https").toString()
@ -238,7 +242,7 @@ internal class MangaParserTest {
@ParameterizedTest(name = "{index}|authorization|{0}") @ParameterizedTest(name = "{index}|authorization|{0}")
@MangaSources @MangaSources
@Disabled @Disabled
fun authorization(source: MangaParserSource) = runTest(timeout = timeout) { fun authorization(source: MangaParserSource) = runTest(timeout = timeout) {
val parser = context.newParserInstance(source) val parser = context.newParserInstance(source)
if (parser is MangaParserAuthProvider) { if (parser is MangaParserAuthProvider) {
val username = parser.getUsername() val username = parser.getUsername()

Loading…
Cancel
Save