From 4eb24b350ba6e6603d9478163a2009df8901cafd Mon Sep 17 00:00:00 2001 From: beerpsi <92439990+beerpiss@users.noreply.github.com> Date: Sat, 27 Jan 2024 16:25:21 +0700 Subject: [PATCH] Add BlogTruyen.vn (unoriginal) (#686) * Add BlogTruyen.vn (unoriginal) * refactor a thing * Add final newline * Epic lint fail * Move date format out of constructor arguments * Remove manifest file in override * Don't display genre list if empty * Apply rate limit only to real BlogTruyen --- .../res/mipmap-hdpi/ic_launcher.png | Bin .../res/mipmap-mdpi/ic_launcher.png | Bin .../res/mipmap-xhdpi/ic_launcher.png | Bin .../res/mipmap-xxhdpi/ic_launcher.png | Bin .../res/mipmap-xxxhdpi/ic_launcher.png | Bin .../blogtruyen/src/BlogTruyenMoi.kt | 67 +++ .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2959 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1736 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3697 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6314 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 8445 bytes .../blogtruyenvn/src/BlogTruyenVn.kt | 56 +++ .../blogtruyen/default}/AndroidManifest.xml | 8 +- .../multisrc/blogtruyen/BlogTruyen.kt | 386 ++++++++++++++++ .../blogtruyen/BlogTruyenGenerator.kt | 25 + .../blogtruyen/BlogTruyenUrlActivity.kt | 2 +- src/vi/blogtruyen/build.gradle | 8 - .../extension/vi/blogtruyen/BlogTruyen.kt | 432 ------------------ 18 files changed, 539 insertions(+), 445 deletions(-) rename {src/vi => multisrc/overrides/blogtruyen}/blogtruyen/res/mipmap-hdpi/ic_launcher.png (100%) rename {src/vi => multisrc/overrides/blogtruyen}/blogtruyen/res/mipmap-mdpi/ic_launcher.png (100%) rename {src/vi => multisrc/overrides/blogtruyen}/blogtruyen/res/mipmap-xhdpi/ic_launcher.png (100%) rename {src/vi => multisrc/overrides/blogtruyen}/blogtruyen/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {src/vi => multisrc/overrides/blogtruyen}/blogtruyen/res/mipmap-xxxhdpi/ic_launcher.png (100%) create mode 100644 multisrc/overrides/blogtruyen/blogtruyen/src/BlogTruyenMoi.kt create mode 100644 multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-hdpi/ic_launcher.png create mode 100644 multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-mdpi/ic_launcher.png create mode 100644 multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xhdpi/ic_launcher.png create mode 100644 multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 multisrc/overrides/blogtruyen/blogtruyenvn/src/BlogTruyenVn.kt rename {src/vi/blogtruyen => multisrc/overrides/blogtruyen/default}/AndroidManifest.xml (87%) create mode 100644 multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyen.kt create mode 100644 multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyenGenerator.kt rename {src/vi/blogtruyen/src/eu/kanade/tachiyomi/extension/vi => multisrc/src/main/java/eu/kanade/tachiyomi/multisrc}/blogtruyen/BlogTruyenUrlActivity.kt (96%) delete mode 100644 src/vi/blogtruyen/build.gradle delete mode 100644 src/vi/blogtruyen/src/eu/kanade/tachiyomi/extension/vi/blogtruyen/BlogTruyen.kt diff --git a/src/vi/blogtruyen/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/blogtruyen/blogtruyen/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/vi/blogtruyen/res/mipmap-hdpi/ic_launcher.png rename to multisrc/overrides/blogtruyen/blogtruyen/res/mipmap-hdpi/ic_launcher.png diff --git a/src/vi/blogtruyen/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/blogtruyen/blogtruyen/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/vi/blogtruyen/res/mipmap-mdpi/ic_launcher.png rename to multisrc/overrides/blogtruyen/blogtruyen/res/mipmap-mdpi/ic_launcher.png diff --git a/src/vi/blogtruyen/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/blogtruyen/blogtruyen/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/vi/blogtruyen/res/mipmap-xhdpi/ic_launcher.png rename to multisrc/overrides/blogtruyen/blogtruyen/res/mipmap-xhdpi/ic_launcher.png diff --git a/src/vi/blogtruyen/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/blogtruyen/blogtruyen/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/vi/blogtruyen/res/mipmap-xxhdpi/ic_launcher.png rename to multisrc/overrides/blogtruyen/blogtruyen/res/mipmap-xxhdpi/ic_launcher.png diff --git a/src/vi/blogtruyen/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/blogtruyen/blogtruyen/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/vi/blogtruyen/res/mipmap-xxxhdpi/ic_launcher.png rename to multisrc/overrides/blogtruyen/blogtruyen/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/multisrc/overrides/blogtruyen/blogtruyen/src/BlogTruyenMoi.kt b/multisrc/overrides/blogtruyen/blogtruyen/src/BlogTruyenMoi.kt new file mode 100644 index 000000000..c61b12808 --- /dev/null +++ b/multisrc/overrides/blogtruyen/blogtruyen/src/BlogTruyenMoi.kt @@ -0,0 +1,67 @@ +package eu.kanade.tachiyomi.extension.vi.blogtruyen + +import eu.kanade.tachiyomi.multisrc.blogtruyen.BlogTruyen +import eu.kanade.tachiyomi.network.interceptor.rateLimit + +class BlogTruyenMoi : BlogTruyen("BlogTruyen", "https://blogtruyenmoi.com", "vi") { + override val client = super.client.newBuilder() + .rateLimit(2) + .build() + + override fun getGenreList() = listOf( + Genre("Action", "1"), + Genre("Adventure", "3"), + Genre("Comedy", "5"), + Genre("Comic", "6"), + Genre("Doujinshi", "7"), + Genre("Drama", "49"), + Genre("Ecchi", "48"), + Genre("Event BT", "60"), + Genre("Fantasy", "50"), + Genre("Full màu", "64"), + Genre("Game", "61"), + Genre("Gender Bender", "51"), + Genre("Harem", "12"), + Genre("Historical", "13"), + Genre("Horror", "14"), + Genre("Isekai/Dị giới/Trọng sinh", "63"), + Genre("Josei", "15"), + Genre("Live action", "16"), + Genre("Magic", "46"), + Genre("manga", "55"), + Genre("Manhua", "17"), + Genre("Manhwa", "18"), + Genre("Martial Arts", "19"), + Genre("Mecha", "21"), + Genre("Mystery", "22"), + Genre("Nấu Ăn", "56"), + Genre("Ngôn Tình", "65"), + Genre("NTR", "62"), + Genre("One shot", "23"), + Genre("Psychological", "24"), + Genre("Romance", "25"), + Genre("School Life", "26"), + Genre("Sci-fi", "27"), + Genre("Seinen", "28"), + Genre("Shoujo", "29"), + Genre("Shoujo Ai", "30"), + Genre("Shounen", "31"), + Genre("Shounen Ai", "32"), + Genre("Slice of life", "33"), + Genre("Smut", "34"), + Genre("Soft Yaoi", "35"), + Genre("Soft Yuri", "36"), + Genre("Sports", "37"), + Genre("Supernatural", "38"), + Genre("Tạp chí truyện tranh", "39"), + Genre("Tragedy", "40"), + Genre("Trap (Crossdressing)", "58"), + Genre("Trinh Thám", "57"), + Genre("Truyện scan", "41"), + Genre("Tu chân - tu tiên", "66"), + Genre("Video Clip", "53"), + Genre("VnComic", "42"), + Genre("Webtoon", "52"), + Genre("Yuri", "59"), + ) +} diff --git a/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..98938b2419e2f40a04db6608462f5dfcfc103950 GIT binary patch literal 2959 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 FV1f{dZv_AV literal 0 HcmV?d00001 diff --git a/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..49c0e36610cdf7aa4517e8706e39e3d47a3fc542 GIT binary patch literal 1736 zcmV;(1~>VMP)^?#$i2J9}s6uEt0p1J|9I zJ9F>(o&P!a-kH$`&-iA0#_fma0BfyaBn4v7II5@LwcM7l^F_Uc*&zkkv15m0adEMB zc6PRSX=%xWh23cUm7O!YcDd)VkMV?rgo}88_5J(z9~uc{0QQWGj8oIo(}!2fz~#WW zT&~mOU(UvhK0GR@+UR$l} zlmXJx(wG8IO9d-n?#!qFWD3N@!~hi|oK+ZD87S7{-~kxxA}+jQv30dV;UVC%kWnDf5E8uzDcgSoWrY(L?_ zKVbKbgt$K#Rsq2@qqKMl?gtN9UEDwm4E^2^HtSP+Sky+L)YQ~41*idnphDY*caFex z-Z#t^XX{az?f(Vh;^IO)r`Z}BSNxdb9A|r&nVAW>htBK9hN-YD05b!;?^VK7-nXn1 zXKN+A>KcQ(y1Eb*A|2qY3VnTjaN@)X=rt>ub z$JI*6@l3**GiQ_m_=P^74+;wlp|i778aASe$_j`8<_a8zDILIR02mz|h1<7po5Ijg zymRMH!1WEgrmL$93JMBXNO6NrmNIGz;L6WZ0Tx2}HM#;7kmH_&#>Pf=+|khipSRn< z@7kh42x-7}{g2>~rw^(R9c0xs_Ge{f!M%I;tW<~|z*K=fM=^wIm~GCh;Q+QegLSZ5Txb-oPa0 zArx?{!VZl-rKP2C{``6A@G16*`)U9VheJ_;W`9Sme_VfhUzwdpBkB?{e-MDcB^78UvY>b{+!e?>i5aIxl6xg`; z%OJp&BRG*wz}d5BS;y_|?NHpi73SPH3hA4e%6vce(SPv4gTt)!$RkPi~A z)IumJDald+29VD~sGNn62yg%a{5pWlc)mn(40l4?XPY29JDb_t(9i&7Wx?Uq0HF?G z=6xmCqF7rOE?j`(;^MGHpbnx2 zux{NtH3f74rv%`D4#0o~H;Ri>+rkXIHS`l)K3NfX(xcJn*s)`h=wYYS0V2gewZs&7 zyH-=+N;$ll77U^O{(kuJ%3rV;m&D8unaYI6vX@fkq3qK_z~wt@XMTPja&mH@x3_m? z0Evl-AqvP~v?SI5oGphDz!%5?v)--F*VorWMMVY7&CRV00N+K1g%EF%!)z@03UeR` zKuLV=+&L>7rS;S%Eyb&sH{1Dh-V;L z9)MLsi=D_GM2v>Zj;&m;*Q){G!?~pb#tzL4HyRs-w`EMr15i~UQs}I}>mrC6fZcA7 zN`Xkv4Nu5$4xt3#Mu0)QgUv2hONvOLWOct{-1E!miT_0`<5=jM8QWxEXk0j={D5^O zR>tvqD+Qu7)a>$$SV8X`edJd&06L$DWit+mv{KuP#bMUsQ!<=9e+|pmSngs8nJi=< z`RE)iVkm^1ShV+rYqu?6*Hc(1bm%=~AbiLqv+u-o-maIm{!UB=69kp8R~*m()nM%+ eOY8ZWSKxm(2X1^(DqM5`0000z-tP? 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 literal 0 HcmV?d00001 diff --git a/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..10f610edfb68b81cdf258768a851852c704f61ce GIT binary patch literal 8445 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 literal 0 HcmV?d00001 diff --git a/multisrc/overrides/blogtruyen/blogtruyenvn/src/BlogTruyenVn.kt b/multisrc/overrides/blogtruyen/blogtruyenvn/src/BlogTruyenVn.kt new file mode 100644 index 000000000..73637c855 --- /dev/null +++ b/multisrc/overrides/blogtruyen/blogtruyenvn/src/BlogTruyenVn.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.extension.vi.blogtruyenvn + +import eu.kanade.tachiyomi.multisrc.blogtruyen.BlogTruyen + +class BlogTruyenVn : BlogTruyen("BlogTruyen.vn (unoriginal)", "https://blogtruyenvn.com", "vi") { + override fun getGenreList() = listOf( + Genre("Action", "1"), + Genre("Adventure", "3"), + Genre("Comedy", "5"), + Genre("Comic", "6"), + Genre("Doujinshi", "7"), + Genre("Drama", "49"), + Genre("Ecchi", "48"), + Genre("Event BT", "60"), + Genre("Fantasy", "50"), + Genre("Full màu", "64"), + Genre("Game", "61"), + Genre("Harem", "12"), + Genre("Historical", "13"), + Genre("Horror", "14"), + Genre("Isekai/Dị giới/Trọng sinh", "63"), + Genre("Josei", "15"), + Genre("Live action", "16"), + Genre("Magic", "46"), + Genre("manga", "55"), + Genre("Manhua", "17"), + Genre("Manhwa", "18"), + Genre("Martial Arts", "19"), + Genre("Mecha", "21"), + Genre("Mystery", "22"), + Genre("Nấu Ăn", "56"), + Genre("Ngôn Tình", "65"), + Genre("NTR", "62"), + Genre("One shot", "23"), + Genre("Psychological", "24"), + Genre("Romance", "25"), + Genre("School Life", "26"), + Genre("Sci-fi", "27"), + Genre("Seinen", "28"), + Genre("Shoujo", "29"), + Genre("Shounen", "31"), + Genre("Shounen Ai", "32"), + Genre("Slice of life", "33"), + Genre("Smut", "34"), + Genre("Sports", "37"), + Genre("Supernatural", "38"), + Genre("Tạp chí truyện tranh", "39"), + Genre("Tragedy", "40"), + Genre("Trinh Thám", "57"), + Genre("Truyện scan", "41"), + Genre("Tu chân - tu tiên", "66"), + Genre("Video Clip", "53"), + Genre("VnComic", "42"), + Genre("Webtoon", "52"), + ) +} diff --git a/src/vi/blogtruyen/AndroidManifest.xml b/multisrc/overrides/blogtruyen/default/AndroidManifest.xml similarity index 87% rename from src/vi/blogtruyen/AndroidManifest.xml rename to multisrc/overrides/blogtruyen/default/AndroidManifest.xml index d17382d14..b4746862b 100644 --- a/src/vi/blogtruyen/AndroidManifest.xml +++ b/multisrc/overrides/blogtruyen/default/AndroidManifest.xml @@ -1,7 +1,7 @@ - @@ -11,9 +11,9 @@ - - - + + + diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyen.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyen.kt new file mode 100644 index 000000000..499673c55 --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyen.kt @@ -0,0 +1,386 @@ +package eu.kanade.tachiyomi.multisrc.blogtruyen + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +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 kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import rx.Single +import rx.schedulers.Schedulers +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +abstract class BlogTruyen( + override val name: String, + override val baseUrl: String, + override val lang: String, +) : ParsedHttpSource() { + + override val supportsLatest = true + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + private val json: Json by injectLazy() + + private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.US).apply { + timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh") + } + + override fun popularMangaRequest(page: Int) = + GET("$baseUrl/ajax/Search/AjaxLoadListManga?key=tatca&orderBy=3&p=$page", headers) + + override fun popularMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + + val manga = document.select(popularMangaSelector()).map { + val tiptip = it.attr("data-tiptip") + + popularMangaFromElement(it, document.getElementById(tiptip)!!) + } + + val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null + + return MangasPage(manga, hasNextPage) + } + + override fun popularMangaSelector() = ".list .tiptip" + + override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException() + + private fun popularMangaFromElement(element: Element, tiptip: Element) = SManga.create().apply { + val anchor = element.selectFirst("a")!! + + setUrlWithoutDomain(anchor.attr("href")) + title = anchor.text() + thumbnail_url = tiptip.selectFirst("img")?.absUrl("src") + description = tiptip.selectFirst(".al-j")?.text() + } + + override fun popularMangaNextPageSelector() = ".paging:last-child:not(.current_page)" + + override fun latestUpdatesRequest(page: Int): Request = + GET(baseUrl + if (page > 1) "/page-$page" else "", headers) + + override fun latestUpdatesSelector() = ".storyitem .fl-l" + + override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply { + val anchor = element.selectFirst("a")!! + + setUrlWithoutDomain(anchor.absUrl("href")) + title = anchor.attr("title") + thumbnail_url = element.selectFirst("img")?.absUrl("src") + } + + override fun latestUpdatesNextPageSelector() = "select.slcPaging option:last-child:not([selected])" + + override fun fetchSearchManga( + page: Int, + query: String, + filters: FilterList, + ): Observable = when { + query.startsWith(PREFIX_ID_SEARCH) -> { + var id = query.removePrefix(PREFIX_ID_SEARCH).trimStart() + + // it's a chapter, resolve to manga ID + if (id.startsWith("c")) { + val document = client.newCall(GET("$baseUrl/$id", headers)).execute().asJsoup() + + id = document.selectFirst(".breadcrumbs a:last-child")!!.attr("href").removePrefix("/") + } + + fetchMangaDetails( + SManga.create().apply { + url = "/$id" + }, + ) + .map { MangasPage(listOf(it), false) } + } + else -> super.fetchSearchManga(page, query, filters) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + ajaxSearchUrls.keys + .firstOrNull { query.startsWith(it) } + ?.let { + val id = extractIdFromQuery(it, query) + val url = "$baseUrl/ajax/${ajaxSearchUrls[it]!!}".toHttpUrl().newBuilder() + .addQueryParameter("id", id) + .addQueryParameter("p", page.toString()) + .build() + + return GET(url, headers) + } + + val url = "$baseUrl/timkiem/nangcao/1".toHttpUrl().newBuilder().apply { + if (query.isNotBlank()) { + addQueryParameter("txt", query) + } + + if (page > 1) { + addQueryParameter("p", page.toString()) + } + + val inclGenres = mutableListOf() + val exclGenres = mutableListOf() + var status = 0 + + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { + is GenreList -> filter.state.forEach { + when (it.state) { + Filter.TriState.STATE_INCLUDE -> inclGenres.add(it.id) + Filter.TriState.STATE_EXCLUDE -> exclGenres.add(it.id) + else -> {} + } + } + is Author -> { + addQueryParameter("aut", filter.state) + } + is Scanlator -> { + addQueryParameter("gr", filter.state) + } + is Status -> { + status = filter.state + } + else -> {} + } + } + + addPathSegment(status.toString()) + addPathSegment(inclGenres.joinToString(",").ifEmpty { "-1" }) + addPathSegment(exclGenres.joinToString(",").ifEmpty { "-1" }) + }.build() + + return GET(url, headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + + val manga = document.select(searchMangaSelector()).map { + val tiptip = it.attr("data-tiptip") + + searchMangaFromElement(it, document.getElementById(tiptip)!!) + } + + val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null + + return MangasPage(manga, hasNextPage) + } + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga = + throw UnsupportedOperationException() + + private fun searchMangaFromElement(element: Element, tiptip: Element) = + popularMangaFromElement(element, tiptip) + + override fun searchMangaNextPageSelector() = ".pagination .glyphicon-step-forward" + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + val anchor = document.selectFirst(".entry-title a")!! + val descriptionBlock = document.selectFirst("div.description")!! + + setUrlWithoutDomain(anchor.absUrl("href")) + title = getMangaTitle(document) + thumbnail_url = document.selectFirst(".thumbnail img")?.absUrl("src") + author = descriptionBlock.select("p:contains(Tác giả) a").joinToString { it.text() } + genre = descriptionBlock.select("span.category").joinToString { it.text() } + status = when (descriptionBlock.selectFirst("p:contains(Trạng thái) span.color-red")?.text()) { + "Đang tiến hành" -> SManga.ONGOING + "Đã hoàn thành" -> SManga.COMPLETED + "Tạm ngưng" -> SManga.ON_HIATUS + else -> SManga.UNKNOWN + } + description = buildString { + document.selectFirst(".manga-detail .detail .content")?.let { + // replace the facebook blockquote in synopsis with the link (if there is one) + it.selectFirst(".fb-page, .fb-group")?.let { fb -> + val link = fb.attr("data-href") + val node = document.createElement("p") + + node.appendText(link) + fb.replaceWith(node) + } + + appendLine(it.textWithNewlines().trim()) + appendLine() + } + + descriptionBlock.select("p:not(:contains(Thể loại)):not(:contains(Tác giả))") + .forEach { e -> + val text = e.text() + + if (text.isBlank()) { + return@forEach + } + + // Uploader and status share the same

+ if (text.contains("Trạng thái")) { + appendLine(text.substringBefore("Trạng thái").trim()) + return@forEach + } + + // "Source", "Updaters" and "Scanlators" use badges with links + if (text.contains("Nguồn") || + text.contains("Tham gia update") || + text.contains("Nhóm dịch") + ) { + val key = text.substringBefore(":") + val value = e.select("a").joinToString { el -> el.text() } + appendLine("$key: $value") + return@forEach + } + + // Generic paragraphs i.e. view count and follower count for this series + // Basically the same trick as [Element.textWithNewlines], just applied to + // different elements. + e.select("a, span").append("\\n") + appendLine( + e.text() + .replace("\\n", "\n") + .replace("\n ", "\n") + .trim(), + ) + } + }.trim() + } + + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + val title = getMangaTitle(document) + return document.select(chapterListSelector()).map { chapterFromElement(it, title) } + } + + override fun chapterListSelector() = "div.list-wrap > p" + + override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException() + + private fun chapterFromElement(element: Element, title: String): SChapter = SChapter.create().apply { + val anchor = element.select("span > a").first()!! + + setUrlWithoutDomain(anchor.attr("href")) + name = anchor.text().removePrefix("$title ") + date_upload = runCatching { + dateFormat.parse( + element.selectFirst("span.publishedDate")!!.text(), + )!!.time + }.getOrDefault(0L) + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + + document.select("#content > img").forEachIndexed { i, e -> + pages.add(Page(i, imageUrl = e.absUrl("src"))) + } + + // Some chapters use js script to render images + document.select("#content > script:containsData(listImageCaption)").lastOrNull() + ?.let { script -> + val imagesStr = script.data().substringBefore(";").substringAfterLast("=").trim() + val imageArr = json.parseToJsonElement(imagesStr).jsonArray + imageArr.forEach { + val imageUrl = it.jsonObject["url"]!!.jsonPrimitive.content + pages.add(Page(pages.size, imageUrl = imageUrl)) + } + } + + runCatching { countView(document) } + return pages + } + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() + + override fun getFilterList(): FilterList { + val filters = mutableListOf>( + Author(), + Scanlator(), + Status(), + ) + val genres = getGenreList() + + if (genres.isNotEmpty()) { + filters.add(GenreList(genres)) + } + + return FilterList(filters) + } + + // copy([...document.querySelectorAll(".CategoryFilter li")].map((e) => `Genre("${e.textContent.trim()}", "${e.dataset.id}"),`).join("\n")) + open fun getGenreList(): List = emptyList() + + private class Status : Filter.Select( + "Status", + arrayOf("Sao cũng được", "Đang tiến hành", "Đã hoàn thành", "Tạm ngưng"), + ) + private class Author : Filter.Text("Tác giả") + private class Scanlator : Filter.Text("Nhóm dịch") + class Genre(name: String, val id: String) : Filter.TriState(name) + private class GenreList(genres: List) : Filter.Group("Thể loại", genres) + + private fun getMangaTitle(document: Document) = + document + .selectFirst(".entry-title a")!! + .attr("title") + .removePrefix("truyện tranh ") + + private fun Element.textWithNewlines() = run { + select("p, br").prepend("\\n") + text().replace("\\n", "\n").replace("\n ", "\n") + } + + private fun extractIdFromQuery(prefix: String, query: String): String = + query.substringAfter(prefix).trimStart().substringAfterLast("-") + + private fun countView(document: Document) { + val mangaId = document.getElementById("MangaId")!!.attr("value") + val chapterId = document.getElementById("ChapterId")!!.attr("value") + val request = POST( + "$baseUrl/Chapter/UpdateView", + headers, + FormBody.Builder() + .add("mangaId", mangaId) + .add("chapterId", chapterId) + .build(), + ) + + Single.fromCallable { + client.newCall(request).execute().close() + } + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe() + } + + private val ajaxSearchUrls: Map = mapOf( + PREFIX_AUTHOR_SEARCH to "Author/AjaxLoadMangaByAuthor?orderBy=3", + PREFIX_TEAM_SEARCH to "TranslateTeam/AjaxLoadMangaByTranslateTeam", + ) + + companion object { + internal const val PREFIX_ID_SEARCH = "id:" + internal const val PREFIX_AUTHOR_SEARCH = "author:" + internal const val PREFIX_TEAM_SEARCH = "team:" + } +} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyenGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyenGenerator.kt new file mode 100644 index 000000000..36980bed0 --- /dev/null +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyenGenerator.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.multisrc.blogtruyen + +import generator.ThemeSourceData.SingleLang +import generator.ThemeSourceGenerator + +class BlogTruyenGenerator : ThemeSourceGenerator { + + override val themePkg = "blogtruyen" + + override val themeClass = "BlogTruyen" + + override val baseVersionCode = 1 + + override val sources = listOf( + SingleLang("BlogTruyen", "https://blogtruyenmoi.com", "vi", className = "BlogTruyenMoi", pkgName = "blogtruyen", overrideVersionCode = 17), + SingleLang("BlogTruyen.vn (unoriginal)", "https://blogtruyenvn.com", "vi", className = "BlogTruyenVn"), + ) + + companion object { + @JvmStatic + fun main(args: Array) { + BlogTruyenGenerator().createAll() + } + } +} diff --git a/src/vi/blogtruyen/src/eu/kanade/tachiyomi/extension/vi/blogtruyen/BlogTruyenUrlActivity.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyenUrlActivity.kt similarity index 96% rename from src/vi/blogtruyen/src/eu/kanade/tachiyomi/extension/vi/blogtruyen/BlogTruyenUrlActivity.kt rename to multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyenUrlActivity.kt index a5859d34e..756968f85 100644 --- a/src/vi/blogtruyen/src/eu/kanade/tachiyomi/extension/vi/blogtruyen/BlogTruyenUrlActivity.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/blogtruyen/BlogTruyenUrlActivity.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.extension.vi.blogtruyen +package eu.kanade.tachiyomi.multisrc.blogtruyen import android.app.Activity import android.content.ActivityNotFoundException diff --git a/src/vi/blogtruyen/build.gradle b/src/vi/blogtruyen/build.gradle deleted file mode 100644 index 60b7f1386..000000000 --- a/src/vi/blogtruyen/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -ext { - extName = 'BlogTruyen' - extClass = '.BlogTruyen' - extVersionCode = 17 - isNsfw = true -} - -apply from: "$rootDir/common.gradle" diff --git a/src/vi/blogtruyen/src/eu/kanade/tachiyomi/extension/vi/blogtruyen/BlogTruyen.kt b/src/vi/blogtruyen/src/eu/kanade/tachiyomi/extension/vi/blogtruyen/BlogTruyen.kt deleted file mode 100644 index e6c65a75f..000000000 --- a/src/vi/blogtruyen/src/eu/kanade/tachiyomi/extension/vi/blogtruyen/BlogTruyen.kt +++ /dev/null @@ -1,432 +0,0 @@ -package eu.kanade.tachiyomi.extension.vi.blogtruyen - -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.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 kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import okhttp3.FormBody -import okhttp3.Headers -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import rx.Observable -import uy.kohesive.injekt.injectLazy -import java.text.SimpleDateFormat -import java.util.Locale - -class BlogTruyen : ParsedHttpSource() { - - override val name = "BlogTruyen" - - override val baseUrl = "https://blogtruyenmoi.com" - - override val lang = "vi" - - override val supportsLatest = true - - override val client: OkHttpClient = network.cloudflareClient.newBuilder() - .rateLimit(1) - .build() - - private val json: Json by injectLazy() - - private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.ENGLISH) - - companion object { - const val PREFIX_ID_SEARCH = "id:" - const val PREFIX_AUTHOR_SEARCH = "author:" - const val PREFIX_TEAM_SEARCH = "team:" - } - - override fun headersBuilder(): Headers.Builder = - super.headersBuilder().add("Referer", "$baseUrl/") - - override fun popularMangaRequest(page: Int): Request = - GET("$baseUrl/ajax/Search/AjaxLoadListManga?key=tatca&orderBy=3&p=$page", headers) - - override fun popularMangaParse(response: Response): MangasPage { - val document = response.asJsoup() - - val manga = document.select(popularMangaSelector()).map { - val tiptip = it.attr("data-tiptip") - popularMangaFromElement(it, document.getElementById(tiptip)!!) - } - - val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null - - return MangasPage(manga, hasNextPage) - } - - override fun popularMangaSelector() = ".list .tiptip" - - override fun popularMangaFromElement(element: Element): SManga = - throw UnsupportedOperationException() - - private fun popularMangaFromElement(element: Element, tiptip: Element) = SManga.create().apply { - val anchor = element.selectFirst("a")!! - setUrlWithoutDomain(anchor.attr("href")) - title = anchor.attr("title").replace("truyện tranh ", "").trim() - - thumbnail_url = tiptip.selectFirst("img")!!.attr("abs:src") - description = tiptip.selectFirst(".al-j")!!.text() - } - - override fun popularMangaNextPageSelector() = ".paging:last-child:not(.current_page)" - - override fun latestUpdatesRequest(page: Int): Request = - GET(baseUrl + if (page != 1) "/page-$page" else "", headers) - - override fun latestUpdatesSelector() = ".storyitem .fl-l" - - override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply { - setUrlWithoutDomain(element.select("a").attr("href")) - title = element.select("a").attr("title") - thumbnail_url = element.select("img").attr("abs:src") - } - - override fun latestUpdatesNextPageSelector() = "select.slcPaging option:last-child:not([selected])" - - override fun fetchSearchManga( - page: Int, - query: String, - filters: FilterList, - ): Observable { - return when { - query.startsWith(PREFIX_ID_SEARCH) -> { - var id = query.removePrefix(PREFIX_ID_SEARCH).trim() - - // it's a chapter, resolve to manga ID - if (id.startsWith("c")) { - val document = client.newCall(GET("$baseUrl/$id", headers)).execute().asJsoup() - id = document.selectFirst(".breadcrumbs a:last-child")!!.attr("href").removePrefix("/") - } - - fetchMangaDetails( - SManga.create().apply { - url = "/$id" - }, - ) - .map { MangasPage(listOf(it.apply { url = "/$id" }), false) } - } - else -> super.fetchSearchManga(page, query, filters) - } - } - - private fun extractIdFromQuery(prefix: String, query: String): String { - val q = query.substringAfter(prefix).trim() - return if (q.contains("-")) { - q.substringAfterLast("-") - } else { - q - } - } - - private val ajaxSearchUrls: Map = mapOf( - PREFIX_AUTHOR_SEARCH to "Author/AjaxLoadMangaByAuthor?orderBy=3", - PREFIX_TEAM_SEARCH to "TranslateTeam/AjaxLoadMangaByTranslateTeam", - ) - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - ajaxSearchUrls.keys.forEach { - if (!query.startsWith(it)) { - return@forEach - } - val id = extractIdFromQuery(it, query) - val url = "$baseUrl/ajax/${ajaxSearchUrls[it]}".toHttpUrl().newBuilder() - .addQueryParameter("id", id) - .addQueryParameter("p", page.toString()) - .build() - .toString() - return GET(url, headers) - } - - val url = "$baseUrl/timkiem/nangcao/1".toHttpUrl().newBuilder().apply { - addQueryParameter("txt", query) - addQueryParameter("p", page.toString()) - - val genres = mutableListOf() - val genresEx = mutableListOf() - var status = 0 - (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> - when (filter) { - is GenreList -> filter.state.forEach { - when (it.state) { - Filter.TriState.STATE_INCLUDE -> genres.add(it.id) - Filter.TriState.STATE_EXCLUDE -> genresEx.add(it.id) - else -> {} - } - } - is Author -> { - addQueryParameter("aut", filter.state) - } - is Scanlator -> { - addQueryParameter("gr", filter.state) - } - is Status -> { - status = filter.state - } - else -> {} - } - } - - addPathSegment(status.toString()) - addPathSegment(genres.joinToString(",")) - addPathSegment(genresEx.joinToString(",")) - }.build().toString() - return GET(url, headers) - } - - override fun searchMangaParse(response: Response): MangasPage { - val document = response.asJsoup() - - val manga = document.select(searchMangaSelector()).map { - val tiptip = it.attr("data-tiptip") - searchMangaFromElement(it, document.getElementById(tiptip)!!) - } - - val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null - - return MangasPage(manga, hasNextPage) - } - - override fun searchMangaSelector() = popularMangaSelector() - - override fun searchMangaFromElement(element: Element): SManga = - throw UnsupportedOperationException() - - private fun searchMangaFromElement(element: Element, tiptip: Element) = - popularMangaFromElement(element, tiptip) - - override fun searchMangaNextPageSelector() = ".pagination .glyphicon-step-forward" - - private fun getMangaTitle(document: Document) = document.selectFirst(".entry-title a")!! - .attr("title") - .replaceFirst("truyện tranh", "", false) - .trim() - - override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { - val anchor = document.selectFirst(".entry-title a")!! - setUrlWithoutDomain(anchor.attr("href")) - title = getMangaTitle(document) - - thumbnail_url = document.select(".thumbnail img").attr("abs:src") - author = document.select("a[href*=tac-gia]").joinToString { it.text() } - genre = document.select("span.category a").joinToString { it.text() } - status = parseStatus( - document.select("span.color-red:not(.bold)").text(), - ) - - description = StringBuilder().apply { - // the actual synopsis - val synopsisBlock = document.selectFirst(".manga-detail .detail .content")!! - - // replace the facebook blockquote in synopsis with the link (if there is one) - val fbElement = synopsisBlock.selectFirst(".fb-page, .fb-group") - if (fbElement != null) { - val fbLink = fbElement.attr("data-href") - - val node = document.createElement("p") - node.appendText(fbLink) - - fbElement.replaceWith(node) - } - appendLine(synopsisBlock.textWithNewlines().trim()) - appendLine() - - // other metadata - document.select(".description p").forEach { - val text = it.text() - if (text.contains("Thể loại") || - text.contains("Tác giả") || - text.isBlank() - ) { - return@forEach - } - - if (text.contains("Trạng thái")) { - appendLine(text.substringBefore("Trạng thái").trim()) - return@forEach - } - - if (text.contains("Nguồn") || - text.contains("Tham gia update") || - text.contains("Nhóm dịch") - ) { - val key = text.substringBefore(":") - val value = it.select("a").joinToString { el -> el.text() } - appendLine("$key: $value") - return@forEach - } - - it.select("a, span").append("\\n") - appendLine(it.text().replace("\\n", "\n").replace("\n ", "\n").trim()) - } - }.toString().trim() - } - - private fun Element.textWithNewlines() = run { - select("p").prepend("\\n") - select("br").prepend("\\n") - text().replace("\\n", "\n").replace("\n ", "\n") - } - - private fun parseStatus(status: String) = when { - status.contains("Đang tiến hành") -> SManga.ONGOING - status.contains("Đã hoàn thành") -> SManga.COMPLETED - status.contains("Tạm ngưng") -> SManga.ON_HIATUS - else -> SManga.UNKNOWN - } - - override fun chapterListParse(response: Response): List { - val document = response.asJsoup() - val title = getMangaTitle(document) - return document.select(chapterListSelector()).map { chapterFromElement(it, title) } - } - - override fun chapterListSelector() = "div.list-wrap > p" - - override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException() - - private fun chapterFromElement(element: Element, title: String): SChapter = SChapter.create().apply { - val anchor = element.select("span > a").first()!! - - setUrlWithoutDomain(anchor.attr("href")) - name = anchor.attr("title").replace(title, "", true).trim() - date_upload = runCatching { - dateFormat.parse( - element.selectFirst("span.publishedDate")!!.text(), - )?.time - }.getOrNull() ?: 0L - } - - private fun countViewRequest(mangaId: String, chapterId: String): Request = POST( - "$baseUrl/Chapter/UpdateView", - headers, - FormBody.Builder() - .add("mangaId", mangaId) - .add("chapterId", chapterId) - .build(), - ) - - private fun countView(document: Document) { - val mangaId = document.getElementById("MangaId")!!.attr("value") - val chapterId = document.getElementById("ChapterId")!!.attr("value") - runCatching { - client.newCall(countViewRequest(mangaId, chapterId)).execute().close() - } - } - - override fun pageListParse(document: Document): List { - val pages = mutableListOf() - - document.select("#content > img").forEachIndexed { i, e -> - pages.add(Page(i, imageUrl = e.attr("abs:src"))) - } - - // Some chapters use js script to render images - document.select("#content > script:containsData(listImageCaption)").lastOrNull() - ?.let { script -> - val imagesStr = script.data().substringBefore(";").substringAfterLast("=").trim() - val imageArr = json.parseToJsonElement(imagesStr).jsonArray - imageArr.forEach { - val imageUrl = it.jsonObject["url"]!!.jsonPrimitive.content - pages.add(Page(pages.size, imageUrl = imageUrl)) - } - } - - countView(document) - return pages - } - - override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() - - private class Status : Filter.Select( - "Status", - arrayOf("Sao cũng được", "Đang tiến hành", "Đã hoàn thành", "Tạm ngưng"), - ) - - private class Author : Filter.Text("Tác giả") - private class Scanlator : Filter.Text("Nhóm dịch") - private class Genre(name: String, val id: Int) : Filter.TriState(name) - private class GenreList(genres: List) : Filter.Group("Thể loại", genres) - - override fun getFilterList() = FilterList( - Author(), - Scanlator(), - Status(), - GenreList(getGenreList()), - ) - - private fun getGenreList() = listOf( - Genre("16+", 54), - Genre("18+", 45), - Genre("Action", 1), - Genre("Adult", 2), - Genre("Adventure", 3), - Genre("Anime", 4), - Genre("Comedy", 5), - Genre("Comic", 6), - Genre("Doujinshi", 7), - Genre("Drama", 49), - Genre("Ecchi", 48), - Genre("Even BT", 60), - Genre("Fantasy", 50), - Genre("Game", 61), - Genre("Gender Bender", 51), - Genre("Harem", 12), - Genre("Historical", 13), - Genre("Horror", 14), - Genre("Isekai/Dị Giới", 63), - Genre("Josei", 15), - Genre("Live Action", 16), - Genre("Magic", 46), - Genre("Manga", 55), - Genre("Manhua", 17), - Genre("Manhwa", 18), - Genre("Martial Arts", 19), - Genre("Mature", 20), - Genre("Mecha", 21), - Genre("Mystery", 22), - Genre("Nấu ăn", 56), - Genre("NTR", 62), - Genre("One shot", 23), - Genre("Psychological", 24), - Genre("Romance", 25), - Genre("School Life", 26), - Genre("Sci-fi", 27), - Genre("Seinen", 28), - Genre("Shoujo", 29), - Genre("Shoujo Ai", 30), - Genre("Shounen", 31), - Genre("Shounen Ai", 32), - Genre("Slice of Life", 33), - Genre("Smut", 34), - Genre("Soft Yaoi", 35), - Genre("Soft Yuri", 36), - Genre("Sports", 37), - Genre("Supernatural", 38), - Genre("Tạp chí truyện tranh", 39), - Genre("Tragedy", 40), - Genre("Trap", 58), - Genre("Trinh thám", 57), - Genre("Truyện scan", 41), - Genre("Video clip", 53), - Genre("VnComic", 42), - Genre("Webtoon", 52), - Genre("Yuri", 59), - ) -}