add Hotcomics (#2249)
* HotComics * Filters * add other similar sources Tomics.Top, de ToomicsFree, en * newline * implementation -> api * daycomics.me * ToomicsFree.info
9
lib-multisrc/hotcomics/build.gradle.kts
Normal file
@ -0,0 +1,9 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:cookieinterceptor"))
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
package eu.kanade.tachiyomi.multisrc.hotcomics
|
||||
|
||||
import eu.kanade.tachiyomi.lib.cookieinterceptor.CookieInterceptor
|
||||
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.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 okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
abstract class HotComics(
|
||||
final override val name: String,
|
||||
final override val lang: String,
|
||||
final override val baseUrl: String,
|
||||
) : HttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addNetworkInterceptor(
|
||||
CookieInterceptor(baseUrl.removePrefix("https://"), "hc_vfs" to "Y"),
|
||||
)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/en", headers)
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/en/new", headers)
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
if (query.isNotEmpty()) {
|
||||
addEncodedPathSegments("en/search")
|
||||
addQueryParameter("keyword", query.trim())
|
||||
} else {
|
||||
val filter = filters.filterIsInstance<BrowseFilter>().first()
|
||||
addEncodedPathSegments(filter.selected)
|
||||
addQueryParameter("page", page.toString())
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
abstract class SelectFilter(
|
||||
name: String,
|
||||
private val options: List<Pair<String, String>>,
|
||||
) : Filter.Select<String>(
|
||||
name,
|
||||
options.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
val selected get() = options[state].second
|
||||
}
|
||||
|
||||
abstract val browseList: List<Pair<String, String>>
|
||||
|
||||
class BrowseFilter(browseList: List<Pair<String, String>>) : SelectFilter("Browse", browseList)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("Doesn't work with Text search"),
|
||||
Filter.Separator(),
|
||||
BrowseFilter(browseList),
|
||||
)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val entries = document.select("li[itemtype*=ComicSeries]:not(.no-comic) > a").map { element ->
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
thumbnail_url = element.selectFirst("div.visual img")?.imgAttr()
|
||||
title = element.selectFirst("div.main-text > h4.title")!!.text()
|
||||
}
|
||||
}.distinctBy { it.url }
|
||||
val hasNextPage = document.selectFirst("div.pagination a.vnext:not(.disabled)") != null
|
||||
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val document = response.asJsoup()
|
||||
|
||||
title = document.selectFirst("h2.episode-title")!!.text()
|
||||
with(document.selectFirst("p.type_box")!!) {
|
||||
author = selectFirst("span.writer")?.text()
|
||||
?.substringAfter("ⓒ")?.trim()
|
||||
genre = selectFirst("span.type")?.text()
|
||||
?.split("/")?.joinToString { it.trim() }
|
||||
status = when (selectFirst("span.date")?.text()) {
|
||||
"End", "Ende" -> SManga.COMPLETED
|
||||
null -> SManga.UNKNOWN
|
||||
else -> SManga.ONGOING
|
||||
}
|
||||
}
|
||||
description = buildString {
|
||||
document.selectFirst("div.episode-contents header")
|
||||
?.text()?.let {
|
||||
append(it)
|
||||
append("\n\n")
|
||||
}
|
||||
document.selectFirst("div.title_content > h2:not(.episode-title)")
|
||||
?.text()?.let { append(it) }
|
||||
}.trim()
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return response.asJsoup().select("#tab-chapter a").map { element ->
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
name = element.selectFirst(".cell-num")!!.text()
|
||||
date_upload = parseDate(element.selectFirst(".cell-time")?.text())
|
||||
}
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH)
|
||||
|
||||
private fun parseDate(date: String?): Long {
|
||||
date ?: return 0L
|
||||
|
||||
return try {
|
||||
dateFormat.parse(date)!!.time
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return response.asJsoup().select("#viewer-img img").mapIndexed { idx, img ->
|
||||
Page(idx, imageUrl = img.imgAttr())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element.imgAttr(): String {
|
||||
return when {
|
||||
hasAttr("data-src") -> absUrl("data-src")
|
||||
else -> absUrl("src")
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
9
src/de/toomicstop/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
ext {
|
||||
extName = 'Toomics.Top'
|
||||
extClass = '.ToomicsTop'
|
||||
themePkg = 'hotcomics'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/de/toomicstop/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/de/toomicstop/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/de/toomicstop/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src/de/toomicstop/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/de/toomicstop/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,44 @@
|
||||
package eu.kanade.tachiyomi.extension.de.toomicstop
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hotcomics.HotComics
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import okhttp3.Response
|
||||
|
||||
class ToomicsTop : HotComics(
|
||||
"Toomics.Top",
|
||||
"de",
|
||||
"https://toomics.top",
|
||||
) {
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val mangasPage = super.searchMangaParse(response)
|
||||
mangasPage.mangas.apply {
|
||||
for (i in indices) {
|
||||
this[i].url = this[i].url.replace(urlIdRegex, ".html")
|
||||
}
|
||||
}
|
||||
|
||||
return mangasPage
|
||||
}
|
||||
|
||||
private val urlIdRegex = Regex("""(/\w+).html""")
|
||||
|
||||
override val browseList = listOf(
|
||||
Pair("Home", "en"),
|
||||
Pair("Weekly", "en/weekly"),
|
||||
Pair("New", "en/new"),
|
||||
Pair("Genre: All", "en/genres"),
|
||||
Pair("Genre: Romantik", "en/genres/Romantik"),
|
||||
Pair("Genre: Drama", "en/genres/Drama"),
|
||||
Pair("Genre: BL", "en/genres/BL"),
|
||||
Pair("Genre: Action", "en/genres/Action"),
|
||||
Pair("Genre: Schulleben", "en/genres/Schulleben"),
|
||||
Pair("Genre: Fantasy", "en/genres/Fantasy"),
|
||||
Pair("Genre: Comedy", "en/genres/Comedy"),
|
||||
Pair("Genre: Historisch", "en/genres/Historisch"),
|
||||
Pair("Genre: Sci-Fi", "en/genres/Sci-Fi"),
|
||||
Pair("Genre: Thriller", "en/genres/Thriller"),
|
||||
Pair("Genre: Horror", "en/genres/Horror"),
|
||||
Pair("Genre: Sport", "en/genres/Sport"),
|
||||
Pair("Genre: GL", "en/genres/GL"),
|
||||
)
|
||||
}
|
9
src/en/daycomicsme/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
ext {
|
||||
extName = 'DAYcomics.me'
|
||||
extClass = '.DAYcomicsMe'
|
||||
themePkg = 'hotcomics'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/daycomicsme/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/en/daycomicsme/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/en/daycomicsme/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/en/daycomicsme/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
src/en/daycomicsme/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,30 @@
|
||||
package eu.kanade.tachiyomi.extension.en.daycomicsme
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hotcomics.HotComics
|
||||
|
||||
class DAYcomicsMe : HotComics(
|
||||
"DAYcomics.me",
|
||||
"en",
|
||||
"https://daycomics.me",
|
||||
) {
|
||||
override val browseList = listOf(
|
||||
Pair("Home", "en"),
|
||||
Pair("Weekly", "en/weekly"),
|
||||
Pair("New", "en/new"),
|
||||
Pair("Genre: All", "en/genres"),
|
||||
Pair("Genre: Romance", "en/genres/Romance"),
|
||||
Pair("Genre: Office", "en/genres/Office"),
|
||||
Pair("Genre: College", "en/genres/College"),
|
||||
Pair("Genre: Drama", "en/genres/Drama"),
|
||||
Pair("Genre: Isekai", "en/genres/Isekai"),
|
||||
Pair("Genre: UNCENSORED", "en/genres/UNCENSORED"),
|
||||
Pair("Genre: Action", "en/genres/Action"),
|
||||
Pair("Genre: BL", "en/genres/BL"),
|
||||
Pair("Genre: New", "en/genres/New"),
|
||||
Pair("Genre: Slice of Life", "en/genres/Slice_of_Life"),
|
||||
Pair("Genre: Supernatural", "en/genres/Supernatural"),
|
||||
Pair("Genre: Historical", "en/genres/Historical"),
|
||||
Pair("Genre: School Life", "en/genres/School_Life"),
|
||||
Pair("Genre: Horror Thriller", "en/genres/Horror_Thriller"),
|
||||
)
|
||||
}
|
9
src/en/hotcomics/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
ext {
|
||||
extName = 'HotComics'
|
||||
extClass = '.HotComics'
|
||||
themePkg = 'hotcomics'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/hotcomics/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/en/hotcomics/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/en/hotcomics/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/en/hotcomics/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
src/en/hotcomics/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,29 @@
|
||||
package eu.kanade.tachiyomi.extension.en.hotcomics
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hotcomics.HotComics
|
||||
|
||||
class HotComics : HotComics(
|
||||
"HotComics",
|
||||
"en",
|
||||
"https://hotcomics.me",
|
||||
) {
|
||||
override val browseList = listOf(
|
||||
Pair("Home", "en"),
|
||||
Pair("Weekly", "en/weekly"),
|
||||
Pair("New", "en/new"),
|
||||
Pair("Genre: All", "en/genres"),
|
||||
Pair("Genre: Sports", "en/genres/Sports"),
|
||||
Pair("Genre: Historical", "en/genres/Historical"),
|
||||
Pair("Genre: Drama", "en/genres/Drama"),
|
||||
Pair("Genre: BL", "en/genres/BL"),
|
||||
Pair("Genre: Thriller", "en/genres/Thriller"),
|
||||
Pair("Genre: School life", "en/genres/School_life"),
|
||||
Pair("Genre: Comedy", "en/genres/Comedy"),
|
||||
Pair("Genre: GL", "en/genres/GL"),
|
||||
Pair("Genre: Action", "en/genres/Action"),
|
||||
Pair("Genre: Sci-fi", "en/genres/Sci-fi"),
|
||||
Pair("Genre: Horror", "en/genres/Horror"),
|
||||
Pair("Genre: Fantasy", "en/genres/Fantasy"),
|
||||
Pair("Genre: Romance", "en/genres/Romance"),
|
||||
)
|
||||
}
|
9
src/en/toomicsfree/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
ext {
|
||||
extName = 'ToomicsFree.com'
|
||||
extClass = '.ToomicsFree'
|
||||
themePkg = 'hotcomics'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/toomicsfree/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/en/toomicsfree/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/en/toomicsfree/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src/en/toomicsfree/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/en/toomicsfree/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,29 @@
|
||||
package eu.kanade.tachiyomi.extension.en.toomicsfree
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hotcomics.HotComics
|
||||
|
||||
class ToomicsFree : HotComics(
|
||||
"ToomicsFree.com",
|
||||
"en",
|
||||
"https://toomicsfree.com",
|
||||
) {
|
||||
override val browseList = listOf(
|
||||
Pair("Home", "en"),
|
||||
Pair("Weekly", "en/weekly"),
|
||||
Pair("New", "en/new"),
|
||||
Pair("Genre: All", "en/genres"),
|
||||
Pair("Genre: Sports", "en/genres/Sports"),
|
||||
Pair("Genre: Historical", "en/genres/Historical"),
|
||||
Pair("Genre: Drama", "en/genres/Drama"),
|
||||
Pair("Genre: BL", "en/genres/BL"),
|
||||
Pair("Genre: Thriller", "en/genres/Thriller"),
|
||||
Pair("Genre: School life", "en/genres/School_life"),
|
||||
Pair("Genre: Comedy", "en/genres/Comedy"),
|
||||
Pair("Genre: GL", "en/genres/GL"),
|
||||
Pair("Genre: Action", "en/genres/Action"),
|
||||
Pair("Genre: Sci-fi", "en/genres/Sci-fi"),
|
||||
Pair("Genre: Horror", "en/genres/Horror"),
|
||||
Pair("Genre: Fantasy", "en/genres/Fantasy"),
|
||||
Pair("Genre: Romance", "en/genres/Romance"),
|
||||
)
|
||||
}
|
9
src/en/toomicsfreeinfo/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
ext {
|
||||
extName = 'ToomicsFree.info'
|
||||
extClass = '.ToomicsFreeInfo'
|
||||
themePkg = 'hotcomics'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/toomicsfreeinfo/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/en/toomicsfreeinfo/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/en/toomicsfreeinfo/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
src/en/toomicsfreeinfo/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/en/toomicsfreeinfo/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,29 @@
|
||||
package eu.kanade.tachiyomi.extension.en.toomicsfreeinfo
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hotcomics.HotComics
|
||||
|
||||
class ToomicsFreeInfo : HotComics(
|
||||
"ToomicsFree.info",
|
||||
"en",
|
||||
"https://toomicsfree.info",
|
||||
) {
|
||||
override val browseList = listOf(
|
||||
Pair("Home", "en"),
|
||||
Pair("Weekly", "en/weekly"),
|
||||
Pair("New", "en/new"),
|
||||
Pair("Genre: All", "en/genres"),
|
||||
Pair("Genre: Sports", "en/genres/Sports"),
|
||||
Pair("Genre: Historical", "en/genres/Historical"),
|
||||
Pair("Genre: Drama", "en/genres/Drama"),
|
||||
Pair("Genre: BL", "en/genres/BL"),
|
||||
Pair("Genre: Thriller", "en/genres/Thriller"),
|
||||
Pair("Genre: School life", "en/genres/School_life"),
|
||||
Pair("Genre: Comedy", "en/genres/Comedy"),
|
||||
Pair("Genre: GL", "en/genres/GL"),
|
||||
Pair("Genre: Action", "en/genres/Action"),
|
||||
Pair("Genre: Sci-fi", "en/genres/Sci-fi"),
|
||||
Pair("Genre: Horror", "en/genres/Horror"),
|
||||
Pair("Genre: Fantasy", "en/genres/Fantasy"),
|
||||
Pair("Genre: Romance", "en/genres/Romance"),
|
||||
)
|
||||
}
|