Fix, change url and add broken
Fix HeamCms Chapter close #970 add Template FuzzyDoodleParser Fix LelScanVf Add hentaislayer , ScyllaComics Close #609 Close #901 Close #440 Add template IkenParser Fix MangaGalaxyParser Add VortexScans Fix HniScantrad Add HastaTeam close #939 Add HotComicsParser close #962 Add HentaiCrot close #913master
parent
3b5a018f8c
commit
a54f030c4e
@ -1,17 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site.foolslide.fr
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.Broken
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
||||||
import org.koitharu.kotatsu.parsers.site.foolslide.FoolSlideParser
|
|
||||||
|
|
||||||
@Broken // Not dead, changed template
|
|
||||||
@MangaSourceParser("HNISCANTRAD", "HniScantrad", "fr")
|
|
||||||
internal class HniScantrad(context: MangaLoaderContext) :
|
|
||||||
FoolSlideParser(context, MangaParserSource.HNISCANTRAD, "hni-scantrad.net") {
|
|
||||||
|
|
||||||
override val pagination = false
|
|
||||||
override val searchUrl = "lel/search/"
|
|
||||||
override val listUrl = "lel/directory/"
|
|
||||||
}
|
|
||||||
@ -0,0 +1,307 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.fuzzydoodle
|
||||||
|
|
||||||
|
import androidx.collection.scatterSetOf
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal abstract class FuzzyDoodleParser(
|
||||||
|
context: MangaLoaderContext,
|
||||||
|
source: MangaParserSource,
|
||||||
|
domain: String,
|
||||||
|
pageSize: Int = 24,
|
||||||
|
) : PagedMangaParser(context, source, pageSize) {
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.NEWEST)
|
||||||
|
|
||||||
|
override val availableStates: Set<MangaState> =
|
||||||
|
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED, MangaState.ABANDONED)
|
||||||
|
|
||||||
|
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||||
|
|
||||||
|
override val isMultipleTagsSupported = true
|
||||||
|
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val ongoing = scatterSetOf(
|
||||||
|
"en cours",
|
||||||
|
"ongoing",
|
||||||
|
"مستمر",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val finished = scatterSetOf(
|
||||||
|
"terminé",
|
||||||
|
"dropped",
|
||||||
|
"cancelled",
|
||||||
|
"متوقف",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val abandoned = scatterSetOf(
|
||||||
|
"canceled",
|
||||||
|
"cancelled",
|
||||||
|
"dropped",
|
||||||
|
"abandonné",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val paused = scatterSetOf(
|
||||||
|
"hiatus",
|
||||||
|
"on Hold",
|
||||||
|
"en pause",
|
||||||
|
"en attente",
|
||||||
|
)
|
||||||
|
|
||||||
|
protected open val ongoingValue = "ongoing"
|
||||||
|
protected open val finishedValue = "completed"
|
||||||
|
protected open val pausedValue = "haitus"
|
||||||
|
protected open val abandonedValue = "dropped"
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||||
|
val url = buildString {
|
||||||
|
append("https://")
|
||||||
|
append(domain)
|
||||||
|
append("/manga?page=")
|
||||||
|
append(page)
|
||||||
|
|
||||||
|
when (filter) {
|
||||||
|
|
||||||
|
is MangaListFilter.Search -> {
|
||||||
|
append("&title=")
|
||||||
|
append(filter.query.urlEncoded())
|
||||||
|
}
|
||||||
|
|
||||||
|
is MangaListFilter.Advanced -> {
|
||||||
|
append("&type=")
|
||||||
|
|
||||||
|
append("&status=")
|
||||||
|
filter.states.oneOrThrowIfMany()?.let {
|
||||||
|
append(
|
||||||
|
when (it) {
|
||||||
|
MangaState.ONGOING -> ongoingValue
|
||||||
|
MangaState.FINISHED -> finishedValue
|
||||||
|
MangaState.PAUSED -> pausedValue
|
||||||
|
MangaState.ABANDONED -> abandonedValue
|
||||||
|
else -> ""
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.tags.forEach {
|
||||||
|
append("&")
|
||||||
|
append("genre[]".urlEncoded())
|
||||||
|
append("=")
|
||||||
|
append(it.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseMangaList(webClient.httpGet(url).parseHtml())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open val selectMangas = "div#card-real"
|
||||||
|
|
||||||
|
protected open fun parseMangaList(doc: Document): List<Manga> {
|
||||||
|
return doc.select(selectMangas).mapNotNull { div ->
|
||||||
|
val href = div.selectFirst("a")?.attr("href") ?: return@mapNotNull null
|
||||||
|
Manga(
|
||||||
|
id = generateUid(href),
|
||||||
|
url = href,
|
||||||
|
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
||||||
|
coverUrl = div.selectFirst("img")?.src().orEmpty(),
|
||||||
|
title = div.selectFirst("h2")?.text().orEmpty(),
|
||||||
|
altTitle = null,
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
tags = emptySet(),
|
||||||
|
author = null,
|
||||||
|
state = null,
|
||||||
|
source = source,
|
||||||
|
isNsfw = isNsfwSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open val selectAltTitle = "div.flex gap-1:contains(Alternative Titles:) span"
|
||||||
|
protected open val selectState = "a[href*=status] span"
|
||||||
|
protected open val selectAuthor =
|
||||||
|
"div#buttons + div.hidden p:contains(Auteur) span, div#buttons + div.hidden p:contains(Author) span, div#buttons + div.hidden p:contains(المؤلف) span"
|
||||||
|
protected open val selectDescription = "div:has(> p#description) p"
|
||||||
|
protected open val selectTagManga = "div.flex > a.inline-block"
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga = coroutineScope {
|
||||||
|
val mangaUrl = manga.url.toAbsoluteUrl(domain)
|
||||||
|
val doc = webClient.httpGet(mangaUrl).parseHtml()
|
||||||
|
|
||||||
|
val maxPageChapterSelect = doc.select("ul.pagination li[onclick]")
|
||||||
|
var maxPageChapter = 1
|
||||||
|
if (!maxPageChapterSelect.isNullOrEmpty()) {
|
||||||
|
maxPageChapterSelect.map {
|
||||||
|
val i = it.attr("onclick").substringAfterLast("=").substringBefore("'").toInt()
|
||||||
|
if (i > maxPageChapter) {
|
||||||
|
maxPageChapter = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.copy(
|
||||||
|
altTitle = doc.selectLast(selectAltTitle)?.text(),
|
||||||
|
state = when (doc.selectFirst(selectState)?.text()?.lowercase().orEmpty()) {
|
||||||
|
in ongoing -> MangaState.ONGOING
|
||||||
|
in finished -> MangaState.FINISHED
|
||||||
|
in abandoned -> MangaState.ABANDONED
|
||||||
|
in paused -> MangaState.PAUSED
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
author = doc.selectFirst(selectAuthor)?.text().orEmpty(),
|
||||||
|
description = doc.select(selectDescription).text(),
|
||||||
|
tags = doc.select(selectTagManga).mapNotNullToSet {
|
||||||
|
val key = it.attr("href").substringAfterLast('=')
|
||||||
|
MangaTag(
|
||||||
|
key = key,
|
||||||
|
title = it.text(),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
chapters = run {
|
||||||
|
if (maxPageChapter == 1) {
|
||||||
|
parseChapters(doc)
|
||||||
|
} else {
|
||||||
|
coroutineScope {
|
||||||
|
val result = ArrayList(parseChapters(doc))
|
||||||
|
result.ensureCapacity(result.size * maxPageChapter)
|
||||||
|
(2..maxPageChapter).map { i ->
|
||||||
|
async {
|
||||||
|
loadChapters(mangaUrl, i)
|
||||||
|
}
|
||||||
|
}.awaitAll()
|
||||||
|
.flattenTo(result)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.reversed(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun loadChapters(baseUrl: String, page: Int): List<MangaChapter> {
|
||||||
|
return parseChapters(webClient.httpGet("$baseUrl?page=$page").parseHtml().body())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open val datePattern = "MMMM d, yyyy"
|
||||||
|
protected open val selectChapters = "div#chapters-list > a[href]"
|
||||||
|
|
||||||
|
private fun parseChapters(doc: Element): List<MangaChapter> {
|
||||||
|
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||||
|
return doc.select(selectChapters)
|
||||||
|
.mapChapters { _, a ->
|
||||||
|
val href = a.attrAsRelativeUrl("href")
|
||||||
|
val name = a.selectFirst("div.gap-2, #item-title")?.text().orEmpty()
|
||||||
|
val dateText = a.selectFirst("div.gap-3 span, div:has( #item-title) span.mt-1")?.text()
|
||||||
|
val chapterN = href.substringAfterLast('/').replace("-", ".").replace("[^0-9.]".toRegex(), "").toFloat()
|
||||||
|
MangaChapter(
|
||||||
|
id = generateUid(href),
|
||||||
|
name = name,
|
||||||
|
number = chapterN,
|
||||||
|
volume = 0,
|
||||||
|
url = href,
|
||||||
|
scanlator = null,
|
||||||
|
uploadDate = parseChapterDate(
|
||||||
|
dateFormat,
|
||||||
|
dateText,
|
||||||
|
),
|
||||||
|
branch = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open val selectPages = "div#chapter-container > img"
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||||
|
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||||
|
return doc.select(selectPages).map { img ->
|
||||||
|
val url = img.src() ?: img.parseFailed("Image src not found")
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open val selectTagsList = "div.mt-1 div.items-center:has(label)"
|
||||||
|
|
||||||
|
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||||
|
val doc = webClient.httpGet("https://$domain/manga").parseHtml()
|
||||||
|
return doc.select(selectTagsList).mapNotNullToSet {
|
||||||
|
val key = it.selectFirst("input")?.attr("value") ?: return@mapNotNullToSet null
|
||||||
|
MangaTag(
|
||||||
|
key = key,
|
||||||
|
title = it.selectFirst("label")?.text() ?: key,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterDate(dateFormat: DateFormat, date: String?): Long {
|
||||||
|
val d = date?.lowercase() ?: return 0
|
||||||
|
return when {
|
||||||
|
d.endsWith(" ago") ||
|
||||||
|
d.endsWith("مضت") || d.startsWith("منذ") ||
|
||||||
|
d.startsWith("il y a") -> parseRelativeDate(date)
|
||||||
|
|
||||||
|
date.contains(Regex("""\d(st|nd|rd|th)""")) -> date.split(" ").map {
|
||||||
|
if (it.contains(Regex("""\d\D\D"""))) {
|
||||||
|
it.replace(Regex("""\D"""), "")
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}.let { dateFormat.tryParse(it.joinToString(" ")) }
|
||||||
|
|
||||||
|
else -> dateFormat.tryParse(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRelativeDate(date: String): Long {
|
||||||
|
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
return when {
|
||||||
|
WordSet("detik", "segundo", "second", "ثوان")
|
||||||
|
.anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||||
|
|
||||||
|
WordSet("menit", "dakika", "min", "minute", "minutes", "minuto", "mins", "phút", "минут", "دقيقة")
|
||||||
|
.anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||||
|
|
||||||
|
WordSet("jam", "saat", "heure", "hora", "horas", "hour", "hours", "h", "ساعات", "ساعة")
|
||||||
|
.anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||||
|
|
||||||
|
WordSet("hari", "gün", "jour", "día", "dia", "day", "days", "d", "день")
|
||||||
|
.anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||||
|
|
||||||
|
WordSet("month", "months", "أشهر", "mois")
|
||||||
|
.anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||||
|
|
||||||
|
WordSet("week", "weeks", "semana", "semanas")
|
||||||
|
.anyWordIn(date) -> cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
|
||||||
|
|
||||||
|
WordSet("year").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.fuzzydoodle.ar
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
|
import org.koitharu.kotatsu.parsers.site.fuzzydoodle.FuzzyDoodleParser
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
|
@MangaSourceParser("HENTAISLAYER", "HentaiSlayer", "ar", ContentType.HENTAI)
|
||||||
|
internal class HentaiSlayer(context: MangaLoaderContext) :
|
||||||
|
FuzzyDoodleParser(context, MangaParserSource.HENTAISLAYER, "hentaislayer.net") {
|
||||||
|
override val availableStates: Set<MangaState> =
|
||||||
|
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED)
|
||||||
|
override val ongoingValue = "مستمر"
|
||||||
|
override val finishedValue = "مكتمل"
|
||||||
|
override val abandonedValue = "متوقف"
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.fuzzydoodle.en
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.fuzzydoodle.FuzzyDoodleParser
|
||||||
|
|
||||||
|
@MangaSourceParser("SCYLLACOMICS", "ScyllaComics", "en")
|
||||||
|
internal class ScyllaComics(context: MangaLoaderContext) :
|
||||||
|
FuzzyDoodleParser(context, MangaParserSource.SCYLLACOMICS, "scyllacomics.xyz")
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.fuzzydoodle.fr
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
|
import org.koitharu.kotatsu.parsers.site.fuzzydoodle.FuzzyDoodleParser
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
|
@MangaSourceParser("LELSCANVF", "LelScanFr", "fr")
|
||||||
|
internal class LelScanVf(context: MangaLoaderContext) :
|
||||||
|
FuzzyDoodleParser(context, MangaParserSource.LELSCANVF, "lelscanfr.com") {
|
||||||
|
override val availableStates: Set<MangaState> = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED)
|
||||||
|
override val ongoingValue = "en-cours"
|
||||||
|
override val finishedValue = "termin"
|
||||||
|
}
|
||||||
@ -0,0 +1,201 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics
|
||||||
|
|
||||||
|
import androidx.collection.ArrayMap
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import okhttp3.Headers
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal abstract class HotComicsParser(
|
||||||
|
context: MangaLoaderContext,
|
||||||
|
source: MangaParserSource,
|
||||||
|
domain: String,
|
||||||
|
pageSize: Int = 24,
|
||||||
|
) : PagedMangaParser(context, source, pageSize) {
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.NEWEST)
|
||||||
|
|
||||||
|
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||||
|
|
||||||
|
override val isMultipleTagsSupported = false
|
||||||
|
|
||||||
|
protected open val mangasUrl = "/genres"
|
||||||
|
|
||||||
|
protected open val onePage = false
|
||||||
|
|
||||||
|
override val headers: Headers = Headers.Builder()
|
||||||
|
.add("User-Agent", UserAgents.CHROME_DESKTOP)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||||
|
if (onePage && page > 1) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = buildString {
|
||||||
|
append("https://")
|
||||||
|
append(domain)
|
||||||
|
when (filter) {
|
||||||
|
|
||||||
|
is MangaListFilter.Search -> {
|
||||||
|
append("/search?keyword=")
|
||||||
|
append(filter.query.urlEncoded())
|
||||||
|
append("&page=")
|
||||||
|
append(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MangaListFilter.Advanced -> {
|
||||||
|
append(mangasUrl)
|
||||||
|
filter.tags.oneOrThrowIfMany()?.let {
|
||||||
|
append('/')
|
||||||
|
append(it.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!onePage) {
|
||||||
|
append("?page=")
|
||||||
|
append(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {
|
||||||
|
append("/genres?page=")
|
||||||
|
append(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val tagMap = getOrCreateTagMap()
|
||||||
|
return parseMangaList(webClient.httpGet(url).parseHtml(), tagMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open val selectMangas = "li[itemtype*=ComicSeries]:not(.no-comic)"
|
||||||
|
|
||||||
|
protected open fun parseMangaList(doc: Document, tagMap: ArrayMap<String, MangaTag>): List<Manga> {
|
||||||
|
|
||||||
|
return doc.select(selectMangas).mapNotNull { li ->
|
||||||
|
val a = li.selectFirstOrThrow("a")
|
||||||
|
val href = a.attr("href")
|
||||||
|
|
||||||
|
val url = if (href.startsWith("/")) {
|
||||||
|
"/" + href.removePrefix("/").substringAfter('/') // remove /$lang/url
|
||||||
|
} else {
|
||||||
|
href
|
||||||
|
}
|
||||||
|
|
||||||
|
val tags = li.select(".etc span").mapNotNullToSet { tagMap[it.text()] }
|
||||||
|
|
||||||
|
Manga(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
publicUrl = url.toAbsoluteUrl(domain),
|
||||||
|
coverUrl = li.selectFirst("img")?.src().orEmpty(),
|
||||||
|
title = li.selectFirst(".title")?.text().orEmpty(),
|
||||||
|
altTitle = null,
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
description = li.selectFirst("p[itemprop*=description]")?.text().orEmpty(),
|
||||||
|
tags = tags,
|
||||||
|
author = li.selectFirst(".writer")?.text().orEmpty(),
|
||||||
|
state = if (doc.selectFirst(".ico_fin") != null) {
|
||||||
|
MangaState.FINISHED
|
||||||
|
} else {
|
||||||
|
MangaState.ONGOING
|
||||||
|
},
|
||||||
|
source = source,
|
||||||
|
isNsfw = a.selectFirst(".ico-18plus") != null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open val selectMangaChapters = "#tab-chapter li"
|
||||||
|
protected open val datePattern = "MMM dd, yyyy"
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val mangaUrl = manga.url.toAbsoluteUrl(domain)
|
||||||
|
val redirectHeaders = Headers.Builder().set("Referer", mangaUrl).build()
|
||||||
|
val doc = webClient.httpGet(mangaUrl, redirectHeaders).parseHtml()
|
||||||
|
val dateFormat = SimpleDateFormat(datePattern, sourceLocale)
|
||||||
|
return manga.copy(
|
||||||
|
description = doc.selectFirst("div.title_content_box h2")?.text() ?: manga.description,
|
||||||
|
chapters = doc.select(selectMangaChapters)
|
||||||
|
.mapChapters { i, li ->
|
||||||
|
val a = li.selectFirstOrThrow("a")
|
||||||
|
val href = a.attr("href")
|
||||||
|
val url = if (href.startsWith("/")) {
|
||||||
|
"/" + href.removePrefix("/").substringAfter('/') // remove /$lang/url
|
||||||
|
} else if (href.startsWith("javascript")) {
|
||||||
|
val h = a.attr("onclick").substringAfterLast("href='").substringBefore("'")
|
||||||
|
"/" + h.removePrefix("/").substringAfter('/') // remove /$lang/url
|
||||||
|
} else {
|
||||||
|
href
|
||||||
|
}
|
||||||
|
val chapterNum = li.selectFirst(".num")?.text()?.toFloat() ?: (i + 1f)
|
||||||
|
MangaChapter(
|
||||||
|
id = generateUid(url),
|
||||||
|
name = "Chapter : $chapterNum",
|
||||||
|
number = chapterNum,
|
||||||
|
volume = 0,
|
||||||
|
url = url,
|
||||||
|
scanlator = null,
|
||||||
|
uploadDate = dateFormat.tryParse(li.selectFirst("time")?.attr("datetime")),
|
||||||
|
branch = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected open val selectPages = "#viewer-img img"
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||||
|
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||||
|
return doc.select(selectPages).map { img ->
|
||||||
|
val url = img.src() ?: img.parseFailed("Image src not found")
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||||
|
val map = getOrCreateTagMap()
|
||||||
|
val tagSet = ArraySet<MangaTag>(map.size)
|
||||||
|
for (entry in map) {
|
||||||
|
tagSet.add(entry.value)
|
||||||
|
}
|
||||||
|
return tagSet
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open val mutex = Mutex()
|
||||||
|
protected open var tagCache: ArrayMap<String, MangaTag>? = null
|
||||||
|
|
||||||
|
protected open val selectTagsList = ".genres-list li:not(.on) a"
|
||||||
|
|
||||||
|
protected open suspend fun getOrCreateTagMap(): ArrayMap<String, MangaTag> = mutex.withLock {
|
||||||
|
tagCache?.let { return@withLock it }
|
||||||
|
val doc = webClient.httpGet("https://$domain$mangasUrl").parseHtml()
|
||||||
|
val tagItems = doc.select(selectTagsList)
|
||||||
|
val result = ArrayMap<String, MangaTag>(tagItems.size)
|
||||||
|
for (item in tagItems) {
|
||||||
|
val title = item.text()
|
||||||
|
val key = item.attr("href").substringAfterLast('/')
|
||||||
|
if (key.isNotEmpty() && title.isNotEmpty()) {
|
||||||
|
result[title] = MangaTag(title = title, key = key, source = source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tagCache = result
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.de
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICSDE", "TooMicsDe", "de")
|
||||||
|
internal class TooMicsDe(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICSDE, "toomics.com/de") {
|
||||||
|
override val isSearchSupported = false
|
||||||
|
override val mangasUrl = "/webtoon/ranking/genre"
|
||||||
|
override val selectMangas = "li > div.visual"
|
||||||
|
override val selectMangaChapters = "li.normal_ep:has(.coin-type1)"
|
||||||
|
override val selectTagsList = "div.genre_list li:not(.on) a"
|
||||||
|
override val selectPages = "div[id^=load_image_] img"
|
||||||
|
override val onePage = true
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.de
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICS", "Toomics", "de")
|
||||||
|
internal class Toomics(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICS, "toomics.top/de") {
|
||||||
|
override val sourceLocale: Locale = Locale.ENGLISH
|
||||||
|
override val isSearchSupported = false
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.en
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("DAYCOMICS", "DayComics", "en")
|
||||||
|
internal class DayComics(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.DAYCOMICS, "daycomics.me/en")
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.en
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("HOTCOMICS", "HotComics", "en")
|
||||||
|
internal class HotComics(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.HOTCOMICS, "hotcomics.me/en")
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.en
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICSEN", "TooMicsEn", "en")
|
||||||
|
internal class TooMicsEn(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICSEN, "toomics.com/en") {
|
||||||
|
override val isSearchSupported = false
|
||||||
|
override val mangasUrl = "/webtoon/ranking/genre"
|
||||||
|
override val selectMangas = "li > div.visual"
|
||||||
|
override val selectMangaChapters = "li.normal_ep:has(.coin-type1)"
|
||||||
|
override val selectTagsList = "div.genre_list li:not(.on) a"
|
||||||
|
override val selectPages = "div[id^=load_image_] img"
|
||||||
|
override val onePage = true
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.es
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICSES", "TooMicsEs", "es")
|
||||||
|
internal class TooMicsEs(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICSES, "toomics.com/es") {
|
||||||
|
override val isSearchSupported = false
|
||||||
|
override val mangasUrl = "/webtoon/ranking/genre"
|
||||||
|
override val selectMangas = "li > div.visual"
|
||||||
|
override val selectMangaChapters = "li.normal_ep:has(.coin-type1)"
|
||||||
|
override val selectTagsList = "div.genre_list li:not(.on) a"
|
||||||
|
override val selectPages = "div[id^=load_image_] img"
|
||||||
|
override val onePage = true
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.es
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICSESLA", "TooMicsEsLa", "es")
|
||||||
|
internal class TooMicsEsLa(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICSESLA, "toomics.com/mx") {
|
||||||
|
override val isSearchSupported = false
|
||||||
|
override val mangasUrl = "/webtoon/ranking/genre"
|
||||||
|
override val selectMangas = "li > div.visual"
|
||||||
|
override val selectMangaChapters = "li.normal_ep:has(.coin-type1)"
|
||||||
|
override val selectTagsList = "div.genre_list li:not(.on) a"
|
||||||
|
override val selectPages = "div[id^=load_image_] img"
|
||||||
|
override val onePage = true
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.fr
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICSFR", "TooMicsFr", "fr")
|
||||||
|
internal class TooMicsFr(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICSFR, "toomics.com/fr") {
|
||||||
|
override val isSearchSupported = false
|
||||||
|
override val mangasUrl = "/webtoon/ranking/genre"
|
||||||
|
override val selectMangas = "li > div.visual"
|
||||||
|
override val selectMangaChapters = "li.normal_ep:has(.coin-type1)"
|
||||||
|
override val selectTagsList = "div.genre_list li:not(.on) a"
|
||||||
|
override val selectPages = "div[id^=load_image_] img"
|
||||||
|
override val onePage = true
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.it
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICSIT", "TooMicsIt", "it")
|
||||||
|
internal class TooMicsIt(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICSIT, "toomics.com/it") {
|
||||||
|
override val isSearchSupported = false
|
||||||
|
override val mangasUrl = "/webtoon/ranking/genre"
|
||||||
|
override val selectMangas = "li > div.visual"
|
||||||
|
override val selectMangaChapters = "li.normal_ep:has(.coin-type1)"
|
||||||
|
override val selectTagsList = "div.genre_list li:not(.on) a"
|
||||||
|
override val selectPages = "div[id^=load_image_] img"
|
||||||
|
override val onePage = true
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.ja
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICSJA", "TooMicsJa", "ja")
|
||||||
|
internal class TooMicsJa(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICSJA, "toomics.com/ja") {
|
||||||
|
override val isSearchSupported = false
|
||||||
|
override val mangasUrl = "/webtoon/ranking/genre"
|
||||||
|
override val selectMangas = "li > div.visual"
|
||||||
|
override val selectMangaChapters = "li.normal_ep:has(.coin-type1)"
|
||||||
|
override val selectTagsList = "div.genre_list li:not(.on) a"
|
||||||
|
override val selectPages = "div[id^=load_image_] img"
|
||||||
|
override val onePage = true
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.pt
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICSPT", "TooMicsPt", "pt")
|
||||||
|
internal class TooMicsPt(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICSPT, "toomics.com/por") {
|
||||||
|
override val isSearchSupported = false
|
||||||
|
override val mangasUrl = "/webtoon/ranking/genre"
|
||||||
|
override val selectMangas = "li > div.visual"
|
||||||
|
override val selectMangaChapters = "li.normal_ep:has(.coin-type1)"
|
||||||
|
override val selectTagsList = "div.genre_list li:not(.on) a"
|
||||||
|
override val selectPages = "div[id^=load_image_] img"
|
||||||
|
override val onePage = true
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.zh
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICSSC", "TooMicsSc", "zh")
|
||||||
|
internal class TooMicsSc(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICSSC, "toomics.com/sc") {
|
||||||
|
override val isSearchSupported = false
|
||||||
|
override val mangasUrl = "/webtoon/ranking/genre"
|
||||||
|
override val selectMangas = "li > div.visual"
|
||||||
|
override val selectMangaChapters = "li.normal_ep:has(.coin-type1)"
|
||||||
|
override val selectTagsList = "div.genre_list li:not(.on) a"
|
||||||
|
override val selectPages = "div[id^=load_image_] img"
|
||||||
|
override val onePage = true
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.hotcomics.zh
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.hotcomics.HotComicsParser
|
||||||
|
|
||||||
|
@MangaSourceParser("TOOMICSTC", "TooMicsTc", "zh")
|
||||||
|
internal class TooMicsTc(context: MangaLoaderContext) :
|
||||||
|
HotComicsParser(context, MangaParserSource.TOOMICSTC, "toomics.com/tc") {
|
||||||
|
override val isSearchSupported = false
|
||||||
|
override val mangasUrl = "/webtoon/ranking/genre"
|
||||||
|
override val selectMangas = "li > div.visual"
|
||||||
|
override val selectMangaChapters = "li.normal_ep:has(.coin-type1)"
|
||||||
|
override val selectTagsList = "div.genre_list li:not(.on) a"
|
||||||
|
override val selectPages = "div[id^=load_image_] img"
|
||||||
|
override val onePage = true
|
||||||
|
}
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.id
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@MangaSourceParser("HENTAICROT", "HentaiCrot", "id", ContentType.HENTAI)
|
||||||
|
internal class HentaiCrot(context: MangaLoaderContext) :
|
||||||
|
PagedMangaParser(context, MangaParserSource.HENTAICROT, 8) {
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||||
|
SortOrder.UPDATED,
|
||||||
|
)
|
||||||
|
override val configKeyDomain = ConfigKey.Domain("hentaicrot.com")
|
||||||
|
|
||||||
|
override val isMultipleTagsSupported = false
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||||
|
|
||||||
|
val url = buildString {
|
||||||
|
append("https://")
|
||||||
|
append(domain)
|
||||||
|
when (filter) {
|
||||||
|
is MangaListFilter.Search -> {
|
||||||
|
append("/page/")
|
||||||
|
append(page)
|
||||||
|
append("/?s=")
|
||||||
|
append(filter.query.urlEncoded())
|
||||||
|
}
|
||||||
|
|
||||||
|
is MangaListFilter.Advanced -> {
|
||||||
|
|
||||||
|
filter.tags.oneOrThrowIfMany()?.let {
|
||||||
|
append("/category/")
|
||||||
|
append(it.key)
|
||||||
|
append('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
append("/page/")
|
||||||
|
append(page)
|
||||||
|
append('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {
|
||||||
|
append("/page/")
|
||||||
|
append(page)
|
||||||
|
append('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val doc = webClient.httpGet(url).parseHtml()
|
||||||
|
return doc.select("div#content article").mapNotNull { div ->
|
||||||
|
val href = div.selectFirst("a")?.attr("href") ?: return@mapNotNull null
|
||||||
|
Manga(
|
||||||
|
id = generateUid(href),
|
||||||
|
url = href,
|
||||||
|
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
||||||
|
coverUrl = div.selectFirst("img")?.src()?.replace("-200x285", "").orEmpty(),
|
||||||
|
title = div.selectFirst("h2")?.text().orEmpty(),
|
||||||
|
altTitle = null,
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
tags = emptySet(),
|
||||||
|
author = null,
|
||||||
|
state = null,
|
||||||
|
source = source,
|
||||||
|
isNsfw = isNsfwSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||||
|
val doc = webClient.httpGet("https://$domain").parseHtml()
|
||||||
|
return doc.select("ul.megamenu li").mapNotNullToSet { li ->
|
||||||
|
val key = li.selectFirstOrThrow("a").attr("href").removeSuffix('/').substringAfterLast('/')
|
||||||
|
val name = li.selectFirstOrThrow("a").text()
|
||||||
|
MangaTag(
|
||||||
|
key = key,
|
||||||
|
title = name,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||||
|
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||||
|
return manga.copy(
|
||||||
|
description = doc.selectFirst("div.entry-content p")?.text().orEmpty(),
|
||||||
|
altTitle = doc.selectFirst("div.entry-content ul li:contains(Alternative Name(s) :) em")?.text().orEmpty(),
|
||||||
|
author = doc.selectFirst("div.entry-content ul li:contains(Artists :) em")?.text().orEmpty(),
|
||||||
|
state = null,
|
||||||
|
chapters = listOf(
|
||||||
|
MangaChapter(
|
||||||
|
id = manga.id,
|
||||||
|
name = manga.title,
|
||||||
|
number = 1f,
|
||||||
|
volume = 0,
|
||||||
|
url = fullUrl,
|
||||||
|
scanlator = null,
|
||||||
|
uploadDate = 0,
|
||||||
|
branch = null,
|
||||||
|
source = source,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||||
|
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||||
|
return doc.select(".thumbnail img, figure.gallery-item img").map { img ->
|
||||||
|
val url = img.src() ?: img.parseFailed("Image src not found")
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.id
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@MangaSourceParser("PIXHENTAI", "PixHentai", "id", ContentType.HENTAI)
|
||||||
|
internal class PixHentai(context: MangaLoaderContext) :
|
||||||
|
PagedMangaParser(context, MangaParserSource.PIXHENTAI, 8) {
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(
|
||||||
|
SortOrder.UPDATED,
|
||||||
|
)
|
||||||
|
override val configKeyDomain = ConfigKey.Domain("pixhentai.com")
|
||||||
|
|
||||||
|
override val isMultipleTagsSupported = false
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||||
|
|
||||||
|
val url = buildString {
|
||||||
|
append("https://")
|
||||||
|
append(domain)
|
||||||
|
when (filter) {
|
||||||
|
is MangaListFilter.Search -> {
|
||||||
|
append("/page/")
|
||||||
|
append(page)
|
||||||
|
append("/?s=")
|
||||||
|
append(filter.query.urlEncoded())
|
||||||
|
}
|
||||||
|
|
||||||
|
is MangaListFilter.Advanced -> {
|
||||||
|
|
||||||
|
filter.tags.oneOrThrowIfMany()?.let {
|
||||||
|
append("/genre/")
|
||||||
|
append(it.key)
|
||||||
|
append('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
append("/page/")
|
||||||
|
append(page)
|
||||||
|
append('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {
|
||||||
|
append("/page/")
|
||||||
|
append(page)
|
||||||
|
append('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val doc = webClient.httpGet(url).parseHtml()
|
||||||
|
return doc.select("div#content article").mapNotNull { div ->
|
||||||
|
val href = div.selectFirst("a")?.attr("href") ?: return@mapNotNull null
|
||||||
|
Manga(
|
||||||
|
id = generateUid(href),
|
||||||
|
url = href,
|
||||||
|
publicUrl = href.toAbsoluteUrl(div.host ?: domain),
|
||||||
|
coverUrl = div.selectFirst("img")?.src()?.replace("-200x285", "").orEmpty(),
|
||||||
|
title = div.selectFirst("h2")?.text().orEmpty(),
|
||||||
|
altTitle = null,
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
tags = emptySet(),
|
||||||
|
author = null,
|
||||||
|
state = null,
|
||||||
|
source = source,
|
||||||
|
isNsfw = isNsfwSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||||
|
val doc = webClient.httpGet("https://$domain").parseHtml()
|
||||||
|
return doc.select("ul.megamenu li").mapNotNullToSet { li ->
|
||||||
|
val key = li.selectFirstOrThrow("a").attr("href").removeSuffix('/').substringAfterLast('/')
|
||||||
|
val name = li.selectFirstOrThrow("a").text()
|
||||||
|
MangaTag(
|
||||||
|
key = key,
|
||||||
|
title = name,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val fullUrl = manga.url.toAbsoluteUrl(domain)
|
||||||
|
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||||
|
return manga.copy(
|
||||||
|
description = doc.selectFirst("div.entry-content p")?.text().orEmpty(),
|
||||||
|
altTitle = doc.selectFirst("div.entry-content ul li:contains(Alternative Name(s) :) em")?.text().orEmpty(),
|
||||||
|
author = doc.selectFirst("div.entry-content ul li:contains(Artists :) em")?.text().orEmpty(),
|
||||||
|
state = null,
|
||||||
|
chapters = listOf(
|
||||||
|
MangaChapter(
|
||||||
|
id = manga.id,
|
||||||
|
name = manga.title,
|
||||||
|
number = 1f,
|
||||||
|
volume = 0,
|
||||||
|
url = fullUrl,
|
||||||
|
scanlator = null,
|
||||||
|
uploadDate = 0,
|
||||||
|
branch = null,
|
||||||
|
source = source,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||||
|
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||||
|
return doc.select(".thumbnail img, figure.gallery-item img").map { img ->
|
||||||
|
val url = img.src() ?: img.parseFailed("Image src not found")
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.iken
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.PagedMangaParser
|
||||||
|
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||||
|
import org.koitharu.kotatsu.parsers.model.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.*
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.toJSONList
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal abstract class IkenParser(
|
||||||
|
context: MangaLoaderContext,
|
||||||
|
source: MangaParserSource,
|
||||||
|
domain: String,
|
||||||
|
pageSize: Int = 18,
|
||||||
|
) : PagedMangaParser(context, source, pageSize) {
|
||||||
|
|
||||||
|
override val availableSortOrders: Set<SortOrder> = EnumSet.of(SortOrder.POPULARITY)
|
||||||
|
|
||||||
|
override val availableStates: Set<MangaState> =
|
||||||
|
EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.ABANDONED, MangaState.UPCOMING)
|
||||||
|
|
||||||
|
override val configKeyDomain = ConfigKey.Domain(domain)
|
||||||
|
|
||||||
|
override val isMultipleTagsSupported = true
|
||||||
|
|
||||||
|
override suspend fun getListPage(page: Int, filter: MangaListFilter?): List<Manga> {
|
||||||
|
val url = buildString {
|
||||||
|
append("https://")
|
||||||
|
append(domain)
|
||||||
|
append("/api/query?page=")
|
||||||
|
append(page)
|
||||||
|
append("&perPage=18&searchTerm=")
|
||||||
|
when (filter) {
|
||||||
|
|
||||||
|
is MangaListFilter.Search -> {
|
||||||
|
append(filter.query.urlEncoded())
|
||||||
|
}
|
||||||
|
|
||||||
|
is MangaListFilter.Advanced -> {
|
||||||
|
|
||||||
|
if (filter.tags.isNotEmpty()) {
|
||||||
|
append("&genreIds=")
|
||||||
|
appendAll(filter.tags, ",") { it.key }
|
||||||
|
}
|
||||||
|
|
||||||
|
append("&seriesType=&seriesStatus=")
|
||||||
|
filter.states.oneOrThrowIfMany()?.let {
|
||||||
|
append(
|
||||||
|
when (it) {
|
||||||
|
MangaState.ONGOING -> "ONGOING"
|
||||||
|
MangaState.FINISHED -> "COMPLETED"
|
||||||
|
MangaState.UPCOMING -> "COMING_SOON"
|
||||||
|
MangaState.ABANDONED -> "DROPPED"
|
||||||
|
else -> ""
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parseMangaList(webClient.httpGet(url).parseJson())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun parseMangaList(json: JSONObject): List<Manga> {
|
||||||
|
return json.getJSONArray("posts").mapJSON {
|
||||||
|
val url = "/series/${it.getString("slug")}"
|
||||||
|
Manga(
|
||||||
|
id = it.getLong("id"),
|
||||||
|
url = url,
|
||||||
|
publicUrl = url.toAbsoluteUrl(domain),
|
||||||
|
coverUrl = it.getString("featuredImage").orEmpty(),
|
||||||
|
title = it.getString("postTitle"),
|
||||||
|
altTitle = it.getString("alternativeTitles"),
|
||||||
|
description = it.getString("postContent"),
|
||||||
|
rating = RATING_UNKNOWN,
|
||||||
|
tags = emptySet(),
|
||||||
|
author = it.getString("author"),
|
||||||
|
state = when (it.getString("seriesStatus")) {
|
||||||
|
"ONGOING" -> MangaState.ONGOING
|
||||||
|
"COMPLETED" -> MangaState.FINISHED
|
||||||
|
"DROPPED", "CANCELLED" -> MangaState.ABANDONED
|
||||||
|
"COMING_SOON" -> MangaState.UPCOMING
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
source = source,
|
||||||
|
isNsfw = it.getBooleanOrDefault("hot", false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected open val datePattern = "yyyy-MM-dd"
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
|
val seriesId = manga.id
|
||||||
|
val url = "https://$domain/api/chapters?postId=$seriesId&skip=0&take=1000&order=desc&userid="
|
||||||
|
val json = webClient.httpGet(url).parseJson().getJSONObject("post")
|
||||||
|
val slug = json.getString("slug")
|
||||||
|
val data = json.getJSONArray("chapters").toJSONList()
|
||||||
|
val dateFormat = SimpleDateFormat(datePattern, Locale.ENGLISH)
|
||||||
|
return manga.copy(
|
||||||
|
chapters = data.mapChapters(reversed = true) { i, it ->
|
||||||
|
val chapterUrl =
|
||||||
|
"/series/$slug/${it.getString("slug")}"
|
||||||
|
MangaChapter(
|
||||||
|
id = it.getLong("id"),
|
||||||
|
name = "Chapter : ${it.getInt("number")}",
|
||||||
|
number = it.getInt("number").toFloat(),
|
||||||
|
volume = 0,
|
||||||
|
url = chapterUrl,
|
||||||
|
scanlator = null,
|
||||||
|
uploadDate = dateFormat.tryParse(it.getString("createdAt").substringBefore("T")),
|
||||||
|
branch = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open val selectPages = "main section > img"
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
|
val fullUrl = chapter.url.toAbsoluteUrl(domain)
|
||||||
|
val doc = webClient.httpGet(fullUrl).parseHtml()
|
||||||
|
return doc.select(selectPages).map { img ->
|
||||||
|
val url = img.src() ?: img.parseFailed("Image src not found")
|
||||||
|
MangaPage(
|
||||||
|
id = generateUid(url),
|
||||||
|
url = url,
|
||||||
|
preview = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAvailableTags(): Set<MangaTag> {
|
||||||
|
val doc = webClient.httpGet("https://$domain/series").parseHtml()
|
||||||
|
return doc.selectLastOrThrow("select").select("option[value]").mapNotNullToSet {
|
||||||
|
val key = it.attr("value") ?: return@mapNotNullToSet null
|
||||||
|
MangaTag(
|
||||||
|
key = key,
|
||||||
|
title = it.text() ?: key,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.iken.en
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.iken.IkenParser
|
||||||
|
|
||||||
|
@MangaSourceParser("MANGAGALAXY", "MangaGalaxy", "en")
|
||||||
|
internal class MangaGalaxyParser(context: MangaLoaderContext) :
|
||||||
|
IkenParser(context, MangaParserSource.MANGAGALAXY, "mangagalaxy.org")
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.iken.en
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.iken.IkenParser
|
||||||
|
|
||||||
|
@MangaSourceParser("VORTEXSCANS", "VortexScans", "en")
|
||||||
|
internal class VortexScans(context: MangaLoaderContext) :
|
||||||
|
IkenParser(context, MangaParserSource.VORTEXSCANS, "vortexscans.org")
|
||||||
@ -1,13 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site.mangareader.en
|
|
||||||
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.Broken
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
||||||
import org.koitharu.kotatsu.parsers.site.mangareader.MangaReaderParser
|
|
||||||
|
|
||||||
@Broken // Not dead, changed template
|
|
||||||
@MangaSourceParser("MANGAGALAXY", "MangaGalaxy", "en")
|
|
||||||
internal class MangaGalaxyParser(context: MangaLoaderContext) :
|
|
||||||
MangaReaderParser(context, MangaParserSource.MANGAGALAXY, "mangagalaxy.org", 20, 10)
|
|
||||||
@ -1,8 +1,9 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site.it.mangaworld
|
package org.koitharu.kotatsu.parsers.site.mangaworld
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.mangaworld.it.MangaWorldParser
|
||||||
|
|
||||||
@MangaSourceParser("MANGAWORLD", "MangaWorld", "it")
|
@MangaSourceParser("MANGAWORLD", "MangaWorld", "it")
|
||||||
internal class MangaWorld(
|
internal class MangaWorld(
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site.it.mangaworld
|
package org.koitharu.kotatsu.parsers.site.mangaworld.it
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site.it.mangaworld
|
package org.koitharu.kotatsu.parsers.site.mangaworld.it
|
||||||
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
@ -1,13 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.parsers.site.mmrcms.fr
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
||||||
import org.koitharu.kotatsu.parsers.site.mmrcms.MmrcmsParser
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@MangaSourceParser("LELSCANVF", "LelScanVf", "fr")
|
|
||||||
internal class LelScanVf(context: MangaLoaderContext) :
|
|
||||||
MmrcmsParser(context, MangaParserSource.LELSCANVF, "lelscanfr.com") {
|
|
||||||
override val sourceLocale: Locale = Locale.ENGLISH
|
|
||||||
}
|
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.pizzareader.fr
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.pizzareader.PizzaReaderParser
|
||||||
|
|
||||||
|
@MangaSourceParser("HNISCANTRAD", "HniScantrad", "fr")
|
||||||
|
internal class HniScantrad(context: MangaLoaderContext) :
|
||||||
|
PizzaReaderParser(context, MangaParserSource.HNISCANTRAD, "hni-scantrad.net") {
|
||||||
|
override val ongoingFilter = "en cours"
|
||||||
|
override val completedFilter = "terminé"
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.parsers.site.pizzareader.it
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.MangaSourceParser
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.site.pizzareader.PizzaReaderParser
|
||||||
|
|
||||||
|
@MangaSourceParser("HASTATEAM", "HastaTeam", "it")
|
||||||
|
internal class HastaTeam(context: MangaLoaderContext) :
|
||||||
|
PizzaReaderParser(context, MangaParserSource.HASTATEAM, "ddt.hastateam.com")
|
||||||
Loading…
Reference in New Issue