Compare commits

..

No commits in common. "e91f02af0291608767ef2369a70e39001bed3d64" and "cb2794830738e740114e15a59b50655413e0f059" have entirely different histories.

788 changed files with 3904 additions and 10258 deletions

View File

@ -105,15 +105,3 @@ body:
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true
- type: textarea
attributes:
label: <!-- footer -->
description: Do **not** modify. This is a reminder for other users to vote.
value: |
---
Add a :+1: [reaction] to [issues you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[issues you find important]: https://github.com/keiyoushi/extensions-source/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

View File

@ -57,15 +57,3 @@ body:
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true
- type: textarea
attributes:
label: <!-- footer -->
description: Do **not** modify. This is a reminder for other users to vote.
value: |
---
Add a :+1: [reaction] to [issues you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[issues you find important]: https://github.com/keiyoushi/extensions-source/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

View File

@ -55,15 +55,3 @@ body:
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true
- type: textarea
attributes:
label: <!-- footer -->
description: Do **not** modify. This is a reminder for other users to vote.
value: |
---
Add a :+1: [reaction] to [issues you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[issues you find important]: https://github.com/keiyoushi/extensions-source/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

View File

@ -61,15 +61,3 @@ body:
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true
- type: textarea
attributes:
label: <!-- footer -->
description: Do **not** modify. This is a reminder for other users to vote.
value: |
---
Add a :+1: [reaction] to [issues you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[issues you find important]: https://github.com/keiyoushi/extensions-source/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

View File

@ -57,15 +57,3 @@ body:
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true
- type: textarea
attributes:
label: <!-- footer -->
description: Do **not** modify. This is a reminder for other users to vote.
value: |
---
Add a :+1: [reaction] to [issues you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[issues you find important]: https://github.com/keiyoushi/extensions-source/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

View File

@ -39,15 +39,3 @@ body:
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true
- type: textarea
attributes:
label: <!-- footer -->
description: Do **not** modify. This is a reminder for other users to vote.
value: |
---
Add a :+1: [reaction] to [issues you find important].
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
[issues you find important]: https://github.com/keiyoushi/extensions-source/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

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.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

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.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-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 = 5 baseVersionCode = 4
dependencies { dependencies {
api(project(":lib:synchrony")) api(project(":lib:synchrony"))

View File

@ -153,7 +153,7 @@ abstract class ColaManga(
override fun mangaDetailsParse(document: Document) = SManga.create().apply { override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1.fed-part-eone")!!.text() title = document.selectFirst("h1.fed-part-eone")!!.text()
thumbnail_url = document.selectFirst("a.fed-list-pics")?.absUrl("data-original") thumbnail_url = document.selectFirst("a.fed-list-pics")?.absUrl("data-orignal")
author = document.selectFirst("span.fed-text-muted:contains($authorTitle) + a")?.text() author = document.selectFirst("span.fed-text-muted:contains($authorTitle) + a")?.text()
genre = document.select("span.fed-text-muted:contains($genreTitle) ~ a").joinToString { it.text() } genre = document.select("span.fed-text-muted:contains($genreTitle) ~ a").joinToString { it.text() }
description = document description = document

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="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

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

View File

@ -1,242 +0,0 @@
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

@ -1,37 +0,0 @@
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

@ -269,32 +269,32 @@ abstract class FMReader(
// languages: en, vi, es, tr // languages: en, vi, es, tr
return when (dateWord) { return when (dateWord) {
"min", "minute", "phút", "minuto", "dakika" -> Calendar.getInstance().apply { "min", "minute", "phút", "minuto", "dakika" -> Calendar.getInstance().apply {
add(Calendar.MINUTE, -value) add(Calendar.MINUTE, value * -1)
set(Calendar.SECOND, 0) set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0) set(Calendar.MILLISECOND, 0)
}.timeInMillis }.timeInMillis
"hour", "giờ", "hora", "saat" -> Calendar.getInstance().apply { "hour", "giờ", "hora", "saat" -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, -value) add(Calendar.HOUR_OF_DAY, value * -1)
set(Calendar.SECOND, 0) set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0) set(Calendar.MILLISECOND, 0)
}.timeInMillis }.timeInMillis
"day", "ngày", "día", "gün" -> Calendar.getInstance().apply { "day", "ngày", "día", "gün" -> Calendar.getInstance().apply {
add(Calendar.DATE, -value) add(Calendar.DATE, value * -1)
set(Calendar.SECOND, 0) set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0) set(Calendar.MILLISECOND, 0)
}.timeInMillis }.timeInMillis
"week", "tuần", "semana", "hafta" -> Calendar.getInstance().apply { "week", "tuần", "semana", "hafta" -> Calendar.getInstance().apply {
add(Calendar.DATE, -value * 7) add(Calendar.DATE, value * 7 * -1)
set(Calendar.SECOND, 0) set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0) set(Calendar.MILLISECOND, 0)
}.timeInMillis }.timeInMillis
"month", "tháng", "mes", "ay" -> Calendar.getInstance().apply { "month", "tháng", "mes", "ay" -> Calendar.getInstance().apply {
add(Calendar.MONTH, -value) add(Calendar.MONTH, value * -1)
set(Calendar.SECOND, 0) set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0) set(Calendar.MILLISECOND, 0)
}.timeInMillis }.timeInMillis
"year", "năm", "año", "yıl" -> Calendar.getInstance().apply { "year", "năm", "año", "yıl" -> Calendar.getInstance().apply {
add(Calendar.YEAR, -value) add(Calendar.YEAR, value * -1)
set(Calendar.SECOND, 0) set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0) set(Calendar.MILLISECOND, 0)
}.timeInMillis }.timeInMillis

View File

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

View File

@ -647,7 +647,6 @@ abstract class GalleryAdults(
"p" -> "png" "p" -> "png"
"b" -> "bmp" "b" -> "bmp"
"g" -> "gif" "g" -> "gif"
"w" -> "webp"
else -> "jpg" else -> "jpg"
} }
val idx = image.key.toInt() val idx = image.key.toInt()

View File

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

View File

@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Call import okhttp3.Call
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
@ -136,22 +137,19 @@ abstract class GigaViewer(
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
val aggregateId = document.selectFirst("script.js-valve")!!.attr("data-giga_series") val readableProductList = document.selectFirst("div.js-readable-product-list")!!
val firstListEndpoint = readableProductList.attr("data-first-list-endpoint")
.toHttpUrl()
val latestListEndpoint = readableProductList.attr("data-latest-list-endpoint")
.toHttpUrlOrNull() ?: firstListEndpoint
val numberSince = latestListEndpoint.queryParameter("number_since")!!.toFloat()
.coerceAtLeast(firstListEndpoint.queryParameter("number_since")!!.toFloat())
val newHeaders = headers.newBuilder() val newHeaders = headers.newBuilder()
.set("Referer", response.request.url.toString()) .set("Referer", response.request.url.toString())
.build() .build()
var readMoreEndpoint = firstListEndpoint.newBuilder()
var readMoreEndpoint = baseUrl.toHttpUrl().newBuilder() .setQueryParameter("number_since", numberSince.toString())
.addPathSegment("api")
.addPathSegment("viewer")
.addPathSegment("readable_products")
.addQueryParameter("aggregate_id", aggregateId)
.addQueryParameter("number_since", Int.MAX_VALUE.toString())
.addQueryParameter("number_until", "0")
.addQueryParameter("read_more_num", "150")
.addQueryParameter("type", "episode")
.build()
.toString() .toString()
val chapters = mutableListOf<SChapter>() val chapters = mutableListOf<SChapter>()

View File

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

View File

@ -65,8 +65,7 @@ abstract class GroupLe(
} }
.build() .build()
private var uagent = preferences.getString(UAGENT_TITLE, UAGENT_DEFAULT)!! private var uagent: String = preferences.getString(UAGENT_TITLE, UAGENT_DEFAULT)!!
override fun headersBuilder() = Headers.Builder().apply { override fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", uagent) add("User-Agent", uagent)
add("Referer", baseUrl) add("Referer", baseUrl)
@ -207,44 +206,28 @@ abstract class GroupLe(
} }
} }
protected open fun getChapterSearchParams(document: Document): String {
return "?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 (( if ((document.select(".expandable.hide-dn").isNotEmpty() && document.select(".user-avatar").isNullOrEmpty() && document.toString().contains("current_user_country_code = 'RU'")) || (document.select("img.logo").first()?.attr("title")?.contains("Allhentai") == true && document.select(".user-avatar").isNullOrEmpty())) {
document.select(".expandable.hide-dn").isNotEmpty() && document.select(".user-avatar")
.isEmpty() && document.toString()
.contains("current_user_country_code = 'RU'")
) || (
document.select("img.logo")
.first()?.attr("title")
?.contains("Allhentai") == true && document.select(".user-avatar").isEmpty()
)
) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E") throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
} }
return document.select(chapterListSelector()).map { chapterFromElement(it, manga) }
val chapterSearchParams = getChapterSearchParams(document)
return document.select(chapterListSelector()).map { chapterFromElement(it, manga, chapterSearchParams) }
} }
override fun chapterListSelector() = override fun chapterListSelector() =
"tr.item-row:has(td > a):has(td.date:not(.text-info))" "tr.item-row:has(td > a):has(td.date:not(.text-info))"
private fun chapterFromElement(element: Element, manga: SManga, chapterSearchParams: String): SChapter { private fun chapterFromElement(element: Element, manga: SManga): SChapter {
val urlElement = element.select("a.chapter-link").first()!! val urlElement = element.select("a.chapter-link").first()!!
val chapterInf = element.select("td.item-title").first()!! val chapterInf = element.select("td.item-title").first()!!
val urlText = urlElement.text() val urlText = urlElement.text()
val chapter = SChapter.create() val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href") + chapterSearchParams) chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mtr=true") // mtr is 18+ fractional skip
val translatorElement = urlElement.attr("title") val translatorElement = urlElement.attr("title")
chapter.scanlator = if (translatorElement.isNotBlank()) { chapter.scanlator = if (!translatorElement.isNullOrBlank()) {
translatorElement translatorElement
.replace("(Переводчик),", "&") .replace("(Переводчик),", "&")
.removeSuffix(" (Переводчик)") .removeSuffix(" (Переводчик)")
@ -268,15 +251,11 @@ abstract class GroupLe(
chapter.chapter_number = chapterInf.attr("data-num").toFloat() / 10 chapter.chapter_number = chapterInf.attr("data-num").toFloat() / 10
chapter.date_upload = element.select("td.d-none").last()?.text()?.let { chapter.date_upload = element.select("td.d-none").last()?.text()?.let {
if (it.isEmpty()) {
0L
} else {
try { try {
SimpleDateFormat("dd.MM.yy", Locale.US).parse(it)?.time ?: 0L SimpleDateFormat("dd.MM.yy", Locale.US).parse(it)?.time ?: 0L
} catch (e: ParseException) { } catch (e: ParseException) {
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it)?.time ?: 0L SimpleDateFormat("dd/MM/yy", Locale.US).parse(it)?.time ?: 0L
} }
}
} ?: 0 } ?: 0
return chapter return chapter
} }
@ -313,15 +292,15 @@ abstract class GroupLe(
val html = document.html() val html = document.html()
val readerMark = "rm_h.readerDoInit([" var readerMark = "rm_h.readerDoInit(["
// allhentai necessary
if (!html.contains(readerMark)) {
readerMark = "rm_h.readerInit( 0,["
}
if (!html.contains(readerMark)) { if (!html.contains(readerMark)) {
if (document.select(".input-lg").isNotEmpty() || ( if (document.select(".input-lg").isNotEmpty() || (document.select(".user-avatar").isNullOrEmpty() && document.select("img.logo").first()?.attr("title")?.contains("Allhentai") == true)) {
document.select(".user-avatar")
.isEmpty() && document.select("img.logo").first()?.attr("title")
?.contains("Allhentai") == true
)
) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E") throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
} }
if (!response.request.url.toString().contains(baseUrl)) { if (!response.request.url.toString().contains(baseUrl)) {

View File

@ -232,7 +232,7 @@ abstract class HentaiHand(
val date = it.jsonObject["added_at"]!!.jsonPrimitive.content val date = it.jsonObject["added_at"]!!.jsonPrimitive.content
date_upload = if (date.contains("day")) { date_upload = if (date.contains("day")) {
Calendar.getInstance().apply { Calendar.getInstance().apply {
add(Calendar.DATE, -date.filter { it.isDigit() }.toInt()) add(Calendar.DATE, date.filter { it.isDigit() }.toInt() * -1)
}.timeInMillis }.timeInMillis
} else { } else {
DATE_FORMAT.parse(it.jsonObject["added_at"]!!.jsonPrimitive.content)?.time ?: 0 DATE_FORMAT.parse(it.jsonObject["added_at"]!!.jsonPrimitive.content)?.time ?: 0
@ -248,7 +248,7 @@ abstract class HentaiHand(
val date = obj.jsonObject["uploaded_at"]!!.jsonPrimitive.content val date = obj.jsonObject["uploaded_at"]!!.jsonPrimitive.content
date_upload = if (date.contains("day")) { date_upload = if (date.contains("day")) {
Calendar.getInstance().apply { Calendar.getInstance().apply {
add(Calendar.DATE, -date.filter { it.isDigit() }.toInt()) add(Calendar.DATE, date.filter { it.isDigit() }.toInt() * -1)
}.timeInMillis }.timeInMillis
} else { } else {
DATE_FORMAT.parse(obj.jsonObject["uploaded_at"]!!.jsonPrimitive.content)?.time ?: 0 DATE_FORMAT.parse(obj.jsonObject["uploaded_at"]!!.jsonPrimitive.content)?.time ?: 0

View File

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

View File

@ -96,13 +96,10 @@ class Chapter(
private val createdBy: Name, private val createdBy: Name,
private val createdAt: String, private val createdAt: String,
private val chapterStatus: String, private val chapterStatus: String,
private val isAccessible: Boolean,
private val mangaPost: ChapterPostDetails, private val mangaPost: ChapterPostDetails,
) { ) {
fun isPublic() = chapterStatus == "PUBLIC" fun isPublic() = chapterStatus == "PUBLIC"
fun isAccessible() = isAccessible
fun toSChapter(mangaSlug: String?) = SChapter.create().apply { fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
val seriesSlug = mangaSlug ?: mangaPost.slug val seriesSlug = mangaSlug ?: mangaPost.slug
url = "/series/$seriesSlug/$slug#$id" url = "/series/$seriesSlug/$slug#$id"

View File

@ -128,7 +128,7 @@ abstract class Iken(
assert(!data.post.isNovel) { "Novels are unsupported" } assert(!data.post.isNovel) { "Novels are unsupported" }
return data.post.chapters return data.post.chapters
.filter { it.isPublic() && it.isAccessible() } .filter { it.isPublic() }
.map { it.toSChapter(data.post.slug) } .map { it.toSChapter(data.post.slug) }
} }

View File

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

View File

@ -233,8 +233,8 @@ open class Kemono(
GET("$baseUrl/$apiPath${chapter.url}", headers) GET("$baseUrl/$apiPath${chapter.url}", headers)
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val postData: KemonoPostDtoWrapped = response.parseAs() val post: KemonoPostDto = response.parseAs()
return postData.post.images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) } return post.images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) }
} }
override fun imageRequest(page: Page): Request { override fun imageRequest(page: Page): Request {

View File

@ -51,11 +51,6 @@ class KemonoCreatorDto(
} }
} }
@Serializable
class KemonoPostDtoWrapped(
val post: KemonoPostDto,
)
@Serializable @Serializable
class KemonoPostDto( class KemonoPostDto(
private val id: String, private val id: String,
@ -70,7 +65,7 @@ class KemonoPostDto(
) { ) {
val images: List<String> val images: List<String>
get() = buildList(attachments.size + 1) { get() = buildList(attachments.size + 1) {
if (file.path != null) add(KemonoAttachmentDto(file.name, file.path)) if (file.path != null) add(KemonoAttachmentDto(file.name!!, file.path))
addAll(attachments) addAll(attachments)
}.filter { }.filter {
when (it.path.substringAfterLast('.').lowercase()) { when (it.path.substringAfterLast('.').lowercase()) {
@ -106,8 +101,8 @@ class KemonoFileDto(val name: String? = null, val path: String? = null)
// name might have ".jpe" extension for JPEG, path might have ".m4v" extension for MP4 // name might have ".jpe" extension for JPEG, path might have ".m4v" extension for MP4
@Serializable @Serializable
class KemonoAttachmentDto(var name: String? = null, val path: String) { class KemonoAttachmentDto(val name: String, val path: String) {
override fun toString() = path + if (name != null) "?f=$name" else "" override fun toString() = "$path?f=$name"
} }
private fun getApiDateFormat() = private fun getApiDateFormat() =

View File

@ -1,4 +0,0 @@
pref_show_paid_chapter_title=عرض الفصول المدفوعة
pref_show_paid_chapter_summary_on=سيتم عرض الفصول المدفوعة
pref_show_paid_chapter_summary_off=سيتم عرض الفصول المجانية فقط.
chapter_page_url_not_found=رابط الصفحة غير موجود

View File

@ -1,4 +1,3 @@
pref_show_paid_chapter_title=Display paid chapters pref_show_paid_chapter_title=Display paid chapters
pref_show_paid_chapter_summary_on=Paid chapters will appear. pref_show_paid_chapter_summary_on=Paid chapters will appear.
pref_show_paid_chapter_summary_off=Only free chapters will be displayed. pref_show_paid_chapter_summary_off=Only free chapters will be displayed.
chapter_page_url_not_found=Page URL not found

View File

@ -1,4 +0,0 @@
pref_show_paid_chapter_title=Afficher les chapitres payants
pref_show_paid_chapter_summary_on=Les chapitres payants apparaitront.
pref_show_paid_chapter_summary_off=Seuls les chapitres gratuits apparaitront.
chapter_page_url_not_found=Page URL non trouvée

View File

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

View File

@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -54,7 +55,7 @@ abstract class Keyoapp(
protected val intl = Intl( protected val intl = Intl(
language = lang, language = lang,
baseLanguage = "en", baseLanguage = "en",
availableLanguages = setOf("ar", "en", "fr"), availableLanguages = setOf("en"),
classLoader = this::class.java.classLoader!!, classLoader = this::class.java.classLoader!!,
) )
@ -258,11 +259,9 @@ abstract class Keyoapp(
// Image list // Image list
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val cdnUrl = getCdnUrl(document)
document.select("#pages > img") document.select("#pages > img")
.map { it.attr("uid") } .map { it.attr("uid") }
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.also { cdnUrl ?: throw Exception(intl["chapter_page_url_not_found"]) }
.mapIndexed { index, img -> .mapIndexed { index, img ->
Page(index, document.location(), "$cdnUrl/$img") Page(index, document.location(), "$cdnUrl/$img")
} }
@ -278,16 +277,7 @@ abstract class Keyoapp(
} }
} }
protected open fun getCdnUrl(document: Document): String? { protected open val cdnUrl = "https://2xffbs-cn8.is1.buzz/uploads"
return document.select("script")
.firstOrNull { CDN_HOST_REGEX.containsMatchIn(it.html()) }
?.let {
val cdnHost = CDN_HOST_REGEX.find(it.html())
?.groups?.get("host")?.value
?.replace(CDN_CLEAN_REGEX, "")
"https://$cdnHost/uploads"
}
}
private val oldImgCdnRegex = Regex("""^(https?:)?//cdn\d*\.keyoapp\.com""") private val oldImgCdnRegex = Regex("""^(https?:)?//cdn\d*\.keyoapp\.com""")
@ -307,7 +297,12 @@ 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("url")?.value element.attr("style")
.substringAfter(":url(", "")
.substringBefore(")", "")
.takeIf { it.isNotEmpty() }
?.toHttpUrlOrNull()?.newBuilder()?.setQueryParameter("w", "480")?.build()
?.toString()
} }
} }
@ -365,8 +360,5 @@ 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*`[^`]+//(?<host>[^/]+)""".toRegex()
val CDN_CLEAN_REGEX = """\$\{[^}]*\}""".toRegex()
val IMG_REGEX = """url\(['"]?(?<url>[^(['"\)])]+)""".toRegex()
} }
} }

View File

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

View File

@ -767,21 +767,12 @@ abstract class Madara(
return when { return when {
element.hasAttr("data-src") -> element.attr("abs:data-src") element.hasAttr("data-src") -> element.attr("abs:data-src")
element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src") element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src")
element.hasAttr("srcset") -> element.attr("abs:srcset").getSrcSetImage() element.hasAttr("srcset") -> element.attr("abs:srcset").substringBefore(" ")
element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc") element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc")
else -> element.attr("abs:src") else -> element.attr("abs:src")
} }
} }
/**
* Get the best image quality available from srcset
*/
private fun String.getSrcSetImage(): String? {
return this.split(" ")
.filter(URL_REGEX::matches)
.maxOfOrNull(String::toString)
}
/** /**
* Set it to true if the source uses the new AJAX endpoint to * Set it to true if the source uses the new AJAX endpoint to
* fetch the manga chapters instead of the old admin-ajax.php one. * fetch the manga chapters instead of the old admin-ajax.php one.
@ -1115,7 +1106,6 @@ abstract class Madara(
companion object { companion object {
const val URL_SEARCH_PREFIX = "slug:" const val URL_SEARCH_PREFIX = "slug:"
val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)${'$'}""".toRegex()
} }
} }

View File

@ -221,9 +221,9 @@ abstract class MangaBox(
val value = date.split(' ')[0].toIntOrNull() val value = date.split(' ')[0].toIntOrNull()
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
when { when {
value != null && "min" in date -> cal.apply { add(Calendar.MINUTE, -value) } value != null && "min" in date -> cal.apply { add(Calendar.MINUTE, value * -1) }
value != null && "hour" in date -> cal.apply { add(Calendar.HOUR_OF_DAY, -value) } value != null && "hour" in date -> cal.apply { add(Calendar.HOUR_OF_DAY, value * -1) }
value != null && "day" in date -> cal.apply { add(Calendar.DATE, -value) } value != null && "day" in date -> cal.apply { add(Calendar.DATE, value * -1) }
else -> null else -> null
}?.timeInMillis }?.timeInMillis
} else { } else {

View File

@ -7,7 +7,6 @@ sort_by_filter_views=Views
sort_by_filter_updated=Updated sort_by_filter_updated=Updated
sort_by_filter_added=Added sort_by_filter_added=Added
status_filter_title=Status status_filter_title=Status
status_filter_all=All
status_filter_ongoing=Ongoing status_filter_ongoing=Ongoing
status_filter_hiatus=Hiatus status_filter_hiatus=Hiatus
status_filter_dropped=Dropped status_filter_dropped=Dropped

View File

@ -7,7 +7,6 @@ sort_by_filter_views=Vistas
sort_by_filter_updated=Actualización sort_by_filter_updated=Actualización
sort_by_filter_added=Agregado sort_by_filter_added=Agregado
status_filter_title=Estado status_filter_title=Estado
status_filter_all=Todos
status_filter_ongoing=En curso status_filter_ongoing=En curso
status_filter_hiatus=En pausa status_filter_hiatus=En pausa
status_filter_dropped=Abandonado status_filter_dropped=Abandonado

View File

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

View File

@ -127,10 +127,8 @@ abstract class MangaEsp(
val statusFilter = filterList.firstInstanceOrNull<StatusFilter>() val statusFilter = filterList.firstInstanceOrNull<StatusFilter>()
if (statusFilter != null) { if (statusFilter != null) {
if (statusFilter.toUriPart() != 0) {
filteredList = filteredList.filter { it.status == statusFilter.toUriPart() }.toMutableList() filteredList = filteredList.filter { it.status == statusFilter.toUriPart() }.toMutableList()
} }
}
val sortByFilter = filterList.firstInstanceOrNull<SortByFilter>() val sortByFilter = filterList.firstInstanceOrNull<SortByFilter>()
@ -218,7 +216,6 @@ abstract class MangaEsp(
) )
protected open fun getStatusList() = arrayOf( protected open fun getStatusList() = arrayOf(
Pair(intl["status_filter_all"], 0),
Pair(intl["status_filter_ongoing"], 1), Pair(intl["status_filter_ongoing"], 1),
Pair(intl["status_filter_hiatus"], 2), Pair(intl["status_filter_hiatus"], 2),
Pair(intl["status_filter_dropped"], 3), Pair(intl["status_filter_dropped"], 3),
@ -249,7 +246,7 @@ abstract class MangaEsp(
companion object { companion object {
private val UNESCAPE_REGEX = """\\(.)""".toRegex() private val UNESCAPE_REGEX = """\\(.)""".toRegex()
val MANGA_LIST_REGEX = """self\.__next_f\.push\(.*data\\":(\[.*trending.*])\}""".toRegex() val MANGA_LIST_REGEX = """self\.__next_f\.push\(.*data\\":(\[.*trending.*])\}""".toRegex()
val MANGA_DETAILS_REGEX = """self\.__next_f\.push\(.*data\\":(\{.*lastChapters.*\}).*\\"numFollow""".toRegex() private val MANGA_DETAILS_REGEX = """self\.__next_f\.push\(.*data\\":(\{.*lastChapters.*\}).*\\"numFollow""".toRegex()
const val MANGAS_PER_PAGE = 15 const val MANGAS_PER_PAGE = 15
} }
} }

View File

@ -292,7 +292,7 @@ abstract class MangaThemesia(
listOf("canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné") listOf("canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné")
.any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED .any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED
listOf("hiatus", "on hold", "pausado", "en espera", "en pause", "en attente", "hiato") listOf("hiatus", "on hold", "pausado", "en espera", "en pause", "en attente")
.any { this.contains(it, ignoreCase = true) } -> SManga.ON_HIATUS .any { this.contains(it, ignoreCase = true) } -> SManga.ON_HIATUS
else -> SManga.UNKNOWN else -> SManga.UNKNOWN

View File

@ -5,5 +5,5 @@ plugins {
baseVersionCode = 9 baseVersionCode = 9
dependencies { dependencies {
implementation(project(":lib:zipinterceptor")) compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
} }

View File

@ -1,7 +1,12 @@
package eu.kanade.tachiyomi.multisrc.peachscan package eu.kanade.tachiyomi.multisrc.peachscan
import android.annotation.SuppressLint import android.annotation.SuppressLint
import eu.kanade.tachiyomi.lib.zipinterceptor.ZipInterceptor import android.app.ActivityManager
import android.app.Application
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect
import android.util.Base64
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -16,16 +21,23 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
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 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.io.ByteArrayOutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import java.util.zip.ZipInputStream
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
abstract class PeachScan( abstract class PeachScan(
@ -41,7 +53,7 @@ abstract class PeachScan(
override val client = network.cloudflareClient override val client = network.cloudflareClient
.newBuilder() .newBuilder()
.addInterceptor(ZipInterceptor()::zipImageInterceptor) .addInterceptor(::zipImageInterceptor)
.build() .build()
private val json: Json by injectLazy() private val json: Json by injectLazy()
@ -180,6 +192,90 @@ abstract class PeachScan(
return GET(page.imageUrl!!, imgHeaders) return GET(page.imageUrl!!, imgHeaders)
} }
private val dataUriRegex = Regex("""base64,([0-9a-zA-Z/+=\s]+)""")
private fun zipImageInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val filename = request.url.pathSegments.last()
if (request.url.fragment != "page" || !filename.contains(".zip")) {
return response
}
val zis = ZipInputStream(response.body.byteStream())
val images = generateSequence { zis.nextEntry }
.mapNotNull {
val entryName = it.name
val splitEntryName = entryName.split('.')
val entryIndex = splitEntryName.first().toInt()
val entryType = splitEntryName.last()
val imageData = if (entryType == "avif" || splitEntryName.size == 1) {
zis.readBytes()
} else {
val svgBytes = zis.readBytes()
val svgContent = svgBytes.toString(Charsets.UTF_8)
val b64 = dataUriRegex.find(svgContent)?.groupValues?.get(1)
?: return@mapNotNull null
Base64.decode(b64, Base64.DEFAULT)
}
entryIndex to PeachScanUtils.decodeImage(imageData, isLowRamDevice, filename, entryName)
}
.sortedBy { it.first }
.toList()
zis.closeEntry()
zis.close()
val totalWidth = images.maxOf { it.second.width }
val totalHeight = images.sumOf { it.second.height }
val result = Bitmap.createBitmap(totalWidth, totalHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
var dy = 0
images.forEach {
val srcRect = Rect(0, 0, it.second.width, it.second.height)
val dstRect = Rect(0, dy, it.second.width, dy + it.second.height)
canvas.drawBitmap(it.second, srcRect, dstRect, null)
dy += it.second.height
}
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
val image = output.toByteArray()
val body = image.toResponseBody("image/jpeg".toMediaType())
return response.newBuilder()
.body(body)
.build()
}
/**
* ActivityManager#isLowRamDevice is based on a system property, which isn't
* necessarily trustworthy. 1GB is supposedly the regular threshold.
*
* Instead, we consider anything with less than 3GB of RAM as low memory
* considering how heavy image processing can be.
*/
private val isLowRamDevice by lazy {
val ctx = Injekt.get<Application>()
val activityManager = ctx.getSystemService("activity") as ActivityManager
val memInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memInfo)
memInfo.totalMem < 3L * 1024 * 1024 * 1024
}
companion object { companion object {
const val URL_SEARCH_PREFIX = "slug:" const val URL_SEARCH_PREFIX = "slug:"
} }

View File

@ -1,29 +1,26 @@
package eu.kanade.tachiyomi.lib.zipinterceptor package eu.kanade.tachiyomi.multisrc.peachscan
import android.app.ActivityManager
import android.app.Application
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.util.Base64
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.lang.reflect.Method import java.lang.reflect.Method
import java.util.zip.ZipInputStream
object ImageDecoderWrapper { /**
* TachiyomiJ2K is on a 2-year-old version of ImageDecoder at the time of writing,
* with a different signature than the one being used as a compile-only dependency.
*
* Because of this, if [ImageDecoder.decode] is called as-is on TachiyomiJ2K, we
* end up with a [NoSuchMethodException].
*
* This is a hack for determining which signature to call when decoding images.
*/
object PeachScanUtils {
private var decodeMethod: Method private var decodeMethod: Method
private var newInstanceMethod: Method private var newInstanceMethod: Method
private var classSignature = ClassSignature.Newest private var classSignature = ClassSignature.Newest
private enum class ClassSignature { private enum class ClassSignature {
@ -38,6 +35,7 @@ object ImageDecoderWrapper {
val inputStreamClass = InputStream::class.java val inputStreamClass = InputStream::class.java
try { try {
// Mihon Preview r6595+
classSignature = ClassSignature.Newest classSignature = ClassSignature.Newest
// decode(region, sampleSize) // decode(region, sampleSize)
@ -56,6 +54,7 @@ object ImageDecoderWrapper {
) )
} catch (_: NoSuchMethodException) { } catch (_: NoSuchMethodException) {
try { try {
// Mihon Stable & forks
classSignature = ClassSignature.New classSignature = ClassSignature.New
// decode(region, rgb565, sampleSize, applyColorManagement, displayProfile) // decode(region, rgb565, sampleSize, applyColorManagement, displayProfile)
@ -75,6 +74,7 @@ object ImageDecoderWrapper {
booleanClass, booleanClass,
) )
} catch (_: NoSuchMethodException) { } catch (_: NoSuchMethodException) {
// Tachiyomi J2k
classSignature = ClassSignature.Old classSignature = ClassSignature.Old
// decode(region, rgb565, sampleSize) // decode(region, rgb565, sampleSize)
@ -122,97 +122,3 @@ object ImageDecoderWrapper {
return bitmap return bitmap
} }
} }
open class ZipInterceptor {
private val dataUriRegex = Regex("""base64,([0-9a-zA-Z/+=\s]+)""")
open fun zipGetByteStream(request: Request, response: Response): InputStream {
return response.body.byteStream()
}
open fun requestIsZipImage(request: Request): Boolean {
return request.url.fragment == "page" && request.url.pathSegments.last().contains(".zip")
}
fun zipImageInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val filename = request.url.pathSegments.last()
if (requestIsZipImage(request).not()) {
return response
}
val zis = ZipInputStream(zipGetByteStream(request, response))
val images = generateSequence { zis.nextEntry }
.mapNotNull {
val entryName = it.name
val splitEntryName = entryName.split('.')
val entryIndex = splitEntryName.first().toInt()
val entryType = splitEntryName.last()
val imageData = if (entryType == "avif" || splitEntryName.size == 1) {
zis.readBytes()
} else {
val svgBytes = zis.readBytes()
val svgContent = svgBytes.toString(Charsets.UTF_8)
val b64 = dataUriRegex.find(svgContent)?.groupValues?.get(1)
?: return@mapNotNull null
Base64.decode(b64, Base64.DEFAULT)
}
entryIndex to ImageDecoderWrapper.decodeImage(imageData, isLowRamDevice, filename, entryName)
}
.sortedBy { it.first }
.toList()
zis.closeEntry()
zis.close()
val totalWidth = images.maxOf { it.second.width }
val totalHeight = images.sumOf { it.second.height }
val result = Bitmap.createBitmap(totalWidth, totalHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
var dy = 0
images.forEach {
val srcRect = Rect(0, 0, it.second.width, it.second.height)
val dstRect = Rect(0, dy, it.second.width, dy + it.second.height)
canvas.drawBitmap(it.second, srcRect, dstRect, null)
dy += it.second.height
}
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
val image = output.toByteArray()
val body = image.toResponseBody("image/jpeg".toMediaType())
return response.newBuilder()
.body(body)
.build()
}
/**
* ActivityManager#isLowRamDevice is based on a system property, which isn't
* necessarily trustworthy. 1GB is supposedly the regular threshold.
*
* Instead, we consider anything with less than 3GB of RAM as low memory
* considering how heavy image processing can be.
*/
private val isLowRamDevice by lazy {
val ctx = Injekt.get<Application>()
val activityManager = ctx.getSystemService("activity") as ActivityManager
val memInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memInfo)
memInfo.totalMem < 3L * 1024 * 1024 * 1024
}
}

View File

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

View File

@ -152,25 +152,25 @@ abstract class Zbulu(
val value = date.split(' ')[0].toInt() val value = date.split(' ')[0].toInt()
when { when {
"second" in date -> Calendar.getInstance().apply { "second" in date -> Calendar.getInstance().apply {
add(Calendar.SECOND, -value) add(Calendar.SECOND, value * -1)
}.timeInMillis }.timeInMillis
"minute" in date -> Calendar.getInstance().apply { "minute" in date -> Calendar.getInstance().apply {
add(Calendar.MINUTE, -value) add(Calendar.MINUTE, value * -1)
}.timeInMillis }.timeInMillis
"hour" in date -> Calendar.getInstance().apply { "hour" in date -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, -value) add(Calendar.HOUR_OF_DAY, value * -1)
}.timeInMillis }.timeInMillis
"day" in date -> Calendar.getInstance().apply { "day" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, -value) add(Calendar.DATE, value * -1)
}.timeInMillis }.timeInMillis
"week" in date -> Calendar.getInstance().apply { "week" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, -value * 7) add(Calendar.DATE, value * 7 * -1)
}.timeInMillis }.timeInMillis
"month" in date -> Calendar.getInstance().apply { "month" in date -> Calendar.getInstance().apply {
add(Calendar.MONTH, -value) add(Calendar.MONTH, value * -1)
}.timeInMillis }.timeInMillis
"year" in date -> Calendar.getInstance().apply { "year" in date -> Calendar.getInstance().apply {
add(Calendar.YEAR, -value) add(Calendar.YEAR, value * -1)
}.timeInMillis }.timeInMillis
else -> { else -> {
0L 0L

View File

@ -5,7 +5,6 @@ import android.util.Base64
import java.security.MessageDigest import java.security.MessageDigest
import java.util.Arrays import java.util.Arrays
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec

View File

@ -1,7 +0,0 @@
plugins {
id("lib-android")
}
dependencies {
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
}

View File

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

View File

@ -38,7 +38,6 @@ import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
@ -117,7 +116,7 @@ open class BatoTo(
.build() .build()
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page", headers) return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page")
} }
override fun latestUpdatesSelector(): String { override fun latestUpdatesSelector(): String {
@ -141,7 +140,7 @@ open class BatoTo(
override fun latestUpdatesNextPageSelector() = "div#mainer nav.d-none .pagination .page-item:last-of-type:not(.disabled)" override fun latestUpdatesNextPageSelector() = "div#mainer nav.d-none .pagination .page-item:last-of-type:not(.disabled)"
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/browse?langs=$siteLang&sort=views_a&page=$page", headers) return GET("$baseUrl/browse?langs=$siteLang&sort=views_a&page=$page")
} }
override fun popularMangaSelector() = latestUpdatesSelector() override fun popularMangaSelector() = latestUpdatesSelector()
@ -324,7 +323,7 @@ open class BatoTo(
return super.mangaDetailsRequest(manga) return super.mangaDetailsRequest(manga)
} }
private var titleRegex: Regex = private var titleRegex: Regex =
Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|《[^》]*》|⌜.+?⌝|⟨[^⟩]*⟩|/.+)") Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|/.+?)\\s*|([|/~].*)|-.*-")
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select("div#mainer div.container-fluid") val infoElement = document.select("div#mainer div.container-fluid")
@ -363,55 +362,44 @@ open class BatoTo(
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
private fun altChapterParse(response: Response): List<SChapter> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val url = client.newCall(
GET(
when {
manga.url.startsWith("http") -> manga.url
else -> "$baseUrl${manga.url}"
},
),
).execute().asJsoup()
if (getAltChapterListPref() || checkChapterLists(url)) {
val id = manga.url.substringBeforeLast("/").substringAfterLast("/").trim()
return client.newCall(GET("$baseUrl/rss/series/$id.xml"))
.asObservableSuccess()
.map { altChapterParse(it, manga.title) }
}
return super.fetchChapterList(manga)
}
private fun altChapterParse(response: Response, title: String): List<SChapter> {
return Jsoup.parse(response.body.string(), response.request.url.toString(), Parser.xmlParser()) return Jsoup.parse(response.body.string(), response.request.url.toString(), Parser.xmlParser())
.select("channel > item").map { item -> .select("channel > item").map { item ->
SChapter.create().apply { SChapter.create().apply {
url = item.selectFirst("guid")!!.text() url = item.selectFirst("guid")!!.text()
name = item.selectFirst("title")!!.text() name = item.selectFirst("title")!!.text().substringAfter(title).trim()
date_upload = parseAltChapterDate(item.selectFirst("pubDate")!!.text()) date_upload = SimpleDateFormat("E, dd MMM yyyy H:m:s Z", Locale.US).parse(item.selectFirst("pubDate")!!.text())?.time ?: 0L
} }
} }
} }
private val altDateFormat = SimpleDateFormat("E, dd MMM yyyy H:m:s Z", Locale.US)
private fun parseAltChapterDate(date: String): Long {
return try {
altDateFormat.parse(date)!!.time
} catch (_: ParseException) {
0L
}
}
private fun checkChapterLists(document: Document): Boolean { private fun checkChapterLists(document: Document): Boolean {
return document.select(".episode-list > .alert-warning").text().contains("This comic has been marked as deleted and the chapter list is not available.") return document.select(".episode-list > .alert-warning").text().contains("This comic has been marked as deleted and the chapter list is not available.")
} }
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
return if (getAltChapterListPref()) { if (manga.url.startsWith("http")) {
val id = manga.url.substringBeforeLast("/").substringAfterLast("/").trim() return GET(manga.url, headers)
GET("$baseUrl/rss/series/$id.xml", headers)
} else if (manga.url.startsWith("http")) {
GET(manga.url, headers)
} else {
super.chapterListRequest(manga)
} }
} return super.chapterListRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
if (getAltChapterListPref()) {
return altChapterParse(response)
}
val document = response.asJsoup()
if (checkChapterLists(document)) {
throw Exception("Deleted from site")
}
return document.select(chapterListSelector())
.map(::chapterFromElement)
} }
override fun chapterListSelector() = "div.main div.p-2" override fun chapterListSelector() = "div.main div.p-2"
@ -440,46 +428,46 @@ open class BatoTo(
return when { return when {
"secs" in date -> Calendar.getInstance().apply { "secs" in date -> Calendar.getInstance().apply {
add(Calendar.SECOND, -value) add(Calendar.SECOND, value * -1)
}.timeInMillis }.timeInMillis
"mins" in date -> Calendar.getInstance().apply { "mins" in date -> Calendar.getInstance().apply {
add(Calendar.MINUTE, -value) add(Calendar.MINUTE, value * -1)
}.timeInMillis }.timeInMillis
"hours" in date -> Calendar.getInstance().apply { "hours" in date -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, -value) add(Calendar.HOUR_OF_DAY, value * -1)
}.timeInMillis }.timeInMillis
"days" in date -> Calendar.getInstance().apply { "days" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, -value) add(Calendar.DATE, value * -1)
}.timeInMillis }.timeInMillis
"weeks" in date -> Calendar.getInstance().apply { "weeks" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, -value * 7) add(Calendar.DATE, value * 7 * -1)
}.timeInMillis }.timeInMillis
"months" in date -> Calendar.getInstance().apply { "months" in date -> Calendar.getInstance().apply {
add(Calendar.MONTH, -value) add(Calendar.MONTH, value * -1)
}.timeInMillis }.timeInMillis
"years" in date -> Calendar.getInstance().apply { "years" in date -> Calendar.getInstance().apply {
add(Calendar.YEAR, -value) add(Calendar.YEAR, value * -1)
}.timeInMillis }.timeInMillis
"sec" in date -> Calendar.getInstance().apply { "sec" in date -> Calendar.getInstance().apply {
add(Calendar.SECOND, -value) add(Calendar.SECOND, value * -1)
}.timeInMillis }.timeInMillis
"min" in date -> Calendar.getInstance().apply { "min" in date -> Calendar.getInstance().apply {
add(Calendar.MINUTE, -value) add(Calendar.MINUTE, value * -1)
}.timeInMillis }.timeInMillis
"hour" in date -> Calendar.getInstance().apply { "hour" in date -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, -value) add(Calendar.HOUR_OF_DAY, value * -1)
}.timeInMillis }.timeInMillis
"day" in date -> Calendar.getInstance().apply { "day" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, -value) add(Calendar.DATE, value * -1)
}.timeInMillis }.timeInMillis
"week" in date -> Calendar.getInstance().apply { "week" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, -value * 7) add(Calendar.DATE, value * 7 * -1)
}.timeInMillis }.timeInMillis
"month" in date -> Calendar.getInstance().apply { "month" in date -> Calendar.getInstance().apply {
add(Calendar.MONTH, -value) add(Calendar.MONTH, value * -1)
}.timeInMillis }.timeInMillis
"year" in date -> Calendar.getInstance().apply { "year" in date -> Calendar.getInstance().apply {
add(Calendar.YEAR, -value) add(Calendar.YEAR, value * -1)
}.timeInMillis }.timeInMillis
else -> { else -> {
return 0 return 0

View File

@ -3,9 +3,6 @@ ignored_groups_summary=Chapters from these groups won't be shown.\nOne group nam
include_tags_title=Include Tags include_tags_title=Include Tags
include_tags_on=More specific, but might contain spoilers! include_tags_on=More specific, but might contain spoilers!
include_tags_off=Only the broader genres include_tags_off=Only the broader genres
group_tags_title=Group Tags (fork must support grouping)
group_tags_on=Will prefix tags with their type
group_tags_off=List all tags together
update_cover_title=Update Covers update_cover_title=Update Covers
update_cover_on=Keep cover updated update_cover_on=Keep cover updated
update_cover_off=Prefer first cover update_cover_off=Prefer first cover

View File

@ -3,9 +3,6 @@ ignored_groups_summary=Capítulos desses grupos não aparecerão.\nUm grupo por
include_tags_title=Incluir Tags include_tags_title=Incluir Tags
include_tags_on=Mais detalhadas, mas podem conter spoilers include_tags_on=Mais detalhadas, mas podem conter spoilers
include_tags_off=Apenas os gêneros básicos include_tags_off=Apenas os gêneros básicos
group_tags_title=Agrupar Tags (necessário fork compatível)
group_tags_on=Prefixar tags com o respectivo tipo
group_tags_off=Listar todas as tags juntas
update_cover_title=Atualizar Capas update_cover_title=Atualizar Capas
update_cover_on=Manter capas atualizadas update_cover_on=Manter capas atualizadas
update_cover_off=Usar apenas a primeira capa update_cover_off=Usar apenas a primeira capa

View File

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

View File

@ -97,20 +97,6 @@ abstract class Comick(
} }
}.also(screen::addPreference) }.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = GROUP_TAGS_PREF
title = intl["group_tags_title"]
summaryOn = intl["group_tags_on"]
summaryOff = intl["group_tags_off"]
setDefaultValue(GROUP_TAGS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(GROUP_TAGS_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
key = FIRST_COVER_PREF key = FIRST_COVER_PREF
title = intl["update_cover_title"] title = intl["update_cover_title"]
@ -163,9 +149,6 @@ abstract class Comick(
private val SharedPreferences.includeMuTags: Boolean private val SharedPreferences.includeMuTags: Boolean
get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT) get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT)
private val SharedPreferences.groupTags: Boolean
get() = getBoolean(GROUP_TAGS_PREF, GROUP_TAGS_DEFAULT)
private val SharedPreferences.updateCover: Boolean private val SharedPreferences.updateCover: Boolean
get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT) get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT)
@ -396,23 +379,22 @@ abstract class Comick(
val coversUrl = val coversUrl =
"$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true" "$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true"
val covers = client.newCall(GET(coversUrl)).execute() val covers = client.newCall(GET(coversUrl)).execute()
.parseAs<Covers>().mdCovers.reversed() .parseAs<Covers>().mdCovers.reversed().toMutableList()
val firstVol = covers.filter { it.vol == "1" }.ifEmpty { covers } if (covers.any { it.vol == "1" }) covers.retainAll { it.vol == "1" }
val originalCovers = firstVol if (
.filter { mangaData.comic.isoLang.orEmpty().startsWith(it.locale.orEmpty()) } covers.any { it.locale == comickLang.split('-').first() }
val localCovers = firstVol ) {
.filter { comickLang.startsWith(it.locale.orEmpty()) } covers.retainAll { it.locale == comickLang.split('-').first() }
}
return mangaData.toSManga( return mangaData.toSManga(
includeMuTags = preferences.includeMuTags, includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition, scorePosition = preferences.scorePosition,
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol }, covers = covers,
groupTags = preferences.groupTags,
) )
} }
return mangaData.toSManga( return mangaData.toSManga(
includeMuTags = preferences.includeMuTags, includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition, scorePosition = preferences.scorePosition,
groupTags = preferences.groupTags,
) )
} }
@ -466,8 +448,7 @@ abstract class Comick(
.map { it.toSChapter(mangaUrl) } .map { it.toSChapter(mangaUrl) }
} }
private val publishedDateFormat = private val publishedDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).apply {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC") timeZone = TimeZone.getTimeZone("UTC")
} }
@ -532,8 +513,6 @@ abstract class Comick(
private const val IGNORED_GROUPS_PREF = "IgnoredGroups" private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags" private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
const val INCLUDE_MU_TAGS_DEFAULT = false const val INCLUDE_MU_TAGS_DEFAULT = false
private const val GROUP_TAGS_PREF = "GroupTags"
const val GROUP_TAGS_DEFAULT = false
private const val MIGRATED_IGNORED_GROUPS = "MigratedIgnoredGroups" private const val MIGRATED_IGNORED_GROUPS = "MigratedIgnoredGroups"
private const val FIRST_COVER_PREF = "DefaultCover" private const val FIRST_COVER_PREF = "DefaultCover"
private const val FIRST_COVER_DEFAULT = true private const val FIRST_COVER_DEFAULT = true

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.all.comickfun package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.GROUP_TAGS_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.INCLUDE_MU_TAGS_DEFAULT import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.INCLUDE_MU_TAGS_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SCORE_POSITION_DEFAULT import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SCORE_POSITION_DEFAULT
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
@ -30,14 +29,13 @@ class Manga(
val comic: Comic, val comic: Comic,
private val artists: List<Name> = emptyList(), private val artists: List<Name> = emptyList(),
private val authors: List<Name> = emptyList(), private val authors: List<Name> = emptyList(),
private val genres: List<Genre> = emptyList(), private val genres: List<Name> = emptyList(),
private val demographic: String? = null, private val demographic: String? = null,
) { ) {
fun toSManga( fun toSManga(
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT, includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
scorePosition: String = SCORE_POSITION_DEFAULT, scorePosition: String = SCORE_POSITION_DEFAULT,
covers: List<MDcovers>? = null, covers: List<MDcovers>? = null,
groupTags: Boolean = GROUP_TAGS_DEFAULT,
) = ) =
SManga.create().apply { SManga.create().apply {
// appennding # at end as part of migration from slug to hid // appennding # at end as part of migration from slug to hid
@ -77,23 +75,19 @@ class Manga(
artist = artists.joinToString { it.name.trim() } artist = artists.joinToString { it.name.trim() }
author = authors.joinToString { it.name.trim() } author = authors.joinToString { it.name.trim() }
genre = buildList { genre = buildList {
comic.origination?.let { add(Genre("Origination", it.name)) } comic.origination?.let(::add)
demographic?.let { add(Genre("Demographic", it)) } demographic?.let { add(Name(it)) }
addAll( addAll(genres)
comic.mdGenres.mapNotNull { it.genre }.sortedBy { it.group } addAll(comic.mdGenres.mapNotNull { it.name })
.sortedBy { it.name },
)
addAll(genres.sortedBy { it.group }.sortedBy { it.name })
if (includeMuTags) { if (includeMuTags) {
addAll( comic.muGenres.categories.forEach { category ->
comic.muGenres.categories.mapNotNull { it?.category?.title }.sorted() category?.category?.title?.let { add(Name(it)) }
.map { Genre("Category", it) }, }
)
} }
} }
.distinctBy { it.name } .distinctBy { it.name }
.filterNot { it.name.isNullOrBlank() || it.group.isNullOrBlank() } .filter { it.name.isNotBlank() }
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" } .joinToString { it.name.trim() }
} }
} }
@ -112,7 +106,6 @@ class Comic(
@SerialName("md_comic_md_genres") val mdGenres: List<MdGenres>, @SerialName("md_comic_md_genres") val mdGenres: List<MdGenres>,
@SerialName("mu_comics") val muGenres: MuComicCategories = MuComicCategories(emptyList()), @SerialName("mu_comics") val muGenres: MuComicCategories = MuComicCategories(emptyList()),
@SerialName("bayesian_rating") val score: String? = null, @SerialName("bayesian_rating") val score: String? = null,
@SerialName("iso639_1") val isoLang: String? = null,
) { ) {
val origination = when (country) { val origination = when (country) {
"jp" -> Name("Manga") "jp" -> Name("Manga")
@ -135,13 +128,7 @@ class Comic(
@Serializable @Serializable
class MdGenres( class MdGenres(
@SerialName("md_genres") val genre: Genre? = null, @SerialName("md_genres") val name: Name? = null,
)
@Serializable
class Genre(
val group: String? = null,
val name: String? = null,
) )
@Serializable @Serializable

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'E-Hentai' extName = 'E-Hentai'
extClass = '.EHFactory' extClass = '.EHFactory'
extVersionCode = 22 extVersionCode = 20
isNsfw = true isNsfw = true
} }

View File

@ -4,17 +4,17 @@ import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.webkit.CookieManager
import androidx.preference.CheckBoxPreference import androidx.preference.CheckBoxPreference
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource 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.Filter.CheckBox import eu.kanade.tachiyomi.source.model.Filter.CheckBox
import eu.kanade.tachiyomi.source.model.Filter.Group
import eu.kanade.tachiyomi.source.model.Filter.Select import eu.kanade.tachiyomi.source.model.Filter.Select
import eu.kanade.tachiyomi.source.model.Filter.Text import eu.kanade.tachiyomi.source.model.Filter.Text
import eu.kanade.tachiyomi.source.model.Filter.TriState
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
@ -39,22 +39,13 @@ abstract class EHentai(
private val ehLang: String, private val ehLang: String,
) : ConfigurableSource, HttpSource() { ) : ConfigurableSource, HttpSource() {
override val name = "E-Hentai"
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() } override val name = "E-Hentai"
private val memberId: String by lazy { getMemberIdPref() }
private val passHash: String by lazy { getPassHashPref() }
override val baseUrl: String override val baseUrl = "https://e-hentai.org"
get() = when {
System.getenv("CI") == "true" -> "https://e-hentai.org"
memberId.isNotEmpty() && passHash.isNotEmpty() -> "https://exhentai.org"
else -> "https://e-hentai.org"
}
override val supportsLatest = true override val supportsLatest = true
@ -82,7 +73,7 @@ abstract class EHentai(
val manga = mangaElements[i].let { val manga = mangaElements[i].let {
SManga.create().apply { SManga.create().apply {
// Get title // Get title
it.selectFirst("a")?.apply { it.select("a")?.first()?.apply {
title = this.select(".glink").text() title = this.select(".glink").text()
url = ExGalleryMetadata.normalizeUrl(attr("href")) url = ExGalleryMetadata.normalizeUrl(attr("href"))
if (i == mangaElements.lastIndex) { if (i == mangaElements.lastIndex) {
@ -140,9 +131,9 @@ abstract class EHentai(
} }
private fun parseChapterPage(response: Element) = with(response) { private fun parseChapterPage(response: Element) = with(response) {
select("#gdt a").map { select(".gdtm a").map {
it.attr("href") Pair(it.child(0).attr("alt").toInt(), it.attr("href"))
} }.sortedBy(Pair<Int, String>::first).map { it.second }
} }
private fun chapterPageCall(np: String) = client.newCall(chapterPageRequest(np)).asObservableSuccess() private fun chapterPageCall(np: String) = client.newCall(chapterPageRequest(np)).asObservableSuccess()
@ -170,23 +161,10 @@ abstract class EHentai(
query.isBlank() -> languageTag(enforceLanguageFilter) query.isBlank() -> languageTag(enforceLanguageFilter)
else -> languageTag(enforceLanguageFilter).let { if (it.isNotEmpty()) "$query,$it" else query } else -> languageTag(enforceLanguageFilter).let { if (it.isNotEmpty()) "$query,$it" else query }
} }
filters.filterIsInstance<TextFilter>().forEach { it -> modifiedQuery += filters.filterIsInstance<TagFilter>()
if (it.state.isNotEmpty()) { .flatMap { it.markedTags() }
val splitted = it.state.split(",").filter(String::isNotBlank) .joinToString(",")
if (splitted.size < 2 && it.type != "tags") { .let { if (it.isNotEmpty()) ",$it" else it }
modifiedQuery += " ${it.type}:\"${it.state.replace(" ", "+")}\""
} else {
splitted.forEach { tag ->
val trimmed = tag.trim().lowercase()
if (trimmed.startsWith('-')) {
modifiedQuery += " -${it.type}:\"${trimmed.removePrefix("-").replace(" ", "+")}\""
} else {
modifiedQuery += " ${it.type}:\"${trimmed.replace(" ", "+")}\""
}
}
}
}
}
uri.appendQueryParameter("f_search", modifiedQuery) uri.appendQueryParameter("f_search", modifiedQuery)
// when attempting to search with no genres selected, will auto select all genres // when attempting to search with no genres selected, will auto select all genres
filters.filterIsInstance<GenreGroup>().firstOrNull()?.state?.let { filters.filterIsInstance<GenreGroup>().firstOrNull()?.state?.let {
@ -374,12 +352,6 @@ abstract class EHentai(
// Bypass "Offensive For Everyone" content warning // Bypass "Offensive For Everyone" content warning
cookies["nw"] = "1" cookies["nw"] = "1"
cookies["ipb_member_id"] = memberId
cookies["ipb_pass_hash"] = passHash
cookies["igneous"] = ""
buildCookies(cookies) buildCookies(cookies)
} }
@ -416,17 +388,12 @@ abstract class EHentai(
EnforceLanguageFilter(getEnforceLanguagePref()), EnforceLanguageFilter(getEnforceLanguagePref()),
Watched(), Watched(),
GenreGroup(), GenreGroup(),
Filter.Header("Separate tags with commas (,)"), TagFilter("Misc Tags", triStateBoxesFrom(miscTags), "other"),
Filter.Header("Prepend with dash (-) to exclude"), TagFilter("Female Tags", triStateBoxesFrom(femaleTags), "female"),
Filter.Header("Use 'Female Tags' or 'Male Tags' for specific categories. 'Tags' searches all categories."), TagFilter("Male Tags", triStateBoxesFrom(maleTags), "male"),
TextFilter("Tags", "tag"),
TextFilter("Female Tags", "female"),
TextFilter("Male Tags", "male"),
AdvancedGroup(), AdvancedGroup(),
) )
internal open class TextFilter(name: String, val type: String, val specific: String = "") : Filter.Text(name)
class Watched : CheckBox("Watched List"), UriFilter { class Watched : CheckBox("Watched List"), UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
if (state) { if (state) {
@ -520,6 +487,17 @@ abstract class EHentai(
private class EnforceLanguageFilter(default: Boolean) : CheckBox("Enforce language", default) private class EnforceLanguageFilter(default: Boolean) : CheckBox("Enforce language", default)
private val miscTags = "3d, already uploaded, anaglyph, animal on animal, animated, anthology, arisa mizuhara, artbook, ashiya noriko, bailey jay, body swap, caption, chouzuki maryou, christian godard, comic, compilation, dakimakura, fe galvao, ffm threesome, figure, forbidden content, full censorship, full color, game sprite, goudoushi, group, gunyou mikan, harada shigemitsu, hardcore, helly von valentine, higurashi rin, hololive, honey select, how to, incest, incomplete, ishiba yoshikazu, jessica nigri, kalinka fox, kanda midori, kira kira, kitami eri, kuroi hiroki, lenfried, lincy leaw, marie claude bourbonnais, matsunaga ayaka, me me me, missing cover, mmf threesome, mmt threesome, mosaic censorship, mtf threesome, multi-work series, no penetration, non-nude, novel, nudity only, oakazaki joe, out of order, paperchild, pm02 colon 20, poor grammar, radio comix, realporn, redraw, replaced, sakaki kasa, sample, saotome love, scanmark, screenshots, sinful goddesses, sketch lines, stereoscopic, story arc, takeuti ken, tankoubon, themeless, tikuma jukou, time stop, tsubaki zakuro, ttm threesome, twins, uncensored, vandych alex, variant set, watermarked, webtoon, western cg, western imageset, western non-h, yamato nadeshiko club, yui okada, yukkuri, zappa go"
private val femaleTags = "ahegao, anal, angel, apron, bandages, bbw, bdsm, beauty mark, big areolae, big ass, big breasts, big clit, big lips, big nipples, bikini, blackmail, bloomers, blowjob, bodysuit, bondage, breast expansion, bukkake, bunny girl, business suit, catgirl, centaur, cheating, chinese dress, christmas, collar, corset, cosplaying, cowgirl, crossdressing, cunnilingus, dark skin, daughter, deepthroat, defloration, demon girl, double penetration, dougi, dragon, drunk, elf, exhibitionism, farting, females only, femdom, filming, fingering, fishnets, footjob, fox girl, furry, futanari, garter belt, ghost, giantess, glasses, gloves, goblin, gothic lolita, growth, guro, gyaru, hair buns, hairy, hairy armpits, handjob, harem, hidden sex, horns, huge breasts, humiliation, impregnation, incest, inverted nipples, kemonomimi, kimono, kissing, lactation, latex, leg lock, leotard, lingerie, lizard girl, maid, masked face, masturbation, midget, miko, milf, mind break, mind control, monster girl, mother, muscle, nakadashi, netorare, nose hook, nun, nurse, oil, paizuri, panda girl, pantyhose, piercing, pixie cut, policewoman, ponytail, pregnant, rape, rimjob, robot, scat, schoolgirl uniform, sex toys, shemale, sister, small breasts, smell, sole dickgirl, sole female, squirting, stockings, sundress, sweating, swimsuit, swinging, tail, tall girl, teacher, tentacles, thigh high boots, tomboy, transformation, twins, twintails, unusual pupils, urination, vore, vtuber, widow, wings, witch, wolf girl, x-ray, yuri, zombie"
private val maleTags = "anal, bbm, big ass, big penis, bikini, blood, blowjob, bondage, catboy, cheating, chikan, condom, crab, crossdressing, dark skin, deepthroat, demon, dickgirl on male, dilf, dog boy, double anal, double penetration, dragon, drunk, exhibitionism, facial hair, feminization, footjob, fox boy, furry, glasses, group, guro, hairy, handjob, hidden sex, horns, huge penis, human on furry, kimono, lingerie, lizard guy, machine, maid, males only, masturbation, mmm threesome, monster, muscle, nakadashi, ninja, octopus, oni, pillory, policeman, possession, prostate massage, public use, schoolboy uniform, schoolgirl uniform, sex toys, shotacon, sleeping, snuff, sole male, stockings, sunglasses, swimsuit, tall man, tentacles, tomgirl, unusual pupils, virginity, waiter, x-ray, yaoi, zombie"
private fun triStateBoxesFrom(tagString: String): List<TagTriState> = tagString.split(", ").map { TagTriState(it) }
class TagTriState(tag: String) : TriState(tag)
class TagFilter(name: String, private val triStateBoxes: List<TagTriState>, private val nameSpace: String) : Group<TagTriState>(name, triStateBoxes) {
fun markedTags() = triStateBoxes.filter { it.isIncluded() }.map { "$nameSpace:${it.name}" } + triStateBoxes.filter { it.isExcluded() }.map { "-$nameSpace:${it.name}" }
}
// map languages to their internal ids // map languages to their internal ids
private val languageMappings = listOf( private val languageMappings = listOf(
Pair("japanese", listOf("0", "1024", "2048")), Pair("japanese", listOf("0", "1024", "2048")),
@ -551,16 +529,6 @@ abstract class EHentai(
private const val ENFORCE_LANGUAGE_PREF_TITLE = "Enforce Language" private const val ENFORCE_LANGUAGE_PREF_TITLE = "Enforce Language"
private const val ENFORCE_LANGUAGE_PREF_SUMMARY = "If checked, forces browsing of manga matching a language tag" private const val ENFORCE_LANGUAGE_PREF_SUMMARY = "If checked, forces browsing of manga matching a language tag"
private const val ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE = false private const val ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE = false
private const val MEMBER_ID_PREF_KEY = "MEMBER_ID"
private const val MEMBER_ID_PREF_TITLE = "ipb_member_id"
private const val MEMBER_ID_PREF_SUMMARY = "ipb_member_id value"
private const val MEMBER_ID_PREF_DEFAULT_VALUE = ""
private const val PASS_HASH_PREF_KEY = "PASS_HASH"
private const val PASS_HASH_PREF_TITLE = "ipb_pass_hash"
private const val PASS_HASH_PREF_SUMMARY = "ipb_pass_hash value"
private const val PASS_HASH_PREF_DEFAULT_VALUE = ""
} }
// Preferences // Preferences
@ -577,56 +545,8 @@ abstract class EHentai(
preferences.edit().putBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", checkValue).commit() preferences.edit().putBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", checkValue).commit()
} }
} }
val memberIdPref = EditTextPreference(screen.context).apply {
key = MEMBER_ID_PREF_KEY
title = MEMBER_ID_PREF_TITLE
summary = MEMBER_ID_PREF_SUMMARY
setDefaultValue(MEMBER_ID_PREF_DEFAULT_VALUE)
}
val passHashPref = EditTextPreference(screen.context).apply {
key = PASS_HASH_PREF_KEY
title = PASS_HASH_PREF_TITLE
summary = PASS_HASH_PREF_SUMMARY
setDefaultValue(PASS_HASH_PREF_DEFAULT_VALUE)
}
screen.addPreference(memberIdPref)
screen.addPreference(passHashPref)
screen.addPreference(enforceLanguagePref) screen.addPreference(enforceLanguagePref)
} }
private fun getEnforceLanguagePref(): Boolean = preferences.getBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE) private fun getEnforceLanguagePref(): Boolean = preferences.getBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
private fun getCookieValue(cookieTitle: String, defaultValue: String, prefKey: String): String {
val cookies = webViewCookieManager.getCookie("https://forums.e-hentai.org")
var value: String? = null
if (cookies != null) {
val cookieArray = cookies.split("; ")
for (cookie in cookieArray) {
if (cookie.startsWith("$cookieTitle=")) {
value = cookie.split("=")[1]
break
}
}
}
if (value == null) {
value = preferences.getString(prefKey, defaultValue) ?: defaultValue
}
return value
}
private fun getPassHashPref(): String {
return getCookieValue(PASS_HASH_PREF_TITLE, PASS_HASH_PREF_DEFAULT_VALUE, PASS_HASH_PREF_KEY)
}
private fun getMemberIdPref(): String {
return getCookieValue(MEMBER_ID_PREF_TITLE, MEMBER_ID_PREF_DEFAULT_VALUE, MEMBER_ID_PREF_KEY)
}
} }

View File

@ -3,7 +3,7 @@ ext {
extClass = '.EternalMangasFactory' extClass = '.EternalMangasFactory'
themePkg = 'mangaesp' themePkg = 'mangaesp'
baseUrl = 'https://eternalmangas.com' baseUrl = 'https://eternalmangas.com'
overrideVersionCode = 1 overrideVersionCode = 0
isNsfw = true isNsfw = true
} }

View File

@ -7,17 +7,11 @@ import eu.kanade.tachiyomi.network.POST
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
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Response import okhttp3.Response
import org.jsoup.Jsoup
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
open class EternalMangas( open class EternalMangas(
lang: String, lang: String,
@ -61,62 +55,9 @@ open class EternalMangas(
return parseComicsList(page, query, filters) return parseComicsList(page, query, filters)
} }
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val body = jsRedirect(response)
MANGA_DETAILS_REGEX.find(body)?.groupValues?.get(1)?.let {
val unescapedJson = it.unescape()
return json.decodeFromString<SeriesDto>(unescapedJson).toSMangaDetails()
}
val document = Jsoup.parse(body)
with(document.selectFirst("div#info")!!) {
title = select("div:has(p.font-bold:contains(Títuto)) > p.text-sm").text()
author = select("div:has(p.font-bold:contains(Autor)) > p.text-sm").text()
artist = select("div:has(p.font-bold:contains(Artista)) > p.text-sm").text()
genre = select("div:has(p.font-bold:contains(Género)) > p.text-sm > span").joinToString { it.ownText() }
}
description = document.select("div#sinopsis p").text()
thumbnail_url = document.selectFirst("div.contenedor img.object-cover")?.imgAttr()
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
override fun chapterListParse(response: Response): List<SChapter> {
val body = jsRedirect(response)
MANGA_DETAILS_REGEX.find(body)?.groupValues?.get(1)?.let {
val unescapedJson = it.unescape()
val series = json.decodeFromString<SeriesDto>(unescapedJson)
return series.chapters.map { chapter -> chapter.toSChapter(seriesPath, series.slug) }
}
val document = Jsoup.parse(body)
return document.select("div.contenedor > div.grid > div > a").map {
SChapter.create().apply {
name = it.selectFirst("span.text-sm")!!.text()
date_upload = try {
it.selectFirst("span.chapter-date")?.attr("data-date")?.let { date ->
dateFormat.parse(date)?.time
} ?: 0
} catch (e: ParseException) {
0
}
setUrlWithoutDomain(it.selectFirst("a")!!.attr("href"))
}
}
}
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val doc = Jsoup.parse(jsRedirect(response)) var document = response.asJsoup()
return doc.select("main > img").mapIndexed { i, img ->
Page(i, imageUrl = img.imgAttr())
}
}
private fun jsRedirect(response: Response): String {
var body = response.body.string()
val document = Jsoup.parse(body)
document.selectFirst("body > form[method=post]")?.let { document.selectFirst("body > form[method=post]")?.let {
val action = it.attr("action") val action = it.attr("action")
val inputs = it.select("input") val inputs = it.select("input")
@ -126,9 +67,12 @@ open class EternalMangas(
form.add(input.attr("name"), input.attr("value")) form.add(input.attr("name"), input.attr("value"))
} }
body = client.newCall(POST(action, headers, form.build())).execute().body.string() document = client.newCall(POST(action, headers, form.build())).execute().asJsoup()
}
return document.select("main > img").mapIndexed { i, img ->
Page(i, imageUrl = img.imgAttr())
} }
return body
} }
@Serializable @Serializable

View File

@ -1,8 +0,0 @@
ext {
extName = 'EveriaClub (unoriginal)'
extClass = '.EveriaClubCom'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1,152 +0,0 @@
package eu.kanade.tachiyomi.extension.all.everiaclubcom
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.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
class EveriaClubCom() : HttpSource() {
override val baseUrl = "https://www.everiaclub.com"
override val lang = "all"
override val name = "EveriaClub (unoriginal)"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val Element.imgSrc: String?
get() = when {
hasAttr("data-original") -> attr("data-original")
hasAttr("data-lazy-src") -> attr("data-lazy-src")
hasAttr("data-src") -> attr("data-src")
hasAttr("src") -> attr("src")
else -> null
}
private fun mangaFromElement(it: Element) = SManga.create().apply {
setUrlWithoutDomain(it.attr("abs:href").removePrefix(baseUrl))
with(it.selectFirst("img")!!) {
thumbnail_url = imgSrc
title = attr("title")
}
}
// Latest
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers)
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(".mainleft .leftp > a").map {
mangaFromElement(it)
}
val isLastPage = document.selectFirst("li:has(span.current) + li > a")
return MangasPage(mangas, isLastPage != null)
}
// Popular
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(".mainright li a").map {
mangaFromElement(it)
}
return MangasPage(mangas, false)
}
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val tagFilter = filters.filterIsInstance<TagFilter>().first()
val categoryFilter = filters.filterIsInstance<CategoryFilter>().first()
val url = when {
tagFilter.state.isNotBlank() -> baseUrl.toHttpUrl().newBuilder()
.addPathSegment("tags")
.addPathSegment(tagFilter.state)
.addPathSegment(page.toString())
categoryFilter.state != 0 -> "$baseUrl/${categoryFilter.toUriPart()}?page=$page".toHttpUrl().newBuilder()
query.isNotBlank() -> baseUrl.toHttpUrl().newBuilder()
.addPathSegment("search")
.addPathSegment("")
.addQueryParameter("keyword", query)
.addQueryParameter("page", page.toString())
else -> "$baseUrl/?page=$page".toHttpUrl().newBuilder()
}
return GET(url.build(), headers)
}
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
// Details
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
return SManga.create().apply {
genre = document.select("div.end span:contains(Tags:) ~ a > p.tags").joinToString {
it.ownText()
}
status = SManga.COMPLETED
}
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapter = SChapter.create().apply {
url = manga.url
name = "Gallery"
chapter_number = 1f
date_upload = 0L
}
return Observable.just(listOf(chapter))
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val images = document.select(".mainleft img")
return images.mapIndexed { index, image ->
Page(index, imageUrl = image.imgSrc)
}
}
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException()
// Filters
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Only one filter will be applied!"),
Filter.Separator(),
TagFilter(),
CategoryFilter(),
)
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 CategoryFilter : UriPartFilter(
"Category",
arrayOf(
Pair("Any", ""),
Pair("Gravure", "Gravure.html"),
Pair("Japan", "Japan.html"),
Pair("Korea", "Korea.html"),
Pair("Thailand", "Thailand.html"),
Pair("Chinese", "Chinese.html"),
Pair("Cosplay", "Cosplay.html"),
),
)
class TagFilter : Filter.Text("Tag")
}

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'FoamGirl' extName = 'FoamGirl'
extClass = '.FoamGirl' extClass = '.FoamGirl'
extVersionCode = 2 extVersionCode = 1
isNsfw = true isNsfw = true
} }

View File

@ -6,8 +6,6 @@ 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.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@ -67,43 +65,22 @@ class FoamGirl() : ParsedHttpSource() {
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = popularMangaSelector()
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
val imageCount = document.select(".post_title_topimg").text().substringAfter("(").substringBefore("P").toInt() val imageCount = document.select(".post_title_topimg").text().substringAfter("(").substringBefore("P").toInt()
val imageUrl = document.select(".imageclick-imgbox").attr("href").toHttpUrl() val imageUrl = document.select(".imageclick-imgbox").attr("href").toHttpUrl()
val baseIndex = imageUrl.pathSegments.last().substringBefore(".") val imagePrefix = imageUrl.pathSegments.last().substringBefore(".").toLong() / 10
for (i in 0 until imageCount) {
return if (baseIndex.isNumber()) { pages.add(
getPagesListByNumber(imageCount, imageUrl, baseIndex)
} else {
getPageListByDocument(document)
}
}
private fun getPagesListByNumber(imageCount: Int, imageUrl: HttpUrl, baseIndex: String): List<Page> {
val imagePrefix = baseIndex.toLong() / 10
return (0 until imageCount).map { index ->
Page( Page(
index, i,
imageUrl = imageUrl.newBuilder().apply { imageUrl = imageUrl.newBuilder().apply {
removePathSegment(imageUrl.pathSize - 1) removePathSegment(imageUrl.pathSize - 1)
addPathSegment("${imagePrefix}${index + 2}.jpg") addPathSegment("${imagePrefix}${i + 2}.jpg")
}.build().toString(), }.build().toString(),
),
) )
} }
} return pages
private fun getPageListByDocument(document: Document): List<Page> {
val pages = document.select("#image_div img").mapIndexed { index, element ->
Page(index, imageUrl = element.absUrl("src"))
}.toList()
val nextPageUrl = document.selectFirst(".page-numbers[title=Next]")
?.absUrl("href")
?.takeIf { HAS_NEXT_PAGE_REGEX in it }
?: return pages
return client.newCall(GET(nextPageUrl, headers)).execute().asJsoup().let {
pages + getPageListByDocument(it)
}
} }
override fun chapterFromElement(element: Element) = SChapter.create().apply { override fun chapterFromElement(element: Element) = SChapter.create().apply {
@ -142,10 +119,7 @@ class FoamGirl() : ParsedHttpSource() {
} }
} }
private fun String.isNumber() = isNotEmpty() && all { it.isDigit() }
companion object { companion object {
val HAS_NEXT_PAGE_REGEX = """(\d+_\d+)""".toRegex()
private val DATE_FORMAT by lazy { private val DATE_FORMAT by lazy {
SimpleDateFormat("yyyy.M.d", Locale.ENGLISH) SimpleDateFormat("yyyy.M.d", Locale.ENGLISH)
} }

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hennojin' extName = 'Hennojin'
extClass = '.HennojinFactory' extClass = '.HennojinFactory'
extVersionCode = 2 extVersionCode = 1
isNsfw = true isNsfw = true
} }

View File

@ -1,32 +1,30 @@
package eu.kanade.tachiyomi.extension.all.hennojin package eu.kanade.tachiyomi.extension.all.hennojin
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
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.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class Hennojin(override val lang: String) : ParsedHttpSource() { class Hennojin(override val lang: String, suffix: String) : ParsedHttpSource() {
override val baseUrl = "https://hennojin.com" override val baseUrl = "https://hennojin.com/home/$suffix"
override val name = "Hennojin" override val name = "Hennojin"
// Popular is latest // Popular is latest
override val supportsLatest = false override val supportsLatest = false
private val httpUrl by lazy { "$baseUrl/home".toHttpUrl() } private val httpUrl by lazy { baseUrl.toHttpUrl() }
override fun latestUpdatesSelector() = popularMangaSelector() override fun latestUpdatesSelector() = popularMangaSelector()
@ -43,23 +41,15 @@ class Hennojin(override val lang: String) : ParsedHttpSource() {
override fun popularMangaNextPageSelector() = ".paginate .next" override fun popularMangaNextPageSelector() = ".paginate .next"
override fun popularMangaRequest(page: Int) = override fun popularMangaRequest(page: Int) =
httpUrl.request { httpUrl.request { addEncodedPathSegments("page/$page") }
when (lang) {
"ja" -> {
addEncodedPathSegments("page/$page/")
addQueryParameter("archive", "raw")
}
else -> addEncodedPathSegments("page/$page")
}
}
override fun popularMangaFromElement(element: Element) = override fun popularMangaFromElement(element: Element) =
SManga.create().apply { SManga.create().apply {
element.selectFirst(".title_link > a").let { element.selectFirst(".title_link > a").let {
title = it!!.text() title = it!!.text()
setUrlWithoutDomain(it.absUrl("href")) setUrlWithoutDomain(it.attr("href"))
} }
thumbnail_url = element.selectFirst("img")?.absUrl("src") thumbnail_url = element.selectFirst("img")!!.attr("src")
} }
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = popularMangaSelector()
@ -76,68 +66,46 @@ class Hennojin(override val lang: String) : ParsedHttpSource() {
override fun searchMangaFromElement(element: Element) = override fun searchMangaFromElement(element: Element) =
popularMangaFromElement(element) popularMangaFromElement(element)
override fun mangaDetailsRequest(manga: SManga) =
GET("https://hennojin.com" + manga.url, headers)
override fun mangaDetailsParse(document: Document) = override fun mangaDetailsParse(document: Document) =
SManga.create().apply { SManga.create().apply {
description = document.select( description = document.selectFirst(
".manga-subtitle + p + p", ".manga-subtitle + p + p",
).joinToString("\n") { )?.html()?.replace("<br> ", "\n")
it
.apply { select(Evaluator.Tag("br")).prepend("\\n") }
.text()
.replace("\\n", "\n")
.replace("\n ", "\n")
}.trim()
genre = document.select( genre = document.select(
".tags-list a[href*=/parody/]," + ".tags-list a[href*=/parody/]," +
".tags-list a[href*=/tags/]," + ".tags-list a[href*=/tags/]," +
".tags-list a[href*=/character/]", ".tags-list a[href*=/character/]",
).joinToString { it.text() } )?.joinToString { it.text() }
artist = document.selectFirst( artist = document.select(
".tags-list a[href*=/artist/]", ".tags-list a[href*=/artist/]",
)?.text() )?.joinToString { it.text() }
author = document.selectFirst( author = document.select(
".tags-list a[href*=/group/]", ".tags-list a[href*=/group/]",
)?.text() ?: artist )?.joinToString { it.text() } ?: artist
status = SManga.COMPLETED status = SManga.COMPLETED
} }
override fun chapterListParse(response: Response): List<SChapter> { override fun fetchChapterList(manga: SManga) =
val document = response.asJsoup(response.body.string()) Request.Builder().url(manga.thumbnail_url!!)
val date = document .head().build().run(client::newCall)
.selectFirst(".manga-thumbnail > img") .asObservableSuccess().map { res ->
?.absUrl("src")
?.let { url ->
Request.Builder()
.url(url)
.head()
.build()
.run(client::newCall)
.execute()
.date
}
return document.select("a:contains(Read Online)").map {
SChapter.create().apply { SChapter.create().apply {
setUrlWithoutDomain(
it
?.absUrl("href")
?.toHttpUrlOrNull()
?.newBuilder()
?.removeAllQueryParameters("view")
?.addQueryParameter("view", "multi")
?.build()
?.toString()
?: it.absUrl("href"),
)
name = "Chapter" name = "Chapter"
date?.run { date_upload = this } url = manga.reader
date_upload = res.date
chapter_number = -1f chapter_number = -1f
} }.let(::listOf)
} }!!
}
override fun pageListRequest(chapter: SChapter) =
GET("https://hennojin.com" + chapter.url, headers)
override fun pageListParse(document: Document) = override fun pageListParse(document: Document) =
document.select(".slideshow-container > img") document.select(".slideshow-container > img")
.mapIndexed { idx, img -> Page(idx, imageUrl = img.absUrl("src")) } .mapIndexed { idx, img -> Page(idx, "", img.absUrl("src")) }
private inline fun HttpUrl.request( private inline fun HttpUrl.request(
block: HttpUrl.Builder.() -> HttpUrl.Builder, block: HttpUrl.Builder.() -> HttpUrl.Builder,
@ -146,6 +114,9 @@ class Hennojin(override val lang: String) : ParsedHttpSource() {
private inline val Response.date: Long private inline val Response.date: Long
get() = headers["Last-Modified"]?.run(httpDate::parse)?.time ?: 0L get() = headers["Last-Modified"]?.run(httpDate::parse)?.time ?: 0L
private inline val SManga.reader: String
get() = "/home/manga-reader/?manga=$title&view=multi"
override fun chapterListSelector() = override fun chapterListSelector() =
throw UnsupportedOperationException() throw UnsupportedOperationException()

View File

@ -4,7 +4,7 @@ import eu.kanade.tachiyomi.source.SourceFactory
class HennojinFactory : SourceFactory { class HennojinFactory : SourceFactory {
override fun createSources() = listOf( override fun createSources() = listOf(
Hennojin("en"), Hennojin("en", ""),
Hennojin("ja"), Hennojin("ja", "?archive=raw"),
) )
} }

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.koharu.KoharuUrlActivity"
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:pathPattern="/g/..*/..*"/>
<data android:host="koharu.to" />
<data android:host="schale.network" />
<data android:host="gehenna.jp" />
<data android:host="niyaniya.moe" />
<data android:host="seia.to" />
<data android:host="shupogaki.moe" />
<data android:host="hoshino.one" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,8 +0,0 @@
ext {
extName = 'SchaleNetwork'
extClass = '.KoharuFactory'
extVersionCode = 11
isNsfw = true
}
apply from: "$rootDir/common.gradle"

View File

@ -1,13 +0,0 @@
package eu.kanade.tachiyomi.extension.all.koharu
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class KoharuFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
Koharu(),
Koharu("en", "english"),
Koharu("ja", "japanese"),
Koharu("zh", "chinese"),
)
}

View File

@ -76,8 +76,6 @@ 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 Alternate Titles
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 = 196 extVersionCode = 194
isNsfw = true isNsfw = true
} }

View File

@ -138,11 +138,6 @@ object MDConstants {
return "${altTitlesInDescPref}_$dexLang" return "${altTitlesInDescPref}_$dexLang"
} }
private const val preferExtensionLangTitlePref = "preferExtensionLangTitle"
fun getPreferExtensionLangTitlePrefKey(dexLang: String): String {
return "${preferExtensionLangTitlePref}_$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

@ -113,7 +113,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
.firstInstanceOrNull<CoverArtDto>() .firstInstanceOrNull<CoverArtDto>()
?.attributes?.fileName ?.attributes?.fileName
} }
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang, preferences.preferExtensionLangTitle) helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
} }
return MangasPage(mangaList, mangaListDto.hasNextPage) return MangasPage(mangaList, mangaListDto.hasNextPage)
@ -177,7 +177,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
.firstInstanceOrNull<CoverArtDto>() .firstInstanceOrNull<CoverArtDto>()
?.attributes?.fileName ?.attributes?.fileName
} }
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang, preferences.preferExtensionLangTitle) helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
} }
return MangasPage(mangaList, chapterListDto.hasNextPage) return MangasPage(mangaList, chapterListDto.hasNextPage)
@ -360,7 +360,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
.firstInstanceOrNull<CoverArtDto>() .firstInstanceOrNull<CoverArtDto>()
?.attributes?.fileName ?.attributes?.fileName
} }
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang, preferences.preferExtensionLangTitle) helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
} }
return mangaList return mangaList
@ -423,7 +423,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
dexLang, dexLang,
preferences.coverQuality, preferences.coverQuality,
preferences.altTitlesInDesc, preferences.altTitlesInDesc,
preferences.preferExtensionLangTitle,
) )
} }
@ -758,27 +757,11 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
} }
} }
val preferExtensionLangTitlePref = SwitchPreferenceCompat(screen.context).apply {
key = MDConstants.getPreferExtensionLangTitlePrefKey(dexLang)
title = helper.intl["prefer_title_in_extension_language"]
summary = helper.intl["prefer_title_in_extension_language_summary"]
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit()
.putBoolean(MDConstants.getPreferExtensionLangTitlePrefKey(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(contentRatingPref) screen.addPreference(contentRatingPref)
screen.addPreference(originalLanguagePref) screen.addPreference(originalLanguagePref)
screen.addPreference(blockedGroupsPref) screen.addPreference(blockedGroupsPref)
@ -857,9 +840,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
private val SharedPreferences.altTitlesInDesc private val SharedPreferences.altTitlesInDesc
get() = getBoolean(MDConstants.getAltTitlesInDescPrefKey(dexLang), false) get() = getBoolean(MDConstants.getAltTitlesInDescPrefKey(dexLang), false)
private val SharedPreferences.preferExtensionLangTitle
get() = getBoolean(MDConstants.getPreferExtensionLangTitlePrefKey(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

@ -6,19 +6,15 @@ import eu.kanade.tachiyomi.source.SourceFactory
class MangaDexFactory : SourceFactory { class MangaDexFactory : SourceFactory {
override fun createSources(): List<Source> = listOf( override fun createSources(): List<Source> = listOf(
MangaDexEnglish(), MangaDexEnglish(),
MangadexAfrikaans(),
MangaDexAlbanian(), MangaDexAlbanian(),
MangaDexArabic(), MangaDexArabic(),
MangaDexAzerbaijani(), MangaDexAzerbaijani(),
MangaDexBasque(),
MangaDexBelarusian(),
MangaDexBengali(), MangaDexBengali(),
MangaDexBulgarian(), MangaDexBulgarian(),
MangaDexBurmese(), MangaDexBurmese(),
MangaDexCatalan(), MangaDexCatalan(),
MangaDexChineseSimplified(), MangaDexChineseSimplified(),
MangaDexChineseTraditional(), MangaDexChineseTraditional(),
MangaDexChuvash(),
MangaDexCroatian(), MangaDexCroatian(),
MangaDexCzech(), MangaDexCzech(),
MangaDexDanish(), MangaDexDanish(),
@ -34,10 +30,8 @@ class MangaDexFactory : SourceFactory {
MangaDexHebrew(), MangaDexHebrew(),
MangaDexHindi(), MangaDexHindi(),
MangaDexHungarian(), MangaDexHungarian(),
MangaDexIrish(),
MangaDexIndonesian(), MangaDexIndonesian(),
MangaDexItalian(), MangaDexItalian(),
MangaDexJavanese(),
MangaDexJapanese(), MangaDexJapanese(),
MangaDexKazakh(), MangaDexKazakh(),
MangaDexKorean(), MangaDexKorean(),
@ -63,25 +57,19 @@ class MangaDexFactory : SourceFactory {
MangaDexThai(), MangaDexThai(),
MangaDexTurkish(), MangaDexTurkish(),
MangaDexUkrainian(), MangaDexUkrainian(),
MangaDexUrdu(),
MangaDexUzbek(),
MangaDexVietnamese(), MangaDexVietnamese(),
) )
} }
class MangadexAfrikaans : MangaDex("af")
class MangaDexAlbanian : MangaDex("sq") class MangaDexAlbanian : MangaDex("sq")
class MangaDexArabic : MangaDex("ar") class MangaDexArabic : MangaDex("ar")
class MangaDexAzerbaijani : MangaDex("az") class MangaDexAzerbaijani : MangaDex("az")
class MangaDexBasque : MangaDex("eu")
class MangaDexBelarusian : MangaDex("be")
class MangaDexBengali : MangaDex("bn") class MangaDexBengali : MangaDex("bn")
class MangaDexBulgarian : MangaDex("bg") class MangaDexBulgarian : MangaDex("bg")
class MangaDexBurmese : MangaDex("my") class MangaDexBurmese : MangaDex("my")
class MangaDexCatalan : MangaDex("ca") class MangaDexCatalan : MangaDex("ca")
class MangaDexChineseSimplified : MangaDex("zh-Hans", "zh") class MangaDexChineseSimplified : MangaDex("zh-Hans", "zh")
class MangaDexChineseTraditional : MangaDex("zh-Hant", "zh-hk") class MangaDexChineseTraditional : MangaDex("zh-Hant", "zh-hk")
class MangaDexChuvash : MangaDex("cv")
class MangaDexCroatian : MangaDex("hr") class MangaDexCroatian : MangaDex("hr")
class MangaDexCzech : MangaDex("cs") class MangaDexCzech : MangaDex("cs")
class MangaDexDanish : MangaDex("da") class MangaDexDanish : MangaDex("da")
@ -98,11 +86,9 @@ class MangaDexGreek : MangaDex("el")
class MangaDexHebrew : MangaDex("he") class MangaDexHebrew : MangaDex("he")
class MangaDexHindi : MangaDex("hi") class MangaDexHindi : MangaDex("hi")
class MangaDexHungarian : MangaDex("hu") class MangaDexHungarian : MangaDex("hu")
class MangaDexIrish : MangaDex("ga")
class MangaDexIndonesian : MangaDex("id") class MangaDexIndonesian : MangaDex("id")
class MangaDexItalian : MangaDex("it") class MangaDexItalian : MangaDex("it")
class MangaDexJapanese : MangaDex("ja") class MangaDexJapanese : MangaDex("ja")
class MangaDexJavanese : MangaDex("jv")
class MangaDexKazakh : MangaDex("kk") class MangaDexKazakh : MangaDex("kk")
class MangaDexKorean : MangaDex("ko") class MangaDexKorean : MangaDex("ko")
class MangaDexLatin : MangaDex("la") class MangaDexLatin : MangaDex("la")
@ -127,6 +113,4 @@ class MangaDexTelugu : MangaDex("te")
class MangaDexThai : MangaDex("th") class MangaDexThai : MangaDex("th")
class MangaDexTurkish : MangaDex("tr") class MangaDexTurkish : MangaDex("tr")
class MangaDexUkrainian : MangaDex("uk") class MangaDexUkrainian : MangaDex("uk")
class MangaDexUrdu : MangaDex("ur")
class MangaDexUzbek : MangaDex("uz")
class MangaDexVietnamese : MangaDex("vi") class MangaDexVietnamese : MangaDex("vi")

View File

@ -275,9 +275,6 @@ class MangaDexHelper(lang: String) {
return GET(tokenRequestUrl, headers, cacheControl) return GET(tokenRequestUrl, headers, cacheControl)
} }
private fun List<Map<String, String>>.findTitleByLang(lang: String): String? =
firstOrNull { it[lang] != null }?.values?.singleOrNull()
/** /**
* Create a [SManga] from the JSON element with only basic attributes filled. * Create a [SManga] from the JSON element with only basic attributes filled.
*/ */
@ -286,24 +283,15 @@ class MangaDexHelper(lang: String) {
coverFileName: String?, coverFileName: String?,
coverSuffix: String?, coverSuffix: String?,
lang: String, lang: String,
preferExtensionLangTitle: Boolean,
): SManga = SManga.create().apply { ): SManga = SManga.create().apply {
url = "/manga/${mangaDataDto.id}" url = "/manga/${mangaDataDto.id}"
val titleMap = mangaDataDto.attributes!!.title val titleMap = mangaDataDto.attributes!!.title
title = with(mangaDataDto.attributes) { val dirtyTitle =
titleMap[lang] ?: altTitles.run { titleMap.values.firstOrNull() // use literally anything from title as first resort
val mainTitle = titleMap.values.firstOrNull() ?: mangaDataDto.attributes.altTitles
val langTitle = findTitleByLang(lang) .find { (it[lang] ?: it["en"]) !== null }
val enTitle = findTitleByLang("en") ?.values?.singleOrNull() // find something else from alt titles
title = dirtyTitle?.removeEntities().orEmpty()
if (preferExtensionLangTitle) {
listOf(langTitle, mainTitle, enTitle)
} else {
listOf(mainTitle, langTitle, enTitle)
}.firstNotNullOfOrNull { it }
}
}?.removeEntities().orEmpty()
coverFileName?.let { coverFileName?.let {
thumbnail_url = when (!coverSuffix.isNullOrEmpty()) { thumbnail_url = when (!coverSuffix.isNullOrEmpty()) {
@ -323,7 +311,6 @@ class MangaDexHelper(lang: String) {
lang: String, lang: String,
coverSuffix: String?, coverSuffix: String?,
altTitlesInDesc: Boolean, altTitlesInDesc: Boolean,
preferExtensionLangTitle: Boolean,
): SManga { ): SManga {
val attr = mangaDataDto.attributes!! val attr = mangaDataDto.attributes!!
@ -383,7 +370,7 @@ class MangaDexHelper(lang: String) {
} }
} }
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang, preferExtensionLangTitle).apply { return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply {
description = desc description = desc
author = authors.joinToString() author = authors.joinToString()
artist = artists.joinToString() artist = artists.joinToString()

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'MANGA Plus by SHUEISHA' extName = 'MANGA Plus by SHUEISHA'
extClass = '.MangaPlusFactory' extClass = '.MangaPlusFactory'
extVersionCode = 54 extVersionCode = 53
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -210,7 +210,6 @@ class Label(val label: LabelCode? = LabelCode.WEEKLY_SHOUNEN_JUMP) {
LabelCode.SHOUNEN_JUMP_PLUS -> "Shounen Jump+" LabelCode.SHOUNEN_JUMP_PLUS -> "Shounen Jump+"
LabelCode.MANGA_PLUS_CREATORS -> "MANGA Plus Creators" LabelCode.MANGA_PLUS_CREATORS -> "MANGA Plus Creators"
LabelCode.SAIKYOU_JUMP -> "Saikyou Jump" LabelCode.SAIKYOU_JUMP -> "Saikyou Jump"
LabelCode.ULTRA_JUMP -> "Ultra Jump"
else -> null else -> null
} }
} }
@ -247,9 +246,6 @@ enum class LabelCode {
@SerialName("WSJ") @SerialName("WSJ")
WEEKLY_SHOUNEN_JUMP, WEEKLY_SHOUNEN_JUMP,
@SerialName("UJ")
ULTRA_JUMP,
} }
@Serializable @Serializable

View File

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

View File

@ -22,11 +22,8 @@ import org.jsoup.select.Evaluator
import rx.Observable import rx.Observable
open class MangaReader( open class MangaReader(
val language: Language, override val lang: String,
) : MangaReader() { ) : MangaReader() {
override val lang = language.code
override val name = "MangaReader" override val name = "MangaReader"
override val baseUrl = "https://mangareader.to" override val baseUrl = "https://mangareader.to"
@ -36,10 +33,10 @@ open class MangaReader(
.build() .build()
override fun latestUpdatesRequest(page: Int) = override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter?sort=latest-updated&language=${language.infix}&page=$page", headers) GET("$baseUrl/filter?sort=latest-updated&language=$lang&page=$page", headers)
override fun popularMangaRequest(page: Int) = override fun popularMangaRequest(page: Int) =
GET("$baseUrl/filter?sort=most-viewed&language=${language.infix}&page=$page", headers) GET("$baseUrl/filter?sort=most-viewed&language=$lang&page=$page", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = baseUrl.toHttpUrl().newBuilder() val urlBuilder = baseUrl.toHttpUrl().newBuilder()
@ -50,7 +47,7 @@ open class MangaReader(
} }
} else { } else {
urlBuilder.addPathSegment("filter").apply { urlBuilder.addPathSegment("filter").apply {
addQueryParameter("language", language.infix) addQueryParameter("language", lang)
addQueryParameter("page", page.toString()) addQueryParameter("page", page.toString())
filters.ifEmpty(::getFilterList).forEach { filter -> filters.ifEmpty(::getFilterList).forEach { filter ->
when (filter) { when (filter) {
@ -145,7 +142,7 @@ open class MangaReader(
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> { override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
val container = response.parseHtmlProperty().run { val container = response.parseHtmlProperty().run {
val type = if (isVolume) "volumes" else "chapters" val type = if (isVolume) "volumes" else "chapters"
selectFirst(Evaluator.Id("${language.chapterInfix}-$type")) ?: return emptyList() selectFirst(Evaluator.Id("$lang-$type")) ?: return emptyList()
} }
return container.children() return container.children()
} }

View File

@ -4,19 +4,5 @@ import eu.kanade.tachiyomi.source.SourceFactory
class MangaReaderFactory : SourceFactory { class MangaReaderFactory : SourceFactory {
override fun createSources() = override fun createSources() =
arrayOf( arrayOf("en", "fr", "ja", "ko", "zh").map(::MangaReader)
Language("en"),
Language("es", chapterInfix = "es-mx"),
Language("fr"),
Language("ja"),
Language("ko"),
Language("pt-BR", infix = "pt"),
Language("zh"),
).map(::MangaReader)
} }
data class Language(
val code: String,
val infix: String = code,
val chapterInfix: String = code.lowercase(),
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -1,115 +0,0 @@
package eu.kanade.tachiyomi.extension.all.meituatop
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.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.select.Evaluator
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
// Uses MACCMS http://www.maccms.la/
class MeituaTop : HttpSource() {
override val name = "Meitua.top"
override val lang = "all"
override val supportsLatest = false
override val baseUrl = "https://88188.meitu.lol"
override fun popularMangaRequest(page: Int) = GET("$baseUrl/arttype/0b-$page.html", headers)
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.selectFirst(Evaluator.Class("thumbnail-group"))!!.children().map {
SManga.create().apply {
url = it.selectFirst(Evaluator.Tag("a"))!!.attr("href")
val image = it.selectFirst(Evaluator.Tag("img"))!!
title = image.attr("alt")
thumbnail_url = image.attr("src")
val info = it.selectFirst(Evaluator.Tag("p"))!!.ownText().split(" - ")
genre = info[0]
description = info[1]
status = SManga.COMPLETED
initialized = true
}
}
val pageLinks = document.select(Evaluator.Class("page_link"))
if (pageLinks.isEmpty()) return MangasPage(mangas, false)
val lastPage = pageLinks[3].attr("href")
val hasNextPage = document.location().pageNumber() != lastPage.pageNumber()
return MangasPage(mangas, hasNextPage)
}
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$baseUrl/artsearch/-------.html".toHttpUrl().newBuilder()
.addQueryParameter("wd", query)
.addQueryParameter("page", page.toString())
.toString()
return GET(url, headers)
}
val filter = filters.filterIsInstance<RegionFilter>().firstOrNull() ?: return popularMangaRequest(page)
return GET("$baseUrl/arttype/${21 + filter.state}b-$page.html", headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.just(manga)
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapter = SChapter.create().apply {
url = manga.url
name = "Gallery"
date_upload = parseDate(manga.description!!)
chapter_number = -2f
}
return Observable.just(listOf(chapter))
}
private fun parseDate(date: String): Long = runCatching {
dateFormat.parse(date)?.time
}.getOrNull() ?: 0L
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val images = document.selectFirst(Evaluator.Class("ttnr"))!!.select(Evaluator.Tag("img"))
.map { it.attr("src") }.distinct()
return images.mapIndexed { index, imageUrl -> Page(index, imageUrl = imageUrl) }
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
override fun getFilterList() = FilterList(
Filter.Header("Category (ignored for text search)"),
RegionFilter(),
)
private class RegionFilter : Filter.Select<String>(
"Region",
arrayOf("All", "国产美女", "韩国美女", "台湾美女", "日本美女", "欧美美女", "泰国美女"),
)
private fun String.pageNumber() = numberRegex.findAll(this).last().value.toInt()
private val numberRegex by lazy { Regex("""\d+""") }
private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
}

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'NHentai' extName = 'NHentai'
extClass = '.NHFactory' extClass = '.NHFactory'
extVersionCode = 50 extVersionCode = 46
isNsfw = true isNsfw = true
} }

View File

@ -1,37 +0,0 @@
package eu.kanade.tachiyomi.extension.all.nhentai
import kotlinx.serialization.Serializable
@Serializable
class Hentai(
var id: Int,
val images: Images,
val media_id: String,
val tags: List<Tag>,
val title: Title,
val upload_date: Long,
val num_favorites: Long,
)
@Serializable
class Title(
var english: String? = null,
val japanese: String? = null,
val pretty: String? = null,
)
@Serializable
class Images(
val pages: List<Image>,
)
@Serializable
class Image(
val t: String,
)
@Serializable
class Tag(
val name: String,
val type: String,
)

View File

@ -1,36 +1,63 @@
package eu.kanade.tachiyomi.extension.all.nhentai package eu.kanade.tachiyomi.extension.all.nhentai
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
object NHUtils { object NHUtils {
fun getArtists(data: Hentai): String { fun getArtists(document: Document): String {
val artists = data.tags.filter { it.type == "artist" } val artists = document.select("#tags > div:nth-child(4) > span > a .name")
return artists.joinToString(", ") { it.name } return artists.joinToString(", ") { it.cleanTag() }
} }
fun getGroups(data: Hentai): String? { fun getGroups(document: Document): String? {
val groups = data.tags.filter { it.type == "group" } val groups = document.select("#tags > div:nth-child(5) > span > a .name")
return groups.joinToString(", ") { it.name }.takeIf { it.isBlank() } return if (groups.isNotEmpty()) {
} groups.joinToString(", ") { it.cleanTag() }
} else {
fun getTagDescription(data: Hentai): String { null
val tags = data.tags.groupBy { it.type }
return buildString {
tags["category"]?.joinToString { it.name }?.let {
append("Categories: ", it, "\n")
}
tags["parody"]?.joinToString { it.name }?.let {
append("Parodies: ", it, "\n")
}
tags["character"]?.joinToString { it.name }?.let {
append("Characters: ", it, "\n\n")
}
} }
} }
fun getTags(data: Hentai): String { fun getTagDescription(document: Document): String {
val artists = data.tags.filter { it.type == "tag" } val stringBuilder = StringBuilder()
return artists.joinToString(", ") { it.name }
val categories = document.select("#tags > div:nth-child(7) > span > a .name")
if (categories.isNotEmpty()) {
stringBuilder.append("Categories: ")
stringBuilder.append(categories.joinToString(", ") { it.cleanTag() })
stringBuilder.append("\n\n")
}
val parodies = document.select("#tags > div:nth-child(1) > span > a .name")
if (parodies.isNotEmpty()) {
stringBuilder.append("Parodies: ")
stringBuilder.append(parodies.joinToString(", ") { it.cleanTag() })
stringBuilder.append("\n\n")
}
val characters = document.select("#tags > div:nth-child(2) > span > a .name")
if (characters.isNotEmpty()) {
stringBuilder.append("Characters: ")
stringBuilder.append(characters.joinToString(", ") { it.cleanTag() })
}
return stringBuilder.toString()
}
fun getTags(document: Document): String {
val tags = document.select("#tags > div:nth-child(3) > span > a .name")
return tags.map { it.cleanTag() }.sorted().joinToString(", ")
}
fun getNumPages(document: Document): String {
return document.select("#tags > div:nth-child(8) > span > a .name").first()!!.cleanTag()
}
fun getTime(document: Document): Long {
val timeString = document.toString().substringAfter("datetime=\"").substringBefore("\">").replace("T", " ")
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSSZ").parse(timeString)?.time ?: 0L
} }
private fun Element.cleanTag(): String = text().replace(Regex("\\(.*\\)"), "").trim() private fun Element.cleanTag(): String = text().replace(Regex("\\(.*\\)"), "").trim()

View File

@ -6,8 +6,10 @@ import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getArtists import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getArtists
import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getGroups import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getGroups
import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getNumPages
import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getTagDescription import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getTagDescription
import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getTags import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getTags
import eu.kanade.tachiyomi.extension.all.nhentai.NHUtils.getTime
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
import eu.kanade.tachiyomi.lib.randomua.getPrefUAType import eu.kanade.tachiyomi.lib.randomua.getPrefUAType
@ -25,8 +27,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
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 okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -36,7 +36,6 @@ import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
open class NHentai( open class NHentai(
override val lang: String, override val lang: String,
@ -51,8 +50,6 @@ open class NHentai(
override val supportsLatest = true override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
@ -74,8 +71,6 @@ open class NHentai(
} }
private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""") private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""")
private val dataRegex = Regex("""JSON\.parse\(\s*"(.*)"\s*\)""")
private val hentaiSelector = "script:containsData(JSON.parse):not(:containsData(media_server))"
private fun String.shortenTitle() = this.replace(shortenTitleRegex, "").trim() private fun String.shortenTitle() = this.replace(shortenTitleRegex, "").trim()
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
@ -108,7 +103,7 @@ open class NHentai(
title = element.select("a > div").text().replace("\"", "").let { title = element.select("a > div").text().replace("\"", "").let {
if (displayFullTitle) it.trim() else it.shortenTitle() if (displayFullTitle) it.trim() else it.shortenTitle()
} }
thumbnail_url = element.selectFirst(".cover img")!!.let { img -> thumbnail_url = element.select(".cover img").first()!!.let { img ->
if (img.hasAttr("data-src")) img.attr("abs:data-src") else img.attr("abs:src") if (img.hasAttr("data-src")) img.attr("abs:data-src") else img.attr("abs:src")
} }
} }
@ -212,26 +207,22 @@ open class NHentai(
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector() override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga {
val script = document.selectFirst(hentaiSelector)!!.data() val fullTitle = document.select("#info > h1").text().replace("\"", "").trim()
val json = dataRegex.find(script)?.groupValues!![1]
val data = json.parseAs<Hentai>()
return SManga.create().apply { return SManga.create().apply {
title = if (displayFullTitle) data.title.english ?: data.title.japanese ?: data.title.pretty!! else data.title.pretty ?: (data.title.english ?: data.title.japanese)!!.shortenTitle() title = if (displayFullTitle) fullTitle else fullTitle.shortenTitle()
thumbnail_url = document.select("#cover > a > img").attr("data-src") thumbnail_url = document.select("#cover > a > img").attr("data-src")
status = SManga.COMPLETED status = SManga.COMPLETED
artist = getArtists(data) artist = getArtists(document)
author = getGroups(data) ?: getArtists(data) author = getGroups(document)
// Some people want these additional details in description // Some people want these additional details in description
description = "Full English and Japanese titles:\n" description = "Full English and Japanese titles:\n"
.plus("${data.title.english ?: data.title.japanese ?: data.title.pretty ?: ""}\n") .plus("$fullTitle\n")
.plus(data.title.japanese ?: "") .plus("${document.select("div#info h2").text()}\n\n")
.plus("\n\n") .plus("Pages: ${getNumPages(document)}\n")
.plus("Pages: ${data.images.pages.size}\n") .plus("Favorited by: ${document.select("div#info i.fa-heart ~ span span").text().removeSurrounding("(", ")")}\n")
.plus("Favorited by: ${data.num_favorites}\n") .plus(getTagDescription(document))
.plus(getTagDescription(data)) genre = getTags(document)
genre = getTags(data)
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
} }
} }
@ -240,16 +231,11 @@ open class NHentai(
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup() val document = response.asJsoup()
val script = document.selectFirst(hentaiSelector)!!.data()
val json = dataRegex.find(script)?.groupValues!![1]
val data = json.parseAs<Hentai>()
return listOf( return listOf(
SChapter.create().apply { SChapter.create().apply {
name = "Chapter" name = "Chapter"
scanlator = getGroups(data) scanlator = getGroups(document)
date_upload = data.upload_date * 1000 date_upload = getTime(document)
setUrlWithoutDomain(response.request.url.encodedPath) setUrlWithoutDomain(response.request.url.encodedPath)
}, },
) )
@ -260,24 +246,11 @@ open class NHentai(
override fun chapterListSelector() = throw UnsupportedOperationException() override fun chapterListSelector() = throw UnsupportedOperationException()
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val script = document.selectFirst("script:containsData(media_server)")!!.data() val script = document.select("script:containsData(media_server)").first()!!.data()
val script2 = document.selectFirst(hentaiSelector)!!.data()
val mediaServer = Regex("""media_server\s*:\s*(\d+)""").find(script)?.groupValues!![1] val mediaServer = Regex("""media_server\s*:\s*(\d+)""").find(script)?.groupValues!![1]
val json = dataRegex.find(script2)?.groupValues!![1]
val data = json.parseAs<Hentai>() return document.select("div.thumbs a > img").mapIndexed { i, img ->
return data.images.pages.mapIndexed { i, image -> Page(i, "", img.attr("abs:data-src").replace("t.nh", "i.nh").replace("t\\d+.nh".toRegex(), "i$mediaServer.nh").replace("t.", "."))
Page(
i,
imageUrl = "${baseUrl.replace("https://", "https://i$mediaServer.")}/galleries/${data.media_id}/${i + 1}" +
when (image.t) {
"w" -> ".webp"
"p" -> ".png"
"g" -> ".gif"
else -> ".jpg"
},
)
} }
} }
@ -330,14 +303,6 @@ open class NHentai(
), ),
) )
private inline fun <reified T> String.parseAs(): T {
val data = Regex("""\\u([0-9A-Fa-f]{4})""").replace(this) {
it.groupValues[1].toInt(16).toChar().toString()
}
return json.decodeFromString(
data,
)
}
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) : private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second fun toUriPart() = vals[state].second

View File

@ -2,8 +2,8 @@ ext {
extName = 'Otaku Sanctuary' extName = 'Otaku Sanctuary'
extClass = '.OtakuSanctuaryFactory' extClass = '.OtakuSanctuaryFactory'
themePkg = 'otakusanctuary' themePkg = 'otakusanctuary'
baseUrl = 'https://otakusan.me' baseUrl = 'https://otakusan.net'
overrideVersionCode = 2 overrideVersionCode = 0
isNsfw = true isNsfw = true
} }

View File

@ -5,11 +5,11 @@ import eu.kanade.tachiyomi.source.SourceFactory
class OtakuSanctuaryFactory : SourceFactory { class OtakuSanctuaryFactory : SourceFactory {
override fun createSources() = listOf( override fun createSources() = listOf(
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "all"), OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "all"),
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "vi"), OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "vi"),
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "en"), OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "en"),
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "it"), OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "it"),
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "fr"), OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "fr"),
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "es"), OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "es"),
) )
} }

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Pururin' extName = 'Pururin'
extClass = '.PururinFactory' extClass = '.PururinFactory'
extVersionCode = 10 extVersionCode = 9
isNsfw = true isNsfw = true
} }

View File

@ -25,7 +25,7 @@ abstract class Pururin(
) : ParsedHttpSource() { ) : ParsedHttpSource() {
override val name = "Pururin" override val name = "Pururin"
final override val baseUrl = "https://pururin.me" final override val baseUrl = "https://pururin.to"
override val supportsLatest = true override val supportsLatest = true
@ -203,24 +203,17 @@ abstract class Pururin(
document.select(".box-gallery").let { e -> document.select(".box-gallery").let { e ->
initialized = true initialized = true
title = e.select(".title").text() title = e.select(".title").text()
author = e.select("a[href*=/circle/]").eachText().joinToString().ifEmpty { e.select("[itemprop=author]").text() } author = e.select("a[href*=/circle/]").text().ifEmpty { e.select("[itemprop=author]").text() }
artist = e.select("[itemprop=author]").eachText().joinToString() artist = e.select("[itemprop=author]").text()
genre = e.select("a[href*=/content/]").eachText().joinToString() genre = e.select("a[href*=/content/]").text()
description = e.select(".box-gallery .table-info tr") description = e.select(".box-gallery .table-info tr")
.filter { tr -> .filter { tr ->
tr.select("td").let { td -> tr.select("td").none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
td.isNotEmpty() &&
td.none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
}
} }
.joinToString("\n") { tr -> .joinToString("\n") { tr ->
tr.select("td").let { td -> tr.select("td")
var a = td.select("a").toList() .joinToString(": ") { it.text() }
if (a.isEmpty()) a = td.drop(1)
td.first()!!.text() + ": " + a.joinToString { it.text() }
} }
}
status = SManga.COMPLETED
thumbnail_url = e.select("img").attr("abs:src") thumbnail_url = e.select("img").attr("abs:src")
} }
} }

View File

@ -3,7 +3,7 @@ ext {
extClass = '.ThunderScansFactory' extClass = '.ThunderScansFactory'
themePkg = 'mangathemesia' themePkg = 'mangathemesia'
baseUrl = 'https://en-thunderscans.com' baseUrl = 'https://en-thunderscans.com'
overrideVersionCode = 7 overrideVersionCode = 5
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

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