new multisrc theme: A3Manga (#14837)

* new multisrc theme: A3MangaTheme

* move NgonPhong into A3MangaTheme

* fix: ignore scanlation groups from search result

* chore: fix lint issues

* rename A3MangaTheme to A3Manga, resolve a bunch of suggestions

* fix: remove references of theme in the configuration

* fix: change the activity name in manifest

* fix: final newline

* chore: remove gradle file (not used)

* fix: rename ngonphong icons

* chore: add ocumeo icons
This commit is contained in:
beerpsi 2023-01-11 02:05:26 +07:00 committed by GitHub
parent c0a6c67cfd
commit 8950d236ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 340 additions and 150 deletions

View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="A3MangaThemeGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="tachiyomi-extensions.multisrc.main" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.a3manga.A3MangaGenerator" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=a3manga" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=a3manga" />
</method>
</configuration>
</component>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,24 @@
<?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.a3manga.A3MangaUrlActivity"
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}" />
<data android:host="*.${SOURCEHOST}" />
<data android:pathPattern="/truyen-tranh/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.extension.vi.ngonphong
import eu.kanade.tachiyomi.multisrc.a3manga.A3Manga
class NgonPhong : A3Manga("Ngôn Phong", "https://www.ngonphong.com", "vi") {
override val id: Long = 7268977637085631557
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@ -0,0 +1,207 @@
package eu.kanade.tachiyomi.multisrc.a3manga
import android.util.Base64
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
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
import java.util.TimeZone
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
open class A3Manga(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ParsedHttpSource() {
override val supportsLatest: Boolean = false
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/page/$page/", headers)
override fun popularMangaSelector() = ".comic-list .comic-item"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.select(".comic-title-link a").attr("href"))
title = element.select(".comic-title").text().trim()
thumbnail_url = element.select(".img-thumbnail").attr("abs:src")
}
override fun popularMangaNextPageSelector() = "li.next:not(.disabled)"
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException("Not used")
override fun latestUpdatesSelector() = throw UnsupportedOperationException("Not used")
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException("Not used")
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used")
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PREFIX_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_ID_SEARCH).trim()
fetchMangaDetails(
SManga.create().apply {
url = "/truyen-tranh/$id/"
}
)
.map {
it.url = "/truyen-tranh/$id/"
MangasPage(listOf(it), false)
}
}
else -> super.fetchSearchManga(page, query, filters)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
POST(
"$baseUrl/wp-admin/admin-ajax.php",
headers,
FormBody.Builder()
.add("action", "searchtax")
.add("keyword", query)
.build(),
)
override fun searchMangaSelector(): String = throw UnsupportedOperationException("Not used")
override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used")
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Not used")
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchResponseDto>()
if (!dto.success) {
return MangasPage(emptyList(), false)
}
val manga = dto.data
.filter { it.cstatus != "Nhóm dịch" }
.map {
SManga.create().apply {
setUrlWithoutDomain(it.link)
title = it.title
thumbnail_url = it.img
}
}
return MangasPage(manga, false)
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.select(".info-title").text()
author = document.select(".comic-info strong:contains(Tác giả) + span").text().trim()
description = document.select(".intro-container .text-justify").text().substringBefore("— Xem Thêm —")
genre = document.select(".comic-info .tags a").joinToString { tag ->
tag.text().split(' ').joinToString(separator = " ") { word ->
word.replaceFirstChar { it.titlecase() }
}
}
thumbnail_url = document.select(".img-thumbnail").attr("abs:src")
val statusString = document.select(".comic-info strong:contains(Tình trạng) + span").text()
status = when (statusString) {
"Đang tiến hành" -> SManga.ONGOING
"Trọn bộ " -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
override fun chapterListSelector(): String = ".chapter-table table tbody tr"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.select("a").attr("href"))
name = element.select("a .hidden-sm").text()
date_upload = kotlin.runCatching {
dateFormat.parse(element.select("td").last().text())?.time
}.getOrNull() ?: 0L
}
override fun pageListParse(document: Document): List<Page> {
val htmlContentScript = document.selectFirst("script:containsData(htmlContent)")?.html()
?.substringAfter("var htmlContent=\"")
?.substringBefore("\";")
?.replace("\\\"", "\"")
?.replace("\\\\", "\\")
?.replace("\\/", "/")
?: throw Exception("Couldn't find script with image data.")
val htmlContent = json.decodeFromString<CipherDto>(htmlContentScript)
val ciphertext = Base64.decode(htmlContent.ciphertext, Base64.DEFAULT)
val iv = htmlContent.iv.decodeHex()
val salt = htmlContent.salt.decodeHex()
val passwordScript = document.selectFirst("script:containsData(chapterHTML)")?.html()
?: throw Exception("Couldn't find password to decrypt image data.")
val passphrase = passwordScript.substringAfter("var chapterHTML=CryptoJSAesDecrypt('")
.substringBefore("',htmlContent")
.replace("'+'", "")
val keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM)
val spec = PBEKeySpec(passphrase.toCharArray(), salt, 999, 256)
val keyS = SecretKeySpec(keyFactory.generateSecret(spec).encoded, "AES")
val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, keyS, IvParameterSpec(iv))
val imgListHtml = cipher.doFinal(ciphertext).toString(Charsets.UTF_8)
return Jsoup.parseBodyFragment(imgListHtml).select("img").mapIndexed { idx, it ->
Page(idx, imageUrl = it.attr("abs:src"))
}
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body?.string().orEmpty())
}
// https://stackoverflow.com/a/66614516
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
companion object {
const val KEY_ALGORITHM = "PBKDF2WithHmacSHA512"
const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS7PADDING"
const val PREFIX_ID_SEARCH = "id:"
val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.US).apply {
timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh")
}
}
}

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.multisrc.a3manga
import kotlinx.serialization.Serializable
@Serializable
data class SearchResponseDto(
val data: List<SearchEntryDto>,
val success: Boolean,
)
@Serializable
data class SearchEntryDto(
val cstatus: String,
val img: String,
val isocm: Int,
val link: String,
val star: Float,
val title: String,
val vote: String,
)
@Serializable
data class CipherDto(
val ciphertext: String,
val iv: String,
val salt: String,
)

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.multisrc.a3manga
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class A3MangaGenerator : ThemeSourceGenerator {
override val themePkg = "a3manga"
override val themeClass = "A3Manga"
override val baseVersionCode: Int = 1
override val sources = listOf(
SingleLang("A3 Manga", "https://www.a3mnga.com", "vi"),
SingleLang("Team Lanh Lung", "https://teamlanhlung.com", "vi", sourceName = "Team Lạnh Lùng"),
SingleLang("Ngon Phong", "https://www.ngonphong.com", "vi", sourceName = "Ngôn Phong", overrideVersionCode = 1),
SingleLang("O Cu Meo", "https://www.ocumeo.com", "vi", sourceName = "Ổ Cú Mèo"),
)
companion object {
@JvmStatic
fun main(args: Array<String>) {
A3MangaGenerator().createAll()
}
}
}

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.multisrc.a3manga
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/*
Springboard that accepts https://<domain>/truyen-tranh/$id/ intents
*/
class A3MangaUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val id = pathSegments[1]
try {
startActivity(
Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "id:$id")
putExtra("filter", packageName)
}
)
} catch (e: ActivityNotFoundException) {
Log.e("A3MangaThemeUrlActivity", e.toString())
}
} else {
Log.e("A3MangaThemeUrlActivity", "Could not parse URI from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,11 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Ngon Phong'
pkgNameSuffix = 'vi.ngonphong'
extClass = '.NgonPhong'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

View File

@ -1,137 +0,0 @@
package eu.kanade.tachiyomi.extension.vi.ngonphong
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
class NgonPhong : ParsedHttpSource() {
override val name = "Ngon Phong"
override val baseUrl = "https://ngonphongcomics.com"
override val lang = "vi"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Referer", baseUrl)
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/danh-sach-truyen/?sort=view&trang=$page", headers)
}
override fun popularMangaSelector() = "div.comic-item"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
element.select(".comic-title-link > a").let {
title = it.attr("title") ?: it.text()
setUrlWithoutDomain(it.attr("href"))
}
thumbnail_url = element.select("img.img-thumbnail").attr("abs:src")
}
}
override fun popularMangaNextPageSelector() = "ul.phantrang li a[title=next]"
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/danh-sach-truyen/?sort=latest&trang=$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/?s=$query", headers)
}
override fun searchMangaSelector() = "table.comic-list-table tbody tr"
override fun searchMangaFromElement(element: Element): SManga {
return SManga.create().apply {
element.select("a").first().let {
title = it.text()
setUrlWithoutDomain(it.attr("href"))
}
}
}
override fun searchMangaNextPageSelector(): String? = null
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select("div.comic-intro div.row").let { info ->
title = info.select("h2").text()
author = info.select("span.green").text()
genre = info.select("strong:contains(Thể loại:) ~ a").joinToString { it.text() }
status = info.select("strong:contains(Tình trạng:) + span").text().toStatus()
thumbnail_url = info.select("img.img-thumbnail").attr("abs:src")
}
description = document.select("div.comic-intro div.row + div p").text()
}
}
private fun String.toStatus() = when {
this.contains("Đang cập nhật", ignoreCase = true) -> SManga.ONGOING
this.contains("Hoàn thành", ignoreCase = true) -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
// Chapters
override fun chapterListSelector() = "table.table tbody tr"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
element.select("a").let {
name = it.text()
setUrlWithoutDomain(it.attr("href"))
}
date_upload = element.select("td:nth-child(2)").text().toChapterDate()
}
}
private fun String.toChapterDate(): Long {
return try {
SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).parse(this)?.time ?: 0L
} catch (_: Exception) {
0L
}
}
// Pages
override fun pageListParse(document: Document): List<Page> {
return document.select("script:containsData(htmlContent)").first().data().substringAfter("htmlContent=[")
.substringBefore("];").replace(Regex("""["\\]"""), "").split(",")
.mapIndexed { i, image -> Page(i, "", image) }
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
override fun getFilterList() = FilterList()
}