Remove some dead sources (#11133)
* Remove Manhwafull * Remove Mangahasu * Remove MangaScans * Remove MyShojo * Remove Iris Scans * Remove Constellar Scans * Remove King of Scans * Remove 1st-Kiss Manga.net * Remove Banana Manga * Remove Todaymic * Remove Comicsekai * Remove KataKomik * Remove Komik Lovers * Remove Mangakyo * Remove MangaYu * Remove Tenshi.id * Remove TukangKomik * Remove YuraManga * Remove Pojok Manga * Remove Scan-Manga * Remove Valkyrie Scan * Remove Starbound Scans * Remove Manga Rock Team * Remove Tres Daos Net
@ -1,10 +0,0 @@
|
||||
ext {
|
||||
extName = 'Banana Manga'
|
||||
extClass = '.BananaManga'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://bananamanga.net'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.bananamanga
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
|
||||
class BananaManga : Madara("Banana Manga", "https://bananamanga.net", "en") {
|
||||
override val useNewChapterEndpoint = true
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
ext {
|
||||
extName = 'Constellar Scans'
|
||||
extClass = '.ConstellarScans'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://constellarcomic.com'
|
||||
overrideVersionCode = 17
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:randomua"))
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 37 KiB |
@ -1,102 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.constellarscans
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
|
||||
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
||||
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
|
||||
import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import keiyoushi.utils.getPreferencesLazy
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
class ConstellarScans :
|
||||
MangaThemesia(
|
||||
"Constellar Scans",
|
||||
"https://constellarcomic.com",
|
||||
"en",
|
||||
),
|
||||
ConfigurableSource {
|
||||
|
||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
addRandomUAPreferenceToScreen(screen)
|
||||
}
|
||||
|
||||
override val client: OkHttpClient by lazy {
|
||||
network.cloudflareClient.newBuilder()
|
||||
.setRandomUserAgent(
|
||||
preferences.getPrefUAType(),
|
||||
preferences.getPrefCustomUA(),
|
||||
)
|
||||
.rateLimit(1, 1)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.add("Accept-Language", "en-US,en;q=0.9")
|
||||
.add("DNT", "1")
|
||||
.add("Upgrade-Insecure-Requests", "1")
|
||||
|
||||
override val seriesStatusSelector = ".status"
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request =
|
||||
super.pageListRequest(chapter).newBuilder()
|
||||
.header(
|
||||
"Accept",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
)
|
||||
.header("Sec-Fetch-Site", "same-origin")
|
||||
.header("Sec-Fetch-Mode", "navigate")
|
||||
.header("Sec-Fetch-Dest", "document")
|
||||
.header("Sec-Fetch-User", "?1")
|
||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
.build()
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
countViews(document)
|
||||
|
||||
val html = document.toString()
|
||||
if (!html.contains("ts_rea_der_._run(\"")) {
|
||||
return super.pageListParse(document)
|
||||
}
|
||||
|
||||
val tsReaderRawData = html
|
||||
.substringAfter("ts_rea_der_._run(\"")
|
||||
.substringBefore("\")")
|
||||
.replace(Regex("""\D"""), "")
|
||||
.chunked(4)
|
||||
.map {
|
||||
val tenthsAndOnes = it.chunked(2).map {
|
||||
val num = it.toInt()
|
||||
num / 10 + num % 10
|
||||
}
|
||||
(tenthsAndOnes[0] * 10 + tenthsAndOnes[1] + 32).toChar()
|
||||
}
|
||||
.joinToString("")
|
||||
|
||||
return json.parseToJsonElement(tsReaderRawData).jsonObject["sources"]!!.jsonArray[0].jsonObject["images"]!!.jsonArray.mapIndexed { idx, it ->
|
||||
Page(idx, imageUrl = it.jsonPrimitive.content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request = super.imageRequest(page).newBuilder()
|
||||
.header("Accept", "image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
||||
.header("Sec-Fetch-Dest", "image")
|
||||
.header("Sec-Fetch-Mode", "no-cors")
|
||||
.header("Sec-Fetch-Site", "same-origin")
|
||||
.build()
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
ext {
|
||||
extName = '1st-Kiss Manga.net'
|
||||
extClass = '.FirstKissMangaNet'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://1stkissmanga.org'
|
||||
overrideVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 54 KiB |
@ -1,12 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.firstkissmanganet
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
|
||||
class FirstKissMangaNet : Madara(
|
||||
"1st-Kiss Manga.net",
|
||||
"https://1stkissmanga.org",
|
||||
"en",
|
||||
) {
|
||||
override val useLoadMoreRequest = LoadMoreStrategy.Never
|
||||
override val useNewChapterEndpoint = false
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
ext {
|
||||
extName = 'Iris Scans'
|
||||
extClass = '.IrisScans'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://irisscans.xyz'
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@ -1,15 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.irisscans
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class IrisScans : MangaThemesia(
|
||||
"Iris Scans",
|
||||
"https://irisscans.xyz",
|
||||
"en",
|
||||
) {
|
||||
override val client: OkHttpClient = super.client.newBuilder()
|
||||
.rateLimit(4)
|
||||
.build()
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
ext {
|
||||
extName = 'King of Scans'
|
||||
extClass = '.KingOfScans'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://kingofscans.com'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@ -1,24 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.kingofscans
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
class KingOfScans : MangaThemesia(
|
||||
"King of Scans",
|
||||
"https://kingofscans.com",
|
||||
"en",
|
||||
mangaUrlDirectory = "/comics",
|
||||
) {
|
||||
// Strip out position- and name-dependant ads. "Read first at ..."
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = super.pageListParse(document)
|
||||
return pages.filterIndexed { index, page ->
|
||||
when (index) {
|
||||
0 -> !page.imageUrl!!.endsWith("/START-KS.jpg")
|
||||
pages.lastIndex -> !page.imageUrl!!.endsWith("/END-KS.jpg")
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
ext {
|
||||
extName = 'Mangahasu'
|
||||
extClass = '.Mangahasu'
|
||||
extVersionCode = 17
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@ -1,369 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangahasu
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Mangahasu : ParsedHttpSource() {
|
||||
|
||||
override val name = "Mangahasu"
|
||||
|
||||
override val baseUrl = "https://mangahasu.me"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.add("Referer", baseUrl)
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/most-popular.html?page=$page", headers)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/latest-releases.html?page=$page", headers)
|
||||
|
||||
// Only selects popular of all time
|
||||
override fun popularMangaSelector() = "div.right div.div_item"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.div_item"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select("img").first()!!.attr("src")
|
||||
element.select("a:has(h3.name-manga), a.name-manga").first()!!.let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a[title = Tiếp]"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/advanced-search.html".toHttpUrl().newBuilder()
|
||||
url.addQueryParameter("keyword", query)
|
||||
url.addQueryParameter("page", page.toString())
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is AuthorFilter -> url.addQueryParameter("author", filter.state)
|
||||
is ArtistFilter -> url.addQueryParameter("artist", filter.state)
|
||||
is StatusFilter -> url.addQueryParameter("status", filter.toUriPart())
|
||||
is TypeFilter -> url.addQueryParameter("typeid", filter.toUriPart())
|
||||
is GenreFilter -> {
|
||||
filter.state.forEach {
|
||||
when (it.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> url.addQueryParameter("g_i[]", it.id)
|
||||
Filter.TriState.STATE_EXCLUDE -> url.addQueryParameter("g_e[]", it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is OrderByFilter -> {
|
||||
filter.state?.let {
|
||||
var sortId = it.index
|
||||
|
||||
// Increment for consistency
|
||||
when (sortId) {
|
||||
1 -> sortId += 1
|
||||
2 -> sortId += 2
|
||||
}
|
||||
|
||||
val value = if (it.ascending) "${(sortId + 1)}" else "$sortId"
|
||||
|
||||
url.addQueryParameter("orderby", value)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = latestUpdatesSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select(".info-c").first()!!
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = isUpdating(infoElement.select(".info")[0].text())
|
||||
manga.artist = isUpdating(infoElement.select(".info")[1].text())
|
||||
manga.genre = isUpdating(infoElement.select(".info")[3].text())
|
||||
manga.status = parseStatus(infoElement.select(".info")[4].text())
|
||||
manga.description =
|
||||
document.select("div.content-info:has(h3:contains(summary)) div").first()?.text()
|
||||
manga.thumbnail_url = document.select("div.info-img img").attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.contains("Ongoing") -> SManga.ONGOING
|
||||
element.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
private fun isUpdating(string: String): String {
|
||||
return if (string == "Updating...") "" else string
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "tbody tr"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()!!
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
urlElement.select("span.name-manga").remove()
|
||||
chapter.name = urlElement.text()
|
||||
|
||||
chapter.date_upload = element.select(".date-updated").last()?.text()?.let {
|
||||
SimpleDateFormat("MMM dd, yyyy", Locale.US).parse(it)?.time ?: 0
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
val basic = Regex("""Chapter\s([0-9]+)""")
|
||||
when {
|
||||
basic.containsMatchIn(chapter.name) -> {
|
||||
basic.find(chapter.name)?.let {
|
||||
chapter.chapter_number = it.groups[1]?.value!!.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
// Grab All Pages from site
|
||||
// Some images are place holders on new chapters.
|
||||
|
||||
val pageList = document.select("div.img img")
|
||||
.mapIndexed { _, el ->
|
||||
val pageNumber = el.attr("class").substringAfter("page").toInt()
|
||||
Page(pageNumber, "", el.attr("src"))
|
||||
}
|
||||
.toMutableList()
|
||||
|
||||
// Some images are not yet loaded onto Mangahasu's image server.
|
||||
// Decode temporary URLs and replace placeholder images.
|
||||
|
||||
val lstDUrls = document.select("script:containsData(lstDUrls)")
|
||||
.html()
|
||||
.substringAfter("lstDUrls")
|
||||
.substringAfter("\"")
|
||||
.substringBefore("\"")
|
||||
|
||||
// Base64 = [] or empty file
|
||||
// Ensures lstDUrls exists, otherwise errors will occur
|
||||
if (lstDUrls.isNotEmpty() && lstDUrls != "W10=") {
|
||||
val jsonRaw = String(Base64.decode(lstDUrls, Base64.DEFAULT))
|
||||
val jsonArr = json.parseToJsonElement(jsonRaw).jsonArray
|
||||
|
||||
jsonArr.forEach { jsonEl ->
|
||||
val jsonObj = jsonEl.jsonObject
|
||||
val pageNumber = jsonObj["page"]!!.jsonPrimitive.int
|
||||
|
||||
pageList.removeAll { page -> page.index == pageNumber }
|
||||
pageList.add(Page(pageNumber, "", jsonObj["url"]!!.jsonPrimitive.content))
|
||||
}
|
||||
}
|
||||
|
||||
return pageList.sortedBy { page -> page.index }
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
AuthorFilter(),
|
||||
ArtistFilter(),
|
||||
OrderByFilter(),
|
||||
StatusFilter(),
|
||||
TypeFilter(),
|
||||
GenreFilter(getGenreList()),
|
||||
)
|
||||
|
||||
private class AuthorFilter : Filter.Text("Author")
|
||||
|
||||
private class ArtistFilter : Filter.Text("Artist")
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private class TypeFilter : UriPartFilter(
|
||||
"Type",
|
||||
arrayOf(
|
||||
Pair("Any", ""),
|
||||
Pair("Manga", "10"),
|
||||
Pair("Manhwa", "12"),
|
||||
Pair("Manhua", "19"),
|
||||
),
|
||||
)
|
||||
|
||||
private class OrderByFilter : Filter.Sort(
|
||||
"Order By",
|
||||
arrayOf(
|
||||
"Updated",
|
||||
"Views",
|
||||
"Subscribers",
|
||||
),
|
||||
Selection(0, false),
|
||||
)
|
||||
|
||||
private class StatusFilter : UriPartFilter(
|
||||
"Status",
|
||||
arrayOf(
|
||||
Pair("Any", ""),
|
||||
Pair("Completed", "1"),
|
||||
Pair("Ongoing", "2"),
|
||||
),
|
||||
)
|
||||
|
||||
private class Genre(name: String, val id: String) : Filter.TriState(name)
|
||||
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("4-koma", "46"),
|
||||
Genre("Action", "1"),
|
||||
Genre("Adaptation", "101"),
|
||||
Genre("Adult", "2"),
|
||||
Genre("Adventure", "3"),
|
||||
Genre("Aliens", "103"),
|
||||
Genre("Animals", "73"),
|
||||
Genre("Anime", "57"),
|
||||
Genre("Anthology", "99"),
|
||||
Genre("Award Winning", "48"),
|
||||
Genre("Bara", "60"),
|
||||
Genre("Comedy", "4"),
|
||||
Genre("Comic", "5"),
|
||||
Genre("Cooking", "6"),
|
||||
Genre("Crime", "92"),
|
||||
Genre("Crossdressing", "86"),
|
||||
Genre("Delinquents", "83"),
|
||||
Genre("Demons", "51"),
|
||||
Genre("Doujinshi", "7"),
|
||||
Genre("Drama", "8"),
|
||||
Genre("Ecchi", "9"),
|
||||
Genre("Fan Colored", "107"),
|
||||
Genre("Fantasy", "10"),
|
||||
Genre("Full Color", "95"),
|
||||
Genre("Game", "68"),
|
||||
Genre("Gender Bender", "11"),
|
||||
Genre("Genderswap", "81"),
|
||||
Genre("Ghosts", "90"),
|
||||
Genre("Gore", "100"),
|
||||
Genre("Gyaru", "97"),
|
||||
Genre("Harem", "12"),
|
||||
Genre("Historical", "13"),
|
||||
Genre("Horror", "14"),
|
||||
Genre("Incest", "84"),
|
||||
Genre("Isekai", "67"),
|
||||
Genre("Josei", "15"),
|
||||
Genre("Live Action", "59"),
|
||||
Genre("Loli", "91"),
|
||||
Genre("Lolicon", "16"),
|
||||
Genre("Long Strip", "93"),
|
||||
Genre("Mafia", "113"),
|
||||
Genre("Magic", "55"),
|
||||
Genre("Magical Girls", "89"),
|
||||
Genre("Manga Reviews", "64"),
|
||||
Genre("Martial Arts", "20"),
|
||||
Genre("Mature", "21"),
|
||||
Genre("Mecha", "22"),
|
||||
Genre("Medical", "23"),
|
||||
Genre("Military", "62"),
|
||||
Genre("Monster Girls", "87"),
|
||||
Genre("Monsters", "72"),
|
||||
Genre("Music", "24"),
|
||||
Genre("Mystery", "25"),
|
||||
Genre("Ninja", "112"),
|
||||
Genre("Office Workers", "80"),
|
||||
Genre("Official Colored", "96"),
|
||||
Genre("One shot", "26"),
|
||||
Genre("Others", "114"),
|
||||
Genre("Philosophical", "110"),
|
||||
Genre("Police", "105"),
|
||||
Genre("Post-Apocalyptic", "76"),
|
||||
Genre("Psychological", "27"),
|
||||
Genre("Reincarnation", "74"),
|
||||
Genre("Reverse harem", "69"),
|
||||
Genre("Romance", "28"),
|
||||
Genre("Samurai", "108"),
|
||||
Genre("School Life", "29"),
|
||||
Genre("Sci-fi", "30"),
|
||||
Genre("Seinen", "31"),
|
||||
Genre("Seinen Supernatural", "66"),
|
||||
Genre("Sexual Violence", "98"),
|
||||
Genre("Shota", "104"),
|
||||
Genre("Shotacon", "32"),
|
||||
Genre("Shoujo", "33"),
|
||||
Genre("Shoujo Ai", "34"),
|
||||
Genre("Shoujoai", "63"),
|
||||
Genre("Shounen", "35"),
|
||||
Genre("Shounen Ai", "36"),
|
||||
Genre("Shounenai", "61"),
|
||||
Genre("Slice of Life", "37"),
|
||||
Genre("Smut", "38"),
|
||||
Genre("Sports", "39"),
|
||||
Genre("Super power", "70"),
|
||||
Genre("Superhero", "88"),
|
||||
Genre("Supernatural", "40"),
|
||||
Genre("Survival", "77"),
|
||||
Genre("Thriller", "75"),
|
||||
Genre("Time Travel", "78"),
|
||||
Genre("Traditional Games", "111"),
|
||||
Genre("Tragedy", "41"),
|
||||
Genre("Uncategorized", "65"),
|
||||
Genre("User Created", "102"),
|
||||
Genre("Vampire", "58"),
|
||||
Genre("Vampires", "82"),
|
||||
Genre("Video Games", "85"),
|
||||
Genre("Virtual Reality", "109"),
|
||||
Genre("Web Comic", "94"),
|
||||
Genre("Webtoon", "42"),
|
||||
Genre("Webtoons", "56"),
|
||||
Genre("Wuxia", "71"),
|
||||
Genre("Yaoi", "43"),
|
||||
Genre("Youkai", "106"),
|
||||
Genre("Yuri", "44"),
|
||||
Genre("Zombies", "79"),
|
||||
)
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
ext {
|
||||
extName = 'Manga Rock Team'
|
||||
extClass = '.MangaRockTeam'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://mangarockteam.com'
|
||||
overrideVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@ -1,5 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangarockteam
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
|
||||
class MangaRockTeam : Madara("Manga Rock Team", "https://mangarockteam.com", "en")
|
||||
@ -1,8 +0,0 @@
|
||||
ext {
|
||||
extName = 'MangaScans'
|
||||
extClass = '.MangaScans'
|
||||
extVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@ -1,131 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangatop
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
interface UriFilter {
|
||||
fun addToUri(builder: HttpUrl.Builder)
|
||||
}
|
||||
|
||||
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
|
||||
|
||||
open class UriMultiSelectFilter(
|
||||
name: String,
|
||||
private val param: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
state.filter { it.state }.forEach {
|
||||
builder.addQueryParameter(param, it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TypeFilter : UriMultiSelectFilter(
|
||||
"Type",
|
||||
"types[]",
|
||||
arrayOf(
|
||||
Pair("Manga", "1"),
|
||||
Pair("Novel", "2"),
|
||||
Pair("One Shot", "3"),
|
||||
Pair("Doujinshi", "4"),
|
||||
Pair("Manhwa", "5"),
|
||||
Pair("Manhua", "6"),
|
||||
Pair("OEL", "7"),
|
||||
Pair("Light Novel", "8"),
|
||||
),
|
||||
)
|
||||
|
||||
class GenreFilter : UriMultiSelectFilter(
|
||||
"Genre",
|
||||
"genres[]",
|
||||
arrayOf(
|
||||
Pair("Action", "1"),
|
||||
Pair("Adventure", "2"),
|
||||
Pair("Avant Garde", "5"),
|
||||
Pair("Award Winning", "46"),
|
||||
Pair("Boys Love", "28"),
|
||||
Pair("Comedy", "4"),
|
||||
Pair("Drama", "8"),
|
||||
Pair("Fantasy", "10"),
|
||||
Pair("Girls Love", "26"),
|
||||
Pair("Gourmet", "47"),
|
||||
Pair("Horror", "14"),
|
||||
Pair("Mystery", "7"),
|
||||
Pair("Romance", "22"),
|
||||
Pair("Sci-Fi", "24"),
|
||||
Pair("Slice of Life", "36"),
|
||||
Pair("Sports", "30"),
|
||||
Pair("Supernatural", "37"),
|
||||
Pair("Suspense", "45"),
|
||||
Pair("Ecchi", "9"),
|
||||
Pair("Erotica", "49"),
|
||||
Pair("Hentai", "12"),
|
||||
Pair("Adult Cast", "50"),
|
||||
Pair("Anthropomorphic", "51"),
|
||||
Pair("CGDCT", "52"),
|
||||
Pair("Childcare", "53"),
|
||||
Pair("Combat Sports", "54"),
|
||||
Pair("Crossdressing", "44"),
|
||||
Pair("Delinquents", "55"),
|
||||
Pair("Detective", "39"),
|
||||
Pair("Educational", "56"),
|
||||
Pair("Gag Humor", "57"),
|
||||
Pair("Gore", "58"),
|
||||
Pair("Harem", "35"),
|
||||
Pair("High Stakes Game", "59"),
|
||||
Pair("Historical", "13"),
|
||||
Pair("Idols (Female)", "60"),
|
||||
Pair("Idols (Male)", "61"),
|
||||
Pair("Isekai", "62"),
|
||||
Pair("Iyashikei", "63"),
|
||||
Pair("Love Polygon", "64"),
|
||||
Pair("Magical Sex Shift", "65"),
|
||||
Pair("Mahou Shoujo", "66"),
|
||||
Pair("Martial Arts", "17"),
|
||||
Pair("Mecha", "18"),
|
||||
Pair("Medical", "67"),
|
||||
Pair("Memoir", "68"),
|
||||
Pair("Military", "38"),
|
||||
Pair("Music", "19"),
|
||||
Pair("Mythology", "6"),
|
||||
Pair("Organized Crime", "69"),
|
||||
Pair("Otaku Culture", "70"),
|
||||
Pair("Parody", "20"),
|
||||
Pair("Performing Arts", "71"),
|
||||
Pair("Pets", "72"),
|
||||
Pair("Psychological", "40"),
|
||||
Pair("Racing", "3"),
|
||||
Pair("Reincarnation", "73"),
|
||||
Pair("Reverse Harem", "74"),
|
||||
Pair("Romantic Subtext", "75"),
|
||||
Pair("Samurai", "21"),
|
||||
Pair("School", "23"),
|
||||
Pair("Showbiz", "76"),
|
||||
Pair("Space", "29"),
|
||||
Pair("Strategy Game", "11"),
|
||||
Pair("Super Power", "31"),
|
||||
Pair("Survival", "77"),
|
||||
Pair("Team Sports", "78"),
|
||||
Pair("Time Travel", "79"),
|
||||
Pair("Vampire", "32"),
|
||||
Pair("Video Game", "80"),
|
||||
Pair("Villainess", "81"),
|
||||
Pair("Visual Arts", "82"),
|
||||
Pair("Workplace", "48"),
|
||||
Pair("Josei", "42"),
|
||||
Pair("Kids", "15"),
|
||||
Pair("Seinen", "41"),
|
||||
Pair("Shoujo", "25"),
|
||||
Pair("Shounen", "27"),
|
||||
),
|
||||
)
|
||||
|
||||
class StatusFilter : UriMultiSelectFilter(
|
||||
"Status",
|
||||
"status[]",
|
||||
arrayOf(
|
||||
Pair("Ongoing", "0"),
|
||||
Pair("Completed", "1"),
|
||||
),
|
||||
)
|
||||
@ -1,358 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangatop
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.lang.Exception
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
class MangaScans : ParsedHttpSource() {
|
||||
|
||||
override val name = "MangaScans"
|
||||
|
||||
override val baseUrl = "https://mangascans.to"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val id = 85127596998931837
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(::tokenInterceptor)
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
||||
|
||||
private var storedToken: String? = null
|
||||
|
||||
// From Akuma
|
||||
private fun tokenInterceptor(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
if (request.method == "POST" && request.header("X-CSRF-TOKEN") == null) {
|
||||
val newRequest = request.newBuilder()
|
||||
val token = getToken()
|
||||
val response = chain.proceed(
|
||||
newRequest
|
||||
.addHeader("X-CSRF-TOKEN", token)
|
||||
.build(),
|
||||
)
|
||||
|
||||
if (response.code == 419) {
|
||||
response.close()
|
||||
storedToken = null // reset the token
|
||||
val newToken = getToken()
|
||||
return chain.proceed(
|
||||
newRequest
|
||||
.addHeader("X-CSRF-TOKEN", newToken)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
private fun getToken(): String {
|
||||
if (storedToken.isNullOrEmpty()) {
|
||||
val request = GET(baseUrl, headers)
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
val document = response.asJsoup()
|
||||
document.updateToken()
|
||||
}
|
||||
|
||||
return storedToken!!
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
document.updateToken()
|
||||
|
||||
val mangaList = document.select(popularMangaSelector())
|
||||
.map(::popularMangaFromElement)
|
||||
|
||||
return MangasPage(mangaList, false)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector(): String = "aside div > article"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")!!.imgAttr()
|
||||
with(element.selectFirst("a:has(h3)")!!) {
|
||||
setUrlWithoutDomain(attr("abs:href"))
|
||||
title = text()
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest?page=$page", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
document.updateToken()
|
||||
|
||||
val mangaList = document.select(latestUpdatesSelector())
|
||||
.map(::latestUpdatesFromElement)
|
||||
|
||||
val hasNextPage = document.selectFirst(latestUpdatesNextPageSelector()) != null
|
||||
|
||||
return MangasPage(mangaList, hasNextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector(): String = "div > article.manga-item"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = "ul.pagination > li.active + li:has(a)"
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val filterList = filters.ifEmpty { getFilterList() }
|
||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("q", query)
|
||||
filterList.filterIsInstance<UriFilter>().forEach {
|
||||
it.addToUri(this)
|
||||
}
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage =
|
||||
latestUpdatesParse(response)
|
||||
|
||||
override fun searchMangaSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaNextPageSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Filters ==============================
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
TypeFilter(),
|
||||
GenreFilter(),
|
||||
StatusFilter(),
|
||||
)
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
thumbnail_url = document.selectFirst("picture img")!!.imgAttr()
|
||||
with(document.selectFirst(".manga-info")!!) {
|
||||
title = selectFirst("h1.page-heading")!!.text()
|
||||
author = selectFirst("ul > li:has(span:contains(Authors))")?.ownText()
|
||||
genre = select("ul > li:has(span:contains(Genres)) a").joinToString { it.text() }
|
||||
status = selectFirst(".text-info").parseStatus()
|
||||
description = selectFirst("#manga-description")?.text()
|
||||
?.split(".")
|
||||
?.filterNot { it.contains("MangaTop") }
|
||||
?.joinToString(".")
|
||||
?.trim()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
document.updateToken()
|
||||
|
||||
val mangaName = document.selectFirst("script:containsData(mangaName)")
|
||||
?.data()
|
||||
?.substringAfter("mangaName")
|
||||
?.substringAfter("'")
|
||||
?.substringBefore("'")
|
||||
?: throw Exception("Failed to get form data")
|
||||
|
||||
val postHeaders = apiHeadersBuilder().apply {
|
||||
set("Referer", response.request.url.toString())
|
||||
}.build()
|
||||
|
||||
val postBody = FormBody.Builder().apply {
|
||||
add("mangaIdx", response.request.url.toString().substringAfterLast("-"))
|
||||
add("mangaName", mangaName)
|
||||
}.build()
|
||||
|
||||
val postResponse = client.newCall(
|
||||
POST("$baseUrl/chapter-list", postHeaders, postBody),
|
||||
).execute()
|
||||
|
||||
return super.chapterListParse(postResponse)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
element.selectFirst(".text-muted")?.also {
|
||||
date_upload = it.text().parseDate()
|
||||
}
|
||||
name = element.selectFirst("span:not(.text-muted)")!!.text()
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
|
||||
}
|
||||
|
||||
private fun String.parseDate(): Long {
|
||||
return if (this.contains("ago")) {
|
||||
this.parseRelativeDate()
|
||||
} else {
|
||||
try {
|
||||
dateFormat.parse(this)!!.time
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.parseRelativeDate(): Long {
|
||||
val now = Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
|
||||
val relativeDate = this.split(" ").firstOrNull()
|
||||
?.toIntOrNull()
|
||||
?: return 0L
|
||||
|
||||
when {
|
||||
"second" in this -> now.add(Calendar.SECOND, -relativeDate) // parse: 30 seconds ago
|
||||
"minute" in this -> now.add(Calendar.MINUTE, -relativeDate) // parses: "42 minutes ago"
|
||||
"hour" in this -> now.add(Calendar.HOUR, -relativeDate) // parses: "1 hour ago" and "2 hours ago"
|
||||
"day" in this -> now.add(Calendar.DAY_OF_YEAR, -relativeDate) // parses: "2 days ago"
|
||||
"week" in this -> now.add(Calendar.WEEK_OF_YEAR, -relativeDate) // parses: "2 weeks ago"
|
||||
"month" in this -> now.add(Calendar.MONTH, -relativeDate) // parses: "2 months ago"
|
||||
"year" in this -> now.add(Calendar.YEAR, -relativeDate) // parse: "2 years ago"
|
||||
}
|
||||
return now.timeInMillis
|
||||
}
|
||||
|
||||
// =============================== Pages ================================
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val chapterId = chapter.url.substringBeforeLast(".html")
|
||||
.substringAfterLast("-")
|
||||
|
||||
val postHeaders = apiHeadersBuilder().apply {
|
||||
set("Referer", baseUrl + chapter.url)
|
||||
}.build()
|
||||
|
||||
val postBody = FormBody.Builder().apply {
|
||||
add("chapterIdx", chapterId)
|
||||
}.build()
|
||||
|
||||
return POST("$baseUrl/chapter-resources", postHeaders, postBody)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class PageListResponse(
|
||||
val data: PageListDataDto,
|
||||
) {
|
||||
@Serializable
|
||||
class PageListDataDto(
|
||||
val resources: List<PageDto>,
|
||||
) {
|
||||
@Serializable
|
||||
class PageDto(
|
||||
val name: Int,
|
||||
val thumb: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return response.parseAs<PageListResponse>().data.resources.map {
|
||||
Page(it.name, imageUrl = it.thumb)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imgHeaders = headersBuilder().apply {
|
||||
add("Accept", "image/avif,image/webp,*/*")
|
||||
add("Host", page.imageUrl!!.toHttpUrl().host)
|
||||
}.build()
|
||||
|
||||
return GET(page.imageUrl!!, imgHeaders)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun Document.updateToken() {
|
||||
storedToken = this.selectFirst("head meta[name*=csrf-token]")
|
||||
?.attr("content")
|
||||
?: throw IOException("Failed to update token")
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = use {
|
||||
json.decodeFromStream(it.body.byteStream())
|
||||
}
|
||||
|
||||
private fun apiHeadersBuilder() = headersBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
add("Origin", baseUrl)
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}
|
||||
|
||||
private fun Element.imgAttr(): String = when {
|
||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||
hasAttr("data-src") -> attr("abs:data-src")
|
||||
else -> attr("abs:src")
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
ext {
|
||||
extName = 'Manhwafull'
|
||||
extClass = '.Manhwafull'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://manhwafull.com'
|
||||
overrideVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.manhwafull
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Manhwafull : Madara("Manhwafull", "https://manhwafull.com", "en", SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH))
|
||||
@ -1,10 +0,0 @@
|
||||
ext {
|
||||
extName = 'MyShojo'
|
||||
extClass = '.MyShojo'
|
||||
themePkg = 'mangathemesia'
|
||||
baseUrl = 'https://myshojo.com'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@ -1,5 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.myshojo
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
|
||||
|
||||
class MyShojo : MangaThemesia("MyShojo", "https://myshojo.com", "en")
|
||||
@ -1,10 +0,0 @@
|
||||
ext {
|
||||
extName = 'Todaymic'
|
||||
extClass = '.Todaymic'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://todaymic.com'
|
||||
overrideVersionCode = 2
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@ -1,10 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.en.todaymic
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
|
||||
class Todaymic : Madara("Todaymic", "https://todaymic.com", "en") {
|
||||
override val filterNonMangaItems = false
|
||||
override val useNewChapterEndpoint = true
|
||||
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
||||
override val mangaDetailsSelectorDescription = ".manga-about > p:nth-child(2)"
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
ext {
|
||||
extName = 'Tres Daos Net'
|
||||
extClass = '.TresDaosNet'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://tresdaos.net'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@ -1,11 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.es.tresdaosnet
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
|
||||
class TresDaosNet : Madara(
|
||||
"Tres Daos Net",
|
||||
"https://tresdaos.net",
|
||||
"es",
|
||||
) {
|
||||
override val useLoadMoreRequest = LoadMoreStrategy.Always
|
||||
}
|
||||
1
src/fr/scanmanga/.gitignore
vendored
@ -1 +0,0 @@
|
||||
local.properties
|
||||
@ -1,8 +0,0 @@
|
||||
ext {
|
||||
extName = 'Scan-Manga'
|
||||
extClass = '.ScanManga'
|
||||
extVersionCode = 12
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@ -1,248 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.fr.scanmanga
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.parseAs
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import java.util.zip.Inflater
|
||||
|
||||
class ScanManga : HttpSource() {
|
||||
override val name = "Scan-Manga"
|
||||
|
||||
override val baseUrl = "https://m.scan-manga.com"
|
||||
private val baseImageUrl = "https://static.scan-manga.com/img/manga"
|
||||
|
||||
override val lang = "fr"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.add("Accept-Language", "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7")
|
||||
.set("User-Agent", "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Mobile Safari/537.36")
|
||||
|
||||
// Popular
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/TOP-Manga-Webtoon-36.html", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val mangas = response.asJsoup().select("#carouselTOPContainer > div.top").map { element ->
|
||||
SManga.create().apply {
|
||||
val titleElement = element.selectFirst("a.atop")!!
|
||||
|
||||
title = titleElement.text()
|
||||
setUrlWithoutDomain(titleElement.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img")?.attr("data-original")
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl, headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select("#content_news .publi").map { element ->
|
||||
SManga.create().apply {
|
||||
val mangaElement = element.selectFirst("a.l_manga")!!
|
||||
|
||||
title = mangaElement.text()
|
||||
setUrlWithoutDomain(mangaElement.attr("href"))
|
||||
|
||||
thumbnail_url = element.selectFirst("img")?.attr("src")
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
// Search
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/api/search/quick.json"
|
||||
.toHttpUrl().newBuilder()
|
||||
.addQueryParameter("term", query)
|
||||
.build()
|
||||
.toString()
|
||||
|
||||
val newHeaders = headers.newBuilder()
|
||||
.add("Content-type", "application/json; charset=UTF-8")
|
||||
.build()
|
||||
|
||||
return GET(url, newHeaders)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val json = response.body.string()
|
||||
if (json == "[]") { return MangasPage(emptyList(), false) }
|
||||
|
||||
return MangasPage(
|
||||
json.parseAs<MangaSearchDto>().title?.map {
|
||||
SManga.create().apply {
|
||||
title = it.nom_match
|
||||
setUrlWithoutDomain(it.url)
|
||||
thumbnail_url = "$baseImageUrl/${it.image}"
|
||||
}
|
||||
} ?: emptyList(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
// Details
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return SManga.create().apply {
|
||||
title = document.select("h1.main_title[itemprop=name]").text()
|
||||
author = document.select("div[itemprop=author]").text()
|
||||
description = document.selectFirst("div.titres_desc[itemprop=description]")?.text()
|
||||
genre = document.selectFirst("div.titres_souspart span[itemprop=genre]")?.text()
|
||||
|
||||
val statutText = document.selectFirst("div.titres_souspart")?.ownText()
|
||||
status = when {
|
||||
statutText?.contains("En cours", ignoreCase = true) == true -> SManga.ONGOING
|
||||
statutText?.contains("Terminé", ignoreCase = true) == true -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
thumbnail_url = document.select("div.full_img_serie img[itemprop=image]").attr("src")
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select("div.chapt_m").map { element ->
|
||||
val linkEl = element.selectFirst("td.publimg span.i a")!!
|
||||
val titleEl = element.selectFirst("td.publititle")
|
||||
|
||||
val chapterName = linkEl.text()
|
||||
val extraTitle = titleEl?.text()
|
||||
|
||||
SChapter.create().apply {
|
||||
name = if (!extraTitle.isNullOrEmpty()) "$chapterName - $extraTitle" else chapterName
|
||||
setUrlWithoutDomain(linkEl.absUrl("href"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
private fun decodeHunter(obfuscatedJs: String): String {
|
||||
val regex = Regex("""eval\(function\(h,u,n,t,e,r\)\{.*?\}\("([^"]+)",\d+,"([^"]+)",(\d+),(\d+),\d+\)\)""")
|
||||
val (encoded, mask, intervalStr, optionStr) = regex.find(obfuscatedJs)?.destructured
|
||||
?: error("Failed to match obfuscation pattern: $obfuscatedJs")
|
||||
|
||||
val interval = intervalStr.toInt()
|
||||
val option = optionStr.toInt()
|
||||
val delimiter = mask[option]
|
||||
val tokens = encoded.split(delimiter).filter { it.isNotEmpty() }
|
||||
val reversedMap = mask.withIndex().associate { it.value to it.index }
|
||||
|
||||
return buildString {
|
||||
for (token in tokens) {
|
||||
// Reverse the hashIt() operation: convert masked characters back to digits
|
||||
val digitString = token.map { c ->
|
||||
reversedMap[c]?.toString() ?: error("Invalid masked character: $c")
|
||||
}.joinToString("")
|
||||
|
||||
// Convert from base `option` to decimal
|
||||
val number = digitString.toIntOrNull(option)
|
||||
?: error("Failed to parse token: $digitString as base $option")
|
||||
|
||||
// Reverse the shift done during encodeIt()
|
||||
val originalCharCode = number - interval
|
||||
|
||||
append(originalCharCode.toChar())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dataAPI(data: String, idc: Int): UrlPayload {
|
||||
// Step 1: Base64 decode the input
|
||||
val compressedBytes = Base64.decode(data, Base64.NO_WRAP or Base64.NO_PADDING)
|
||||
|
||||
// Step 2: Inflate (zlib decompress)
|
||||
val inflater = Inflater()
|
||||
inflater.setInput(compressedBytes)
|
||||
val outputBuffer = ByteArray(512 * 1024) // 512 KB buffer, should be more than enough
|
||||
val decompressedLength = inflater.inflate(outputBuffer)
|
||||
inflater.end()
|
||||
|
||||
val inflated = String(outputBuffer, 0, decompressedLength)
|
||||
|
||||
// Step 3: Remove trailing hex string and reverse
|
||||
val hexIdc = idc.toString(16)
|
||||
val cleaned = inflated.removeSuffix(hexIdc)
|
||||
val reversed = cleaned.reversed()
|
||||
|
||||
// Step 4: Base64 decode and parse JSON
|
||||
val finalJsonStr = String(Base64.decode(reversed, Base64.DEFAULT))
|
||||
|
||||
return finalJsonStr.parseAs<UrlPayload>()
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val packedScript = document.selectFirst("script:containsData(h,u,n,t,e,r)")!!.data()
|
||||
|
||||
val unpackedScript = decodeHunter(packedScript)
|
||||
val parametersRegex = Regex("""sml = '([^']+)';\n?.*var sme = '([^']+)'""")
|
||||
|
||||
val (sml, sme) = parametersRegex.find(unpackedScript)?.destructured
|
||||
?: error("Failed to extract parameters from script.")
|
||||
|
||||
val chapterInfoRegex = Regex("""const idc = (\d+)""")
|
||||
val (chapterId) = chapterInfoRegex.find(packedScript)?.destructured
|
||||
?: error("Failed to extract chapter ID.")
|
||||
|
||||
val mediaType = "application/json; charset=UTF-8".toMediaType()
|
||||
val requestBody = """{"a":"$sme","b":"$sml"}"""
|
||||
|
||||
val documentUrl = document.baseUri().toHttpUrl()
|
||||
|
||||
val pageListRequest = POST(
|
||||
"$baseUrl/api/lel/$chapterId.json",
|
||||
headers.newBuilder()
|
||||
.add("Origin", "${documentUrl.scheme}://${documentUrl.host}")
|
||||
.add("Referer", documentUrl.toString())
|
||||
.add("Token", "yf")
|
||||
.build(),
|
||||
requestBody.toRequestBody(mediaType),
|
||||
)
|
||||
|
||||
val lelResponse = client.newBuilder().cookieJar(CookieJar.NO_COOKIES).build()
|
||||
.newCall(pageListRequest).execute().use { response ->
|
||||
if (!response.isSuccessful) { error("Unexpected error while fetching lel.") }
|
||||
dataAPI(response.body.string(), chapterId.toInt())
|
||||
}
|
||||
|
||||
return lelResponse.generateImageUrls().map { Page(it.first, imageUrl = it.second) }
|
||||
}
|
||||
|
||||
// Page
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imgHeaders = headers.newBuilder()
|
||||
.add("Origin", baseUrl)
|
||||
.build()
|
||||
|
||||
return GET(page.imageUrl!!, imgHeaders)
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.fr.scanmanga
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class Page(
|
||||
val f: String, // filename
|
||||
val e: String, // extension
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class UrlPayload(
|
||||
private val dN: String,
|
||||
private val s: String,
|
||||
private val v: String,
|
||||
private val c: String,
|
||||
private val p: Map<String, Page>,
|
||||
) {
|
||||
fun generateImageUrls(): List<Pair<Int, String>> {
|
||||
val baseUrl = "https://$dN/$s/$v/$c"
|
||||
return p.entries
|
||||
.mapNotNull { (key, page) ->
|
||||
key.toIntOrNull()?.let { pageIndex ->
|
||||
pageIndex to "$baseUrl/${page.f}.${page.e}"
|
||||
}
|
||||
}
|
||||
.sortedBy { it.first } // sort by page index
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaSearchDto(
|
||||
val title: List<MangaItemDto>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class MangaItemDto(
|
||||
val nom_match: String,
|
||||
val url: String,
|
||||
val image: String,
|
||||
)
|
||||
@ -1,10 +0,0 @@
|
||||
ext {
|
||||
extName = 'Starbound Scans'
|
||||
extClass = '.StarboundScans'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://starboundscans.com'
|
||||
overrideVersionCode = 2
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB |