Compare commits

..

No commits in common. "2cfdda0bcff616fe7759bfa692761886afd2b1aa" and "dfb448cbffa8cd9ca190cbd1f689911d2f02c2ab" have entirely different histories.

1466 changed files with 15124 additions and 5994 deletions

1
.gitignore vendored
View File

@ -10,4 +10,3 @@ repo/
apk/ apk/
gen gen
generated-src/ generated-src/
.kotlin

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -38,7 +38,6 @@ kotlinter {
dependencies { dependencies {
compileOnly(versionCatalogs.named("libs").findBundle("common").get()) compileOnly(versionCatalogs.named("libs").findBundle("common").get())
implementation(project(":utils"))
} }
tasks { tasks {
@ -52,9 +51,3 @@ tasks {
} }
} }
} }
tasks.register("printDependentExtensions") {
doLast {
project.printDependentExtensions()
}
}

View File

@ -103,7 +103,6 @@ dependencies {
if (theme != null) implementation(theme) // Overrides core launcher icons if (theme != null) implementation(theme) // Overrides core launcher icons
implementation(project(":core")) implementation(project(":core"))
compileOnly(libs.bundles.common) compileOnly(libs.bundles.common)
implementation(project(":utils"))
} }
tasks.register("writeManifestFile") { tasks.register("writeManifestFile") {

View File

@ -9,7 +9,7 @@ gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" } gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" } gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
tachiyomi-lib = { module = "com.github.keiyoushi:extensions-lib", version = "v1.4.2.1" } tachiyomi-lib = { module = "com.github.tachiyomiorg:extensions-lib", version = "1.4.2" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin_version" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin_version" }
kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" } kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" }

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 7 baseVersionCode = 5
dependencies { dependencies {
api(project(":lib:synchrony")) api(project(":lib:synchrony"))

View File

@ -276,14 +276,14 @@ abstract class ColaManga(
}.also(screen::addPreference) }.also(screen::addPreference)
} }
private val keyMappingRegex = Regex("""if\s*\(\s*([a-zA-Z0-9_]+)\s*==\s*(\d+)\s*\)\s*\{\s*return\s*'([a-zA-Z0-9_]+)'\s*;""") private val keyMappingRegex = Regex("""if\s*\(\s*([a-zA-Z0-9_]+)\s*==\s*(?<keyType>\d+)\s*\)\s*\{\s*return\s*'(?<key>[a-zA-Z0-9_]+)'\s*;""")
private val keyMapping by lazy { private val keyMapping by lazy {
val obfuscatedReadJs = client.newCall(GET("$baseUrl/js/manga.read.js")).execute().body.string() val obfuscatedReadJs = client.newCall(GET("$baseUrl/js/manga.read.js")).execute().body.string()
val readJs = Deobfuscator.deobfuscateScript(obfuscatedReadJs) val readJs = Deobfuscator.deobfuscateScript(obfuscatedReadJs)
?: throw Exception(intl.couldNotDeobufscateScript) ?: throw Exception(intl.couldNotDeobufscateScript)
keyMappingRegex.findAll(readJs).associate { it.groups[2]!!.value to it.groups[3]!!.value } keyMappingRegex.findAll(readJs).associate { it.groups["keyType"]!!.value to it.groups["key"]!!.value }
} }
private fun randomString() = buildString(15) { private fun randomString() = buildString(15) {

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.kanade.tachiyomi.multisrc.etoshore.EtoshoreUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${SOURCEHOST}"
android:pathPattern="/.*/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,5 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -0,0 +1,242 @@
package eu.kanade.tachiyomi.multisrc.etoshore
import eu.kanade.tachiyomi.network.GET
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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
abstract class Etoshore(
override val name: String,
override val baseUrl: String,
final override val lang: String,
) : ParsedHttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient
// ============================== Popular ==============================
open val popularFilter = FilterList(
SelectionList("", listOf(Tag(value = "views", query = "sort"))),
)
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun popularMangaSelector() = throw UnsupportedOperationException()
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Latest ===============================
open val latestFilter = FilterList(
SelectionList("", listOf(Tag(value = "date", query = "sort"))),
)
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/page/$page".toHttpUrl().newBuilder()
.addQueryParameter("s", query)
filters.forEach { filter ->
when (filter) {
is SelectionList -> {
val selected = filter.selected()
url.addQueryParameter(selected.query, selected.value)
}
else -> {}
}
}
return GET(url.build(), headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.substringAfter(PREFIX_SEARCH)
return fetchMangaDetails(SManga.create().apply { url = "/manga/$slug/" })
.map { manga -> MangasPage(listOf(manga), false) }
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaSelector() = ".search-posts .chapter-box .poster a"
override fun searchMangaNextPageSelector() = ".navigation .naviright:has(a)"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.attr("title")
thumbnail_url = element.selectFirst("img")?.let(::imageFromElement)
setUrlWithoutDomain(element.absUrl("href"))
}
override fun searchMangaParse(response: Response): MangasPage {
if (filterList.isEmpty()) {
filterParse(response)
}
return super.searchMangaParse(response)
}
// ============================== Details ===============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1")!!.text()
description = document.selectFirst(".excerpt p")?.text()
document.selectFirst(".details-right-con img")?.let { thumbnail_url = imageFromElement(it) }
genre = document.select("div.meta-item span.meta-title:contains(Genres) + span a")
.joinToString { it.text() }
author = document.selectFirst("div.meta-item span.meta-title:contains(Author) + span a")
?.text()
document.selectFirst(".status")?.text()?.let {
status = it.toMangaStatus()
}
setUrlWithoutDomain(document.location())
}
protected open fun imageFromElement(element: Element): String? {
return when {
element.hasAttr("data-src") -> element.attr("abs:data-src")
element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src")
element.hasAttr("srcset") -> element.attr("abs:srcset").getSrcSetImage()
element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc")
else -> element.attr("abs:src")
}
}
protected open fun String.getSrcSetImage(): String? {
return this.split(" ")
.filter(URL_REGEX::matches)
.maxOfOrNull(String::toString)
}
protected val completedStatusList: Array<String> = arrayOf(
"Finished",
"Completo",
)
protected open val ongoingStatusList: Array<String> = arrayOf(
"Publishing",
"Ativo",
)
protected val hiatusStatusList: Array<String> = arrayOf(
"on hiatus",
)
protected val canceledStatusList: Array<String> = arrayOf(
"Canceled",
"Discontinued",
)
open fun String.toMangaStatus(): Int {
return when {
containsIn(completedStatusList) -> SManga.COMPLETED
containsIn(ongoingStatusList) -> SManga.ONGOING
containsIn(hiatusStatusList) -> SManga.ON_HIATUS
containsIn(canceledStatusList) -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
// ============================== Chapters ============================
override fun chapterListSelector() = ".chapter-list li a"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".title")!!.text()
setUrlWithoutDomain(element.absUrl("href"))
}
// ============================== Pages ===============================
override fun pageListParse(document: Document): List<Page> {
return document.select(".chapter-images .chapter-item > img").mapIndexed { index, element ->
Page(index, imageUrl = imageFromElement(element))
}
}
override fun imageUrlParse(document: Document) = ""
// ============================= Filters ==============================
private var filterList = emptyList<Pair<String, List<Tag>>>()
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>()
filters += if (filterList.isNotEmpty()) {
filterList.map { SelectionList(it.first, it.second) }
} else {
listOf(Filter.Header("Aperte 'Redefinir' para tentar mostrar os filtros"))
}
return FilterList(filters)
}
protected open fun parseSelection(document: Document, selector: String): Pair<String, List<Tag>>? {
val selectorFilter = "#filter-form $selector .select-item-head .text"
return document.selectFirst(selectorFilter)?.text()?.let { displayName ->
displayName to document.select("#filter-form $selector li").map { element ->
element.selectFirst("input")!!.let { input ->
Tag(
name = element.selectFirst(".text")!!.text(),
value = input.attr("value"),
query = input.attr("name"),
)
}
}
}
}
open val filterListSelector: List<String> = listOf(
".filter-genre",
".filter-status",
".filter-type",
".filter-year",
".filter-sort",
)
open fun filterParse(response: Response) {
val document = Jsoup.parseBodyFragment(response.peekBody(Long.MAX_VALUE).string())
filterList = filterListSelector.mapNotNull { selector -> parseSelection(document, selector) }
}
protected data class Tag(val name: String = "", val value: String = "", val query: String = "")
private open class SelectionList(displayName: String, private val vals: List<Tag>, state: Int = 0) :
Filter.Select<String>(displayName, vals.map { it.name }.toTypedArray(), state) {
fun selected() = vals[state]
}
// ============================= Utils ==============================
private fun String.containsIn(array: Array<String>): Boolean {
return this.lowercase() in array.map { it.lowercase() }
}
companion object {
const val PREFIX_SEARCH = "id:"
val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)${'$'}""".toRegex()
}
}

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.multisrc.etoshore
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class EtoshoreUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Etoshore.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 30 baseVersionCode = 28

View File

@ -129,7 +129,7 @@ abstract class GroupLe(
infoElement.select(".info-icon").attr("data-content").substringBeforeLast("/5</b><br/>") infoElement.select(".info-icon").attr("data-content").substringBeforeLast("/5</b><br/>")
.substringAfterLast(": <b>").replace(",", ".").toFloat() * 2 .substringAfterLast(": <b>").replace(",", ".").toFloat() * 2
val ratingVotes = val ratingVotes =
infoElement.select(".col-sm-6 .user-rating meta[itemprop=\"ratingCount\"]") infoElement.select(".col-sm-7 .user-rating meta[itemprop=\"ratingCount\"]")
.attr("content") .attr("content")
val ratingStar = when { val ratingStar = when {
ratingValue > 9.5 -> "★★★★★" ratingValue > 9.5 -> "★★★★★"
@ -209,16 +209,14 @@ abstract class GroupLe(
} }
protected open fun getChapterSearchParams(document: Document): String { protected open fun getChapterSearchParams(document: Document): String {
val scriptContent = document.selectFirst("script:containsData(user_hash)")?.data() return "?mtr=true"
val userHash = scriptContent?.let { USER_HASH_REGEX.find(it)?.groupValues?.get(1) }
return userHash?.let { "?d=$it&mtr=true" } ?: "?mtr=true"
} }
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> { private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
if (document.select(".user-avatar").isEmpty() && if (document.select(".user-avatar").isEmpty() &&
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") || contains("RuMix") } document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
) { ) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E") throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
} }
@ -311,7 +309,7 @@ abstract class GroupLe(
val html = document.html() val html = document.html()
if (document.select(".user-avatar").isEmpty() && if (document.select(".user-avatar").isEmpty() &&
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") || contains("RuMix") } document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
) { ) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E") throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
@ -324,9 +322,6 @@ abstract class GroupLe(
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}") throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
} }
else -> { else -> {
if (document.selectFirst("div.alert") != null || document.selectFirst("form.purchase-form") != null) {
throw Exception("Эта глава платная. Используйте сайт, чтобы купить и прочитать ее.")
}
throw Exception("Дизайн сайта обновлен, для дальнейшей работы необходимо обновление дополнения") throw Exception("Дизайн сайта обновлен, для дальнейшей работы необходимо обновление дополнения")
} }
} }
@ -441,6 +436,5 @@ abstract class GroupLe(
private const val UAGENT_TITLE = "User-Agent(для некоторых стран)" private const val UAGENT_TITLE = "User-Agent(для некоторых стран)"
private const val UAGENT_DEFAULT = "arora" private const val UAGENT_DEFAULT = "arora"
const val PREFIX_SLUG_SEARCH = "slug:" const val PREFIX_SLUG_SEARCH = "slug:"
private val USER_HASH_REGEX = "user_hash.+'(.+)'".toRegex()
} }
} }

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 28 baseVersionCode = 27
dependencies { dependencies {
api(project(":lib:i18n")) api(project(":lib:i18n"))

View File

@ -105,7 +105,7 @@ class HeanCmsChapterDto(
@SerialName("chapter_name") private val name: String, @SerialName("chapter_name") private val name: String,
@SerialName("chapter_title") private val title: String? = null, @SerialName("chapter_title") private val title: String? = null,
@SerialName("chapter_slug") private val slug: String, @SerialName("chapter_slug") private val slug: String,
@SerialName("created_at") private val createdAt: String? = null, @SerialName("created_at") private val createdAt: String,
val price: Int? = null, val price: Int? = null,
) { ) {
fun toSChapter( fun toSChapter(

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 18 baseVersionCode = 16

View File

@ -107,7 +107,7 @@ open class Kemono(
} }
var mangas = mangasCache var mangas = mangasCache
if (page == 1 || mangasCache.isEmpty()) { if (page == 1) {
var favourites: List<KemonoFavouritesDto> = emptyList() var favourites: List<KemonoFavouritesDto> = emptyList()
if (fav != null) { if (fav != null) {
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute() val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
@ -132,7 +132,7 @@ open class Kemono(
includeType && !excludeType && isFavourited && includeType && !excludeType && isFavourited &&
regularSearch regularSearch
}.also { mangasCache = it } }.also { mangasCache = mangas }
} }
val sorted = when (sort.first) { val sorted = when (sort.first) {

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 13 baseVersionCode = 12
dependencies { dependencies {
api(project(":lib:i18n")) api(project(":lib:i18n"))

View File

@ -290,7 +290,7 @@ abstract class Keyoapp(
.firstOrNull { CDN_HOST_REGEX.containsMatchIn(it.html()) } .firstOrNull { CDN_HOST_REGEX.containsMatchIn(it.html()) }
?.let { ?.let {
val cdnHost = CDN_HOST_REGEX.find(it.html()) val cdnHost = CDN_HOST_REGEX.find(it.html())
?.groups?.get(1)?.value ?.groups?.get("host")?.value
?.replace(CDN_CLEAN_REGEX, "") ?.replace(CDN_CLEAN_REGEX, "")
"https://$cdnHost/uploads" "https://$cdnHost/uploads"
} }
@ -314,7 +314,7 @@ abstract class Keyoapp(
protected open fun Element.getImageUrl(selector: String): String? { protected open fun Element.getImageUrl(selector: String): String? {
return this.selectFirst(selector)?.let { element -> return this.selectFirst(selector)?.let { element ->
IMG_REGEX.find(element.attr("style"))?.groups?.get(1)?.value IMG_REGEX.find(element.attr("style"))?.groups?.get("url")?.value
?.toHttpUrlOrNull()?.let { ?.toHttpUrlOrNull()?.let {
it.newBuilder() it.newBuilder()
.setQueryParameter("w", "480") // Keyoapp returns the dynamic size of the thumbnail to any size .setQueryParameter("w", "480") // Keyoapp returns the dynamic size of the thumbnail to any size
@ -376,8 +376,8 @@ abstract class Keyoapp(
companion object { companion object {
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap" private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
private const val SHOW_PAID_CHAPTERS_DEFAULT = false private const val SHOW_PAID_CHAPTERS_DEFAULT = false
val CDN_HOST_REGEX = """realUrl\s*=\s*`[^`]+//([^/]+)""".toRegex() val CDN_HOST_REGEX = """realUrl\s*=\s*`[^`]+//(?<host>[^/]+)""".toRegex()
val CDN_CLEAN_REGEX = """\$\{[^}]*\}""".toRegex() val CDN_CLEAN_REGEX = """\$\{[^}]*\}""".toRegex()
val IMG_REGEX = """url\(['"]?([^(['"\)])]+)""".toRegex() val IMG_REGEX = """url\(['"]?(?<url>[^(['"\)])]+)""".toRegex()
} }
} }

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 35 baseVersionCode = 34

View File

@ -313,33 +313,37 @@ abstract class LibGroup(
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val slugUrl = response.request.url.toString().substringAfter("manga/").substringBefore("/chapters") val slugUrl = response.request.url.toString().substringAfter("manga/").substringBefore("/chapters")
val chaptersData = response.parseAs<Data<List<Chapter>>>() val chaptersData = response.parseAs<Data<List<Chapter>>>()
.also { if (it.data.isEmpty()) return emptyList() } if (chaptersData.data.isEmpty()) {
throw Exception("Нет глав")
}
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing") val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
val defaultBranchId = if (sortingList == "ms_mixing" && chaptersData.data.getBranchCount() > 1) { val defaultBranchId = if (chaptersData.data.getBranchCount() > 1) { // excess request if branchesCount is only alone = slow update library witch rateLimitHost(apiDomain.toHttpUrl(), 1)
runCatching { getDefaultBranch(slugUrl.substringBefore("-")).first().id }.getOrNull() runCatching { getDefaultBranch(slugUrl.substringBefore("-")).first().id }.getOrNull()
} else { } else {
null null
} }
return chaptersData.data.flatMap { chapter -> val chapters = mutableListOf<SChapter>()
when { for (it in chaptersData.data.withIndex()) {
chapter.branchesCount > 1 && sortingList == "ms_mixing" -> { if (it.value.branchesCount > 1) {
val branch = chapter.branches for (currentBranch in it.value.branches.withIndex()) {
.firstOrNull { it.branchId == defaultBranchId }?.branchId if (currentBranch.value.branchId == defaultBranchId && sortingList == "ms_mixing") { // ms_mixing with default branch from api
?: chapter.branches.first().branchId chapters.add(it.value.toSChapter(slugUrl, defaultBranchId, isScanUser()))
} else if (defaultBranchId == null && sortingList == "ms_mixing") { // ms_mixing with first branch in chapter
listOf( if (chapters.any { chpIt -> chpIt.chapter_number == it.value.number.toFloat() }) {
chapter.toSChapter(slugUrl, branch, isScanUser()), chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
) }
} } else if (sortingList == "ms_combining") { // ms_combining
chapter.branchesCount > 1 && sortingList == "ms_combining" -> { chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
chapter.branches.map { branch ->
chapter.toSChapter(slugUrl, branch.branchId, isScanUser())
} }
} }
else -> listOf(chapter.toSChapter(slugUrl, isScanUser = isScanUser())) } else {
chapters.add(it.value.toSChapter(slugUrl, isScanUser = isScanUser()))
} }
}.reversed() }
return chapters.reversed()
} }
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {

View File

@ -1,6 +0,0 @@
font_size_title=Font size
font_size_summary=Font changes will not be applied to downloaded or cached chapters. The font size will be adjusted according to the size of the dialog box.
font_size_message=Font size changed to %s
default_font_size=Default
disable_website_setting_title=Disable source settings
disable_website_setting_summary=Site fonts will be disabled and your device's fonts will be applied. This does not apply to downloaded or cached chapters.

View File

@ -1 +0,0 @@
font_size_title=Tamaño de letra

View File

@ -1 +0,0 @@
font_size_title=Taille de la police

View File

@ -1 +0,0 @@
font_size_title=Ukuran font

View File

@ -1 +0,0 @@
font_size_title=Dimensione del carattere

View File

@ -1,6 +0,0 @@
font_size_title=Tamanho da fonte
font_size_summary=As alterações de fonte não serão aplicadas aos capítulos baixados ou armazenados em cache. O tamanho da fonte será ajustado de acordo com o tamanho da caixa de diálogo.
font_size_message=Tamanho da fonte foi alterada para %s
default_font_size=Padrão
disable_website_setting_title=Desativar configurações do site
disable_website_setting_summary=As fontes do site serão desativadas e as fontes de seu dispositivo serão aplicadas. Isso não se aplica a capítulos baixados ou armazenados em cache.

View File

@ -2,8 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 4 baseVersionCode = 2
dependencies {
api(project(":lib:i18n"))
}

View File

@ -1,18 +1,9 @@
package eu.kanade.tachiyomi.multisrc.machinetranslations package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.app.Application
import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
@ -24,26 +15,21 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
abstract class MachineTranslations( abstract class MachineTranslations(
override val name: String, override val name: String,
override val baseUrl: String, override val baseUrl: String,
private val language: Language, val language: Language,
) : ParsedHttpSource(), ConfigurableSource { ) : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
@ -51,66 +37,9 @@ abstract class MachineTranslations(
override val lang = language.lang override val lang = language.lang
protected val preferences: SharedPreferences by lazy { override val client = network.cloudflareClient.newBuilder()
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) .addInterceptor(ComposedImageInterceptor(baseUrl, language))
} .build()
/**
* A flag that tracks whether the settings have been changed. It is used to indicate if
* any configuration change has occurred. Once the value is accessed, it resets to `false`.
* This is useful for tracking whether a preference has been modified, and ensures that
* the change status is cleared after it has been accessed, to prevent multiple triggers.
*/
private var isSettingsChanged: Boolean = false
get() {
val current = field
field = false
return current
}
protected var fontSize: Int
get() = preferences.getString(FONT_SIZE_PREF, DEFAULT_FONT_SIZE)!!.toInt()
set(value) = preferences.edit().putString(FONT_SIZE_PREF, value.toString()).apply()
protected var disableSourceSettings: Boolean
get() = preferences.getBoolean(DISABLE_SOURCE_SETTINGS_PREF, language.disableSourceSettings)
set(value) = preferences.edit().putBoolean(DISABLE_SOURCE_SETTINGS_PREF, value).apply()
private val intl = Intl(
language = language.lang,
baseLanguage = "en",
availableLanguages = setOf("en", "es", "fr", "id", "it", "pt-BR"),
classLoader = this::class.java.classLoader!!,
)
private val settings get() = language.apply {
fontSize = this@MachineTranslations.fontSize
}
open val useDefaultComposedImageInterceptor: Boolean = true
override val client: OkHttpClient get() = clientInstance!!
/**
* This ensures that the `OkHttpClient` instance is only created when required, and it is rebuilt
* when there are configuration changes to ensure that the client uses the most up-to-date settings.
*/
private var clientInstance: OkHttpClient? = null
get() {
if (field == null || isSettingsChanged) {
field = clientBuilder().build()
}
return field
}
protected open fun clientBuilder() = network.cloudflareClient.newBuilder()
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(2, TimeUnit.MINUTES)
.addInterceptorIf(useDefaultComposedImageInterceptor, ComposedImageInterceptor(baseUrl, settings))
private fun OkHttpClient.Builder.addInterceptorIf(condition: Boolean, interceptor: Interceptor): OkHttpClient.Builder {
return this.takeIf { condition.not() } ?: this.addInterceptor(interceptor)
}
// ============================== Popular =============================== // ============================== Popular ===============================
@ -274,76 +203,9 @@ abstract class MachineTranslations(
return FilterList(filters) return FilterList(filters)
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
// Some libreoffice font sizes
val sizes = arrayOf(
"24", "26", "28",
"32", "36", "40",
"42", "44", "48",
"54", "60", "72",
"80", "88", "96",
)
ListPreference(screen.context).apply {
key = FONT_SIZE_PREF
title = intl["font_size_title"]
entries = sizes.map {
"${it}pt" + if (it == DEFAULT_FONT_SIZE) " - ${intl["default_font_size"]}" else ""
}.toTypedArray()
entryValues = sizes
summary = intl["font_size_summary"]
setOnPreferenceChange { _, newValue ->
val selected = newValue as String
val index = this.findIndexOfValue(selected)
val entry = entries[index] as String
fontSize = selected.toInt()
Toast.makeText(
screen.context,
intl["font_size_message"].format(entry),
Toast.LENGTH_LONG,
).show()
true // It's necessary to update the user interface
}
}.also(screen::addPreference)
if (language.disableSourceSettings.not()) {
SwitchPreferenceCompat(screen.context).apply {
key = DISABLE_SOURCE_SETTINGS_PREF
title = "${intl["disable_website_setting_title"]}"
summary = intl["disable_website_setting_summary"]
setDefaultValue(false)
setOnPreferenceChange { _, newValue ->
disableSourceSettings = newValue as Boolean
true
}
}.also(screen::addPreference)
}
}
/**
* Sets an `OnPreferenceChangeListener` for the preference, and before triggering the original listener,
* marks that the configuration has changed by setting `isSettingsChanged` to `true`.
* This behavior is useful for applying runtime configurations in the HTTP client,
* ensuring that the preference change is registered before invoking the original listener.
*/
protected fun Preference.setOnPreferenceChange(onPreferenceChangeListener: Preference.OnPreferenceChangeListener) {
setOnPreferenceChangeListener { preference, newValue ->
isSettingsChanged = true
onPreferenceChangeListener.onPreferenceChange(preference, newValue)
}
}
companion object { companion object {
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE) val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
const val PREFIX_SEARCH = "id:" const val PREFIX_SEARCH = "id:"
private const val FONT_SIZE_PREF = "fontSizePref"
private const val DISABLE_SOURCE_SETTINGS_PREF = "disableSourceSettingsPref"
private const val DEFAULT_FONT_SIZE = "24"
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US) private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
} }
} }

View File

@ -2,18 +2,4 @@ package eu.kanade.tachiyomi.multisrc.machinetranslations
class MachineTranslationsFactoryUtils class MachineTranslationsFactoryUtils
interface Language { data class Language(val lang: String, val target: String = lang, val origin: String = "en")
val lang: String
val target: String
val origin: String
var fontSize: Int
var disableSourceSettings: Boolean
}
data class LanguageImpl(
override val lang: String,
override val target: String = lang,
override val origin: String = "en",
override var fontSize: Int = 24,
override var disableSourceSettings: Boolean = false,
) : Language

View File

@ -26,13 +26,16 @@ import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import kotlin.math.pow
import kotlin.math.sqrt
// The Interceptor joins the dialogues and pages of the manga. // The Interceptor joins the dialogues and pages of the manga.
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
class ComposedImageInterceptor( class ComposedImageInterceptor(
baseUrl: String, baseUrl: String,
var language: Language, val language: Language,
) : Interceptor { ) : Interceptor {
private val json: Json by injectLazy() private val json: Json by injectLazy()
@ -52,7 +55,7 @@ class ComposedImageInterceptor(
} }
val dialogues = request.url.fragment?.parseAs<List<Dialog>>() val dialogues = request.url.fragment?.parseAs<List<Dialog>>()
?: emptyList() ?: throw IOException("Dialogues not found")
val imageRequest = request.newBuilder() val imageRequest = request.newBuilder()
.url(url) .url(url)
@ -60,9 +63,7 @@ class ComposedImageInterceptor(
// Load the fonts before opening the connection to load the image, // Load the fonts before opening the connection to load the image,
// so there aren't two open connections inside the interceptor. // so there aren't two open connections inside the interceptor.
if (language.disableSourceSettings.not()) { loadAllFont(chain)
loadAllFont(chain)
}
val response = chain.proceed(imageRequest) val response = chain.proceed(imageRequest)
@ -77,9 +78,9 @@ class ComposedImageInterceptor(
dialogues.forEach { dialog -> dialogues.forEach { dialog ->
val textPaint = createTextPaint(selectFontFamily(dialog.type)) val textPaint = createTextPaint(selectFontFamily(dialog.type))
val dialogBox = createDialogBox(dialog, textPaint) val dialogBox = createDialogBox(dialog, textPaint, bitmap)
val y = getYAxis(textPaint, dialog, dialogBox) val y = getYAxis(textPaint, dialog, dialogBox)
canvas.draw(textPaint, dialogBox, dialog, dialog.x1, y) canvas.draw(dialogBox, dialog, dialog.x1, y)
} }
val output = ByteArrayOutputStream() val output = ByteArrayOutputStream()
@ -103,7 +104,7 @@ class ComposedImageInterceptor(
} }
private fun createTextPaint(font: Typeface?): TextPaint { private fun createTextPaint(font: Typeface?): TextPaint {
val defaultTextSize = language.fontSize.pt val defaultTextSize = 24.pt // arbitrary
return TextPaint().apply { return TextPaint().apply {
color = Color.BLACK color = Color.BLACK
textSize = defaultTextSize textSize = defaultTextSize
@ -115,10 +116,6 @@ class ComposedImageInterceptor(
} }
private fun selectFontFamily(type: String): Typeface? { private fun selectFontFamily(type: String): Typeface? {
if (language.disableSourceSettings) {
return null
}
if (type in fontFamily) { if (type in fontFamily) {
return fontFamily[type]?.second return fontFamily[type]?.second
} }
@ -209,7 +206,7 @@ class ComposedImageInterceptor(
} }
} }
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint): StaticLayout { private fun createDialogBox(dialog: Dialog, textPaint: TextPaint, bitmap: Bitmap): StaticLayout {
var dialogBox = createBoxLayout(dialog, textPaint) var dialogBox = createBoxLayout(dialog, textPaint)
/** /**
@ -220,8 +217,18 @@ class ComposedImageInterceptor(
dialogBox = createBoxLayout(dialog, textPaint) dialogBox = createBoxLayout(dialog, textPaint)
} }
textPaint.color = Color.BLACK // Use source setup
textPaint.bgColor = Color.WHITE if (dialog.isNewApi) {
textPaint.color = dialog.foregroundColor
textPaint.bgColor = dialog.backgroundColor
textPaint.style = if (dialog.isBold) Paint.Style.FILL_AND_STROKE else Paint.Style.FILL
}
/**
* Forces font color correction if the background color of the dialog box and the font color are too similar.
* It's a source configuration problem.
*/
textPaint.adjustTextColor(dialog, bitmap)
return dialogBox return dialogBox
} }
@ -234,46 +241,59 @@ class ComposedImageInterceptor(
setIncludePad(false) setIncludePad(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED) setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED)
setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
} }
}.build() }.build()
} }
// Invert color in black dialog box.
private fun TextPaint.adjustTextColor(dialog: Dialog, bitmap: Bitmap) {
val pixelColor = bitmap.getPixel(dialog.centerX.toInt(), dialog.centerY.toInt())
val inverseColor = (Color.WHITE - pixelColor) or Color.BLACK
val minDistance = 80f // arbitrary
if (colorDistance(pixelColor, dialog.foregroundColor) > minDistance) {
return
}
color = inverseColor
}
private inline fun <reified T> String.parseAs(): T { private inline fun <reified T> String.parseAs(): T {
return json.decodeFromString(this) return json.decodeFromString(this)
} }
private fun Canvas.draw(textPaint: TextPaint, layout: StaticLayout, dialog: Dialog, x: Float, y: Float) { private fun Canvas.draw(layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
save() save()
translate(x, y) translate(x, y)
rotate(dialog.angle) rotate(dialog.angle)
drawTextOutline(textPaint, layout) layout.draw(this)
drawText(textPaint, layout)
restore() restore()
} }
private fun Canvas.drawText(textPaint: TextPaint, layout: StaticLayout) {
textPaint.style = Paint.Style.FILL
layout.draw(this)
}
private fun Canvas.drawTextOutline(textPaint: TextPaint, layout: StaticLayout) {
val foregroundColor = textPaint.color
val style = textPaint.style
textPaint.strokeWidth = 5F
textPaint.color = textPaint.bgColor
textPaint.style = Paint.Style.FILL_AND_STROKE
layout.draw(this)
textPaint.color = foregroundColor
textPaint.style = style
}
// https://pixelsconverter.com/pt-to-px // https://pixelsconverter.com/pt-to-px
private val Int.pt: Float get() = this / SCALED_DENSITY private val Int.pt: Float get() = this / SCALED_DENSITY
// ============================= Utils ======================================
/**
* Calculates the Euclidean distance between two colors in RGB space.
*
* This function takes two integer values representing hexadecimal colors,
* converts them to their RGB components, and calculates the Euclidean distance
* between the two colors. The distance provides a measure of how similar or
* different the two colors are.
*
*/
private fun colorDistance(colorA: Int, colorB: Int): Double {
val a = Color.valueOf(colorA)
val b = Color.valueOf(colorB)
return sqrt(
(b.red() - a.red()).toDouble().pow(2) +
(b.green() - a.green()).toDouble().pow(2) +
(b.blue() - a.blue()).toDouble().pow(2),
)
}
companion object { companion object {
// w3: Absolute Lengths [...](https://www.w3.org/TR/css3-values/#absolute-lengths) // w3: Absolute Lengths [...](https://www.w3.org/TR/css3-values/#absolute-lengths)
const val SCALED_DENSITY = 0.75f // 1px = 0.75pt const val SCALED_DENSITY = 0.75f // 1px = 0.75pt

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 40 baseVersionCode = 37
dependencies { dependencies {
api(project(":lib:cryptoaes")) api(project(":lib:cryptoaes"))

View File

@ -82,12 +82,7 @@ abstract class Madara(
/** /**
* Automatically fetched genres from the source to be used in the filters. * Automatically fetched genres from the source to be used in the filters.
*/ */
protected open var genresList: List<Genre> = emptyList() private var genresList: List<Genre> = emptyList()
/**
* Whether genres have been fetched
*/
private var genresFetched: Boolean = false
/** /**
* Inner variable to control how much tries the genres request was called. * Inner variable to control how much tries the genres request was called.
@ -242,14 +237,11 @@ abstract class Madara(
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(URL_SEARCH_PREFIX)) { if (query.startsWith(URL_SEARCH_PREFIX)) {
val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply { val mangaUrl = "/$mangaSubString/${query.substringAfter(URL_SEARCH_PREFIX)}/"
addPathSegment(mangaSubString) return client.newCall(GET("$baseUrl$mangaUrl", headers))
addPathSegment(query.substringAfter(URL_SEARCH_PREFIX))
}.build()
return client.newCall(GET(mangaUrl, headers))
.asObservableSuccess().map { response -> .asObservableSuccess().map { response ->
val manga = mangaDetailsParse(response).apply { val manga = mangaDetailsParse(response).apply {
setUrlWithoutDomain(mangaUrl.toString()) url = mangaUrl
} }
MangasPage(listOf(manga), false) MangasPage(listOf(manga), false)
@ -586,13 +578,11 @@ abstract class Madara(
override fun searchMangaSelector() = "div.c-tabs-item__content" override fun searchMangaSelector() = "div.c-tabs-item__content"
protected open val searchMangaUrlSelector = "div.post-title a"
override fun searchMangaFromElement(element: Element): SManga { override fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create() val manga = SManga.create()
with(element) { with(element) {
selectFirst(searchMangaUrlSelector)!!.let { selectFirst("div.post-title a")!!.let {
manga.setUrlWithoutDomain(it.attr("abs:href")) manga.setUrlWithoutDomain(it.attr("abs:href"))
manga.title = it.ownText() manga.title = it.ownText()
} }
@ -633,7 +623,7 @@ abstract class Madara(
"Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor", "Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor",
"Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision", "Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision",
"Curso", "En marcha", "Publicandose", "Publicándose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo", "Curso", "En marcha", "Publicandose", "Publicándose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso", "Atualizações Semanais", "Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso",
) )
protected val hiatusStatusList: Array<String> = arrayOf( protected val hiatusStatusList: Array<String> = arrayOf(
@ -688,7 +678,7 @@ abstract class Madara(
manga.thumbnail_url = imageFromElement(it) manga.thumbnail_url = imageFromElement(it)
} }
select(mangaDetailsSelectorStatus).last()?.let { select(mangaDetailsSelectorStatus).last()?.let {
manga.status = with(it.text().filter { ch -> ch.isLetterOrDigit() || ch.isWhitespace() }.trim()) { manga.status = with(it.text()) {
when { when {
containsIn(completedStatusList) -> SManga.COMPLETED containsIn(completedStatusList) -> SManga.COMPLETED
containsIn(ongoingStatusList) -> SManga.ONGOING containsIn(ongoingStatusList) -> SManga.ONGOING
@ -752,7 +742,7 @@ abstract class Madara(
// Manga Details Selector // Manga Details Selector
open val mangaDetailsSelectorTitle = "div.post-title h3, div.post-title h1, #manga-title > h1" open val mangaDetailsSelectorTitle = "div.post-title h3, div.post-title h1, #manga-title > h1"
open val mangaDetailsSelectorAuthor = "div.author-content > a, div.manga-authors > a" open val mangaDetailsSelectorAuthor = "div.author-content > a"
open val mangaDetailsSelectorArtist = "div.artist-content > a" open val mangaDetailsSelectorArtist = "div.artist-content > a"
open val mangaDetailsSelectorStatus = "div.summary-content" open val mangaDetailsSelectorStatus = "div.summary-content"
open val mangaDetailsSelectorDescription = "div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt" open val mangaDetailsSelectorDescription = "div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt"
@ -786,7 +776,7 @@ abstract class Madara(
/** /**
* Get the best image quality available from srcset * Get the best image quality available from srcset
*/ */
protected fun String.getSrcSetImage(): String? { private fun String.getSrcSetImage(): String? {
return this.split(" ") return this.split(" ")
.filter(URL_REGEX::matches) .filter(URL_REGEX::matches)
.maxOfOrNull(String::toString) .maxOfOrNull(String::toString)
@ -930,10 +920,6 @@ abstract class Madara(
WordSet("hace").startsWith(date) -> { WordSet("hace").startsWith(date) -> {
parseRelativeDate(date) parseRelativeDate(date)
} }
// Handle "jour" with a number before it
date.contains(Regex("""\b\d+ jour""")) -> {
parseRelativeDate(date)
}
date.contains(Regex("""\d(st|nd|rd|th)""")) -> { date.contains(Regex("""\d(st|nd|rd|th)""")) -> {
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it // Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
date.split(" ").map { date.split(" ").map {
@ -1077,17 +1063,10 @@ abstract class Madara(
* Fetch the genres from the source to be used in the filters. * Fetch the genres from the source to be used in the filters.
*/ */
protected fun fetchGenres() { protected fun fetchGenres() {
if (fetchGenres && fetchGenresAttempts < 3 && !genresFetched) { if (fetchGenres && fetchGenresAttempts < 3 && genresList.isEmpty()) {
try { try {
client.newCall(genresRequest()).execute() genresList = client.newCall(genresRequest()).execute()
.use { parseGenres(it.asJsoup()) } .use { parseGenres(it.asJsoup()) }
.also {
genresFetched = true
}
.takeIf { it.isNotEmpty() }
?.also {
genresList = it
}
} catch (_: Exception) { } catch (_: Exception) {
} finally { } finally {
fetchGenresAttempts++ fetchGenresAttempts++

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 17 baseVersionCode = 14

View File

@ -11,8 +11,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -22,6 +25,7 @@ import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
@ -51,6 +55,8 @@ abstract class MadTheme(
add("Referer", "$baseUrl/") add("Referer", "$baseUrl/")
} }
private val json: Json by injectLazy()
private var genreKey = "genre[]" private var genreKey = "genre[]"
// Popular // Popular
@ -171,53 +177,19 @@ abstract class MadTheme(
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
if (response.request.url.fragment == "idFound") { if (response.code in 200..299) {
return super.chapterListParse(response) return super.chapterListParse(response)
} }
val document = response.asJsoup() // Try to show message/error from site
response.body.let { body ->
// Need the total chapters to check against the request json.decodeFromString<JsonObject>(body.string())["message"]
val totalChapters = document.selectFirst(".title span:containsOwn(CHAPTERS \\()")?.text() ?.jsonPrimitive
?.substringAfter("(") ?.content
?.substringBefore(")") ?.let { throw Exception(it) }
?.toIntOrNull()
val script = document.selectFirst("script:containsData(bookId)")
?: throw Exception("Cannot find script")
val bookId = script.data().substringAfter("bookId = ").substringBefore(";")
val bookSlug = script.data().substringAfter("bookSlug = \"").substringBefore("\";")
// Use slug search by default
val slugRequest = chapterClient.newCall(GET(buildChapterUrl(bookSlug), headers)).execute()
if (!slugRequest.isSuccessful) {
throw Exception("HTTP error ${slugRequest.code}")
} }
var finalDocument = slugRequest.asJsoup().select(chapterListSelector()) throw Exception("HTTP error ${response.code}")
if (totalChapters != null && finalDocument.size < totalChapters) {
val idRequest = chapterClient.newCall(GET(buildChapterUrl(bookId), headers)).execute()
finalDocument = idRequest.asJsoup().select(chapterListSelector())
}
return finalDocument.map {
SChapter.create().apply {
url = it.selectFirst("a")!!.absUrl("href").removePrefix(baseUrl)
name = it.selectFirst(".chapter-title")!!.text()
date_upload = parseChapterDate(it.selectFirst(".chapter-update")?.text())
}
}
}
private fun buildChapterUrl(fetchByParam: String): HttpUrl {
return baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("api")
addPathSegment("manga")
addPathSegment(fetchByParam)
addPathSegment("chapters")
addQueryParameter("source", "detail")
}.build()
} }
override fun chapterListRequest(manga: SManga): Request = override fun chapterListRequest(manga: SManga): Request =
@ -225,11 +197,10 @@ abstract class MadTheme(
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder() val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId) .addQueryParameter("manga_id", mangaId)
.addQueryParameter("manga_name", manga.title) .addQueryParameter("manga_name", manga.title)
.fragment("idFound")
.build() .build()
GET(url, headers) GET(url, headers)
} ?: GET("$baseUrl${manga.url}", headers) } ?: GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
if (genresList == null) { if (genresList == null) {

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 14 baseVersionCode = 13

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 3 baseVersionCode = 2

View File

@ -1,356 +1,129 @@
package eu.kanade.tachiyomi.multisrc.mangareader package eu.kanade.tachiyomi.multisrc.mangareader
import eu.kanade.tachiyomi.network.GET import android.app.Application
import eu.kanade.tachiyomi.source.model.Filter import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.model.FilterList import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.MangasPage 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode import org.jsoup.select.Evaluator
import uy.kohesive.injekt.injectLazy import rx.Observable
import java.net.URLEncoder import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
abstract class MangaReader( abstract class MangaReader : HttpSource(), ConfigurableSource {
override val name: String,
override val baseUrl: String,
final override val lang: String,
) : HttpSource() {
override val supportsLatest = true override val supportsLatest = true
override val client = network.cloudflareClient override val client = network.cloudflareClient
private val json: Json by injectLazy() final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
open fun addPage(page: Int, builder: HttpUrl.Builder) {
builder.addQueryParameter("page", page.toString())
}
// ============================== Popular ===============================
protected open val sortPopularValue = "most-viewed"
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(sortFilterName, sortFilterParam, sortFilterValues(), sortPopularValue)),
)
}
final override fun popularMangaParse(response: Response) = searchMangaParse(response) final override fun popularMangaParse(response: Response) = searchMangaParse(response)
// =============================== Latest =============================== final override fun searchMangaParse(response: Response): MangasPage {
protected open val sortLatestValue = "latest-updated"
override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(sortFilterName, sortFilterParam, sortFilterValues(), sortLatestValue)),
)
}
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
// =============================== Search ===============================
protected open val searchPathSegment = "search"
protected open val searchKeyword = "keyword"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (query.isNotBlank()) {
addPathSegment(searchPathSegment)
addQueryParameter(searchKeyword, query)
} else {
addPathSegment("filter")
val filterList = filters.ifEmpty { getFilterList() }
filterList.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
}
addPage(page, this)
}.build()
return GET(url, headers)
}
open fun searchMangaSelector(): String = ".manga_list-sbs .manga-poster"
open fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.attr("href"))
element.selectFirst("img")!!.let {
title = it.attr("alt")
thumbnail_url = it.imgAttr()
}
}
open fun searchMangaNextPageSelector(): String = "ul.pagination > li.active + li"
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val document = response.asJsoup()
val entries = document.select(searchMangaSelector()) var entries = document.select(searchMangaSelector()).map(::searchMangaFromElement)
.map(::searchMangaFromElement) if (preferences.getBoolean(SHOW_VOLUME_PREF, false)) {
entries = entries.flatMapTo(ArrayList(entries.size * 2)) { manga ->
val volume = SManga.create().apply {
url = manga.url + VOLUME_URL_SUFFIX
title = VOLUME_TITLE_PREFIX + manga.title
thumbnail_url = manga.thumbnail_url
}
listOf(manga, volume)
}
}
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
return MangasPage(entries, hasNextPage) return MangasPage(entries, hasNextPage)
} }
// =========================== Manga Details ============================ final override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url abstract fun searchMangaSelector(): String
private val authorText: String = when (lang) { abstract fun searchMangaNextPageSelector(): String
"ja" -> "著者"
else -> "Authors"
}
private val statusText: String = when (lang) { abstract fun searchMangaFromElement(element: Element): SManga
"ja" -> "地位"
else -> "Status"
}
override fun mangaDetailsParse(response: Response): SManga { abstract fun mangaDetailsParse(document: Document): SManga
final override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup() val document = response.asJsoup()
val manga = mangaDetailsParse(document)
return SManga.create().apply { if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
document.selectFirst("#ani_detail")!!.run { manga.title = VOLUME_TITLE_PREFIX + manga.title
title = selectFirst(".manga-name")!!.ownText()
thumbnail_url = selectFirst("img")?.imgAttr()
genre = select(".genres > a").joinToString { it.ownText() }
description = buildString {
selectFirst(".description")?.ownText()?.let { append(it) }
append("\n\n")
selectFirst(".manga-name-or")?.ownText()?.let {
if (it.isNotEmpty() && it != title) {
append("Alternative Title: ")
append(it)
}
}
}.trim()
select(".anisc-info > .item").forEach { info ->
when (info.selectFirst(".item-head")?.ownText()) {
"$authorText:" -> info.parseAuthorsTo(this@apply)
"$statusText:" -> info.parseStatus(this@apply)
}
}
}
} }
}
private fun Element.parseAuthorsTo(manga: SManga): SManga {
val authors = select("a")
val text = authors.map { it.ownText().replace(",", "") }
val count = authors.size
when (count) {
0 -> return manga
1 -> {
manga.author = text.first()
return manga
}
}
val authorList = ArrayList<String>(count)
val artistList = ArrayList<String>(count)
for ((index, author) in authors.withIndex()) {
val textNode = author.nextSibling() as? TextNode
val list = if (textNode?.wholeText?.contains("(Art)") == true) artistList else authorList
list.add(text[index])
}
if (authorList.isNotEmpty()) manga.author = authorList.joinToString()
if (artistList.isNotEmpty()) manga.artist = artistList.joinToString()
return manga return manga
} }
private fun Element.parseStatus(manga: SManga): SManga { abstract val chapterType: String
manga.status = this.selectFirst(".name")?.text().getStatus() abstract val volumeType: String
return manga
}
open fun String?.getStatus(): Int = when (this?.lowercase()) { abstract fun chapterListRequest(mangaUrl: String, type: String): Request
"ongoing", "publishing", "releasing" -> SManga.ONGOING
"completed", "finished" -> SManga.COMPLETED
"on-hold", "on_hiatus" -> SManga.ON_HIATUS
"canceled", "discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
// ============================== Chapters ============================== abstract fun parseChapterElements(response: Response, isVolume: Boolean): List<Element>
override fun getChapterUrl(chapter: SChapter): String { override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
return baseUrl + chapter.url.substringBeforeLast('#')
}
open val chapterIdSelect = "en-chapters" open fun updateChapterList(manga: SManga, chapters: List<SChapter>) = Unit
open fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
element.selectFirst("a")!!.run { val path = manga.url
setUrlWithoutDomain(attr("href") + "#${element.attr("data-id")}") val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
name = selectFirst(".name")?.text() ?: text() val type = if (isVolume) volumeType else chapterType
} val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type)
} val response = client.newCall(request).execute()
override fun chapterListParse(response: Response): List<SChapter> { val abbrPrefix = if (isVolume) "Vol" else "Chap"
val document = response.asJsoup() val fullPrefix = if (isVolume) "Volume" else "Chapter"
return document.select("#$chapterIdSelect > li.chapter-item").map(::chapterFromElement) val linkSelector = Evaluator.Tag("a")
} parseChapterElements(response, isVolume).map { element ->
SChapter.create().apply {
val number = element.attr("data-number")
chapter_number = number.toFloatOrNull() ?: -1f
// =============================== Pages ================================ val link = element.selectFirst(linkSelector)!!
name = run {
open fun getChapterId(chapter: SChapter): String { val name = link.text()
val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup() val prefix = "$abbrPrefix $number: "
return document.selectFirst("div[data-reading-id]") if (!name.startsWith(prefix)) return@run name
?.attr("data-reading-id") val realName = name.removePrefix(prefix)
.orEmpty() if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
.ifEmpty {
throw Exception("Unable to retrieve chapter id")
}
}
open fun getAjaxUrl(id: String): String {
return "$baseUrl//ajax/image/list/$id?mode=vertical"
}
override fun pageListRequest(chapter: SChapter): Request {
val chapterId = chapter.url.substringAfterLast('#').ifEmpty {
getChapterId(chapter)
}
val ajaxHeaders = super.headersBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Referer", URLEncoder.encode(baseUrl + chapter.url.substringBeforeLast("#"), "utf-8"))
add("X-Requested-With", "XMLHttpRequest")
}.build()
return GET(getAjaxUrl(chapterId), ajaxHeaders)
}
open fun pageListParseSelector(): String = ".container-reader-chapter > div > img"
override fun pageListParse(response: Response): List<Page> {
val document = response.parseHtmlProperty()
val pageList = document.select(pageListParseSelector()).mapIndexed { index, element ->
val imgUrl = element.imgAttr().ifEmpty {
element.selectFirst("img")!!.imgAttr()
}
Page(index, imageUrl = imgUrl)
}
return pageList
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
open fun Element.imgAttr(): String = when {
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-url") -> attr("abs:data-url")
else -> attr("abs:src")
}
open fun Response.parseHtmlProperty(): Document {
val html = json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
return Jsoup.parseBodyFragment(html)
}
// =============================== Filters ==============================
object Note : Filter.Header("NOTE: Ignored if using text search!")
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
open class UriPartFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
private val join: String? = null,
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
if (join == null) {
checked.forEach {
builder.addQueryParameter(param, it.value)
} }
} else { setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id"))
builder.addQueryParameter(param, checked.joinToString(join) { it.value })
} }
} }.also { if (!isVolume && it.isNotEmpty()) updateChapterList(manga, it) }
} }
open class SortFilter( final override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast('#')
title: String,
param: String,
values: Array<Pair<String, String>>,
default: String? = null,
) : UriPartFilter(title, param, values, default)
private val sortFilterName: String = when (lang) { override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
"ja" -> "選別"
else -> "Sort" val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
} }
protected open val sortFilterParam: String = "sort" override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
protected open fun sortFilterValues(): Array<Pair<String, String>> { key = SHOW_VOLUME_PREF
return arrayOf( title = "Show volume entries in search result"
Pair("Default", "default"), setDefaultValue(false)
Pair("Latest Updated", sortLatestValue), }.let(screen::addPreference)
Pair("Score", "score"),
Pair("Name A-Z", "name-az"),
Pair("Release Date", "release-date"),
Pair("Most Viewed", sortPopularValue),
)
} }
open fun getSortFilter() = SortFilter(sortFilterName, sortFilterParam, sortFilterValues()) companion object {
private const val SHOW_VOLUME_PREF = "show_volume"
override fun getFilterList(): FilterList = FilterList( private const val VOLUME_URL_FRAGMENT = "vol"
getSortFilter(), private const val VOLUME_URL_SUFFIX = "#" + VOLUME_URL_FRAGMENT
) private const val VOLUME_TITLE_PREFIX = "[VOL] "
}
} }

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 3 baseVersionCode = 2

View File

@ -1,35 +0,0 @@
package eu.kanade.tachiyomi.multisrc.mangaworld
import eu.kanade.tachiyomi.network.GET
import okhttp3.Cookie
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
class CookieRedirectInterceptor(private val client: OkHttpClient) : Interceptor {
private val cookieRegex = Regex("""document\.cookie="(MWCookie[^"]+)""")
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
// ignore requests that already have completed the JS challenge
if (response.headers["vary"] != null) return response
val content = response.body.string()
val results = cookieRegex.find(content)
?: return response.newBuilder().body(content.toResponseBody(response.body.contentType())).build()
val (cookieString) = results.destructured
return chain.proceed(loadCookie(request, cookieString))
}
private fun loadCookie(request: Request, cookieString: String): Request {
val cookie = Cookie.parse(request.url, cookieString)!!
client.cookieJar.saveFromResponse(request.url, listOf(cookie))
val headers = request.headers.newBuilder()
.add("Cookie", cookie.toString())
.build()
return GET(request.url, headers)
}
}

View File

@ -28,11 +28,7 @@ abstract class MangaWorld(
) : ParsedHttpSource() { ) : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
// CookieRedirectInterceptor extracts MWCookie from the page's JS code, applies it and then redirects to the page
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor(CookieRedirectInterceptor(network.cloudflareClient))
.build()
companion object { companion object {
protected val CHAPTER_NUMBER_REGEX by lazy { Regex("""(?i)capitolo\s([0-9]+)""") } protected val CHAPTER_NUMBER_REGEX by lazy { Regex("""(?i)capitolo\s([0-9]+)""") }

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 4 baseVersionCode = 3

View File

@ -267,7 +267,7 @@ abstract class SlimeReadTheme(
companion object { companion object {
const val PREFIX_SEARCH = "id:" const val PREFIX_SEARCH = "id:"
val FUNCTION_REGEX = """(\[""\.concat\("[^,]+,"\."\)\.concat\(([^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL) val FUNCTION_REGEX = """(?<script>\[""\.concat\("[^,]+,"\."\)\.concat\((?<infix>[^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
} }
} }

View File

@ -1,5 +1,4 @@
include(":core") include(":core")
include(":utils")
// Load all modules under /lib // Load all modules under /lib
File(rootDir, "lib").eachDir { include("lib:${it.name}") } File(rootDir, "lib").eachDir { include("lib:${it.name}") }

View File

@ -14,12 +14,8 @@
<data android:host="*.bato.to" /> <data android:host="*.bato.to" />
<data android:host="bato.to" /> <data android:host="bato.to" />
<data android:host="*.batocomic.com" /> <data android:host="*.batocc.com" />
<data android:host="batocomic.com" /> <data android:host="batocc.com" />
<data android:host="*.batocomic.net" />
<data android:host="batocomic.net" />
<data android:host="*.batocomic.org" />
<data android:host="batocomic.org" />
<data android:host="*.batotoo.com" /> <data android:host="*.batotoo.com" />
<data android:host="batotoo.com" /> <data android:host="batotoo.com" />
<data android:host="*.batotwo.com" /> <data android:host="*.batotwo.com" />
@ -28,40 +24,18 @@
<data android:host="battwo.com" /> <data android:host="battwo.com" />
<data android:host="*.comiko.net" /> <data android:host="*.comiko.net" />
<data android:host="comiko.net" /> <data android:host="comiko.net" />
<data android:host="*.comiko.org" />
<data android:host="comiko.org" />
<data android:host="*.mangatoto.com" /> <data android:host="*.mangatoto.com" />
<data android:host="mangatoto.com" /> <data android:host="mangatoto.com" />
<data android:host="*.mangatoto.net" /> <data android:host="*.mangatoto.net" />
<data android:host="mangatoto.net" /> <data android:host="mangatoto.net" />
<data android:host="*.mangatoto.org" /> <data android:host="*.mangatoto.org" />
<data android:host="mangatoto.org" /> <data android:host="mangatoto.org" />
<data android:host="*.readtoto.com" /> <data android:host="*.mycordant.co.uk" />
<data android:host="readtoto.com" /> <data android:host="mycordant.co.uk" />
<data android:host="*.readtoto.net" />
<data android:host="readtoto.net" />
<data android:host="*.readtoto.org" />
<data android:host="readtoto.org" />
<data android:host="*.xbato.com" />
<data android:host="xbato.com" />
<data android:host="*.xbato.net" />
<data android:host="xbato.net" />
<data android:host="*.xbato.org" />
<data android:host="xbato.org" />
<data android:host="*.zbato.com" />
<data android:host="zbato.com" />
<data android:host="*.zbato.net" />
<data android:host="zbato.net" />
<data android:host="*.zbato.org" />
<data android:host="zbato.org" />
<data android:host="*.dto.to" /> <data android:host="*.dto.to" />
<data android:host="dto.to" /> <data android:host="dto.to" />
<data android:host="*.fto.to" />
<data android:host="fto.to" />
<data android:host="*.hto.to" /> <data android:host="*.hto.to" />
<data android:host="hto.to" /> <data android:host="hto.to" />
<data android:host="*.jto.to" />
<data android:host="jto.to" />
<data android:host="*.mto.to" /> <data android:host="*.mto.to" />
<data android:host="mto.to" /> <data android:host="mto.to" />
<data android:host="*.wto.to" /> <data android:host="*.wto.to" />

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Bato.to' extName = 'Bato.to'
extClass = '.BatoToFactory' extClass = '.BatoToFactory'
extVersionCode = 48 extVersionCode = 46
isNsfw = true isNsfw = true
} }

View File

@ -5,7 +5,6 @@ import android.content.SharedPreferences
import androidx.preference.CheckBoxPreference import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -101,25 +100,11 @@ open class BatoTo(
if (current.isNotEmpty()) { if (current.isNotEmpty()) {
return current return current
} }
field = getMirrorPref() field = getMirrorPref()!!
return field return field
} }
private fun getMirrorPref(): String { private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
return preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
?.takeUnless { it == MIRROR_PREF_DEFAULT_VALUE }
?: let {
val seed = runCatching {
val pm = Injekt.get<Application>().packageManager
pm.getPackageInfo(BuildConfig.APPLICATION_ID, 0).lastUpdateTime
}.getOrElse {
BuildConfig.VERSION_NAME.hashCode().toLong()
}
MIRROR_PREF_ENTRY_VALUES[1 + (seed % (MIRROR_PREF_ENTRIES.size - 1)).toInt()]
}
}
private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE) private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
private fun isRemoveTitleVersion(): Boolean { private fun isRemoveTitleVersion(): Boolean {
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false) return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
@ -342,12 +327,6 @@ open class BatoTo(
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
if (manga.url.startsWith("http")) { if (manga.url.startsWith("http")) {
// Check if trying to use a deprecated mirror, force current mirror
val httpUrl = manga.url.toHttpUrl()
if ("https://${httpUrl.host}" in DEPRECATED_MIRRORS) {
val newHttpUrl = httpUrl.newBuilder().host(getMirrorPref().toHttpUrl().host)
return GET(newHttpUrl.build(), headers)
}
return GET(manga.url, headers) return GET(manga.url, headers)
} }
return super.mangaDetailsRequest(manga) return super.mangaDetailsRequest(manga)
@ -358,8 +337,8 @@ open class BatoTo(
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.selectFirst("div#mainer div.container-fluid")!! val infoElement = document.selectFirst("div#mainer div.container-fluid")!!
val manga = SManga.create() val manga = SManga.create()
val workStatus = infoElement.selectFirst("div.attr-item:contains(original work) span")?.text() val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
val uploadStatus = infoElement.selectFirst("div.attr-item:contains(upload status) span")?.text() val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
val originalTitle = infoElement.select("h3").text().removeEntities() val originalTitle = infoElement.select("h3").text().removeEntities()
val description = buildString { val description = buildString {
append(infoElement.select("div.limit-html").text()) append(infoElement.select("div.limit-html").text())
@ -367,13 +346,13 @@ open class BatoTo(
append("\n\n${it.text()}") append("\n\n${it.text()}")
} }
infoElement.selectFirst("h5:containsOwn(Extra Info:) + div")?.also { infoElement.selectFirst("h5:containsOwn(Extra Info:) + div")?.also {
append("\n\nExtra Info:\n${it.wholeText()}") append("\n\nExtra Info:\n${it.text()}")
} }
document.selectFirst("div.pb-2.alias-set.line-b-f")?.takeIf { it.hasText() }?.also { document.selectFirst("div.pb-2.alias-set.line-b-f")?.also {
append("\n\nAlternative Titles:\n") append("\n\nAlternative Titles:\n")
append(it.text().split('/').joinToString("\n") { "${it.trim()}" }) append(it.text().split('/').joinToString("\n") { "${it.trim()}" })
} }
}.trim() }
val cleanedTitle = if (isRemoveTitleVersion()) { val cleanedTitle = if (isRemoveTitleVersion()) {
originalTitle.replace(titleRegex, "").trim() originalTitle.replace(titleRegex, "").trim()
@ -390,19 +369,16 @@ open class BatoTo(
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src") manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
return manga return manga
} }
private fun parseStatus(workStatus: String?, uploadStatus: String?): Int { private fun parseStatus(workStatus: String?, uploadStatus: String?) = when {
val status = workStatus ?: uploadStatus workStatus == null -> SManga.UNKNOWN
return when { workStatus.contains("Ongoing") -> SManga.ONGOING
status == null -> SManga.UNKNOWN workStatus.contains("Cancelled") -> SManga.CANCELLED
status.contains("Ongoing") -> SManga.ONGOING workStatus.contains("Hiatus") -> SManga.ON_HIATUS
status.contains("Cancelled") -> SManga.CANCELLED workStatus.contains("Completed") -> when {
status.contains("Hiatus") -> SManga.ON_HIATUS uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
status.contains("Completed") -> when { else -> SManga.COMPLETED
uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
else -> SManga.COMPLETED
}
else -> SManga.UNKNOWN
} }
else -> SManga.UNKNOWN
} }
private fun altChapterParse(response: Response): List<SChapter> { private fun altChapterParse(response: Response): List<SChapter> {
@ -435,12 +411,6 @@ open class BatoTo(
GET("$baseUrl/rss/series/$id.xml", headers) GET("$baseUrl/rss/series/$id.xml", headers)
} else if (manga.url.startsWith("http")) { } else if (manga.url.startsWith("http")) {
// Check if trying to use a deprecated mirror, force current mirror
val httpUrl = manga.url.toHttpUrl()
if ("https://${httpUrl.host}" in DEPRECATED_MIRRORS) {
val newHttpUrl = httpUrl.newBuilder().host(getMirrorPref().toHttpUrl().host)
return GET(newHttpUrl.build(), headers)
}
GET(manga.url, headers) GET(manga.url, headers)
} else { } else {
super.chapterListRequest(manga) super.chapterListRequest(manga)
@ -537,12 +507,6 @@ open class BatoTo(
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
if (chapter.url.startsWith("http")) { if (chapter.url.startsWith("http")) {
// Check if trying to use a deprecated mirror, force current mirror
val httpUrl = chapter.url.toHttpUrl()
if ("https://${httpUrl.host}" in DEPRECATED_MIRRORS) {
val newHttpUrl = httpUrl.newBuilder().host(getMirrorPref().toHttpUrl().host)
return GET(newHttpUrl.build(), headers)
}
return GET(chapter.url, headers) return GET(chapter.url, headers)
} }
return super.pageListRequest(chapter) return super.pageListRequest(chapter)
@ -1037,7 +1001,7 @@ open class BatoTo(
private const val MIRROR_PREF_TITLE = "Mirror" private const val MIRROR_PREF_TITLE = "Mirror"
private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION" private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION"
private val MIRROR_PREF_ENTRIES = arrayOf( private val MIRROR_PREF_ENTRIES = arrayOf(
"Auto", "zbato.org",
"batocomic.com", "batocomic.com",
"batocomic.net", "batocomic.net",
"batocomic.org", "batocomic.org",
@ -1049,25 +1013,23 @@ open class BatoTo(
"readtoto.com", "readtoto.com",
"readtoto.net", "readtoto.net",
"readtoto.org", "readtoto.org",
"dto.to",
"fto.to",
"jto.to",
"hto.to",
"mto.to",
"wto.to",
"xbato.com", "xbato.com",
"xbato.net", "xbato.net",
"xbato.org", "xbato.org",
"zbato.com", "zbato.com",
"zbato.net", "zbato.net",
"zbato.org",
"dto.to",
"fto.to",
"hto.to",
"jto.to",
"mto.to",
"wto.to",
) )
private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray() private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray()
private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0] private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
private val DEPRECATED_MIRRORS = listOf( private val DEPRECATED_MIRRORS = listOf(
"https://bato.to", "https://bato.to",
"https://batocc.com", // parked
"https://mangatoto.com", "https://mangatoto.com",
"https://mangatoto.net", "https://mangatoto.net",
"https://mangatoto.org", "https://mangatoto.org",

View File

@ -1,9 +0,0 @@
ext {
extName = 'Comic Growl'
extClass = '.ComicGrowl'
themePkg = 'gigaviewer'
baseUrl = 'https://comic-growl.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,63 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comicgrowl
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.nodes.Element
// TODO: get manga status
// TODO: filter by status
// TODO: change cdnUrl as a array(upstream)
class ComicGrowl : GigaViewer(
"コミックグロウル",
"https://comic-growl.com",
"all",
"https://cdn-img.comic-growl.com/public/page",
) {
override val publisher = "BUSHIROAD WORKS"
override val chapterListMode = CHAPTER_LIST_LOCKED
override val supportsLatest: Boolean = true
override val client: OkHttpClient =
super.client.newBuilder().addInterceptor(::imageIntercept).build()
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
// Show only ongoing works
override fun popularMangaSelector(): String = "ul[class=\"lineup-list ongoing\"] > li > div > a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.select("h5").text()
thumbnail_url = element.select("div > img").attr("data-src")
setUrlWithoutDomain(element.attr("href"))
}
override fun latestUpdatesSelector() =
"div[class=\"update latest\"] > div.card-board > " + "div[class~=card]:not([class~=ad]) > div > a"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
title = element.select("div.data h3").text()
thumbnail_url = element.select("div.thumb-container img").attr("data-src")
setUrlWithoutDomain(element.attr("href"))
}
override fun getCollections(): List<Collection> = listOf(
Collection("連載作品", ""),
)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$baseUrl/search".toHttpUrl().newBuilder().addQueryParameter("q", query)
return GET(url.build(), headers)
}
return GET(baseUrl, headers) // Currently just get all ongoing works
}
}

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Comick' extName = 'Comick'
extClass = '.ComickFactory' extClass = '.ComickFactory'
extVersionCode = 52 extVersionCode = 51
isNsfw = true isNsfw = true
} }

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Comico' extName = 'Comico'
extClass = '.ComicoFactory' extClass = '.ComicoFactory'
extVersionCode = 6 extVersionCode = 5
isNsfw = true isNsfw = true
} }

View File

@ -24,7 +24,6 @@ import okhttp3.Headers
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.security.MessageDigest import java.security.MessageDigest
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
@ -160,7 +159,7 @@ open class Comico(
if (!chapter.name.endsWith(LOCK)) { if (!chapter.name.endsWith(LOCK)) {
super.fetchPageList(chapter) super.fetchPageList(chapter)
} else { } else {
throw Exception("You are not authorized to view this!") throw Error("You are not authorized to view this!")
} }
private fun search(query: String, page: Int) = private fun search(query: String, page: Int) =
@ -177,7 +176,7 @@ open class Comico(
private val Response.data: JsonElement? private val Response.data: JsonElement?
get() = json.parseToJsonElement(body.string()).jsonObject.also { get() = json.parseToJsonElement(body.string()).jsonObject.also {
val code = it["result"]["code"].jsonPrimitive.int val code = it["result"]["code"].jsonPrimitive.int
if (code != 200) throw Exception(status(code)) if (code != 200) throw Error(status(code))
}["data"] }["data"]
private operator fun JsonElement?.get(key: String) = private operator fun JsonElement?.get(key: String) =

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'CosplayTele' extName = 'CosplayTele'
extClass = '.CosplayTele' extClass = '.CosplayTele'
extVersionCode = 4 extVersionCode = 3
isNsfw = true isNsfw = true
} }

View File

@ -54,7 +54,7 @@ class CosplayTele : ParsedHttpSource() {
override fun latestUpdatesNextPageSelector() = ".next.page-number" override fun latestUpdatesNextPageSelector() = ".next.page-number"
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/") override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/")
override fun latestUpdatesSelector() = "main div.box" override fun latestUpdatesSelector() = "div.box"
// Popular // Popular
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga {

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'DeviantArt' extName = 'DeviantArt'
extClass = '.DeviantArt' extClass = '.DeviantArt'
extVersionCode = 6 extVersionCode = 3
isNsfw = true isNsfw = true
} }

View File

@ -1,11 +1,6 @@
package eu.kanade.tachiyomi.extension.all.deviantart package eu.kanade.tachiyomi.extension.all.deviantart
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -20,22 +15,16 @@ import okhttp3.Response
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class DeviantArt : HttpSource(), ConfigurableSource { class DeviantArt : HttpSource() {
override val name = "DeviantArt" override val name = "DeviantArt"
override val baseUrl = "https://www.deviantart.com" override val baseUrl = "https://deviantart.com"
override val lang = "all" override val lang = "all"
override val supportsLatest = false override val supportsLatest = false
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder() = Headers.Builder().apply { override fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0") add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0")
} }
@ -87,32 +76,28 @@ class DeviantArt : HttpSource(), ConfigurableSource {
override fun mangaDetailsParse(response: Response): SManga { override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup() val document = response.asJsoup()
val gallery = document.selectFirst("#sub-folder-gallery") val subFolderGallery = document.selectFirst("#sub-folder-gallery")
val manga = SManga.create().apply {
// If manga is sub-gallery then use sub-gallery name, else use gallery name // If manga is sub-gallery then use sub-gallery name, else use gallery name
val galleryName = gallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ") title = subFolderGallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
?: gallery?.selectFirst("[aria-haspopup=listbox] > div")!!.ownText() ?: subFolderGallery?.selectFirst("[aria-haspopup=listbox] > div")!!.ownText()
val artistInTitle = preferences.artistInTitle == ArtistInTitle.ALWAYS.name ||
preferences.artistInTitle == ArtistInTitle.ONLY_ALL_GALLERIES.name && galleryName == "All"
return SManga.create().apply {
setUrlWithoutDomain(response.request.url.toString())
author = document.title().substringBefore(" ") author = document.title().substringBefore(" ")
title = when (artistInTitle) { description = subFolderGallery?.selectFirst(".legacy-journal")?.wholeText()
true -> "$author - $galleryName" thumbnail_url = subFolderGallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
false -> galleryName
}
description = gallery?.selectFirst(".legacy-journal")?.wholeText()
thumbnail_url = gallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
} }
manga.setUrlWithoutDomain(response.request.url.toString())
return manga
} }
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
val pathSegments = getMangaUrl(manga).toHttpUrl().pathSegments val pathSegments = getMangaUrl(manga).toHttpUrl().pathSegments
val username = pathSegments[0] val username = pathSegments[0]
val query = when (val folderId = pathSegments[2]) { val folderId = pathSegments[2]
"all" -> "gallery:$username"
else -> "gallery:$username/$folderId" val query = if (folderId == "all") {
"gallery:$username"
} else {
"gallery:$username/$folderId"
} }
val url = backendBuilder() val url = backendBuilder()
@ -138,14 +123,15 @@ class DeviantArt : HttpSource(), ConfigurableSource {
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href") nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
} }
return chapterList.toList().also(::indexChapterList) return indexChapterList(chapterList.toList())
} }
private fun parseToChapterList(document: Document): List<SChapter> { private fun parseToChapterList(document: Document): List<SChapter> {
val items = document.select("item") val items = document.select("item")
return items.map { return items.map {
SChapter.create().apply { val chapter = SChapter.create()
setUrlWithoutDomain(it.selectFirst("link")!!.text()) chapter.setUrlWithoutDomain(it.selectFirst("link")!!.text())
chapter.apply {
name = it.selectFirst("title")!!.text() name = it.selectFirst("title")!!.text()
date_upload = parseDate(it.selectFirst("pubDate")?.text()) date_upload = parseDate(it.selectFirst("pubDate")?.text())
scanlator = it.selectFirst("media|credit")?.text() scanlator = it.selectFirst("media|credit")?.text()
@ -153,34 +139,24 @@ class DeviantArt : HttpSource(), ConfigurableSource {
} }
} }
private fun indexChapterList(chapterList: List<SChapter>) { private fun indexChapterList(chapterList: List<SChapter>): List<SChapter> {
// DeviantArt allows users to arrange galleries arbitrarily so we will // DeviantArt allows users to arrange galleries arbitrarily so we will
// primitively index the list by checking the first and last dates // primitively index the list by checking the first and last dates
if (chapterList.first().date_upload > chapterList.last().date_upload) { return if (chapterList.first().date_upload > chapterList.last().date_upload) {
chapterList.forEachIndexed { i, chapter -> chapterList.mapIndexed { i, chapter ->
chapter.chapter_number = chapterList.size - i.toFloat() chapter.apply { chapter_number = chapterList.size - i.toFloat() }
} }
} else { } else {
chapterList.forEachIndexed { i, chapter -> chapterList.mapIndexed { i, chapter ->
chapter.chapter_number = i.toFloat() + 1 chapter.apply { chapter_number = i.toFloat() + 1 }
} }
} }
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup() val document = response.asJsoup()
val firstImageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src") val imageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
return when (val buttons = document.selectFirst("[draggable=false]")?.children()) { return listOf(Page(0, imageUrl = imageUrl))
null -> listOf(Page(0, imageUrl = firstImageUrl))
else -> buttons.mapIndexed { i, button ->
// Remove everything past "/v1/" to get original instead of thumbnail
val imageUrl = button.selectFirst("img")?.absUrl("src")?.substringBefore("/v1/")
Page(i, imageUrl = imageUrl)
}.also {
// First image needs token to get original, which is included in firstImageUrl
it[0].imageUrl = firstImageUrl
}
}
} }
override fun imageUrlParse(response: Response): String { override fun imageUrlParse(response: Response): String {
@ -191,38 +167,7 @@ class DeviantArt : HttpSource(), ConfigurableSource {
return Jsoup.parse(body.string(), request.url.toString(), Parser.xmlParser()) return Jsoup.parse(body.string(), request.url.toString(), Parser.xmlParser())
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val artistInTitlePref = ListPreference(screen.context).apply {
key = ArtistInTitle.PREF_KEY
title = "Artist name in manga title"
entries = ArtistInTitle.values().map { it.text }.toTypedArray()
entryValues = ArtistInTitle.values().map { it.name }.toTypedArray()
summary = "Current: %s\n\n" +
"Changing this preference will not automatically apply to manga in Library " +
"and History, so refresh all DeviantArt manga and/or clear database in Settings " +
"> Advanced after doing so."
setDefaultValue(ArtistInTitle.defaultValue.name)
}
screen.addPreference(artistInTitlePref)
}
private enum class ArtistInTitle(val text: String) {
NEVER("Never"),
ALWAYS("Always"),
ONLY_ALL_GALLERIES("Only in \"All\" galleries"),
;
companion object {
const val PREF_KEY = "artistInTitlePref"
val defaultValue = ONLY_ALL_GALLERIES
}
}
private val SharedPreferences.artistInTitle
get() = getString(ArtistInTitle.PREF_KEY, ArtistInTitle.defaultValue.name)
companion object { companion object {
private const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}" const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
} }
} }

View File

@ -1,8 +1,8 @@
ext { ext {
extName = 'Manga18Free' extName = 'Eromanhwa'
extClass = '.manga18free' extClass = '.Eromanhwa'
themePkg = 'madara' themePkg = 'madara'
baseUrl = 'https://manga18free.com' baseUrl = 'https://eromanhwa.org'
overrideVersionCode = 1 overrideVersionCode = 1
isNsfw = true isNsfw = true
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.extension.all.eromanhwa
import eu.kanade.tachiyomi.multisrc.madara.Madara
class Eromanhwa : Madara(
"Eromanhwa",
"https://eromanhwa.org",
"all",
) {
override val id = 3597355706480775153 // accidently set lang to en...
override val useNewChapterEndpoint = true
}

View File

@ -0,0 +1,8 @@
ext {
extName = 'Frelein Books'
extClass = '.FreleinBooks'
extVersionCode = 1
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -0,0 +1,271 @@
package eu.kanade.tachiyomi.extension.all.freleinbooks
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
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.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class FreleinBooks() : ParsedHttpSource() {
override val baseUrl = "https://books.frelein.my.id"
override val lang = "all"
override val name = "Frelein Books"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val Element.imgSrc: String
get() = attr("data-lazy-src")
.ifEmpty { attr("data-src") }
.ifEmpty { attr("src") }
// Latest
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = element.selectFirst("img")!!.imgSrc
manga.title = element.select(".postTitle").text()
manga.setUrlWithoutDomain(element.select(".postTitle > a").attr("abs:href"))
return manga
}
override fun latestUpdatesNextPageSelector() = ".olderLink"
override fun latestUpdatesRequest(page: Int): Request {
return if (page == 1) {
GET(baseUrl)
} else {
val dateParam = page * 7 * 2
// Calendar set to the current date
val calendar: Calendar = Calendar.getInstance()
// rollback 14 days
calendar.add(Calendar.DAY_OF_YEAR, -dateParam)
val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
// now the date is 14 days back
GET("$baseUrl/search?updated-max=${formatter.format(calendar.time)}T12:38:00%2B07:00&max-results=12&start=12&by-date=false")
}
}
override fun latestUpdatesSelector() = ".blogPosts > article"
// Popular
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = element.selectFirst("img")!!.imgSrc
manga.title = element.select("h3").text()
manga.setUrlWithoutDomain(element.select("h3 > a").attr("abs:href"))
return manga
}
override fun popularMangaNextPageSelector(): String? = null
override fun popularMangaRequest(page: Int) = latestUpdatesRequest(page)
override fun popularMangaSelector() = ".itemPopulars article"
// Search
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val tagFilter = filterList.findInstance<TagFilter>()!!
val groupFilter = filterList.findInstance<GroupFilter>()!!
val magazineFilter = filterList.findInstance<MagazineFilter>()!!
val fashionMagazineFilter = filterList.findInstance<FashionMagazineFilter>()!!
return when {
query.isEmpty() && groupFilter.state != 0 -> GET("$baseUrl/search/label/${groupFilter.toUriPart()}")
query.isEmpty() && magazineFilter.state != 0 -> GET("$baseUrl/search/label/${magazineFilter.toUriPart()}")
query.isEmpty() && fashionMagazineFilter.state != 0 -> GET("$baseUrl/search/label/${fashionMagazineFilter.toUriPart()}")
query.isEmpty() && tagFilter.state.isNotEmpty() -> GET("$baseUrl/search/label/${tagFilter.state}")
query.isNotEmpty() -> GET("$baseUrl/search?q=$query")
else -> latestUpdatesRequest(page)
}
}
override fun searchMangaSelector() = latestUpdatesSelector()
// Details
override fun mangaDetailsParse(document: Document): SManga {
val manga = SManga.create()
manga.title = document.select(".postTitle").text()
manga.description = "Read ${document.select(".postTitle").text()} \n \nNote: If you encounters error when opening the magazine, please press the WebView button then leave a comment on our web so we can update it soon."
manga.genre = document.select(".labelLink > a")
.joinToString(", ") { it.text() }
manga.status = SManga.COMPLETED
return manga
}
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(element.select("link[rel=\"canonical\"]").attr("href"))
chapter.name = "Gallery"
chapter.date_upload = getDate(element.select("link[rel=\"canonical\"]").attr("href"))
return chapter
}
override fun chapterListSelector() = "html"
// Pages
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
document.select("noscript").remove()
document.select(".gallerybox a > img").forEachIndexed { i, it ->
// format new img/b/
if (it.imgSrc.contains("img/b/")) {
if (it.imgSrc.contains("/w768-rw/")) {
val itUrl = it.imgSrc.replace("/w768-rw/", "/s0/")
pages.add(Page(i, itUrl, itUrl))
}
if (it.imgSrc.contains("/w480-rw/")) {
val itUrl = it.imgSrc.replace("/w480-rw/", "/s0/")
pages.add(Page(i, itUrl, itUrl))
}
}
// format new img/b/
else {
if (it.imgSrc.contains("=w768-rw")) {
val itUrl = it.imgSrc.replace("=w768-rw", "")
pages.add(Page(i, itUrl, itUrl))
} else if (it.imgSrc.contains("=w480-rw")) {
val itUrl = it.imgSrc.replace("=w480-rw", "")
pages.add(Page(i, itUrl, itUrl))
} else {
val itUrl = it.imgSrc
pages.add(Page(i, itUrl, itUrl))
}
}
}
return pages
}
override fun imageUrlParse(document: Document): String =
throw UnsupportedOperationException()
// Filters
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Only one filter will be applied!"),
Filter.Separator(),
GroupFilter(),
MagazineFilter(),
FashionMagazineFilter(),
TagFilter(),
)
open class UriPartFilter(
displayName: String,
private val valuePair: Array<Pair<String, String>>,
) : Filter.Select<String>(displayName, valuePair.map { it.first }.toTypedArray()) {
fun toUriPart() = valuePair[state].second
}
class MagazineFilter : UriPartFilter(
"Magazine",
arrayOf(
Pair("Any", ""),
Pair("B.L.T.", "B.L.T."),
Pair("BIG ONE GIRLS", "BIG ONE GIRLS"),
Pair("BOMB!", "BOMB!"),
Pair("BRODY", "BRODY"),
Pair("BUBKA", "BUBKA"),
Pair("ENTAME", "ENTAME"),
Pair("EX Taishu", "EX Taishu"),
Pair("FINEBOYS", "FINEBOYS"),
Pair("FLASH", "FLASH"),
Pair("Fine", "Fine"),
Pair("Friday", "Friday"),
Pair("HINA_SATSU", "HINA_SATSU"),
Pair("IDOL AND READ", "IDOL AND READ"),
Pair("Kadokawa Scene 07", "Kadokawa Scene 07"),
Pair("Monthly Basketball", "Monthly Basketball"),
Pair("Monthly Young Magazine", "Monthly Young Magazine"),
Pair("NOGI_SATSU", "NOGI_SATSU"),
Pair("Nylon Japan", "Nylon Japan"),
Pair("Platinum FLASH", "Platinum FLASH"),
Pair("Shonen Magazine", "Shonen Magazine"),
Pair("Shukan Post", "Shukan Post"),
Pair("TOKYO NEWS MOOK", "TOKYO NEWS MOOK"),
Pair("TV LIFE,Tarzan", "TV LIFE,Tarzan"),
Pair("Tokyo Calendar", "Tokyo Calendar"),
Pair("Top Yell NEO", "Top Yell NEO"),
Pair("UTB", "UTB"),
Pair("Weekly Playboy", "Weekly Playboy"),
Pair("Weekly SPA", "Weekly SPA"),
Pair("Weekly SPA!", "Weekly SPA!"),
Pair("Weekly Shonen Champion", "Weekly Shonen Champion"),
Pair("Weekly Shonen Magazine", "Weekly Shonen Magazine"),
Pair("Weekly Shonen Sunday", "Weekly Shonen Sunday"),
Pair("Weekly Shounen Magazine", "Weekly Shounen Magazine"),
Pair("Weekly The Television Plus", "Weekly The Television Plus"),
Pair("Weekly Zero Jump", "Weekly Zero Jump"),
Pair("Yanmaga Web", "Yanmaga Web"),
Pair("Young Animal", "Young Animal"),
Pair("Young Champion", "Young Champion"),
Pair("Young Gangan", "Young Gangan"),
Pair("Young Jump", "Young Jump"),
Pair("Young Magazine", "Young Magazine"),
Pair("blt graph.", "blt graph."),
Pair("mini", "mini"),
),
)
class FashionMagazineFilter : UriPartFilter(
"Fashion Magazine",
arrayOf(
Pair("Any", ""),
Pair("BAILA", "BAILA"),
Pair("Biteki", "Biteki"),
Pair("CLASSY", "CLASSY"),
Pair("CanCam", "CanCam"),
Pair("JJ", "JJ"),
Pair("LARME", "LARME"),
Pair("MARQUEE", "MARQUEE"),
Pair("Maquia", "Maquia"),
Pair("Men's non-no", "Men's non-no"),
Pair("More", "More"),
Pair("Oggi", "Oggi"),
Pair("Ray", "Ray"),
Pair("Seventeen", "Seventeen"),
Pair("Sweet", "Sweet"),
Pair("VOCE", "VOCE"),
Pair("ViVi", "ViVi"),
Pair("With", "With"),
Pair("aR", "aR"),
Pair("anan", "anan"),
Pair("bis", "bis"),
Pair("non-no", "non-no"),
),
)
class GroupFilter : UriPartFilter(
"Group",
arrayOf(
Pair("Any", ""),
Pair("Hinatazaka46", "Hinatazaka46"),
Pair("Nogizaka46", "Nogizaka46"),
Pair("Sakurazaka46", "Sakurazaka46"),
Pair("Keyakizaka46", "Keyakizaka46"),
),
)
class TagFilter : Filter.Text("Tag")
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
private fun getDate(str: String): Long {
val regex = "[0-9]{4}\\/[0-9]{2}\\/[0-9]{2}".toRegex()
val match = regex.find(str)
return runCatching { DATE_FORMAT.parse(match!!.value)?.time }.getOrNull() ?: 0L
}
companion object {
private val DATE_FORMAT by lazy {
SimpleDateFormat("yyyy/MM/dd", Locale.US)
}
}
}

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hitomi' extName = 'Hitomi'
extClass = '.HitomiFactory' extClass = '.HitomiFactory'
extVersionCode = 36 extVersionCode = 35
isNsfw = true isNsfw = true
} }

View File

@ -64,11 +64,8 @@ class Hitomi(
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val REGEX_IMAGE_URL = """https://.*?a\.$domain/(jxl|avif|webp)/\d+?/\d+/([0-9a-f]{64})\.\1""".toRegex()
override val client = network.cloudflareClient.newBuilder() override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::jxlContentTypeInterceptor) .addInterceptor(::jxlContentTypeInterceptor)
.addInterceptor(::updateImageUrlInterceptor)
.apply { .apply {
interceptors().add(0, ::streamResetRetry) interceptors().add(0, ::streamResetRetry)
} }
@ -751,25 +748,6 @@ class Hitomi(
} }
} }
private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val cleanUrl = request.url.run { "$scheme://$host$encodedPath" }
REGEX_IMAGE_URL.matchEntire(cleanUrl)?.let { match ->
val (ext, hash) = match.destructured
val commonId = runBlocking { commonImageId() }
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + runBlocking { subdomainOffset(imageId) }
val newUrl = "https://${subDomain}a.$domain/$ext/$commonId$imageId/$hash.$ext"
val newRequest = request.newBuilder().url(newUrl).build()
return chain.proceed(newRequest)
}
return chain.proceed(request)
}
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Little Garden' extName = 'Little Garden'
extClass = '.LittleGarden' extClass = '.LittleGarden'
extVersionCode = 3 extVersionCode = 2
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -31,7 +31,7 @@ class LittleGarden : ParsedHttpSource() {
private const val cdnUrl = "https://littlexgarden.com/static/images/webp/" private const val cdnUrl = "https://littlexgarden.com/static/images/webp/"
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull() private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
private val slugRegex = Regex("\\\\\"slug\\\\\":\\\\\"(.*?(?=\\\\\"))") private val slugRegex = Regex("\\\\\"slug\\\\\":\\\\\"(.*?(?=\\\\\"))")
private val oricolPageRegex = Regex("\\{colored:(.*?(?=,)),original:(.*?(?=,))") private val oricolPageRegex = Regex("\\{colored:(?<colored>.*?(?=,)),original:(?<original>.*?(?=,))")
private val oriPageRegex = Regex("""original:"(.*?(?="))""") private val oriPageRegex = Regex("""original:"(.*?(?="))""")
} }
@ -176,10 +176,10 @@ class LittleGarden : ParsedHttpSource() {
val engChaps: IntArray = intArrayOf(970, 987, 992) val engChaps: IntArray = intArrayOf(970, 987, 992)
if (document.selectFirst("div.manga-name")!!.text().trim() == "One Piece" && (engChaps.contains(chapNb) || chapNb > 1004)) { // Permits to get French pages rather than English pages for some chapters if (document.selectFirst("div.manga-name")!!.text().trim() == "One Piece" && (engChaps.contains(chapNb) || chapNb > 1004)) { // Permits to get French pages rather than English pages for some chapters
oricolPageRegex.findAll(document.select("script:containsData(pages)").toString()).asIterable().mapIndexed { i, it -> oricolPageRegex.findAll(document.select("script:containsData(pages)").toString()).asIterable().mapIndexed { i, it ->
if (it.groups[1]?.value?.contains("\"") == true) { // Their JS dict has " " around the link only when available. Also uses colored pages rather than B&W as it's the main strength of this site if (it.groups["colored"]?.value?.contains("\"") == true) { // Their JS dict has " " around the link only when available. Also uses colored pages rather than B&W as it's the main strength of this site
pages.add(Page(i, "", cdnUrl + it.groups[1]?.value?.replace("\"", "") + ".webp")) pages.add(Page(i, "", cdnUrl + it.groups["colored"]?.value?.replace("\"", "") + ".webp"))
} else { } else {
pages.add(Page(i, "", cdnUrl + it.groups[2]?.value?.replace("\"", "") + ".webp")) pages.add(Page(i, "", cdnUrl + it.groups["original"]?.value?.replace("\"", "") + ".webp"))
} }
} }
} else { } else {

View File

@ -23,9 +23,6 @@ data_saver_summary=Enables smaller, more compressed images
excluded_tags_mode=Excluded tags mode excluded_tags_mode=Excluded tags mode
filter_original_languages=Filter original languages filter_original_languages=Filter original languages
filter_original_languages_summary=Only show content that was originally published in the selected languages in both latest and browse filter_original_languages_summary=Only show content that was originally published in the selected languages in both latest and browse
final_chapter=Final chapter:
final_chapter_in_description=Final chapter in description
final_chapter_in_description_summary=Include a manga's final chapter number at the end of its description
format=Format format=Format
format_adaptation=Adaptation format_adaptation=Adaptation
format_anthology=Anthology format_anthology=Anthology
@ -79,8 +76,8 @@ original_language=Original language
original_language_filter_chinese=%s (Manhua) original_language_filter_chinese=%s (Manhua)
original_language_filter_japanese=%s (Manga) original_language_filter_japanese=%s (Manga)
original_language_filter_korean=%s (Manhwa) original_language_filter_korean=%s (Manhwa)
prefer_title_in_extension_language=Use alternative titles prefer_title_in_extension_language=Use Alternate Titles
prefer_title_in_extension_language_summary=If there is an alternative title available which matches the extension language, it will be used prefer_title_in_extension_language_summary=If there is an alternate title available which matches the extension language, it will be used
publication_demographic=Publication demographic publication_demographic=Publication demographic
publication_demographic_josei=Josei publication_demographic_josei=Josei
publication_demographic_none=None publication_demographic_none=None

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'MangaDex' extName = 'MangaDex'
extClass = '.MangaDexFactory' extClass = '.MangaDexFactory'
extVersionCode = 199 extVersionCode = 196
isNsfw = true isNsfw = true
} }

View File

@ -143,11 +143,6 @@ object MDConstants {
return "${preferExtensionLangTitlePref}_$dexLang" return "${preferExtensionLangTitlePref}_$dexLang"
} }
private const val finalChapterInDescPref = "finalChapterInDesc"
fun getFinalChapterInDescPrefKey(dexLang: String): String {
return "${finalChapterInDescPref}_$dexLang"
}
private const val tagGroupContent = "content" private const val tagGroupContent = "content"
private const val tagGroupFormat = "format" private const val tagGroupFormat = "format"
private const val tagGroupGenre = "genre" private const val tagGroupGenre = "genre"

View File

@ -424,7 +424,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
preferences.coverQuality, preferences.coverQuality,
preferences.altTitlesInDesc, preferences.altTitlesInDesc,
preferences.preferExtensionLangTitle, preferences.preferExtensionLangTitle,
preferences.finalChapterInDesc,
) )
} }
@ -774,28 +773,12 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
} }
} }
val finalChapterInDescPref = SwitchPreferenceCompat(screen.context).apply {
key = MDConstants.getFinalChapterInDescPrefKey(dexLang)
title = helper.intl["final_chapter_in_description"]
summary = helper.intl["final_chapter_in_description_summary"]
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit()
.putBoolean(MDConstants.getFinalChapterInDescPrefKey(dexLang), checkValue)
.commit()
}
}
screen.addPreference(coverQualityPref) screen.addPreference(coverQualityPref)
screen.addPreference(tryUsingFirstVolumeCoverPref) screen.addPreference(tryUsingFirstVolumeCoverPref)
screen.addPreference(dataSaverPref) screen.addPreference(dataSaverPref)
screen.addPreference(standardHttpsPortPref) screen.addPreference(standardHttpsPortPref)
screen.addPreference(altTitlesInDescPref) screen.addPreference(altTitlesInDescPref)
screen.addPreference(preferExtensionLangTitlePref) screen.addPreference(preferExtensionLangTitlePref)
screen.addPreference(finalChapterInDescPref)
screen.addPreference(contentRatingPref) screen.addPreference(contentRatingPref)
screen.addPreference(originalLanguagePref) screen.addPreference(originalLanguagePref)
screen.addPreference(blockedGroupsPref) screen.addPreference(blockedGroupsPref)
@ -877,9 +860,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
private val SharedPreferences.preferExtensionLangTitle private val SharedPreferences.preferExtensionLangTitle
get() = getBoolean(MDConstants.getPreferExtensionLangTitlePrefKey(dexLang), true) get() = getBoolean(MDConstants.getPreferExtensionLangTitlePrefKey(dexLang), true)
private val SharedPreferences.finalChapterInDesc
get() = getBoolean(MDConstants.getFinalChapterInDescPrefKey(dexLang), true)
/** /**
* Previous versions of the extension allowed invalid UUID values to be stored in the * Previous versions of the extension allowed invalid UUID values to be stored in the
* preferences. This method clear invalid UUIDs in case the user have updated from * preferences. This method clear invalid UUIDs in case the user have updated from

View File

@ -156,7 +156,7 @@ class MangaDexHelper(lang: String) {
*/ */
private fun String.removeEntitiesAndMarkdown(): String { private fun String.removeEntitiesAndMarkdown(): String {
return removeEntities() return removeEntities()
.substringBefore("\n---") .substringBefore("---")
.replace(markdownLinksRegex, "$1") .replace(markdownLinksRegex, "$1")
.replace(markdownItalicBoldRegex, "$1") .replace(markdownItalicBoldRegex, "$1")
.replace(markdownItalicRegex, "$1") .replace(markdownItalicRegex, "$1")
@ -324,7 +324,6 @@ class MangaDexHelper(lang: String) {
coverSuffix: String?, coverSuffix: String?,
altTitlesInDesc: Boolean, altTitlesInDesc: Boolean,
preferExtensionLangTitle: Boolean, preferExtensionLangTitle: Boolean,
finalChapterInDesc: Boolean,
): SManga { ): SManga {
val attr = mangaDataDto.attributes!! val attr = mangaDataDto.attributes!!
@ -366,12 +365,9 @@ class MangaDexHelper(lang: String) {
val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres
// Build description var desc = (attr.description[lang] ?: attr.description["en"])
val desc = mutableListOf<String>()
(attr.description[lang] ?: attr.description["en"])
?.removeEntitiesAndMarkdown() ?.removeEntitiesAndMarkdown()
?.let { desc.add(it) } .orEmpty()
if (altTitlesInDesc) { if (altTitlesInDesc) {
val romanizedOriginalLang = MDConstants.romanizedLangCodes[attr.originalLanguage].orEmpty() val romanizedOriginalLang = MDConstants.romanizedLangCodes[attr.originalLanguage].orEmpty()
@ -383,24 +379,12 @@ class MangaDexHelper(lang: String) {
if (altTitles.isNotEmpty()) { if (altTitles.isNotEmpty()) {
val altTitlesDesc = altTitles val altTitlesDesc = altTitles
.joinToString("\n", "${intl["alternative_titles"]}\n") { "$it" } .joinToString("\n", "${intl["alternative_titles"]}\n") { "$it" }
desc.add(altTitlesDesc.removeEntities()) desc += (if (desc.isBlank()) "" else "\n\n") + altTitlesDesc.removeEntities()
}
}
if (finalChapterInDesc) {
val finalChapter = mutableListOf<String>()
attr.lastVolume?.takeIf { it.isNotEmpty() }?.let { finalChapter.add("Vol.$it") }
attr.lastChapter?.takeIf { it.isNotEmpty() }?.let { finalChapter.add("Ch.$it") }
if (finalChapter.isNotEmpty()) {
val finalChapterDesc = finalChapter
.joinToString(" ", "${intl["final_chapter"]}\n")
desc.add(finalChapterDesc.removeEntities())
} }
} }
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang, preferExtensionLangTitle).apply { return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang, preferExtensionLangTitle).apply {
description = desc.joinToString("\n\n") description = desc
author = authors.joinToString() author = authors.joinToString()
artist = artists.joinToString() artist = artists.joinToString()
status = getPublicationStatus(attr, chapters) status = getPublicationStatus(attr, chapters)

View File

@ -1,7 +1,9 @@
ext { ext {
extName = 'MangaFire' extName = 'MangaFire'
extClass = '.MangaFireFactory' extClass = '.MangaFireFactory'
extVersionCode = 10 themePkg = 'mangareader'
baseUrl = 'https://mangafire.to'
overrideVersionCode = 5
isNsfw = true isNsfw = true
} }

View File

@ -1,190 +1,166 @@
package eu.kanade.tachiyomi.extension.all.mangafire package eu.kanade.tachiyomi.extension.all.mangafire
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
import java.util.Calendar
interface UriFilter { class Entry(name: String, val id: String) : Filter.CheckBox(name) {
fun addToUri(builder: HttpUrl.Builder) constructor(name: String) : this(name, name)
} }
open class UriPartFilter( sealed class Group(
name: String, name: String,
private val param: String, val param: String,
private val vals: Array<Pair<String, String>>, values: List<Entry>,
defaultValue: String? = null, ) : Filter.Group<Entry>(name, values)
) : Filter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name) sealed class Select(
open class UriMultiSelectFilter(
name: String, name: String,
private val param: String, val param: String,
private val vals: Array<Pair<String, String>>, private val valuesMap: Map<String, String>,
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter { ) : Filter.Select<String>(name, valuesMap.keys.toTypedArray()) {
override fun addToUri(builder: HttpUrl.Builder) { open val selection: String
val checked = state.filter { it.state } get() = valuesMap[values[state]]!!
checked.forEach {
builder.addQueryParameter(param, it.value)
}
}
} }
open class UriTriSelectOption(name: String, val value: String) : Filter.TriState(name) class TypeFilter : Group("Type", "type[]", types)
open class UriTriSelectFilter( private val types: List<Entry>
name: String, get() = listOf(
private val param: String, Entry("Manga", "manga"),
private val vals: Array<Pair<String, String>>, Entry("One-Shot", "one_shot"),
) : Filter.Group<UriTriSelectOption>(name, vals.map { UriTriSelectOption(it.first, it.second) }), UriFilter { Entry("Doujinshi", "doujinshi"),
override fun addToUri(builder: HttpUrl.Builder) { Entry("Light-Novel", "light_novel"),
state.forEach { s -> Entry("Novel", "novel"),
when (s.state) { Entry("Manhwa", "manhwa"),
TriState.STATE_INCLUDE -> builder.addQueryParameter(param, s.value) Entry("Manhua", "manhua"),
TriState.STATE_EXCLUDE -> builder.addQueryParameter(param, "-${s.value}") )
}
} class Genre(name: String, val id: String) : Filter.TriState(name) {
} val selection: String
get() = (if (isExcluded()) "-" else "") + id
} }
class TypeFilter : UriMultiSelectFilter( class GenresFilter : Filter.Group<Genre>("Genre", genres) {
"Type", val param = "genre[]"
"type",
arrayOf(
Pair("Manga", "manga"),
Pair("One-Shot", "one_shot"),
Pair("Doujinshi", "doujinshi"),
Pair("Novel", "novel"),
Pair("Manhwa", "manhwa"),
Pair("Manhua", "manhua"),
),
)
class GenreFilter : UriTriSelectFilter( val combineMode: Boolean
"Genres", get() = state.filter { !it.isIgnored() }.size > 1
"genre[]",
arrayOf(
Pair("Action", "1"),
Pair("Adventure", "78"),
Pair("Avant Garde", "3"),
Pair("Boys Love", "4"),
Pair("Comedy", "5"),
Pair("Demons", "77"),
Pair("Drama", "6"),
Pair("Ecchi", "7"),
Pair("Fantasy", "79"),
Pair("Girls Love", "9"),
Pair("Gourmet", "10"),
Pair("Harem", "11"),
Pair("Horror", "530"),
Pair("Isekai", "13"),
Pair("Iyashikei", "531"),
Pair("Josei", "15"),
Pair("Kids", "532"),
Pair("Magic", "539"),
Pair("Mahou Shoujo", "533"),
Pair("Martial Arts", "534"),
Pair("Mecha", "19"),
Pair("Military", "535"),
Pair("Music", "21"),
Pair("Mystery", "22"),
Pair("Parody", "23"),
Pair("Psychological", "536"),
Pair("Reverse Harem", "25"),
Pair("Romance", "26"),
Pair("School", "73"),
Pair("Sci-Fi", "28"),
Pair("Seinen", "537"),
Pair("Shoujo", "30"),
Pair("Shounen", "31"),
Pair("Slice of Life", "538"),
Pair("Space", "33"),
Pair("Sports", "34"),
Pair("Super Power", "75"),
Pair("Supernatural", "76"),
Pair("Suspense", "37"),
Pair("Thriller", "38"),
Pair("Vampire", "39"),
),
)
class GenreModeFilter : Filter.CheckBox("Must have all the selected genres"), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
if (state) {
builder.addQueryParameter("genre_mode", "and")
}
}
} }
class StatusFilter : UriMultiSelectFilter( private val genres: List<Genre>
"Status", get() = listOf(
"status[]", Genre("Action", "1"),
arrayOf( Genre("Adventure", "78"),
Pair("Completed", "completed"), Genre("Avant Garde", "3"),
Pair("Releasing", "releasing"), Genre("Boys Love", "4"),
Pair("On Hiatus", "on_hiatus"), Genre("Comedy", "5"),
Pair("Discontinued", "discontinued"), Genre("Demons", "77"),
Pair("Not Yet Published", "info"), Genre("Drama", "6"),
), Genre("Ecchi", "7"),
) Genre("Fantasy", "79"),
Genre("Girls Love", "9"),
Genre("Gourmet", "10"),
Genre("Harem", "11"),
Genre("Horror", "530"),
Genre("Isekai", "13"),
Genre("Iyashikei", "531"),
Genre("Josei", "15"),
Genre("Kids", "532"),
Genre("Magic", "539"),
Genre("Mahou Shoujo", "533"),
Genre("Martial Arts", "534"),
Genre("Mecha", "19"),
Genre("Military", "535"),
Genre("Music", "21"),
Genre("Mystery", "22"),
Genre("Parody", "23"),
Genre("Psychological", "536"),
Genre("Reverse Harem", "25"),
Genre("Romance", "26"),
Genre("School", "73"),
Genre("Sci-Fi", "28"),
Genre("Seinen", "537"),
Genre("Shoujo", "30"),
Genre("Shounen", "31"),
Genre("Slice of Life", "538"),
Genre("Space", "33"),
Genre("Sports", "34"),
Genre("Super Power", "75"),
Genre("Supernatural", "76"),
Genre("Suspense", "37"),
Genre("Thriller", "38"),
Genre("Vampire", "39"),
)
class YearFilter : UriMultiSelectFilter( class StatusFilter : Group("Status", "status[]", statuses)
"Year",
"year[]",
years,
) {
companion object {
private val currentYear by lazy {
Calendar.getInstance()[Calendar.YEAR]
}
private val years: Array<Pair<String, String>> = buildList(29) { private val statuses: List<Entry>
addAll( get() = listOf(
(currentYear downTo (currentYear - 20)).map(Int::toString), Entry("Completed", "completed"),
) Entry("Releasing", "releasing"),
Entry("On Hiatus", "on_hiatus"),
Entry("Discontinued", "discontinued"),
Entry("Not Yet Published", "info"),
)
addAll( class YearFilter : Group("Year", "year[]", years)
(2000 downTo 1930 step 10).map { "${it}s" },
)
}.map { Pair(it, it) }.toTypedArray()
}
}
class MinChapterFilter : Filter.Text("Minimum chapter length"), UriFilter { private val years: List<Entry>
override fun addToUri(builder: HttpUrl.Builder) { get() = listOf(
if (state.isNotEmpty()) { Entry("2023"),
val value = state.toIntOrNull()?.takeIf { it > 0 } Entry("2022"),
?: throw IllegalArgumentException("Minimum chapter length must be a positive integer greater than 0") Entry("2021"),
Entry("2020"),
Entry("2019"),
Entry("2018"),
Entry("2017"),
Entry("2016"),
Entry("2015"),
Entry("2014"),
Entry("2013"),
Entry("2012"),
Entry("2011"),
Entry("2010"),
Entry("2009"),
Entry("2008"),
Entry("2007"),
Entry("2006"),
Entry("2005"),
Entry("2004"),
Entry("2003"),
Entry("2000s"),
Entry("1990s"),
Entry("1980s"),
Entry("1970s"),
Entry("1960s"),
Entry("1950s"),
Entry("1940s"),
)
builder.addQueryParameter("minchap", value.toString()) class ChapterCountFilter : Select("Chapter Count", "minchap", chapterCounts)
}
}
}
class SortFilter(defaultValue: String? = null) : UriPartFilter( private val chapterCounts
"Sort", get() = mapOf(
"sort", "Any" to "",
arrayOf( "At least 1 chapter" to "1",
Pair("Most relevance", "most_relevance"), "At least 3 chapters" to "3",
Pair("Recently updated", "recently_updated"), "At least 5 chapters" to "5",
Pair("Recently added", "recently_added"), "At least 10 chapters" to "10",
Pair("Release date", "release_date"), "At least 20 chapters" to "20",
Pair("Trending", "trending"), "At least 30 chapters" to "30",
Pair("Name A-Z", "title_az"), "At least 50 chapters" to "50",
Pair("Scores", "scores"), )
Pair("MAL scores", "mal_scores"),
Pair("Most viewed", "most_viewed"), class SortFilter : Select("Sort", "sort", orders)
Pair("Most favourited", "most_favourited"),
), private val orders
defaultValue, get() = mapOf(
) "Trending" to "trending",
"Recently updated" to "recently_updated",
"Recently added" to "recently_added",
"Release date" to "release_date",
"Name A-Z" to "title_az",
"Score" to "scores",
"MAL score" to "mal_scores",
"Most viewed" to "most_viewed",
"Most favourited" to "most_favourited",
)

View File

@ -1,17 +1,12 @@
package eu.kanade.tachiyomi.extension.all.mangafire package eu.kanade.tachiyomi.extension.all.mangafire
import android.app.Application import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -23,245 +18,182 @@ import okhttp3.Response
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import org.jsoup.select.Evaluator
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class MangaFire( open class MangaFire(
override val lang: String, override val lang: String,
private val langCode: String = lang, private val langCode: String = lang,
) : ConfigurableSource, HttpSource() { ) : MangaReader() {
override val name = "MangaFire" override val name = "MangaFire"
override val baseUrl = "https://mangafire.to" override val baseUrl = "https://mangafire.to"
override val supportsLatest = true
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val preferences by lazy { override val client = super.client.newBuilder()
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!! .addInterceptor(ImageInterceptor)
} .build()
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build() override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
// ============================== Popular =============================== override fun popularMangaRequest(page: Int) =
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(defaultValue = "most_viewed")),
)
}
override fun popularMangaParse(response: Response) = searchMangaParse(response)
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
return searchMangaRequest(
page,
"",
FilterList(SortFilter(defaultValue = "recently_updated")),
)
}
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply { val urlBuilder = baseUrl.toHttpUrl().newBuilder()
addPathSegment("filter") if (query.isNotBlank()) {
urlBuilder.addPathSegment("filter").apply {
if (query.isNotBlank()) {
addQueryParameter("keyword", query) addQueryParameter("keyword", query)
addQueryParameter("page", page.toString())
} }
} else {
val filterList = filters.ifEmpty { getFilterList() } urlBuilder.addPathSegment("filter").apply {
filterList.filterIsInstance<UriFilter>().forEach { addQueryParameter("language[]", langCode)
it.addToUri(this) addQueryParameter("page", page.toString())
} filters.ifEmpty(::getFilterList).forEach { filter ->
when (filter) {
addQueryParameter("language[]", langCode) is Group -> {
addQueryParameter("page", page.toString()) filter.state.forEach {
}.build() if (it.state) {
addQueryParameter(filter.param, it.id)
return GET(url, headers) }
} }
}
override fun searchMangaParse(response: Response): MangasPage { is Select -> {
val document = response.asJsoup() addQueryParameter(filter.param, filter.selection)
var entries = document.select(searchMangaSelector()).map(::searchMangaFromElement) }
if (preferences.getBoolean(SHOW_VOLUME_PREF, false)) { is GenresFilter -> {
entries = entries.flatMapTo(ArrayList(entries.size * 2)) { manga -> filter.state.forEach {
val volume = SManga.create().apply { if (it.state != 0) {
url = manga.url + VOLUME_URL_SUFFIX addQueryParameter(filter.param, it.selection)
title = VOLUME_TITLE_PREFIX + manga.title }
thumbnail_url = manga.thumbnail_url }
if (filter.combineMode) {
addQueryParameter("genre_mode", "and")
}
}
else -> {}
}
} }
listOf(manga, volume)
} }
} }
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null return GET(urlBuilder.build(), headers)
return MangasPage(entries, hasNextPage)
} }
private fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link" override fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
private fun searchMangaSelector() = ".original.card-lg .unit .inner" override fun searchMangaSelector() = ".original.card-lg .unit .inner"
private fun searchMangaFromElement(element: Element) = SManga.create().apply { override fun searchMangaFromElement(element: Element) =
element.selectFirst(".info > a")!!.let { SManga.create().apply {
setUrlWithoutDomain(it.attr("href")) element.selectFirst(".info > a")!!.let {
title = it.ownText() setUrlWithoutDomain(it.attr("href"))
} title = it.ownText()
thumbnail_url = element.selectFirst("img")?.attr("abs:src") }
} element.selectFirst(Evaluator.Tag("img"))!!.let {
thumbnail_url = it.attr("src")
// =============================== Filters ==============================
override fun getFilterList() = FilterList(
TypeFilter(),
GenreFilter(),
GenreModeFilter(),
StatusFilter(),
YearFilter(),
MinChapterFilter(),
SortFilter(),
)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
override fun mangaDetailsParse(response: Response): SManga {
return mangaDetailsParse(response.asJsoup()).apply {
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
title = VOLUME_TITLE_PREFIX + title
} }
} }
}
private fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
with(document.selectFirst(".main-inner:not(.manga-bottom)")!!) { val root = document.selectFirst(".info")!!
title = selectFirst("h1")!!.text() val mangaTitle = root.child(1).ownText()
thumbnail_url = selectFirst(".poster img")?.attr("src") title = mangaTitle
status = selectFirst(".info > p").parseStatus() description = document.run {
description = buildString { val description = selectFirst(Evaluator.Class("description"))!!.ownText()
document.selectFirst("#synopsis .modal-content")?.textNodes()?.let { when (val altTitle = root.child(2).ownText()) {
append(it.joinToString("\n\n")) "", mangaTitle -> description
} else -> "$description\n\nAlternative Title: $altTitle"
selectFirst("h6")?.let {
append("\n\nAlternative title: ${it.text()}")
}
}.trim()
selectFirst(".meta")?.let {
author = it.selectFirst("span:contains(Author:) + span")?.text()
val type = it.selectFirst("span:contains(Type:) + span")?.text()
val genres = it.selectFirst("span:contains(Genres:) + span")?.text()
genre = listOfNotNull(type, genres).joinToString()
} }
} }
thumbnail_url = document.selectFirst(".poster")!!
.selectFirst("img")!!.attr("src")
status = when (root.child(0).ownText()) {
"Completed" -> SManga.COMPLETED
"Releasing" -> SManga.ONGOING
"On_hiatus" -> SManga.ON_HIATUS
"Discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
with(document.selectFirst(Evaluator.Class("meta"))!!) {
author = selectFirst("span:contains(Author:) + span")?.text()
val type = selectFirst("span:contains(Type:) + span")?.text()
val genres = selectFirst("span:contains(Genres:) + span")?.text()
genre = listOfNotNull(type, genres).joinToString()
}
} }
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) { override val chapterType get() = "chapter"
"releasing" -> SManga.ONGOING override val volumeType get() = "volume"
"completed" -> SManga.COMPLETED
"on_hiatus" -> SManga.ON_HIATUS override fun chapterListRequest(mangaUrl: String, type: String): Request {
"discontinued" -> SManga.CANCELLED val id = mangaUrl.substringAfterLast('.')
else -> SManga.UNKNOWN return GET("$baseUrl/ajax/manga/$id/$type/$langCode", headers)
} }
// ============================== Chapters ============================== override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
override fun getChapterUrl(chapter: SChapter): String { val document = Jsoup.parse(result)
return baseUrl + chapter.url.substringBeforeLast("#") val selector = if (isVolume) "div.unit" else "ul li"
} val elements = document.select(selector)
if (elements.size > 0) {
private fun getAjaxRequest(ajaxType: String, mangaId: String, chapterType: String): Request { val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
return GET("$baseUrl/ajax/$ajaxType/$mangaId/$chapterType/$langCode", headers) val mangaId = linkToFirstChapter.toString().substringAfter('.').substringBefore('/')
val type = if (isVolume) volumeType else chapterType
val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers)
val response = client.newCall(request).execute()
val res = json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
val chapterInfoDocument = Jsoup.parse(res)
val chapters = chapterInfoDocument.select("ul li")
for ((i, it) in elements.withIndex()) {
it.attr("data-id", chapters[i].select("a").attr("data-id"))
}
}
return elements.toList()
} }
@Serializable @Serializable
class AjaxReadDto( class ChapterIdsDto(
val html: String, val html: String,
val title_format: String,
) )
override fun chapterListParse(response: Response): List<SChapter> { override fun updateChapterList(manga: SManga, chapters: List<SChapter>) {
throw UnsupportedOperationException() val request = chapterListRequest(manga.url, chapterType)
} val response = client.newCall(request).execute()
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
val document = Jsoup.parse(result)
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { val elements = document.selectFirst(".scroll-sm")!!.children()
val path = manga.url val chapterCount = chapters.size
val mangaId = path.removeSuffix(VOLUME_URL_SUFFIX).substringAfterLast(".") if (elements.size != chapterCount) throw Exception("Chapter count doesn't match. Try updating again.")
val isVolume = path.endsWith(VOLUME_URL_SUFFIX) val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
for (i in 0 until chapterCount) {
val type = if (isVolume) "volume" else "chapter" val chapter = chapters[i]
val abbrPrefix = if (isVolume) "Vol" else "Chap" val element = elements[i]
val fullPrefix = if (isVolume) "Volume" else "Chapter" val number = element.attr("data-number").toFloatOrNull() ?: -1f
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
val ajaxMangaList = client.newCall(getAjaxRequest("manga", mangaId, type)) chapter.name = element.select(Evaluator.Tag("span"))[0].ownText()
.execute().parseAs<ResponseDto<String>>().result val date = element.select(Evaluator.Tag("span"))[1].ownText()
.toBodyFragment() chapter.date_upload = try {
.select(if (isVolume) ".vol-list > .item" else "li") dateFormat.parse(date)!!.time
} catch (_: Throwable) {
val ajaxReadList = client.newCall(getAjaxRequest("read", mangaId, type)) 0
.execute().parseAs<ResponseDto<AjaxReadDto>>().result.html
.toBodyFragment()
.select("ul a")
val chapterList = ajaxMangaList.zip(ajaxReadList) { m, r ->
val link = r.selectFirst("a")!!
if (!r.attr("abs:href").toHttpUrl().pathSegments.last().contains(type)) {
return Observable.just(emptyList())
}
assert(m.attr("data-number") == r.attr("data-number")) {
"Chapter count doesn't match. Try updating again."
}
val number = m.attr("data-number")
val dateStr = m.select("span").getOrNull(1)?.text() ?: ""
SChapter.create().apply {
setUrlWithoutDomain("${link.attr("href")}#$type/${r.attr("data-id")}")
chapter_number = number.toFloatOrNull() ?: -1f
name = run {
val name = link.text()
val prefix = "$abbrPrefix $number: "
if (!name.startsWith(prefix)) return@run name
val realName = name.removePrefix(prefix)
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
}
date_upload = try {
dateFormat.parse(dateStr)!!.time
} catch (_: ParseException) {
0L
}
} }
} }
return Observable.just(chapterList)
} }
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val typeAndId = chapter.url.substringAfterLast('#') val typeAndId = chapter.url.substringAfterLast('#')
return GET("$baseUrl/ajax/read/$typeAndId", headers) return GET("$baseUrl/ajax/read/$typeAndId", headers)
} }
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<ResponseDto<PageListDto>>().result val result = json.decodeFromString<ResponseDto<PageListDto>>(response.body.string()).result
return result.pages.mapIndexed { index, image -> return result.pages.mapIndexed { index, image ->
val url = image.url val url = image.url
@ -274,49 +206,27 @@ class MangaFire(
@Serializable @Serializable
class PageListDto(private val images: List<List<JsonPrimitive>>) { class PageListDto(private val images: List<List<JsonPrimitive>>) {
val pages val pages get() = images.map {
get() = images.map { Image(it[0].content, it[2].int)
Image(it[0].content, it[2].int) }
}
} }
class Image(val url: String, val offset: Int) class Image(val url: String, val offset: Int)
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// ============================ Preferences =============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_VOLUME_PREF
title = "Show volume entries in search result"
setDefaultValue(false)
}.let(screen::addPreference)
}
// ============================= Utilities ==============================
@Serializable @Serializable
class ResponseDto<T>( class ResponseDto<T>(
val result: T, val result: T,
val status: Int,
) )
private inline fun <reified T> Response.parseAs(): T { override fun getFilterList() =
return json.decodeFromString(body.string()) FilterList(
} Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
private fun String.toBodyFragment(): Document { TypeFilter(),
return Jsoup.parseBodyFragment(this, baseUrl) GenresFilter(),
} StatusFilter(),
YearFilter(),
companion object { ChapterCountFilter(),
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US) SortFilter(),
private const val SHOW_VOLUME_PREF = "show_volume" )
private const val VOLUME_URL_FRAGMENT = "vol"
private const val VOLUME_URL_SUFFIX = "#$VOLUME_URL_FRAGMENT"
private const val VOLUME_TITLE_PREFIX = "[VOL] "
}
} }

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.mangahosted.MangaHostedUrlActivity"
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:scheme="https"
android:host="mangahosted.org"
android:pathPattern="/.*/..*" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,202 +0,0 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
override val lang = langOption.lang
override val name: String = "Manga Hosted${langOption.nameSuffix}"
override val baseUrl: String = "https://mangahosted.org"
override val supportsLatest = true
private val json: Json by injectLazy()
override val client = network.client.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.set("Referer", "$baseUrl/")
// ================================= Popular ==========================================
override fun popularMangaRequest(page: Int): Request {
val maxResult = 24
return GET("$apiUrl/${langOption.infix}/HomeTopFllow/$maxResult/${page - 1}")
}
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<Pageable<MangaDto>>()
val mangas = dto.data.map(::mangaParse)
return MangasPage(
mangas = mangas,
hasNextPage = dto.hasNextPage(),
)
}
// ================================= Latest ===========================================
override fun latestUpdatesRequest(page: Int): Request {
val maxResult = 24
val url = "$apiUrl/${langOption.infix}/HomeLastUpdate".toHttpUrl().newBuilder()
.addPathSegment("$maxResult")
.addPathSegment("${page - 1}")
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
// ================================= Search ===========================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val maxResult = 20
val url = "$apiUrl/${langOption.infix}/SeachPage/$maxResult/${page - 1}".toHttpUrl().newBuilder()
.addPathSegment(query)
.build()
return GET(url, headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(SEARCH_PREFIX)) {
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
return client.newCall(GET(url, headers))
.asObservableSuccess().map { response ->
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
MangasPage(mangas, false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchDto>()
return MangasPage(
dto.mangas.map(::mangaParse),
false,
)
}
// ================================= Details ==========================================
override fun mangaDetailsRequest(manga: SManga): Request {
val url = "$apiUrl/${langOption.infix}/getInfoManga".toHttpUrl().newBuilder()
.addPathSegment(manga.slug())
.build()
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val dto = response.parseAs<MangaDetailsDto>()
return mangaParse(dto.details)
}
override fun getMangaUrl(manga: SManga): String {
return baseUrl + manga.url.replace(langOption.infix, langOption.mangaSubstring)
}
// ================================= Chapter ==========================================
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapters = mutableListOf<SChapter>()
var currentPage = 0
do {
val chaptersDto = fetchChapterListPageable(manga, currentPage++)
chapters += chaptersDto.data.map { chapter ->
SChapter.create().apply {
name = chapter.name
date_upload = chapter.date.toDate()
url = chapter.toChapterUrl(langOption.infix)
}
}
} while (chaptersDto.hasNextPage())
return Observable.just(chapters)
}
private fun fetchChapterListPageable(manga: SManga, page: Int): Pageable<ChapterDto> {
val maxResult = 100
val url = "$apiUrl/${langOption.infix}/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/${langOption.orderBy}"
return client.newCall(GET(url, headers)).execute()
.parseAs<Pageable<ChapterDto>>()
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
// ================================= Pages ============================================
override fun pageListRequest(chapter: SChapter): Request {
val chapterSlug = chapter.url.substringAfter(langOption.infix)
val url = "$apiUrl/${langOption.infix}/GetImageChapter$chapterSlug"
return GET(url, headers)
}
override fun imageRequest(page: Page): Request {
val imageHeaders = headers.newBuilder()
.set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.removeAll("Referer")
.build()
return super.imageRequest(page).newBuilder()
.headers(imageHeaders)
.build()
}
override fun pageListParse(response: Response): List<Page> {
val location = response.request.url.toString()
val dto = response.parseAs<PageDto>()
return dto.pages.mapIndexed { index, url ->
Page(index, location, imageUrl = url)
}
}
override fun imageUrlParse(response: Response): String = ""
// ================================= Utilities =======================================
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun SManga.slug() = this.url.split("/").last()
private fun mangaParse(dto: MangaDto): SManga {
return SManga.create().apply {
title = dto.title
thumbnail_url = dto.thumbnailUrl
status = dto.status
url = "/${langOption.infix}/${dto.slug}"
genre = dto.genres
initialized = true
}
}
private fun String.toDate(): Long =
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
companion object {
const val SEARCH_PREFIX = "slug:"
val baseApiUrl = "https://api.novelfull.us"
val apiUrl = "$baseApiUrl/api"
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
}
}

View File

@ -1,68 +0,0 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class MangaDetailsDto(private val data: Props) {
val details: MangaDto get() = data.details
@Serializable
class Props(
@SerialName("infoDoc") val details: MangaDto,
)
}
@Serializable
open class Pageable<T>(
var currentPage: Int,
var totalPage: Int,
val data: List<T>,
) {
fun hasNextPage() = (currentPage + 1) <= totalPage
}
@Serializable
class ChapterDto(
val date: String,
@SerialName("idDoc") val slugManga: String,
@SerialName("idDetail") val id: String,
@SerialName("nameChapter") val name: String,
) {
fun toChapterUrl(lang: String) = "/$lang/${this.slugManga}/$id"
}
@Serializable
class MangaDto(
@SerialName("name") val title: String,
@SerialName("image") private val _thumbnailUrl: String,
@SerialName("idDoc") val slug: String,
@SerialName("genresName") val genres: String,
@SerialName("status") val _status: String,
) {
val thumbnailUrl get() = "${MangaHosted.baseApiUrl}$_thumbnailUrl"
val status get() = when (_status) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
@Serializable
class SearchDto(
@SerialName("data")
val mangas: List<MangaDto>,
)
@Serializable
class PageDto(val `data`: Data) {
val pages: List<String> get() = `data`.detailDocuments.source.split("#")
@Serializable
class Data(@SerialName("detail_documents") val detailDocuments: DetailDocuments)
@Serializable
class DetailDocuments(val source: String)
}

View File

@ -1,30 +0,0 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class MangaHostedFactory : SourceFactory {
override fun createSources(): List<Source> = languages.map { MangaHosted(it) }
}
class LanguageOption(
val lang: String,
val infix: String = lang,
val mangaSubstring: String = infix,
val nameSuffix: String = "",
val orderBy: String = "DESC",
)
val languages = listOf(
LanguageOption("en", "manga", "scan"),
LanguageOption("en", "manga-v2", "kaka", " v2"),
LanguageOption("en", "comic", "comic-dc", " Comics"),
LanguageOption("es", "manga-spanish", "manga-es"),
LanguageOption("id", "manga-indo", "id"),
LanguageOption("it", "manga-italia", "manga-it"),
LanguageOption("ja", "mangaraw", "raw"),
LanguageOption("pt-BR", "manga-br", orderBy = "ASC"),
LanguageOption("ru", "manga-ru", "mangaru"),
LanguageOption("ru", "manga-ru-hentai", "hentai", " +18"),
LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"),
)

View File

@ -1,7 +1,3 @@
## 1.4.7
- Reworked the lib-multisrc theme
## 1.3.4 ## 1.3.4
- Refactor and make multisrc - Refactor and make multisrc

View File

@ -3,7 +3,7 @@ ext {
extClass = '.MangaReaderFactory' extClass = '.MangaReaderFactory'
themePkg = 'mangareader' themePkg = 'mangareader'
baseUrl = 'https://mangareader.to' baseUrl = 'https://mangareader.to'
overrideVersionCode = 5 overrideVersionCode = 4
isNsfw = true isNsfw = true
} }

View File

@ -1,208 +1,247 @@
package eu.kanade.tachiyomi.extension.all.mangareaderto package eu.kanade.tachiyomi.extension.all.mangareaderto
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriFilter
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriMultiSelectFilter
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriPartFilter
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
import java.util.Calendar import java.util.Calendar
class TypeFilter : UriPartFilter( object Note : Filter.Header("NOTE: Ignored if using text search!")
"Type",
"type",
arrayOf(
Pair("All", ""),
Pair("Manga", "1"),
Pair("One-Shot", "2"),
Pair("Doujinshi", "3"),
Pair("Light Novel", "4"),
Pair("Manhwa", "5"),
Pair("Manhua", "6"),
Pair("Comic", "7"),
),
)
class StatusFilter : UriPartFilter( sealed class Select(
"Status", name: String,
"status", val param: String,
arrayOf( values: Array<String>,
Pair("All", ""), ) : Filter.Select<String>(name, values) {
Pair("Finished", "1"), open val selection: String
Pair("Publishing", "2"), get() = if (state == 0) "" else state.toString()
Pair("On Hiatus", "3"), }
Pair("Discontinued", "4"),
Pair("Not yet published", "5"),
),
)
class RatingFilter : UriPartFilter( class TypeFilter(
"Rating Type", values: Array<String> = types,
"rating_type", ) : Select("Type", "type", values) {
arrayOf( companion object {
Pair("All", ""), private val types: Array<String>
Pair("G - All Ages", "1"), get() = arrayOf(
Pair("PG - Children", "2"), "All",
Pair("PG-13 - Teens 13 or older", "3"), "Manga",
Pair("R - 17+ (violence & profanity)", "4"), "One-Shot",
Pair("R+ - Mild Nudity", "5"), "Doujinshi",
Pair("Rx - Hentai", "6"), "Light Novel",
), "Manhwa",
) "Manhua",
"Comic",
)
}
}
class ScoreFilter : UriPartFilter( class StatusFilter(
"Score", values: Array<String> = statuses,
"score", ) : Select("Status", "status", values) {
arrayOf( companion object {
Pair("All", ""), private val statuses: Array<String>
Pair("(1) Appalling", "1"), get() = arrayOf(
Pair("(2) Horrible", "2"), "All",
Pair("(3) Very Bad", "3"), "Finished",
Pair("(4) Bad", "4"), "Publishing",
Pair("(5) Average", "5"), "On Hiatus",
Pair("(6) Fine", "6"), "Discontinued",
Pair("(7) Good", "7"), "Not yet published",
Pair("(8) Very Good", "8"), )
Pair("(9) Great", "9"), }
Pair("(10) Masterpiece", "10"), }
),
)
class YearFilter(name: String, param: String) : UriPartFilter( class RatingFilter(
name, values: Array<String> = ratings,
param, ) : Select("Rating Type", "rating_type", values) {
years, companion object {
) { private val ratings: Array<String>
get() = arrayOf(
"All",
"G - All Ages",
"PG - Children",
"PG-13 - Teens 13 or older",
"R - 17+ (violence & profanity)",
"R+ - Mild Nudity",
"Rx - Hentai",
)
}
}
class ScoreFilter(
values: Array<String> = scores,
) : Select("Score", "score", values) {
companion object {
private val scores: Array<String>
get() = arrayOf(
"All",
"(1) Appalling",
"(2) Horrible",
"(3) Very Bad",
"(4) Bad",
"(5) Average",
"(6) Fine",
"(7) Good",
"(8) Very Good",
"(9) Great",
"(10) Masterpiece",
)
}
}
sealed class DateSelect(
name: String,
param: String,
values: Array<String>,
) : Select(name, param, values) {
override val selection: String
get() = if (state == 0) "" else values[state]
}
class YearFilter(
param: String,
values: Array<String> = years,
) : DateSelect("Year", param, values) {
companion object { companion object {
private val nextYear by lazy { private val nextYear by lazy {
Calendar.getInstance()[Calendar.YEAR] + 1 Calendar.getInstance()[Calendar.YEAR] + 1
} }
private val years = Array(nextYear - 1916) { year -> private val years: Array<String>
if (year == 0) { get() = Array(nextYear - 1916) {
Pair("Any", "") if (it == 0) "Any" else (nextYear - it).toString()
} else {
(nextYear - year).toString().let { Pair(it, it) }
} }
}
} }
} }
class MonthFilter(name: String, param: String) : UriPartFilter( class MonthFilter(
name, param: String,
param, values: Array<String> = months,
months, ) : DateSelect("Month", param, values) {
) {
companion object { companion object {
private val months = Array(13) { months -> private val months: Array<String>
if (months == 0) { get() = Array(13) {
Pair("Any", "") if (it == 0) "Any" else "%02d".format(it)
} else {
Pair("%02d".format(months), months.toString())
} }
}
} }
} }
class DayFilter(name: String, param: String) : UriPartFilter( class DayFilter(
name, param: String,
param, values: Array<String> = days,
days, ) : DateSelect("Day", param, values) {
) {
companion object { companion object {
private val days = Array(32) { day -> private val days: Array<String>
if (day == 0) { get() = Array(32) {
Pair("Any", "") if (it == 0) "Any" else "%02d".format(it)
} else {
Pair("%02d".format(day), day.toString())
} }
}
} }
} }
sealed class DateFilter( sealed class DateFilter(
type: String, type: String,
private val values: List<UriPartFilter>, values: List<DateSelect>,
) : Filter.Group<UriPartFilter>("$type Date", values), UriFilter { ) : Filter.Group<DateSelect>("$type Date", values)
override fun addToUri(builder: HttpUrl.Builder) {
values.forEach {
it.addToUri(builder)
}
}
}
class StartDateFilter( class StartDateFilter(
values: List<UriPartFilter> = parts, values: List<DateSelect> = parts,
) : DateFilter("Start", values) { ) : DateFilter("Start", values) {
companion object { companion object {
private val parts = listOf( private val parts: List<DateSelect>
YearFilter("Year", "sy"), get() = listOf(
MonthFilter("Month", "sm"), YearFilter("sy"),
DayFilter("Day", "sd"), MonthFilter("sm"),
) DayFilter("sd"),
)
} }
} }
class EndDateFilter( class EndDateFilter(
values: List<UriPartFilter> = parts, values: List<DateSelect> = parts,
) : DateFilter("End", values) { ) : DateFilter("End", values) {
companion object { companion object {
private val parts = listOf( private val parts: List<DateSelect>
YearFilter("Year", "ey"), get() = listOf(
MonthFilter("Month", "em"), YearFilter("ey"),
DayFilter("Day", "ed"), MonthFilter("em"),
DayFilter("ed"),
)
}
}
class SortFilter(
values: Array<String> = orders.keys.toTypedArray(),
) : Select("Sort", "sort", values) {
override val selection: String
get() = orders[values[state]]!!
companion object {
private val orders = mapOf(
"Default" to "default",
"Latest Updated" to "latest-updated",
"Score" to "score",
"Name A-Z" to "name-az",
"Release Date" to "release-date",
"Most Viewed" to "most-viewed",
) )
} }
} }
class GenreFilter : UriMultiSelectFilter( class Genre(name: String, val id: String) : Filter.CheckBox(name)
"Genres",
"genres", class GenresFilter(
arrayOf( values: List<Genre> = genres,
Pair("Action", "1"), ) : Filter.Group<Genre>("Genres", values) {
Pair("Adventure", "2"), val param = "genres"
Pair("Cars", "3"),
Pair("Comedy", "4"), val selection: String
Pair("Dementia", "5"), get() = state.filter { it.state }.joinToString(",") { it.id }
Pair("Demons", "6"),
Pair("Doujinshi", "7"), companion object {
Pair("Drama", "8"), private val genres: List<Genre>
Pair("Ecchi", "9"), get() = listOf(
Pair("Fantasy", "10"), Genre("Action", "1"),
Pair("Game", "11"), Genre("Adventure", "2"),
Pair("Gender Bender", "12"), Genre("Cars", "3"),
Pair("Harem", "13"), Genre("Comedy", "4"),
Pair("Hentai", "14"), Genre("Dementia", "5"),
Pair("Historical", "15"), Genre("Demons", "6"),
Pair("Horror", "16"), Genre("Doujinshi", "7"),
Pair("Josei", "17"), Genre("Drama", "8"),
Pair("Kids", "18"), Genre("Ecchi", "9"),
Pair("Magic", "19"), Genre("Fantasy", "10"),
Pair("Martial Arts", "20"), Genre("Game", "11"),
Pair("Mecha", "21"), Genre("Gender Bender", "12"),
Pair("Military", "22"), Genre("Harem", "13"),
Pair("Music", "23"), Genre("Hentai", "14"),
Pair("Mystery", "24"), Genre("Historical", "15"),
Pair("Parody", "25"), Genre("Horror", "16"),
Pair("Police", "26"), Genre("Josei", "17"),
Pair("Psychological", "27"), Genre("Kids", "18"),
Pair("Romance", "28"), Genre("Magic", "19"),
Pair("Samurai", "29"), Genre("Martial Arts", "20"),
Pair("School", "30"), Genre("Mecha", "21"),
Pair("Sci-Fi", "31"), Genre("Military", "22"),
Pair("Seinen", "32"), Genre("Music", "23"),
Pair("Shoujo", "33"), Genre("Mystery", "24"),
Pair("Shoujo Ai", "34"), Genre("Parody", "25"),
Pair("Shounen", "35"), Genre("Police", "26"),
Pair("Shounen Ai", "36"), Genre("Psychological", "27"),
Pair("Slice of Life", "37"), Genre("Romance", "28"),
Pair("Space", "38"), Genre("Samurai", "29"),
Pair("Sports", "39"), Genre("School", "30"),
Pair("Super Power", "40"), Genre("Sci-Fi", "31"),
Pair("Supernatural", "41"), Genre("Seinen", "32"),
Pair("Thriller", "42"), Genre("Shoujo", "33"),
Pair("Vampire", "43"), Genre("Shoujo Ai", "34"),
Pair("Yaoi", "44"), Genre("Shounen", "35"),
Pair("Yuri", "45"), Genre("Shounen Ai", "36"),
), Genre("Slice of Life", "37"),
",", Genre("Space", "38"),
) Genre("Sports", "39"),
Genre("Super Power", "40"),
Genre("Supernatural", "41"),
Genre("Thriller", "42"),
Genre("Vampire", "43"),
Genre("Yaoi", "44"),
Genre("Yuri", "45"),
)
}
}

Some files were not shown because too many files have changed in this diff Show More