Add todaymanga (#1289)
* add todaymanga * small changes to filters * apply recommended changes * Small touch-up to date parsing
This commit is contained in:
parent
3ce4e729f9
commit
ca8faa3a7e
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'TodayManga'
|
||||
extClass = '.TodayManga'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
|
@ -0,0 +1,336 @@
|
|||
package eu.kanade.tachiyomi.extension.en.todaymanga
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.TextNode
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
open class TodayManga : ParsedHttpSource() {
|
||||
|
||||
override val name = "TodayManga"
|
||||
|
||||
override val baseUrl = "https://todaymanga.com"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/category/most-popular".addPage(page), headers)
|
||||
|
||||
override fun popularMangaSelector(): String = "section div.serie"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("abs:href"))
|
||||
thumbnail_url = element.selectFirst("img")!!.imgAttr()
|
||||
title = element.selectFirst("h2")!!.text()
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String =
|
||||
".pagination > ul > li.active + li:has(a)"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/category/recent".addPage(page), headers)
|
||||
|
||||
override fun latestUpdatesSelector(): String = "ul.series > li"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply {
|
||||
with(element.selectFirst("a[title][href]")!!) {
|
||||
setUrlWithoutDomain(attr("abs:href"))
|
||||
title = attr("title")
|
||||
}
|
||||
thumbnail_url = element.selectFirst("img")!!.imgAttr()
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String =
|
||||
popularMangaNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val categoryFilter = filterList.filterIsInstance<CategoryFilter>().first()
|
||||
val genreFilter = filterList.filterIsInstance<GenreFilter>().first()
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
when {
|
||||
categoryFilter.state != 0 -> {
|
||||
addPathSegment("category")
|
||||
addPathSegments(categoryFilter.toUriPart())
|
||||
}
|
||||
genreFilter.state != 0 -> {
|
||||
addPathSegment("genre")
|
||||
addPathSegment(genreFilter.toUriPart())
|
||||
}
|
||||
query.isNotBlank() -> {
|
||||
addPathSegment("search")
|
||||
addQueryParameter("q", query)
|
||||
}
|
||||
else -> { // Default to popular
|
||||
addPathSegments("category/most-popular")
|
||||
}
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
addQueryParameter("page", page.toString())
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector(): String =
|
||||
popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangaList = document.select(searchMangaSelector())
|
||||
.map(::searchMangaFromElement)
|
||||
.ifEmpty {
|
||||
document.select(latestUpdatesSelector())
|
||||
.map(::latestUpdatesFromElement)
|
||||
}
|
||||
|
||||
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
|
||||
return MangasPage(mangaList, hasNextPage)
|
||||
}
|
||||
|
||||
// =============================== Filters ==============================
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
Filter.Header("Ignored when using text search"),
|
||||
Filter.Header("NOTE: Only one filter will be applied!"),
|
||||
Filter.Separator(),
|
||||
CategoryFilter(),
|
||||
GenreFilter(),
|
||||
)
|
||||
|
||||
class CategoryFilter : UriPartFilter(
|
||||
"Category",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Most Popular Manga", "most-popular"),
|
||||
Pair("Highest Rated Manga", "highest-rated"),
|
||||
Pair("Trending This Week", "trending"),
|
||||
Pair("Recent Updated Manga", "recent"),
|
||||
Pair("Editors' Choices", "editor-pick"),
|
||||
Pair("Completed Comedy Manga", "completed-comedy-manga"),
|
||||
Pair("Completed Drama Manga", "completed-drama-manga"),
|
||||
Pair("Completed Fantasy Manga", "completed-fantasy-manga"),
|
||||
Pair("Completed Romance Manga", "completed-romance-manga"),
|
||||
),
|
||||
)
|
||||
|
||||
// The site doesn't seem to list all available genres, so instead the genres
|
||||
// were sampled from the first 5 pages of recently updated
|
||||
class GenreFilter : UriPartFilter(
|
||||
"Genre",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Action", "action"),
|
||||
Pair("Adventure", "adventure"),
|
||||
Pair("Comedy", "comedy"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Ecchi", "ecchi"),
|
||||
Pair("Fantasy", "fantasy"),
|
||||
Pair("Gender Bender", "gender-bender"),
|
||||
Pair("Harem", "harem"),
|
||||
Pair("Historical", "historical"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Martial Arts", "martial-arts"),
|
||||
Pair("Mature", "mature"),
|
||||
Pair("Music", "music"),
|
||||
Pair("Mystery", "mystery"),
|
||||
Pair("One Shot", "one-shot"),
|
||||
Pair("Psychological", "psychological"),
|
||||
Pair("Reverse Harem", "reverse-harem"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("School Life", "school-life"),
|
||||
Pair("Sci fi", "sci-fi"),
|
||||
Pair("Seinen", "seinen"),
|
||||
Pair("Shoujo", "shoujo"),
|
||||
Pair("Shounen Ai", "shounen-ai"),
|
||||
Pair("Shounen", "shounen"),
|
||||
Pair("Slice Of Life", "slice-of-life"),
|
||||
Pair("Sports", "sports"),
|
||||
Pair("Supernatural", "supernatural"),
|
||||
Pair("Tragedy", "tragedy"),
|
||||
Pair("Vampire", "vampire"),
|
||||
Pair("Webtoons", "webtoons"),
|
||||
),
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
with(document.selectFirst(".serie")!!) {
|
||||
title = selectFirst("h1")!!.text()
|
||||
thumbnail_url = selectFirst("img")!!.imgAttr()
|
||||
genre = select(".serie-info-head .tags > .tag-item").joinToString { it.text() }
|
||||
author = select(".authors a").joinToString { it.text() }
|
||||
status = selectFirst("li:contains(status) span").parseStatus()
|
||||
}
|
||||
|
||||
description = buildString {
|
||||
val summary = document.selectFirst(".serie-summary")!!
|
||||
summary.childNodes().forEach { node ->
|
||||
if (node is TextNode) {
|
||||
append(node.text())
|
||||
}
|
||||
if (node.nodeName() == "br") {
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
summary.selectFirst("div[style]")?.also {
|
||||
append("\n\n")
|
||||
append(it.text())
|
||||
}
|
||||
}.trim()
|
||||
}
|
||||
|
||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
"complete" -> SManga.COMPLETED
|
||||
"on going" -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val chapterHeaders = headersBuilder().apply {
|
||||
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
set("Referer", baseUrl + manga.url)
|
||||
}.build()
|
||||
return GET("$baseUrl${manga.url}/chapter-list", chapterHeaders)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "ul.chapters-list > li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
with(element.selectFirst("a")!!) {
|
||||
name = text()
|
||||
setUrlWithoutDomain(attr("abs:href"))
|
||||
}
|
||||
|
||||
val dateText = element.selectFirst(".subtitle")?.text()
|
||||
date_upload = if (dateText == null) {
|
||||
0L
|
||||
} else if (dateText.contains("ago")) {
|
||||
dateText.parseRelativeDate()
|
||||
} else {
|
||||
parseDate(dateText)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(dateStr: String): Long {
|
||||
return try {
|
||||
dateFormat.parse(dateStr)!!.time
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
// From OppaiStream
|
||||
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()
|
||||
?.replace("one", "1")
|
||||
?.replace("a", "1")
|
||||
?.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 pageListParse(document: Document): List<Page> {
|
||||
return document.select(".chapter-content > img[data-index]").map { img ->
|
||||
val index = img.attr("data-index").toInt()
|
||||
val url = img.imgAttr()
|
||||
Page(index, imageUrl = url)
|
||||
}.sortedBy { it.index }
|
||||
}
|
||||
|
||||
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 String.addPage(page: Int): HttpUrl {
|
||||
return this.toHttpUrl().newBuilder().apply {
|
||||
if (page > 1) {
|
||||
addQueryParameter("page", page.toString())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue