remove unessery code

Remove old chapter manga and change last source for new manga chapter
Simplify parseChapterDate
Fix fetchAvailableTags on some source need to override
master
devi 2 years ago
parent 336c4a4d49
commit 600eab20a1

@ -38,28 +38,6 @@ public class MangaChapter(
@JvmField public val source: MangaSource, @JvmField public val source: MangaSource,
) { ) {
@Deprecated(message = "Consider using constructor with volume value")
internal constructor(
id: Long,
name: String,
number: Int,
url: String,
scanlator: String?,
uploadDate: Long,
branch: String?,
source: MangaSource,
) : this(
id = id,
name = name,
number = number.toFloat(),
volume = 0,
url = url,
scanlator = scanlator,
uploadDate = uploadDate,
branch = branch,
source = source,
)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

@ -5,7 +5,7 @@ import org.koitharu.kotatsu.parsers.InternalParsersApi
@InternalParsersApi @InternalParsersApi
public class WordSet(private vararg val words: String) { public class WordSet(private vararg val words: String) {
public fun anyWordIn(dateString: String): Boolean = words.any { public fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) }
dateString.contains(it, ignoreCase = true) public fun startsWith(dateString: String): Boolean = words.any { dateString.startsWith(it, ignoreCase = true) }
} public fun endsWith(dateString: String): Boolean = words.any { dateString.endsWith(it, ignoreCase = true) }
} }

@ -71,8 +71,6 @@ internal class BatoToParser(context: MangaLoaderContext) : PagedMangaParser(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.allOf(MangaState::class.java), availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = EnumSet.of(ContentRating.SAFE), availableContentRating = EnumSet.of(ContentRating.SAFE),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = setOf( availableLocales = setOf(
Locale.CHINESE, Locale.ENGLISH, Locale.US, Locale.FRENCH, Locale.GERMAN, Locale.ITALIAN, Locale.JAPANESE, Locale.CHINESE, Locale.ENGLISH, Locale.US, Locale.FRENCH, Locale.GERMAN, Locale.ITALIAN, Locale.JAPANESE,
Locale("af"), Locale("ar"), Locale("az"), Locale("eu"), Locale("be"), Locale("af"), Locale("ar"), Locale("az"), Locale("eu"), Locale("be"),

@ -47,7 +47,6 @@ internal class ComickFunParser(context: MangaLoaderContext) :
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentRating = emptySet(),
availableContentTypes = EnumSet.of( availableContentTypes = EnumSet.of(
ContentType.MANGA, ContentType.MANGA,
ContentType.MANHWA, ContentType.MANHWA,
@ -55,7 +54,6 @@ internal class ComickFunParser(context: MangaLoaderContext) :
ContentType.OTHER, ContentType.OTHER,
), ),
availableDemographics = EnumSet.allOf(Demographic::class.java), availableDemographics = EnumSet.allOf(Demographic::class.java),
availableLocales = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {

@ -482,14 +482,14 @@ internal class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context
title = doc.selectFirstOrThrow("h1").text(), title = doc.selectFirstOrThrow("h1").text(),
url = id.toString(), url = id.toString(),
coverUrl = coverUrl =
"https:" + "https:" +
doc.selectFirstOrThrow("picture > source") doc.selectFirstOrThrow("picture > source")
.attr("data-srcset") .attr("data-srcset")
.substringBefore(" "), .substringBefore(" "),
publicUrl = publicUrl =
doc.selectFirstOrThrow("h1 > a") doc.selectFirstOrThrow("h1 > a")
.attrAsRelativeUrl("href") .attrAsRelativeUrl("href")
.toAbsoluteUrl(domain), .toAbsoluteUrl(domain),
author = null, author = null,
tags = emptySet(), tags = emptySet(),
isNsfw = true, isNsfw = true,
@ -512,37 +512,37 @@ internal class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context
return manga.copy( return manga.copy(
title = json.getString("title"), title = json.getString("title"),
largeCoverUrl = largeCoverUrl =
json.getJSONArray("files").getJSONObject(0).let { json.getJSONArray("files").getJSONObject(0).let {
val hash = it.getString("hash") val hash = it.getString("hash")
val commonId = commonImageId() val commonId = commonImageId()
val imageId = imageIdFromHash(hash) val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId) val subDomain = 'a' + subdomainOffset(imageId)
"https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp" "https://${getDomain("${subDomain}a")}/webp/$commonId$imageId/$hash.webp"
}, },
author = author =
json.optJSONArray("artists") json.optJSONArray("artists")
?.mapJSON { it.getString("artist").toCamelCase() } ?.mapJSON { it.getString("artist").toCamelCase() }
?.joinToString(), ?.joinToString(),
publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain), publicUrl = json.getString("galleryurl").toAbsoluteUrl(domain),
tags = tags =
buildSet { buildSet {
json.optJSONArray("characters") json.optJSONArray("characters")
?.mapToTags("character") ?.mapToTags("character")
?.let(::addAll) ?.let(::addAll)
json.optJSONArray("tags") json.optJSONArray("tags")
?.mapToTags("tag") ?.mapToTags("tag")
?.let(::addAll) ?.let(::addAll)
json.optJSONArray("artists") json.optJSONArray("artists")
?.mapToTags("artist") ?.mapToTags("artist")
?.let(::addAll) ?.let(::addAll)
json.optJSONArray("parodys") json.optJSONArray("parodys")
?.mapToTags("parody") ?.mapToTags("parody")
?.let(::addAll) ?.let(::addAll)
json.optJSONArray("groups") json.optJSONArray("groups")
?.mapToTags("group") ?.mapToTags("group")
?.let(::addAll) ?.let(::addAll)
}, },
chapters = listOf( chapters = listOf(
MangaChapter( MangaChapter(
id = generateUid(manga.url), id = generateUid(manga.url),
@ -566,15 +566,15 @@ internal class HitomiLaParser(context: MangaLoaderContext) : MangaParser(context
mapJSON { mapJSON {
MangaTag( MangaTag(
title = title =
it.getString(key).toCamelCase().let { title -> it.getString(key).toCamelCase().let { title ->
if (it.getStringOrNull("female")?.toIntOrNull() == 1) { if (it.getStringOrNull("female")?.toIntOrNull() == 1) {
"$title" "$title"
} else if (it.getStringOrNull("male")?.toIntOrNull() == 1) { } else if (it.getStringOrNull("male")?.toIntOrNull() == 1) {
"$title" "$title"
} else { } else {
title title
} }
}, },
key = it.getString("url").tagUrlToTag(), key = it.getString("url").tagUrlToTag(),
source = source, source = source,
).let(tags::add) ).let(tags::add)

@ -69,7 +69,6 @@ internal class MangaDexParser(context: MangaLoaderContext) : MangaParser(context
MangaState.ABANDONED, MangaState.ABANDONED,
), ),
availableContentRating = EnumSet.allOf(ContentRating::class.java), availableContentRating = EnumSet.allOf(ContentRating::class.java),
availableContentTypes = emptySet(),
availableDemographics = EnumSet.allOf(Demographic::class.java), availableDemographics = EnumSet.allOf(Demographic::class.java),
availableLocales = localesDeferred.await(), availableLocales = localesDeferred.await(),
) )

@ -81,10 +81,6 @@ internal abstract class MangaFireParser(
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tags.get().values.toSet(), availableTags = tags.get().values.toSet(),
availableStates = EnumSet.allOf(MangaState::class.java), availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {

@ -32,8 +32,6 @@ internal class MangaPark(context: MangaLoaderContext) :
availableTags = tagsMap.get().values.toSet(), availableTags = tagsMap.get().values.toSet(),
availableStates = EnumSet.allOf(MangaState::class.java), availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = EnumSet.of(ContentRating.SAFE), availableContentRating = EnumSet.of(ContentRating.SAFE),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = setOf( availableLocales = setOf(
Locale("af"), Locale("sq"), Locale("am"), Locale("ar"), Locale("hy"), Locale("af"), Locale("sq"), Locale("am"), Locale("ar"), Locale("hy"),
Locale("az"), Locale("be"), Locale("bn"), Locale("zh_hk"), Locale("zh_tw"), Locale("az"), Locale("be"), Locale("bn"), Locale("zh_hk"), Locale("zh_tw"),

@ -77,10 +77,6 @@ internal class MangaReaderToParser(context: MangaLoaderContext) :
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = tags.get().values.toSet(), availableTags = tags.get().values.toSet(),
availableStates = EnumSet.allOf(MangaState::class.java), availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {

@ -53,10 +53,6 @@ internal abstract class NineMangaParser(
MangaState.ONGOING, MangaState.ONGOING,
MangaState.FINISHED, MangaState.FINISHED,
), ),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {

@ -31,9 +31,6 @@ internal class FlixScans(context: MangaLoaderContext) : PagedMangaParser(context
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.allOf(MangaState::class.java), availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = EnumSet.of(ContentRating.ADULT), availableContentRating = EnumSet.of(ContentRating.ADULT),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {

@ -33,10 +33,6 @@ internal class TeamXNovel(context: MangaLoaderContext) : PagedMangaParser(contex
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {

@ -35,10 +35,6 @@ internal abstract class CupFoxParser(
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {

@ -36,10 +36,6 @@ internal class AsuraScansParser(context: MangaLoaderContext) :
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = getOrCreateTagMap().values.toSet(), availableTags = getOrCreateTagMap().values.toSet(),
availableStates = EnumSet.allOf(MangaState::class.java), availableStates = EnumSet.allOf(MangaState::class.java),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {

@ -29,7 +29,6 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
availableContentRating = emptySet(),
) )
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {

@ -39,7 +39,6 @@ internal class MangaTownParser(context: MangaLoaderContext) :
MangaState.ONGOING, MangaState.ONGOING,
MangaState.FINISHED, MangaState.FINISHED,
), ),
availableContentRating = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {

@ -39,10 +39,6 @@ internal class Mangaowl(context: MangaLoaderContext) :
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {

@ -45,7 +45,6 @@ internal class Manhwa18Parser(context: MangaLoaderContext) :
MangaState.FINISHED, MangaState.FINISHED,
MangaState.PAUSED, MangaState.PAUSED,
), ),
availableContentRating = emptySet(),
) )
override suspend fun getFavicons(): Favicons { override suspend fun getFavicons(): Favicons {

@ -32,7 +32,6 @@ internal class ManhwasMen(context: MangaLoaderContext) :
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
availableContentRating = emptySet(),
) )
override suspend fun getListPage( override suspend fun getListPage(

@ -40,7 +40,6 @@ internal class VyManga(context: MangaLoaderContext) :
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED),
availableContentRating = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {

@ -78,10 +78,6 @@ internal abstract class FmreaderParser(
MangaState.FINISHED, MangaState.FINISHED,
MangaState.ABANDONED, MangaState.ABANDONED,
), ),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
@ -253,31 +249,19 @@ internal abstract class FmreaderParser(
} }
protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
val d = date?.lowercase() ?: return 0 val d = date?.lowercase() ?: return 0
return when { return when {
d.endsWith(" ago") ||
d.endsWith(" atrás") || WordSet(" ago", " atrás", " h", " d").endsWith(d) -> { parseRelativeDate(d) }
// short Hours
d.endsWith(" h") || WordSet("today").startsWith(d) -> {
// short Day Calendar.getInstance().apply {
d.endsWith(" d") -> parseRelativeDate(date) set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
// Handle 'yesterday' and 'today', using midnight set(Calendar.SECOND, 0)
d.startsWith("year") -> Calendar.getInstance().apply { set(Calendar.MILLISECOND, 0)
add(Calendar.DAY_OF_MONTH, -1) // yesterday }.timeInMillis
set(Calendar.HOUR_OF_DAY, 0) }
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
d.startsWith("today") -> Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map { date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map {
if (it.contains(Regex("""\d\D\D"""))) { if (it.contains(Regex("""\d\D\D"""))) {
@ -291,49 +275,24 @@ internal abstract class FmreaderParser(
} }
} }
// Parses dates in this form:
// 21 hours ago
private fun parseRelativeDate(date: String): Long { private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
return when { return when {
WordSet("second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis WordSet("second")
WordSet("min", "minute", "minutes", "minuto", "minutos").anyWordIn(date) -> cal.apply { .anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
add( WordSet("min", "minute", "minutes", "minuto", "minutos")
Calendar.MINUTE, .anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
-number, WordSet("hour", "hours", "hora", "horas", "h")
) .anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
}.timeInMillis WordSet("day", "days", "día", "dia")
.anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("hour", "hours", "hora", "horas", "h").anyWordIn(date) -> cal.apply { WordSet("week", "weeks", "semana", "semanas")
add( .anyWordIn(date) -> cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
Calendar.HOUR, WordSet("month", "months", "mes", "meses")
-number, .anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
) WordSet("year", "año", "años")
}.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
WordSet("day", "days", "día", "dia").anyWordIn(date) -> cal.apply {
add(
Calendar.DAY_OF_MONTH,
-number,
)
}.timeInMillis
WordSet("week", "weeks", "semana", "semanas").anyWordIn(date) -> cal.apply {
add(
Calendar.WEEK_OF_YEAR,
-number,
)
}.timeInMillis
WordSet("month", "months", "mes", "meses").anyWordIn(date) -> cal.apply {
add(
Calendar.MONTH,
-number,
)
}.timeInMillis
WordSet("year", "año", "años").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0 else -> 0
} }
} }

@ -52,7 +52,6 @@ internal class BentomangaParser(context: MangaLoaderContext) :
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentRating = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {

@ -28,7 +28,6 @@ internal class LegacyScansParser(context: MangaLoaderContext) :
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED, MangaState.PAUSED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED, MangaState.PAUSED),
availableContentRating = emptySet(),
) )
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {

@ -33,12 +33,7 @@ internal class LugnicaScans(context: MangaLoaderContext) :
) )
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = emptySet(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {

@ -42,7 +42,6 @@ internal class MangaMana(context: MangaLoaderContext) : PagedMangaParser(context
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED),
availableContentRating = emptySet(),
) )
override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) { override fun onCreateConfig(keys: MutableCollection<ConfigKey<*>>) {

@ -76,7 +76,6 @@ internal abstract class FuzzyDoodleParser(
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentRating = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {

@ -54,10 +54,6 @@ internal abstract class HeanCms(
override suspend fun getFilterOptions() = MangaListFilterOptions( override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = fetchAvailableTags(), availableTags = fetchAvailableTags(),
availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED), availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED),
availableContentRating = emptySet(),
availableContentTypes = emptySet(),
availableDemographics = emptySet(),
availableLocales = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {

@ -149,7 +149,7 @@ internal class NicovideoSeigaParser(context: MangaLoaderContext) :
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val fullUrl = chapter.url.toAbsoluteUrl(getDomain("seiga")) val fullUrl = chapter.url.toAbsoluteUrl(getDomain("seiga"))
val doc = webClient.httpGet(fullUrl).parseHtml() val doc = webClient.httpGet(fullUrl).parseHtml()
if (!doc.select("#login_manga").isEmpty) if (!doc.select("#login_manga").isEmpty())
throw AuthRequiredException(source) throw AuthRequiredException(source)
val root = doc.body().select("#page_contents > li") val root = doc.body().select("#page_contents > li")
return root.map { li -> return root.map { li ->

@ -266,22 +266,17 @@ internal abstract class KeyoappParser(
protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
val d = date?.lowercase() ?: return 0 val d = date?.lowercase() ?: return 0
return when { return when {
d.endsWith(" ago") -> parseRelativeDate(date)
WordSet(" ago").endsWith(d) -> { parseRelativeDate(d) }
d.startsWith("year") -> Calendar.getInstance().apply {
add(Calendar.DAY_OF_MONTH, -1) WordSet("today").startsWith(d) -> {
set(Calendar.HOUR_OF_DAY, 0) Calendar.getInstance().apply {
set(Calendar.MINUTE, 0) set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.SECOND, 0) set(Calendar.MINUTE, 0)
set(Calendar.MILLISECOND, 0) set(Calendar.SECOND, 0)
}.timeInMillis set(Calendar.MILLISECOND, 0)
}.timeInMillis
d.startsWith("today") -> Calendar.getInstance().apply { }
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map { date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map {
if (it.contains(Regex("""\d\D\D"""))) { if (it.contains(Regex("""\d\D\D"""))) {
@ -299,17 +294,18 @@ internal abstract class KeyoappParser(
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
return when { return when {
WordSet("second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis WordSet("second")
.anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("minute", "minutes").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis WordSet("minute", "minutes")
.anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("hour", "hours").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis WordSet("hour", "hours")
.anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("day", "days").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis WordSet("day", "days")
.anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis WordSet("month", "months")
.anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis WordSet("year")
.anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0 else -> 0
} }
} }

@ -746,56 +746,46 @@ internal abstract class MadaraParser(
} }
protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
val d = date?.lowercase() ?: return 0 val d = date?.lowercase() ?: return 0
return when { return when {
d.endsWith(" ago") || d.endsWith(" atrás") || // Handle translated 'ago' in Portuguese.
d.startsWith("") || // other translated 'ago' in Portuguese. WordSet(" ago", "atrás", " hace", " publicado"," назад", " önce", " trước", "مضت",
d.endsWith(" hace") || // other translated 'ago' in Spanish " h", " d", " días", " jour", " horas", " heure", " mins", " minutos", " minute", " mois").endsWith(d) -> {
d.endsWith(" publicado") || parseRelativeDate(d)
d.endsWith(" назад") || // other translated 'ago' in Russian }
d.endsWith(" önce") || // Handle translated 'ago' in Turkish.
d.endsWith(" trước") || // Handle translated 'ago' in Viêt Nam. WordSet("", "منذ", "il y a" ).startsWith(d) -> {
d.endsWith("مضت") || // Handle translated 'ago' in Arabic parseRelativeDate(d)
d.startsWith("منذ") || }
d.startsWith("il y a") || // Handle translated 'ago' in French.
//If there is no ago but just a motion of time WordSet("yesterday", "يوم واحد").startsWith(d) -> {
// short Hours Calendar.getInstance().apply {
d.endsWith(" h") || add(Calendar.DAY_OF_MONTH, -1) // yesterday
// short Day set(Calendar.HOUR_OF_DAY, 0)
d.endsWith(" d") || set(Calendar.MINUTE, 0)
// Day in Portuguese set(Calendar.SECOND, 0)
d.endsWith(" días") || d.endsWith(" día") || set(Calendar.MILLISECOND, 0)
// Day in French }.timeInMillis
d.endsWith(" jour") || d.endsWith(" jours") || }
// Hours in Portuguese
d.endsWith(" horas") || d.endsWith(" hora") || WordSet("today").startsWith(d) -> {
// Hours in french Calendar.getInstance().apply {
d.endsWith(" heure") || d.endsWith(" heures") || set(Calendar.HOUR_OF_DAY, 0)
// Minutes in English set(Calendar.MINUTE, 0)
d.endsWith(" mins") || set(Calendar.SECOND, 0)
// Minutes in Portuguese set(Calendar.MILLISECOND, 0)
d.endsWith(" minutos") || d.endsWith(" minuto") || }.timeInMillis
//Minutes in French }
d.endsWith(" minute") || d.endsWith(" minutes") ||
//month in French WordSet("يومين").startsWith(d) -> {
d.endsWith(" mois") -> parseRelativeDate(date) Calendar.getInstance().apply {
add(Calendar.DAY_OF_MONTH, -2) // day before yesterday
// Handle 'yesterday' and 'today', using midnight set(Calendar.HOUR_OF_DAY, 0)
d.startsWith("year") -> Calendar.getInstance().apply { set(Calendar.MINUTE, 0)
add(Calendar.DAY_OF_MONTH, -1) // yesterday set(Calendar.SECOND, 0)
set(Calendar.HOUR_OF_DAY, 0) set(Calendar.MILLISECOND, 0)
set(Calendar.MINUTE, 0) }.timeInMillis
set(Calendar.SECOND, 0) }
set(Calendar.MILLISECOND, 0)
}.timeInMillis
d.startsWith("today") -> Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map { date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map {
if (it.contains(Regex("""\d\D\D"""))) { if (it.contains(Regex("""\d\D\D"""))) {
@ -812,27 +802,19 @@ internal abstract class MadaraParser(
private fun parseRelativeDate(date: String): Long { private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
return when { return when {
WordSet("detik", "segundo", "second", "ثوان") WordSet("detik", "segundo", "second", "ثوان")
.anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("menit", "dakika", "min", "minute", "minutes", "minuto", "mins", "phút", "минут", "دقيقة") WordSet("menit", "dakika", "min", "minute", "minutes", "minuto", "mins", "phút", "минут", "دقيقة")
.anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("jam", "saat", "heure", "hora", "horas", "hour", "hours", "h", "ساعات", "ساعة") WordSet("jam", "saat", "heure", "hora", "horas", "hour", "hours", "h", "ساعات", "ساعة")
.anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("hari", "gün", "jour", "día", "dia", "day", "days", "d", "день") WordSet("hari", "gün", "jour", "día", "dia", "day", "days", "d", "день")
.anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("month", "months", "أشهر", "mois") WordSet("month", "months", "أشهر", "mois")
.anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year") WordSet("year")
.anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0 else -> 0
} }
} }

@ -15,159 +15,159 @@ import java.text.SimpleDateFormat
@MangaSourceParser("ADULT_WEBTOON", "AdultWebtoon", "en", ContentType.HENTAI) @MangaSourceParser("ADULT_WEBTOON", "AdultWebtoon", "en", ContentType.HENTAI)
internal class AdultWebtoon(context: MangaLoaderContext) : internal class AdultWebtoon(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.ADULT_WEBTOON, "adultwebtoon.com") { MadaraParser(context, MangaParserSource.ADULT_WEBTOON, "adultwebtoon.com") {
override val tagPrefix = "adult-webtoon-genre/" override val tagPrefix = "adult-webtoon-genre/"
override val listUrl = "adult-webtoon/" override val listUrl = "adult-webtoon/"
override val postReq = true override val postReq = true
override val withoutAjax = true override val withoutAjax = true
override suspend fun getFilterOptions() = super.getFilterOptions().copy( override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableStates = emptySet(), availableStates = emptySet(),
availableContentRating = emptySet(), availableContentRating = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val pages = page + 1 val pages = page + 1
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when { when {
!filter.query.isNullOrEmpty() -> { !filter.query.isNullOrEmpty() -> {
if (pages > 1) { if (pages > 1) {
append("/page/") append("/page/")
append(pages.toString()) append(pages.toString())
} }
append("/?s=") append("/?s=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&post_type=wp-manga") append("&post_type=wp-manga")
} }
else -> { else -> {
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
filter.tags.oneOrThrowIfMany()?.let { filter.tags.oneOrThrowIfMany()?.let {
append('/') append('/')
append(tagPrefix) append(tagPrefix)
append(it.key) append(it.key)
append('/') append('/')
} }
} else { } else {
append('/') append('/')
append(listUrl) append(listUrl)
} }
if (pages > 1) { if (pages > 1) {
append("page/") append("page/")
append(pages) append(pages)
append('/') append('/')
} }
append("?m_orderby=") append("?m_orderby=")
when (order) { when (order) {
SortOrder.POPULARITY -> append("views") SortOrder.POPULARITY -> append("views")
SortOrder.UPDATED -> append("latest") SortOrder.UPDATED -> append("latest")
SortOrder.NEWEST -> append("new-manga") SortOrder.NEWEST -> append("new-manga")
SortOrder.ALPHABETICAL -> append("alphabet") SortOrder.ALPHABETICAL -> append("alphabet")
SortOrder.RATING -> append("rating") SortOrder.RATING -> append("rating")
else -> append("latest") else -> append("latest")
} }
} }
} }
} }
return parseMangaList(webClient.httpGet(url).parseHtml()) return parseMangaList(webClient.httpGet(url).parseHtml())
} }
override suspend fun getDetails(manga: Manga): Manga = coroutineScope { override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
val fullUrl = manga.url.toAbsoluteUrl(domain) val fullUrl = manga.url.toAbsoluteUrl(domain)
val doc = webClient.httpGet(fullUrl).parseHtml() val doc = webClient.httpGet(fullUrl).parseHtml()
val body = doc.body() val body = doc.body()
val chaptersDeferred = async { loadChapters(manga.url, doc) } val chaptersDeferred = async { loadChapters(manga.url, doc) }
val desc = body.select(selectDesc).html() val desc = body.select(selectDesc).html()
val stateDiv = if (selectState.isEmpty()) { val stateDiv = if (selectState.isEmpty()) {
body.selectFirst("div.post-content_item:contains(Status)")?.selectLast("div.summary-content") body.selectFirst("div.post-content_item:contains(Status)")?.selectLast("div.summary-content")
} else { } else {
body.selectFirst(selectState) body.selectFirst(selectState)
} }
val state = stateDiv?.let { val state = stateDiv?.let {
when (it.text()) { when (it.text()) {
in ongoing -> MangaState.ONGOING in ongoing -> MangaState.ONGOING
in finished -> MangaState.FINISHED in finished -> MangaState.FINISHED
in abandoned -> MangaState.ABANDONED in abandoned -> MangaState.ABANDONED
in paused -> MangaState.PAUSED in paused -> MangaState.PAUSED
else -> null else -> null
} }
} }
val alt = val alt =
doc.body().select(".post-content_item:contains(Alt) .summary-content").firstOrNull()?.tableValue()?.text() doc.body().select(".post-content_item:contains(Alt) .summary-content").firstOrNull()?.tableValue()?.text()
?.trim() ?.trim()
manga.copy( manga.copy(
tags = doc.body().select(selectGenre).mapNotNullToSet { a -> tags = doc.body().select(selectGenre).mapNotNullToSet { a ->
MangaTag( MangaTag(
key = a.attr("href").removeSuffix("/").substringAfterLast('/'), key = a.attr("href").removeSuffix("/").substringAfterLast('/'),
title = a.text().toTitleCase(), title = a.text().toTitleCase(),
source = source, source = source,
) )
}, },
description = desc, description = desc,
altTitle = alt, altTitle = alt,
state = state, state = state,
chapters = chaptersDeferred.await(), chapters = chaptersDeferred.await(),
) )
} }
override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> { override suspend fun loadChapters(mangaUrl: String, document: Document): List<MangaChapter> {
val mangaId = document.select("div#manga-chapters-holder").attr("data-id") val mangaId = document.select("div#manga-chapters-holder").attr("data-id")
val url = "https://$domain/wp-admin/admin-ajax.php" val url = "https://$domain/wp-admin/admin-ajax.php"
val postData = "post_id=$mangaId&action=ajax_chap" val postData = "post_id=$mangaId&action=ajax_chap"
val headers = Headers.Builder().add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8").build() val headers = Headers.Builder().add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8").build()
val doc = makeRequest(url, postData.toRequestBody(), headers) val doc = makeRequest(url, postData.toRequestBody(), headers)
val dateFormat = SimpleDateFormat(datePattern, sourceLocale) val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
return doc.select(selectChapter).mapChapters(reversed = true) { i, li -> return doc.select(selectChapter).mapChapters(reversed = true) { i, li ->
val a = li.selectFirst("a") val a = li.selectFirst("a")
val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing") val href = a?.attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing")
val link = href + stylePage val link = href + stylePage
val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text() val dateText = li.selectFirst("a.c-new-tag")?.attr("title") ?: li.selectFirst(selectDate)?.text()
val name = a.selectFirst("p")?.text() ?: a.ownText() val name = a.selectFirst("p")?.text() ?: a.ownText()
MangaChapter( MangaChapter(
id = generateUid(href), id = generateUid(href),
url = link, url = link,
name = name, name = name,
number = i + 1f, number = i + 1f,
volume = 0, volume = 0,
branch = null, branch = null,
uploadDate = parseChapterDate( uploadDate = parseChapterDate(
dateFormat, dateFormat,
dateText, dateText,
), ),
scanlator = null, scanlator = null,
source = source, source = source,
) )
} }
} }
private suspend fun makeRequest(url: String, payload: RequestBody, headers: Headers): Document { private suspend fun makeRequest(url: String, payload: RequestBody, headers: Headers): Document {
var retryCount = 0 var retryCount = 0
val backoffDelay = 2000L // Initial delay (milliseconds) val backoffDelay = 2000L // Initial delay (milliseconds)
val request = Request.Builder().url(url).post(payload).headers(headers).build() val request = Request.Builder().url(url).post(payload).headers(headers).build()
while (true) { while (true) {
try { try {
return context.httpClient.newCall(request).execute().parseHtml() return context.httpClient.newCall(request).execute().parseHtml()
} catch (e: Exception) { } catch (e: Exception) {
// Log or handle the exception as needed // Log or handle the exception as needed
if (++retryCount <= 5) { if (++retryCount <= 5) {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
delay(backoffDelay) delay(backoffDelay)
} }
} else { } else {
throw e throw e
} }
} }
} }
} }
} }

@ -13,4 +13,4 @@ internal class MmScans(context: MangaLoaderContext) :
override val selectChapter = "li.chapter-li" override val selectChapter = "li.chapter-li"
override val selectDesc = "div.summary-text" override val selectDesc = "div.summary-text"
override val withoutAjax = true override val withoutAjax = true
} }

@ -12,66 +12,66 @@ import java.util.*
@MangaSourceParser("DRAGONTRANSLATION", "Dragon Translation", "es") @MangaSourceParser("DRAGONTRANSLATION", "Dragon Translation", "es")
internal class DragonTranslationParser(context: MangaLoaderContext) : internal class DragonTranslationParser(context: MangaLoaderContext) :
MadaraParser(context, MangaParserSource.DRAGONTRANSLATION, "dragontranslation.net", 30) { MadaraParser(context, MangaParserSource.DRAGONTRANSLATION, "dragontranslation.net", 30) {
override val selectPage = "div#chapter_imgs img" override val selectPage = "div#chapter_imgs img"
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED) override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.UPDATED)
init { init {
paginator.firstPage = 1 paginator.firstPage = 1
searchPaginator.firstPage = 1 searchPaginator.firstPage = 1
} }
override suspend fun getFilterOptions() = super.getFilterOptions().copy( override suspend fun getFilterOptions() = super.getFilterOptions().copy(
availableStates = emptySet(), availableStates = emptySet(),
availableContentRating = emptySet(), availableContentRating = emptySet(),
) )
override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> { override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List<Manga> {
val url = buildString { val url = buildString {
append("https://") append("https://")
append(domain) append(domain)
when { when {
!filter.query.isNullOrEmpty() -> { !filter.query.isNullOrEmpty() -> {
append("/mangas?buscar=") append("/mangas?buscar=")
append(filter.query.urlEncoded()) append(filter.query.urlEncoded())
append("&page=") append("&page=")
append(page.toString()) append(page.toString())
} }
else -> { else -> {
append("/mangas?page=") append("/mangas?page=")
append(page.toString()) append(page.toString())
val tag = filter.tags.oneOrThrowIfMany() val tag = filter.tags.oneOrThrowIfMany()
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
append("&tag=") append("&tag=")
append(tag?.key.orEmpty()) append(tag?.key.orEmpty())
} }
} }
} }
} }
val doc = webClient.httpGet(url).parseHtml() val doc = webClient.httpGet(url).parseHtml()
return doc.select("div.video-bg div.col-6 ").map { div -> return doc.select("div.video-bg div.col-6 ").map { div ->
val href = val href =
div.selectFirst("a.series-link")?.attrAsRelativeUrlOrNull("href") ?: div.parseFailed("Link not found") div.selectFirst("a.series-link")?.attrAsRelativeUrlOrNull("href") ?: div.parseFailed("Link not found")
Manga( Manga(
id = generateUid(href), id = generateUid(href),
url = href, url = href,
publicUrl = href.toAbsoluteUrl(div.host ?: domain), publicUrl = href.toAbsoluteUrl(div.host ?: domain),
coverUrl = div.selectFirst("img.thumb-img")?.src().orEmpty(), coverUrl = div.selectFirst("img.thumb-img")?.src().orEmpty(),
title = div.selectFirst("div.series-box h5")?.text().orEmpty(), title = div.selectFirst("div.series-box h5")?.text().orEmpty(),
altTitle = null, altTitle = null,
rating = div.selectFirst("span.total_votes")?.ownText()?.toFloatOrNull()?.div(5f) ?: -1f, rating = div.selectFirst("span.total_votes")?.ownText()?.toFloatOrNull()?.div(5f) ?: -1f,
tags = emptySet(), tags = emptySet(),
author = null, author = null,
state = null, state = null,
source = source, source = source,
isNsfw = isNsfwSource, isNsfw = isNsfwSource,
) )
} }
} }
} }

@ -271,30 +271,19 @@ internal abstract class MadthemeParser(
} }
protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
val d = date?.lowercase() ?: return 0 val d = date?.lowercase() ?: return 0
return when { return when {
d.endsWith(" ago") ||
// short Hours WordSet(" ago", " h", " d").endsWith(d) -> { parseRelativeDate(d) }
d.endsWith(" h") ||
// short Day WordSet("today").startsWith(d) -> {
d.endsWith(" d") -> parseRelativeDate(date) Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
// Handle 'yesterday' and 'today', using midnight set(Calendar.MINUTE, 0)
d.startsWith("year") -> Calendar.getInstance().apply { set(Calendar.SECOND, 0)
add(Calendar.DAY_OF_MONTH, -1) // yesterday set(Calendar.MILLISECOND, 0)
set(Calendar.HOUR_OF_DAY, 0) }.timeInMillis
set(Calendar.MINUTE, 0) }
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
d.startsWith("today") -> Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map { date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map {
if (it.contains(Regex("""\d\D\D"""))) { if (it.contains(Regex("""\d\D\D"""))) {
@ -308,45 +297,22 @@ internal abstract class MadthemeParser(
} }
} }
// Parses dates in this form:
// 21 hours ago
private fun parseRelativeDate(date: String): Long { private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
return when { return when {
WordSet( WordSet("second")
"day", .anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
"days", WordSet("min", "minute", "minutes")
).anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("hour", "hours", "h")
WordSet("hour", "hours", "h").anyWordIn(date) -> cal.apply { .anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
add( WordSet("day", "days")
Calendar.HOUR, .anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
-number, WordSet("month", "months")
) .anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
}.timeInMillis WordSet("year")
.anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
WordSet(
"min",
"minute",
"minutes",
).anyWordIn(date) -> cal.apply {
add(
Calendar.MINUTE,
-number,
)
}.timeInMillis
WordSet("second").anyWordIn(date) -> cal.apply {
add(
Calendar.SECOND,
-number,
)
}.timeInMillis
WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0 else -> 0
} }
} }

@ -5,8 +5,8 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaSourceParser import org.koitharu.kotatsu.parsers.MangaSourceParser
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.site.manga18.Manga18Parser import org.koitharu.kotatsu.parsers.site.manga18.Manga18Parser
import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl import org.koitharu.kotatsu.parsers.util.attrAsRelativeUrl
import org.koitharu.kotatsu.parsers.util.generateUid import org.koitharu.kotatsu.parsers.util.generateUid
@ -17,6 +17,10 @@ import org.koitharu.kotatsu.parsers.util.selectFirstOrThrow
internal class Hanman18(context: MangaLoaderContext) : internal class Hanman18(context: MangaLoaderContext) :
Manga18Parser(context, MangaParserSource.HANMAN18, "hanman18.com") { Manga18Parser(context, MangaParserSource.HANMAN18, "hanman18.com") {
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = emptySet()
)
override suspend fun getChapters(doc: Document): List<MangaChapter> { override suspend fun getChapters(doc: Document): List<MangaChapter> {
return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li -> return doc.body().select(selectChapter).mapChapters(reversed = true) { i, li ->
val a = li.selectFirstOrThrow("a") val a = li.selectFirstOrThrow("a")
@ -34,6 +38,4 @@ internal class Hanman18(context: MangaLoaderContext) :
) )
} }
} }
private suspend fun fetchAvailableTags(): Set<MangaTag> = emptySet() // search by tag does not work
} }

@ -146,7 +146,7 @@ internal abstract class MangaboxParser(
protected open val selectTagMap = "div.panel-genres-list a:not(.genres-select)" protected open val selectTagMap = "div.panel-genres-list a:not(.genres-select)"
private suspend fun fetchAvailableTags(): Set<MangaTag> { protected open suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
val tags = doc.select(selectTagMap).drop(1) // remove all tags val tags = doc.select(selectTagMap).drop(1) // remove all tags
return tags.mapNotNullToSet { a -> return tags.mapNotNullToSet { a ->
@ -263,30 +263,18 @@ internal abstract class MangaboxParser(
} }
protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
val d = date?.lowercase() ?: return 0 val d = date?.lowercase() ?: return 0
return when { return when {
d.endsWith(" ago") || WordSet(" ago", " h", " d").endsWith(d) -> { parseRelativeDate(d) }
// short Hours
d.endsWith(" h") || WordSet("today").startsWith(d) -> {
// short Day Calendar.getInstance().apply {
d.endsWith(" d") -> parseRelativeDate(date) set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
// Handle 'yesterday' and 'today', using midnight set(Calendar.SECOND, 0)
d.startsWith("year") -> Calendar.getInstance().apply { set(Calendar.MILLISECOND, 0)
add(Calendar.DAY_OF_MONTH, -1) // yesterday }.timeInMillis
set(Calendar.HOUR_OF_DAY, 0) }
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
d.startsWith("today") -> Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map { date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map {
if (it.contains(Regex("""\d\D\D"""))) { if (it.contains(Regex("""\d\D\D"""))) {
@ -300,45 +288,22 @@ internal abstract class MangaboxParser(
} }
} }
// Parses dates in this form:
// 21 hours ago
private fun parseRelativeDate(date: String): Long { private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
return when { return when {
WordSet( WordSet("second")
"day", .anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
"days", WordSet("min", "minute", "minutes")
).anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("hour", "hours", "h")
WordSet("hour", "hours", "h").anyWordIn(date) -> cal.apply { .anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
add( WordSet("day", "days")
Calendar.HOUR, .anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
-number, WordSet("month", "months")
) .anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
}.timeInMillis WordSet("year")
.anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
WordSet(
"min",
"minute",
"minutes",
).anyWordIn(date) -> cal.apply {
add(
Calendar.MINUTE,
-number,
)
}.timeInMillis
WordSet("second").anyWordIn(date) -> cal.apply {
add(
Calendar.SECOND,
-number,
)
}.timeInMillis
WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0 else -> 0
} }
} }

@ -108,7 +108,7 @@ internal class Mangairo(context: MangaLoaderContext) :
} }
} }
private suspend fun fetchAvailableTags(): Set<MangaTag> { override suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl/type-latest/ctg-all/state-all/page-1").parseHtml() val doc = webClient.httpGet("https://$domain/$listUrl/type-latest/ctg-all/state-all/page-1").parseHtml()
return doc.select("div.panel_category a:not(.ctg_select)").mapNotNullToSet { a -> return doc.select("div.panel_category a:not(.ctg_select)").mapNotNullToSet { a ->
val key = a.attr("href").substringAfterLast("ctg-").substringBefore("/") val key = a.attr("href").substringAfterLast("ctg-").substringBefore("/")

@ -97,7 +97,7 @@ internal class Mangakakalot(context: MangaLoaderContext) :
} }
} }
private suspend fun fetchAvailableTags(): Set<MangaTag> { override suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
val tags = doc.select("ul.tag li a").drop(1) val tags = doc.select("ul.tag li a").drop(1)
return tags.mapNotNullToSet { a -> return tags.mapNotNullToSet { a ->

@ -128,7 +128,7 @@ internal class MangakakalotTv(context: MangaLoaderContext) :
override val selectTagMap = "ul.tag li a" override val selectTagMap = "ul.tag li a"
private suspend fun fetchAvailableTags(): Set<MangaTag> { override suspend fun fetchAvailableTags(): Set<MangaTag> {
val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml()
return doc.select(selectTagMap).mapNotNullToSet { a -> return doc.select(selectTagMap).mapNotNullToSet { a ->
MangaTag( MangaTag(

@ -66,7 +66,6 @@ internal abstract class MmrcmsParser(
override val filterCapabilities: MangaListFilterCapabilities override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities( get() = MangaListFilterCapabilities(
isMultipleTagsSupported = false,
isSearchSupported = true, isSearchSupported = true,
) )

@ -241,61 +241,30 @@ internal abstract class OtakuSanctuaryParser(
} }
protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
val d = date?.lowercase() ?: return 0 val d = date?.lowercase() ?: return 0
return when { return when {
d.endsWith(" ago") || d.endsWith(" atrás") || d.startsWith("cách đây ") -> parseRelativeDate(date) WordSet(" ago", " atrás").endsWith(d) -> { parseRelativeDate(d) }
WordSet("cách đây ").startsWith(d) -> { parseRelativeDate(d) }
else -> dateFormat.tryParse(date) else -> dateFormat.tryParse(date)
} }
} }
// Parses dates in this form:
// 21 hours ago
private fun parseRelativeDate(date: String): Long { private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
return when { return when {
WordSet( WordSet("second", "giây")
"day", .anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
"days", WordSet("min", "minute", "minutes", "phút")
"d", .anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
"ngày", WordSet("tiếng", "hour", "hours")
).anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("day", "days", "d", "ngày")
WordSet( .anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
"tiếng", WordSet("month", "months")
"hour", .anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
"hours", WordSet("year")
).anyWordIn(date) -> cal.apply { .anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
add(
Calendar.HOUR,
-number,
)
}.timeInMillis
WordSet(
"min",
"minute",
"minutes",
"phút",
).anyWordIn(date) -> cal.apply {
add(
Calendar.MINUTE,
-number,
)
}.timeInMillis
WordSet("second", "giây").anyWordIn(date) -> cal.apply {
add(
Calendar.SECOND,
-number,
)
}.timeInMillis
WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0 else -> 0
} }
} }

@ -71,7 +71,7 @@ internal class MangaWtfParser(
SortOrder.POPULARITY -> "viewsCount,desc" SortOrder.POPULARITY -> "viewsCount,desc"
SortOrder.RATING -> "likesCount,desc" SortOrder.RATING -> "likesCount,desc"
SortOrder.NEWEST -> "createdAt,desc" SortOrder.NEWEST -> "createdAt,desc"
else -> throw IllegalArgumentException("Unsupported ${order}") else -> throw IllegalArgumentException("Unsupported $order")
}, },
) )
if (filter.tags.isNotEmpty()) { if (filter.tags.isNotEmpty()) {
@ -137,13 +137,13 @@ internal class MangaWtfParser(
tags = jo.getJSONArray("labels").mapJSONToSet { it.toMangaTag() }, tags = jo.getJSONArray("labels").mapJSONToSet { it.toMangaTag() },
state = jo.getStringOrNull("status")?.toMangaState(), state = jo.getStringOrNull("status")?.toMangaState(),
author = author =
jo.getJSONArray("relations").toJSONList().firstNotNullOfOrNull { jo.getJSONArray("relations").toJSONList().firstNotNullOfOrNull {
if (it.getStringOrNull("type") == "AUTHOR") { if (it.getStringOrNull("type") == "AUTHOR") {
it.getJSONObject("publisher").getStringOrNull("name") it.getJSONObject("publisher").getStringOrNull("name")
} else { } else {
null null
} }
}, },
source = source, source = source,
largeCoverUrl = null, largeCoverUrl = null,
description = jo.getString("description").nl2br(), description = jo.getString("description").nl2br(),
@ -209,10 +209,10 @@ internal class MangaWtfParser(
MangaChapter( MangaChapter(
id = generateUid(jo.getString("id")), id = generateUid(jo.getString("id")),
name = name =
jo.getStringOrNull("name") ?: buildString { jo.getStringOrNull("name") ?: buildString {
if (volume > 0) append("Том ").append(volume).append(' ') if (volume > 0) append("Том ").append(volume).append(' ')
if (number > 0) append("Глава ").append(number) else append("Без имени") if (number > 0) append("Глава ").append(number) else append("Без имени")
}, },
number = number, number = number,
volume = volume, volume = volume,
url = jo.getString("id"), url = jo.getString("id"),

@ -174,7 +174,8 @@ internal class RemangaParser(
MangaChapter( MangaChapter(
id = generateUid(id), id = generateUid(id),
url = "/api/titles/chapters/$id/", url = "/api/titles/chapters/$id/",
number = jo.getIntOrDefault("index", chapters.size - i), number = jo.getIntOrDefault("index", chapters.size - i).toFloat(),
volume = 0,
name = buildString { name = buildString {
append("Том ") append("Том ")
append(jo.optString("tome", "0")) append(jo.optString("tome", "0"))

@ -216,7 +216,7 @@ internal abstract class ChanParser(
when (order) { when (order) {
SortOrder.RATING, SortOrder.RATING,
SortOrder.POPULARITY, SortOrder.POPULARITY,
-> "favdesc" -> "favdesc"
SortOrder.ALPHABETICAL -> "abcasc" SortOrder.ALPHABETICAL -> "abcasc"
else -> "" // SortOrder.NEWEST else -> "" // SortOrder.NEWEST

@ -95,7 +95,7 @@ internal abstract class LibSocialParser(
SortOrder.NEWEST -> "created_at" SortOrder.NEWEST -> "created_at"
SortOrder.ALPHABETICAL, SortOrder.ALPHABETICAL,
SortOrder.ALPHABETICAL_DESC, SortOrder.ALPHABETICAL_DESC,
-> "rus_name" -> "rus_name"
else -> null else -> null
}, },
@ -108,7 +108,7 @@ internal abstract class LibSocialParser(
SortOrder.RATING, SortOrder.RATING,
SortOrder.NEWEST, SortOrder.NEWEST,
SortOrder.ALPHABETICAL_DESC, SortOrder.ALPHABETICAL_DESC,
-> "desc" -> "desc"
SortOrder.ALPHABETICAL -> "asc" SortOrder.ALPHABETICAL -> "asc"
else -> null else -> null

@ -12,8 +12,9 @@ internal class MangaFr(context: MangaLoaderContext) :
ScanParser(context, MangaParserSource.MANGAFR, "www.mangafr.org") { ScanParser(context, MangaParserSource.MANGAFR, "www.mangafr.org") {
override val listUrl = "/series" override val listUrl = "/series"
private suspend fun fetchAvailableTags(): Set<MangaTag> = emptySet() override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = emptySet()
)
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml()
val dateFormat = SimpleDateFormat("MM-dd-yyyy", sourceLocale) val dateFormat = SimpleDateFormat("MM-dd-yyyy", sourceLocale)

@ -124,7 +124,8 @@ internal class MangaInUaParser(context: MangaLoaderContext) : PagedMangaParser(
prevChapterName = name prevChapterName = name
name name
}, },
number = i, number = i.toFloat(),
volume = 0,
url = href, url = href,
scanlator = null, scanlator = null,
branch = if (isAlternative) { branch = if (isAlternative) {

@ -279,24 +279,17 @@ internal abstract class WpComicsParser(
protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
val d = date?.lowercase() ?: return 0 val d = date?.lowercase() ?: return 0
return when { return when {
d.endsWith(" ago") ||
d.endsWith(" trước") WordSet(" ago", " trước").endsWith(d) -> { parseRelativeDate(d) }
-> parseRelativeDate(date)
WordSet("today").startsWith(d) -> {
d.startsWith("year") -> Calendar.getInstance().apply { Calendar.getInstance().apply {
add(Calendar.DAY_OF_MONTH, -1) set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.HOUR_OF_DAY, 0) set(Calendar.MINUTE, 0)
set(Calendar.MINUTE, 0) set(Calendar.SECOND, 0)
set(Calendar.SECOND, 0) set(Calendar.MILLISECOND, 0)
set(Calendar.MILLISECOND, 0) }.timeInMillis
}.timeInMillis }
d.startsWith("today") -> Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map { date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map {
if (it.contains(Regex("""\d\D\D"""))) { if (it.contains(Regex("""\d\D\D"""))) {
@ -314,28 +307,16 @@ internal abstract class WpComicsParser(
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
return when { return when {
WordSet("second", "giây")
WordSet("second", "giây").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("min", "minute", "minutes", "mins", "phút")
WordSet("min", "minute", "minutes", "mins", "phút").anyWordIn(date) -> cal.apply { .anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
add(Calendar.MINUTE, -number) WordSet("jam", "saat", "heure", "hora", "horas", "hour", "hours", "h", "giờ")
}.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("day", "days", "d", "ngày")
WordSet("jam", "saat", "heure", "hora", "horas", "hour", "hours", "h", "giờ").anyWordIn(date) -> cal.apply { .anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
add(Calendar.HOUR, -number) WordSet("month", "months", "tháng")
}.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("day", "days", "d", "ngày").anyWordIn(date) -> cal.apply {
add(Calendar.DAY_OF_MONTH, -number)
}.timeInMillis
WordSet("month", "months", "tháng").anyWordIn(date) -> cal.apply {
add(
Calendar.MONTH,
-number,
)
}.timeInMillis
WordSet("year", "năm").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis WordSet("year", "năm").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0 else -> 0
} }

@ -110,7 +110,7 @@ internal class XoxoComics(context: MangaLoaderContext) :
source = source, source = source,
) )
} }
val result = list.associateByTo(ArrayMap<String, MangaTag>(list.size)) { it.title } val result = list.associateByTo(ArrayMap(list.size)) { it.title }
tagCache = result tagCache = result
result result
} }

@ -250,30 +250,18 @@ internal abstract class ZMangaParser(
} }
protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long { protected fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
val d = date?.lowercase() ?: return 0 val d = date?.lowercase() ?: return 0
return when { return when {
d.endsWith(" ago") || WordSet(" ago", " h", " d").endsWith(d) -> { parseRelativeDate(d) }
// short Hours
d.endsWith(" h") || WordSet("today").startsWith(d) -> {
// short Day Calendar.getInstance().apply {
d.endsWith(" d") -> parseRelativeDate(date) set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
// Handle 'yesterday' and 'today', using midnight set(Calendar.SECOND, 0)
d.startsWith("year") -> Calendar.getInstance().apply { set(Calendar.MILLISECOND, 0)
add(Calendar.DAY_OF_MONTH, -1) // yesterday }.timeInMillis
set(Calendar.HOUR_OF_DAY, 0) }
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
d.startsWith("today") -> Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map { date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map {
if (it.contains(Regex("""\d\D\D"""))) { if (it.contains(Regex("""\d\D\D"""))) {
@ -287,45 +275,22 @@ internal abstract class ZMangaParser(
} }
} }
// Parses dates in this form:
// 21 hours ago
private fun parseRelativeDate(date: String): Long { private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
return when { return when {
WordSet( WordSet("second")
"day", .anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
"days", WordSet("min", "minute", "minutes")
).anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("hour", "hours", "h")
WordSet("hour", "hours", "h").anyWordIn(date) -> cal.apply { .anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
add( WordSet("day", "days").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
Calendar.HOUR, WordSet("month", "months")
-number, .anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
) WordSet("year")
}.timeInMillis .anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
WordSet(
"min",
"minute",
"minutes",
).anyWordIn(date) -> cal.apply {
add(
Calendar.MINUTE,
-number,
)
}.timeInMillis
WordSet("second").anyWordIn(date) -> cal.apply {
add(
Calendar.SECOND,
-number,
)
}.timeInMillis
WordSet("month", "months").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0 else -> 0
} }
} }

Loading…
Cancel
Save