new multisrc theme: MyMangaCMS (#12702)
* Initial MyMangaCMS commit * Finish up the new template * Add URL intent filter * more lenient rate limit since the website loads like a ton of shit * Delete scaffold.py
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi.extension">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.multisrc.mymangacms.MyMangaCMSUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:host="${SOURCEHOST}"
|
||||
android:pathPattern="/..*"
|
||||
android:scheme="${SOURCESCHEME}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 45 KiB |
|
@ -0,0 +1,121 @@
|
|||
package eu.kanade.tachiyomi.extension.vi.lkdtt
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mymangacms.MyMangaCMS
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
class LKDTT : MyMangaCMS("LKDTT", "https://lkdtt.com", "vi") {
|
||||
override val dateFormatter = SimpleDateFormat("dd/MM/yy", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone(super.timeZone)
|
||||
}
|
||||
|
||||
override fun dateUpdatedParser(date: String): Long =
|
||||
runCatching { super.dateUpdatedParser(date.split(" - ")[1]) }.getOrNull() ?: 0L
|
||||
|
||||
override fun getGenreList() = listOf(
|
||||
Genre("Học đường", 1),
|
||||
Genre("Hài hước", 2),
|
||||
Genre("Cổ Đại", 3),
|
||||
Genre("Hiện đại", 4),
|
||||
Genre("Kinh dị", 5),
|
||||
Genre("Tổng tài", 6),
|
||||
Genre("Xuyên không", 7),
|
||||
Genre("Manhua", 8),
|
||||
Genre("Manhwa", 9),
|
||||
Genre("Mystery", 10),
|
||||
Genre("One shot", 11),
|
||||
Genre("Smut", 12),
|
||||
Genre("Webtoon", 13),
|
||||
Genre("Yaoi", 14),
|
||||
Genre("Yuri", 15),
|
||||
Genre("Trinh Thám", 16),
|
||||
Genre("Tình Cảm", 17),
|
||||
Genre("Drama", 18),
|
||||
Genre("Comedy", 19),
|
||||
Genre("Fantasy", 20),
|
||||
Genre("Novel", 21),
|
||||
Genre("Action", 22),
|
||||
Genre("Manga", 23),
|
||||
Genre("Đam Mỹ", 24),
|
||||
Genre("Trọng Sinh", 25),
|
||||
Genre("Ngôn Tình", 26),
|
||||
Genre("Phiêu Lưu", 27),
|
||||
Genre("Boy Love", 28),
|
||||
Genre("giới giải trí", 29),
|
||||
Genre("đô thị", 30),
|
||||
Genre("Romance", 31),
|
||||
Genre("Đô Thị", 32),
|
||||
Genre("Shoujo", 33),
|
||||
Genre("Historical", 34),
|
||||
Genre("Slice of life", 35),
|
||||
Genre("Mature", 36),
|
||||
Genre("GL", 37),
|
||||
Genre("Adult", 38),
|
||||
Genre("Huyền huyễn", 39),
|
||||
Genre("Baby", 40),
|
||||
Genre("Tragedy", 41),
|
||||
Genre("Truyện Màu", 42),
|
||||
Genre("School Life", 43),
|
||||
Genre("Josei", 44),
|
||||
Genre("Oneshot", 45),
|
||||
Genre("Gender Bender", 46),
|
||||
Genre("Nữ cường", 47),
|
||||
Genre("Harem", 48),
|
||||
Genre("Reverse Harem", 49),
|
||||
Genre("Isekai", 50),
|
||||
Genre("Adventure", 51),
|
||||
Genre("Chuyển Sinh", 52),
|
||||
Genre("Đại Nữ Chủ", 53),
|
||||
Genre("Shounen", 54),
|
||||
Genre("Sports", 55),
|
||||
Genre("Sủng Ngọt", 56),
|
||||
Genre("Truyện 18+", 57),
|
||||
Genre("Trung Cổ", 58),
|
||||
Genre("Ma Thuật", 59),
|
||||
Genre("Webtoons", 60),
|
||||
Genre("Xuyên", 61),
|
||||
Genre("Ngôn", 62),
|
||||
Genre("Tiểu Bạch Thỏ", 63),
|
||||
Genre("Sủng", 65),
|
||||
Genre("Trùng Sinh", 66),
|
||||
Genre("Ma Cà Rồng", 67),
|
||||
Genre("Tái Sinh", 68),
|
||||
Genre("Quân Nhân", 69),
|
||||
Genre("Showbiz", 70),
|
||||
Genre("Comic", 71),
|
||||
Genre("Phép Thuật", 72),
|
||||
Genre("Psychological", 73),
|
||||
Genre("Supernatural", 74),
|
||||
Genre("Lãng Mạn", 75),
|
||||
Genre("Gender", 76),
|
||||
Genre("Bender", 77),
|
||||
Genre("Vườn Trường", 78),
|
||||
Genre("Magic", 79),
|
||||
Genre("Nhân Thú", 80),
|
||||
Genre("Soft Yaoi", 81),
|
||||
Genre("Hôn Nhân Hợp Đồng", 82),
|
||||
Genre("Cưới Trước Yêu Sau", 83),
|
||||
Genre("Bi Kịch", 84),
|
||||
Genre("Horror", 85),
|
||||
Genre("Reincarnation", 86),
|
||||
Genre("Hồi Sinh", 87),
|
||||
Genre("Hoàng Gia", 88),
|
||||
Genre("Giả Tưởng", 89),
|
||||
Genre("Xuyên Sách", 90),
|
||||
Genre("Hài", 91),
|
||||
Genre("Ngọt", 92),
|
||||
Genre("Nam Cường", 93),
|
||||
Genre("Chủ Nam", 94),
|
||||
Genre("Minh Tinh", 95),
|
||||
Genre("Cổ Trang", 96),
|
||||
Genre("Xuyên Game", 97),
|
||||
Genre("Villainess", 98),
|
||||
Genre("Cung Đấu", 99),
|
||||
Genre("Hành Động", 100),
|
||||
Genre("Truyện Tranh", 101),
|
||||
Genre("Adaptation", 102),
|
||||
Genre("Magi", 103),
|
||||
Genre("Âu Cổ", 104),
|
||||
)
|
||||
}
|
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 226 KiB |
|
@ -0,0 +1,68 @@
|
|||
package eu.kanade.tachiyomi.extension.vi.phemanga
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mymangacms.MyMangaCMS
|
||||
|
||||
class PheManga : MyMangaCMS("Phê Manga", "https://phemanga.net", "vi") {
|
||||
override fun dateUpdatedParser(date: String): Long =
|
||||
runCatching { super.dateUpdatedParser(date.split(" - ")[1]) }.getOrNull() ?: 0L
|
||||
|
||||
override fun getGenreList() = listOf(
|
||||
Genre("16+", 1),
|
||||
Genre("18+", 2),
|
||||
Genre("Action", 3),
|
||||
Genre("Adult", 4),
|
||||
Genre("Adventure", 5),
|
||||
Genre("Anime", 6),
|
||||
Genre("Comedy", 7),
|
||||
Genre("Comic", 8),
|
||||
Genre("Doujinshi", 9),
|
||||
Genre("Drama", 10),
|
||||
Genre("Ecchi", 11),
|
||||
Genre("Fantasy", 13),
|
||||
Genre("Full màu", 14),
|
||||
Genre("Game", 15),
|
||||
Genre("Gender Bender", 16),
|
||||
Genre("Harem", 17),
|
||||
Genre("Historical", 18),
|
||||
Genre("Horror", 19),
|
||||
Genre("Isekai/Dị giới/Trọng sinh", 20),
|
||||
Genre("Josei", 21),
|
||||
Genre("Live action", 22),
|
||||
Genre("Magic", 23),
|
||||
Genre("Manga", 24),
|
||||
Genre("Manhua", 25),
|
||||
Genre("Manhwa", 26),
|
||||
Genre("Martial Arts", 27),
|
||||
Genre("Mature", 28),
|
||||
Genre("Mecha", 29),
|
||||
Genre("Mystery", 30),
|
||||
Genre("Nấu Ăn", 31),
|
||||
Genre("Ngôn Tình", 32),
|
||||
Genre("NTR", 33),
|
||||
Genre("One shot", 34),
|
||||
Genre("Psychological", 35),
|
||||
Genre("Romance", 36),
|
||||
Genre("School Life", 37),
|
||||
Genre("Sci-fi", 38),
|
||||
Genre("Seinen", 39),
|
||||
Genre("Shoujo", 40),
|
||||
Genre("Shoujo Ai", 41),
|
||||
Genre("Shounen", 42),
|
||||
Genre("Shounen Ai", 43),
|
||||
Genre("Slice of life", 44),
|
||||
Genre("Smut", 45),
|
||||
Genre("Soft Yaoi", 46),
|
||||
Genre("Soft Yuri", 47),
|
||||
Genre("Sports", 48),
|
||||
Genre("Supernatural", 49),
|
||||
Genre("Tạp chí truyện tranh", 50),
|
||||
Genre("Tragedy", 51),
|
||||
Genre("Trap (Crossdressing)", 52),
|
||||
Genre("Trinh Thám", 53),
|
||||
Genre("Truyện scan", 54),
|
||||
Genre("Tu chân - tu tiên", 55),
|
||||
Genre("VnComic", 56),
|
||||
Genre("Webtoon", 57),
|
||||
Genre("Yuri", 58),
|
||||
)
|
||||
}
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
@ -0,0 +1,68 @@
|
|||
package eu.kanade.tachiyomi.extension.vi.truyentranhlh
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mymangacms.MyMangaCMS
|
||||
|
||||
class TruyenTranhLH : MyMangaCMS("TruyenTranhLH", "https://truyentranhlh.net", "vi") {
|
||||
override val id: Long = 7969606392351831672
|
||||
|
||||
override fun getGenreList() = listOf(
|
||||
Genre("Action", 1),
|
||||
Genre("Adult", 2),
|
||||
Genre("Adventure", 3),
|
||||
Genre("Anime", 4),
|
||||
Genre("Chuyển Sinh", 5),
|
||||
Genre("Cổ Đại", 6),
|
||||
Genre("Comedy", 7),
|
||||
Genre("Comic", 8),
|
||||
Genre("Demons", 9),
|
||||
Genre("Detective", 10),
|
||||
Genre("Doujinshi", 11),
|
||||
Genre("Drama", 12),
|
||||
Genre("Đam Mỹ", 13),
|
||||
Genre("Ecchi", 14),
|
||||
Genre("Fantasy", 15),
|
||||
Genre("Gender Bender", 16),
|
||||
Genre("Harem", 17),
|
||||
Genre("Historical", 18),
|
||||
Genre("Horror", 19),
|
||||
Genre("Huyền Huyễn", 20),
|
||||
Genre("Isekai", 21),
|
||||
Genre("Josei", 22),
|
||||
Genre("Mafia", 23),
|
||||
Genre("Magic", 24),
|
||||
Genre("Manhua", 25),
|
||||
Genre("Manhwa", 26),
|
||||
Genre("Martial Arts", 27),
|
||||
Genre("Mature", 28),
|
||||
Genre("Military", 29),
|
||||
Genre("Mystery", 30),
|
||||
Genre("Ngôn Tình", 31),
|
||||
Genre("One shot", 32),
|
||||
Genre("Psychological", 33),
|
||||
Genre("Romance", 34),
|
||||
Genre("School Life", 35),
|
||||
Genre("Sci-fi", 36),
|
||||
Genre("Seinen", 37),
|
||||
Genre("Shoujo", 38),
|
||||
Genre("Shoujo Ai", 39),
|
||||
Genre("Shounen", 40),
|
||||
Genre("Shounen Ai", 41),
|
||||
Genre("Slice of life", 42),
|
||||
Genre("Smut", 43),
|
||||
Genre("Sports", 44),
|
||||
Genre("Supernatural", 45),
|
||||
Genre("Tragedy", 46),
|
||||
Genre("Trọng Sinh", 47),
|
||||
Genre("Truyện Màu", 48),
|
||||
Genre("Webtoon", 49),
|
||||
Genre("Xuyên Không", 50),
|
||||
Genre("Yaoi", 51),
|
||||
Genre("Yuri", 52),
|
||||
Genre("Mecha", 53),
|
||||
Genre("Cooking", 54),
|
||||
Genre("Trùng Sinh", 55),
|
||||
Genre("Gourmet", 56),
|
||||
Genre("Dark Fantasy", 57),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mymangacms
|
||||
|
||||
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.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class MyMangaCMS(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder().apply {
|
||||
rateLimit(3, 1)
|
||||
connectTimeout(1, TimeUnit.MINUTES)
|
||||
readTimeout(1, TimeUnit.MINUTES)
|
||||
writeTimeout(1, TimeUnit.MINUTES)
|
||||
}.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
|
||||
add("Referer", "$baseUrl/")
|
||||
add(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0"
|
||||
)
|
||||
}
|
||||
|
||||
//region Source settings
|
||||
|
||||
open val timeZone = "Asia/Ho_Chi_Minh"
|
||||
|
||||
open val dateFormatter = SimpleDateFormat("dd/MM/yyyy", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone(this@MyMangaCMS.timeZone)
|
||||
}
|
||||
|
||||
open fun dateUpdatedParser(date: String): Long =
|
||||
runCatching { dateFormatter.parse(date)?.time }.getOrNull() ?: 0L
|
||||
|
||||
private val floatingNumberRegex = Regex("""([+-]?(?:[0-9]*[.])?[0-9]+)""")
|
||||
|
||||
/**
|
||||
* Regex for extracting URL from CSS `background-image: url()` property.
|
||||
*
|
||||
* - `url\(` matches the opening `url(`
|
||||
* - `['"]?` checks for the existence (or lack thereof) of single/double quotes
|
||||
* - `(.*?)` captures everything up to but not including the next quote
|
||||
* - `\)` to match the closing bracket.
|
||||
*/
|
||||
private val backgroundImageRegex = Regex("""url\(['"]?(.*?)['"]?\)""")
|
||||
//endregion
|
||||
|
||||
//region Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET(
|
||||
baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("tim-kiem")
|
||||
addQueryParameter("sort", "top")
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build().toString()
|
||||
)
|
||||
|
||||
override fun popularMangaSelector(): String = "div.thumb-item-flow.col-6.col-md-2"
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? =
|
||||
"div.pagination_wrap a.paging_item:last-of-type:not(.disabled)"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.select("a").first().attr("abs:href"))
|
||||
title = element.select("div.thumb_attr.series-title a[title]").first().text()
|
||||
thumbnail_url = element.select("div[data-bg]").first().attr("data-bg")
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET(
|
||||
baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("tim-kiem")
|
||||
addQueryParameter("sort", "update")
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build().toString()
|
||||
)
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
//endregion
|
||||
|
||||
//region Search
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return when {
|
||||
query.startsWith(PREFIX_URL_SEARCH) -> {
|
||||
fetchMangaDetails(SManga.create().apply {
|
||||
url = query.removePrefix(PREFIX_URL_SEARCH).trim().replace(baseUrl, "")
|
||||
})
|
||||
.map { MangasPage(listOf(it), false) }
|
||||
}
|
||||
else -> super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
|
||||
GET(
|
||||
baseUrl.toHttpUrl().newBuilder().apply {
|
||||
val genres = mutableListOf<Int>()
|
||||
val genresEx = mutableListOf<Int>()
|
||||
addPathSegment("tim-kiem")
|
||||
addQueryParameter("page", page.toString())
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach {
|
||||
when (it) {
|
||||
is GenreList -> it.state.forEach { genre ->
|
||||
when (genre.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> genres.add(genre.id)
|
||||
Filter.TriState.STATE_EXCLUDE -> genresEx.add(genre.id)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
is Author -> if (it.state.isNotEmpty()) {
|
||||
addQueryParameter("artist", it.state)
|
||||
}
|
||||
is Sort -> addQueryParameter("sort", it.toUriPart())
|
||||
is Status -> if (it.state != 0) {
|
||||
addQueryParameter("status", it.state.toString())
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (genresEx.isNotEmpty()) {
|
||||
addQueryParameter("reject_genres", genresEx.joinToString(","))
|
||||
}
|
||||
if (genres.isNotEmpty()) {
|
||||
addQueryParameter("accept_genres", genres.joinToString(","))
|
||||
}
|
||||
if (query.isNotEmpty()) {
|
||||
addQueryParameter("q", query)
|
||||
}
|
||||
}.build().toString()
|
||||
)
|
||||
|
||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector(): String? = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga =
|
||||
popularMangaFromElement(element)
|
||||
//endregion
|
||||
|
||||
//region Manga details
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request = GET("$baseUrl${manga.url}")
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
setUrlWithoutDomain(
|
||||
document.select(".series-name-group a")
|
||||
.first()
|
||||
.attr("abs:href")
|
||||
)
|
||||
title = document.select(".series-name").first().text().trim()
|
||||
|
||||
var alternativeNames: String? = null
|
||||
document.select(".info-item").forEach {
|
||||
val value = it.select(".info-value")
|
||||
when (it.select(".info-name").text().trim()) {
|
||||
"Tên khác:" -> alternativeNames = value.joinToString(", ") { name ->
|
||||
name.text().trim()
|
||||
}
|
||||
"Tác giả:" -> author = value.joinToString(", ") { auth ->
|
||||
auth.text().trim()
|
||||
}
|
||||
"Tình trạng:" -> status = when (value.first().text().lowercase().trim()) {
|
||||
"đang tiến hành" -> SManga.ONGOING
|
||||
"tạm ngưng" -> SManga.ON_HIATUS
|
||||
"đã hoàn thành" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val descElem = document.select(".summary-content")
|
||||
description = if (descElem.select("p").any()) {
|
||||
descElem.select("p").joinToString("\n") {
|
||||
it.run {
|
||||
select(Evaluator.Tag("br")).prepend("\\n")
|
||||
this.text()
|
||||
.replace("\\n", "\n")
|
||||
.replace("\n ", "\n")
|
||||
}
|
||||
}.trim()
|
||||
} else {
|
||||
descElem.text().trim()
|
||||
}
|
||||
|
||||
if (!alternativeNames.isNullOrEmpty()) {
|
||||
description = "Tên khác: ${alternativeNames}\n\n" + description
|
||||
}
|
||||
|
||||
genre = document.select("a[href*=the-loai] span.badge")
|
||||
.joinToString(", ") { it.text().trim() }
|
||||
|
||||
thumbnail_url = document
|
||||
.select("div.content.img-in-ratio")
|
||||
.first()
|
||||
.attr("style")
|
||||
.let { backgroundImageRegex.find(it)?.groups?.get(1)?.value }
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Chapter list
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
|
||||
|
||||
override fun chapterListSelector(): String = "ul.list-chapters > a"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = throw Exception("Not used")
|
||||
|
||||
private fun chapterFromElement(element: Element, scanlator: String?): SChapter =
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.attr("abs:href"))
|
||||
name = element.select("div.chapter-name").first().text()
|
||||
date_upload = dateUpdatedParser(
|
||||
element.select("div.chapter-time").first().text()
|
||||
)
|
||||
|
||||
val match = floatingNumberRegex.find(name)
|
||||
chapter_number = if (name.lowercase().startsWith("vol")) {
|
||||
match?.groups?.get(2)
|
||||
} else {
|
||||
match?.groups?.get(1)
|
||||
}?.value?.toFloat() ?: -1f
|
||||
|
||||
this.scanlator = scanlator
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val originalScanlator = document.select("div.fantrans-value a")
|
||||
val scanlator: String? = if (originalScanlator.isEmpty() ||
|
||||
originalScanlator.first().text().trim().lowercase() == "đang cập nhật") {
|
||||
null
|
||||
} else {
|
||||
originalScanlator.first().text().trim()
|
||||
}
|
||||
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, scanlator) }
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Pages
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request = GET("$baseUrl${chapter.url}")
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> =
|
||||
document
|
||||
.select("div#chapter-content img")
|
||||
.filterNot { it.attr("abs:data-src").isNullOrEmpty() }
|
||||
.mapIndexed { index, elem -> Page(index, "", elem.attr("abs:data-src")) }
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw Exception("Not used")
|
||||
//endregion
|
||||
|
||||
//region Filters
|
||||
open class UriPartFilter(
|
||||
displayName: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
state: Int = 0,
|
||||
) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
private class Status : Filter.Select<String>(
|
||||
"Tình trạng",
|
||||
arrayOf(
|
||||
"Tất cả",
|
||||
"Đang tiến hành",
|
||||
"Tạm ngưng",
|
||||
"Hoàn thành"
|
||||
)
|
||||
)
|
||||
private class Sort : UriPartFilter(
|
||||
"Sắp xếp",
|
||||
arrayOf(
|
||||
Pair("A-Z", "az"),
|
||||
Pair("Z-A", "za"),
|
||||
Pair("Mới cập nhật", "update"),
|
||||
Pair("Truyện mới", "new"),
|
||||
Pair("Xem nhiều", "top"),
|
||||
Pair("Được thích nhiều", "like"),
|
||||
),
|
||||
4
|
||||
)
|
||||
open class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||
private class Author : Filter.Text("Tác giả")
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Thể loại", genres)
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
Author(),
|
||||
Status(),
|
||||
Sort(),
|
||||
GenreList(getGenreList()),
|
||||
)
|
||||
|
||||
// To populate this list:
|
||||
// console.log([...document.querySelectorAll("div.search-gerne_item")].map(elem => `Genre("${elem.textContent.trim()}", ${elem.querySelector("label").getAttribute("data-genre-id")}),`).join("\n"))
|
||||
abstract fun getGenreList(): List<Genre>
|
||||
//endregion
|
||||
|
||||
companion object {
|
||||
const val PREFIX_URL_SEARCH = "url:"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mymangacms
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class MyMangaCMSGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "mymangacms"
|
||||
|
||||
override val themeClass = "MyMangaCMS"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang(
|
||||
"TruyenTranhLH",
|
||||
"https://truyentranhlh.net",
|
||||
"vi",
|
||||
overrideVersionCode = 9
|
||||
),
|
||||
SingleLang(
|
||||
"Phê Manga",
|
||||
"https://phemanga.net",
|
||||
"vi",
|
||||
true,
|
||||
"PheManga",
|
||||
"phemanga",
|
||||
),
|
||||
SingleLang("LKDTT", "https://lkdtt.com", "vi", true)
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
MyMangaCMSGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mymangacms
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class MyMangaCMSUrlActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size > 1) {
|
||||
try {
|
||||
startActivity(Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${MyMangaCMS.PREFIX_URL_SEARCH}${intent?.data?.path}")
|
||||
putExtra("filter", packageName)
|
||||
})
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("MyMangaCMSUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("MyMangaCMSUrlActivity", "Could not parse URI from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -1,11 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'TruyenTranhLH'
|
||||
pkgNameSuffix = 'vi.truyentranhlh'
|
||||
extClass = '.TruyenTranhLH'
|
||||
extVersionCode = 9
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,126 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.vi.truyentranhlh
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TruyenTranhLH : ParsedHttpSource() {
|
||||
|
||||
override val name = "TruyenTranhLH"
|
||||
|
||||
override val baseUrl = "https://truyentranhlh.net"
|
||||
|
||||
override val lang = "vi"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
.writeTimeout(1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0")
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/tim-kiem?sort=top&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.thumb-item-flow"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
element.select("div.series-title a").let {
|
||||
title = it.text()
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
}
|
||||
thumbnail_url = element.select("div.content").attr("abs:data-bg")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.pagination_wrap a.page_num.current + a:not(.disabled)"
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/tim-kiem?sort=update&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/tim-kiem?q=$query&sort=update&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.top-part")
|
||||
return SManga.create().apply {
|
||||
genre = infoElement.select("span.info-name:contains(Thể loại) + span a").joinToString { it.text() }
|
||||
author = infoElement.select("span.info-name:contains(Tác giả) + span").text()
|
||||
status = infoElement.select("span.info-name:contains(Tình trạng) + span").text().toStatus()
|
||||
thumbnail_url = infoElement.select("div.content").attr("style")
|
||||
.let { Regex("""url\("(.*)"\)""").find(it)?.groups?.get(1)?.value }
|
||||
description = document.select("div.summary-content").text()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.toStatus() = when {
|
||||
this == null -> SManga.UNKNOWN
|
||||
this.contains("Đang tiến hành", ignoreCase = true) -> SManga.ONGOING
|
||||
this.contains("Đã hoàn thành", ignoreCase = true) -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListSelector(): String = "ul.list-chapters a"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
name = element.select("div.chapter-name").text()
|
||||
date_upload = element.select("div.chapter-time").firstOrNull()?.text()
|
||||
?.let { SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).parse(it)?.time ?: 0L } ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div#chapter-content img")
|
||||
.filterNot { imgEl -> imgEl.attr("abs:data-src").isNullOrEmpty() }
|
||||
.mapIndexed { i, img ->
|
||||
Page(i, "", img.attr("abs:data-src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
}
|