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 000000000..98938b241 Binary files /dev/null and b/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-hdpi/ic_launcher.png differ 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 000000000..49c0e3661 Binary files /dev/null and b/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-mdpi/ic_launcher.png differ diff --git a/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..79e4c550d Binary files /dev/null and b/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..3127b6184 Binary files /dev/null and b/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xxhdpi/ic_launcher.png differ 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 000000000..10f610edf Binary files /dev/null and b/multisrc/overrides/blogtruyen/blogtruyenvn/res/mipmap-xxxhdpi/ic_launcher.png differ 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), - ) -}