Compare commits
No commits in common. "e91f02af0291608767ef2369a70e39001bed3d64" and "cb2794830738e740114e15a59b50655413e0f059" have entirely different histories.
e91f02af02
...
cb27948307
12
.github/ISSUE_TEMPLATE/01_report_issue.yml
vendored
@ -105,15 +105,3 @@ body:
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
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
|
||||
|
12
.github/ISSUE_TEMPLATE/02_request_source.yml
vendored
@ -57,15 +57,3 @@ body:
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
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
|
||||
|
12
.github/ISSUE_TEMPLATE/03_report_url_change.yml
vendored
@ -55,15 +55,3 @@ body:
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
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
|
||||
|
12
.github/ISSUE_TEMPLATE/04_report_dead_source.yml
vendored
@ -61,15 +61,3 @@ body:
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
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
|
||||
|
12
.github/ISSUE_TEMPLATE/05_request_feature.yml
vendored
@ -57,15 +57,3 @@ body:
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
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
|
||||
|
12
.github/ISSUE_TEMPLATE/06_request_meta.yml
vendored
@ -39,15 +39,3 @@ body:
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
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
|
||||
|
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
||||
baseVersionCode = 4
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:synchrony"))
|
||||
|
@ -153,7 +153,7 @@ abstract class ColaManga(
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
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()
|
||||
genre = document.select("span.fed-text-muted:contains($genreTitle) ~ a").joinToString { it.text() }
|
||||
description = document
|
||||
|
@ -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>
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -269,32 +269,32 @@ abstract class FMReader(
|
||||
// languages: en, vi, es, tr
|
||||
return when (dateWord) {
|
||||
"min", "minute", "phút", "minuto", "dakika" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, -value)
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"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.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"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.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"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.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"month", "tháng", "mes", "ay" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -value)
|
||||
add(Calendar.MONTH, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"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.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 2
|
||||
|
@ -647,7 +647,6 @@ abstract class GalleryAdults(
|
||||
"p" -> "png"
|
||||
"b" -> "bmp"
|
||||
"g" -> "gif"
|
||||
"w" -> "webp"
|
||||
else -> "jpg"
|
||||
}
|
||||
val idx = image.key.toInt()
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 6
|
||||
baseVersionCode = 5
|
||||
|
@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Call
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
@ -136,22 +137,19 @@ abstract class GigaViewer(
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
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()
|
||||
.set("Referer", response.request.url.toString())
|
||||
.build()
|
||||
|
||||
var readMoreEndpoint = baseUrl.toHttpUrl().newBuilder()
|
||||
.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()
|
||||
var readMoreEndpoint = firstListEndpoint.newBuilder()
|
||||
.setQueryParameter("number_since", numberSince.toString())
|
||||
.toString()
|
||||
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 26
|
||||
baseVersionCode = 24
|
||||
|
@ -65,8 +65,7 @@ abstract class GroupLe(
|
||||
}
|
||||
.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 {
|
||||
add("User-Agent", uagent)
|
||||
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> {
|
||||
val document = response.asJsoup()
|
||||
if ((
|
||||
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()
|
||||
)
|
||||
) {
|
||||
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())) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
|
||||
val chapterSearchParams = getChapterSearchParams(document)
|
||||
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, manga, chapterSearchParams) }
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, manga) }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() =
|
||||
"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 chapterInf = element.select("td.item-title").first()!!
|
||||
val urlText = urlElement.text()
|
||||
|
||||
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")
|
||||
|
||||
chapter.scanlator = if (translatorElement.isNotBlank()) {
|
||||
chapter.scanlator = if (!translatorElement.isNullOrBlank()) {
|
||||
translatorElement
|
||||
.replace("(Переводчик),", "&")
|
||||
.removeSuffix(" (Переводчик)")
|
||||
@ -268,14 +251,10 @@ abstract class GroupLe(
|
||||
chapter.chapter_number = chapterInf.attr("data-num").toFloat() / 10
|
||||
|
||||
chapter.date_upload = element.select("td.d-none").last()?.text()?.let {
|
||||
if (it.isEmpty()) {
|
||||
0L
|
||||
} else {
|
||||
try {
|
||||
SimpleDateFormat("dd.MM.yy", Locale.US).parse(it)?.time ?: 0L
|
||||
} catch (e: ParseException) {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it)?.time ?: 0L
|
||||
}
|
||||
try {
|
||||
SimpleDateFormat("dd.MM.yy", Locale.US).parse(it)?.time ?: 0L
|
||||
} catch (e: ParseException) {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it)?.time ?: 0L
|
||||
}
|
||||
} ?: 0
|
||||
return chapter
|
||||
@ -313,15 +292,15 @@ abstract class GroupLe(
|
||||
|
||||
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 (document.select(".input-lg").isNotEmpty() || (
|
||||
document.select(".user-avatar")
|
||||
.isEmpty() && document.select("img.logo").first()?.attr("title")
|
||||
?.contains("Allhentai") == true
|
||||
)
|
||||
) {
|
||||
if (document.select(".input-lg").isNotEmpty() || (document.select(".user-avatar").isNullOrEmpty() && document.select("img.logo").first()?.attr("title")?.contains("Allhentai") == true)) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
if (!response.request.url.toString().contains(baseUrl)) {
|
||||
|
@ -232,7 +232,7 @@ abstract class HentaiHand(
|
||||
val date = it.jsonObject["added_at"]!!.jsonPrimitive.content
|
||||
date_upload = if (date.contains("day")) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -date.filter { it.isDigit() }.toInt())
|
||||
add(Calendar.DATE, date.filter { it.isDigit() }.toInt() * -1)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
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
|
||||
date_upload = if (date.contains("day")) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -date.filter { it.isDigit() }.toInt())
|
||||
add(Calendar.DATE, date.filter { it.isDigit() }.toInt() * -1)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
DATE_FORMAT.parse(obj.jsonObject["uploaded_at"]!!.jsonPrimitive.content)?.time ?: 0
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
||||
baseVersionCode = 4
|
||||
|
@ -96,13 +96,10 @@ class Chapter(
|
||||
private val createdBy: Name,
|
||||
private val createdAt: String,
|
||||
private val chapterStatus: String,
|
||||
private val isAccessible: Boolean,
|
||||
private val mangaPost: ChapterPostDetails,
|
||||
) {
|
||||
fun isPublic() = chapterStatus == "PUBLIC"
|
||||
|
||||
fun isAccessible() = isAccessible
|
||||
|
||||
fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
|
||||
val seriesSlug = mangaSlug ?: mangaPost.slug
|
||||
url = "/series/$seriesSlug/$slug#$id"
|
||||
|
@ -128,7 +128,7 @@ abstract class Iken(
|
||||
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||
|
||||
return data.post.chapters
|
||||
.filter { it.isPublic() && it.isAccessible() }
|
||||
.filter { it.isPublic() }
|
||||
.map { it.toSChapter(data.post.slug) }
|
||||
}
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 16
|
||||
baseVersionCode = 14
|
||||
|
@ -233,8 +233,8 @@ open class Kemono(
|
||||
GET("$baseUrl/$apiPath${chapter.url}", headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val postData: KemonoPostDtoWrapped = response.parseAs()
|
||||
return postData.post.images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) }
|
||||
val post: KemonoPostDto = response.parseAs()
|
||||
return post.images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) }
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
|
@ -51,11 +51,6 @@ class KemonoCreatorDto(
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class KemonoPostDtoWrapped(
|
||||
val post: KemonoPostDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class KemonoPostDto(
|
||||
private val id: String,
|
||||
@ -70,7 +65,7 @@ class KemonoPostDto(
|
||||
) {
|
||||
val images: List<String>
|
||||
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)
|
||||
}.filter {
|
||||
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
|
||||
@Serializable
|
||||
class KemonoAttachmentDto(var name: String? = null, val path: String) {
|
||||
override fun toString() = path + if (name != null) "?f=$name" else ""
|
||||
class KemonoAttachmentDto(val name: String, val path: String) {
|
||||
override fun toString() = "$path?f=$name"
|
||||
}
|
||||
|
||||
private fun getApiDateFormat() =
|
||||
|
@ -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=رابط الصفحة غير موجود
|
@ -1,4 +1,3 @@
|
||||
pref_show_paid_chapter_title=Display paid chapters
|
||||
pref_show_paid_chapter_summary_on=Paid chapters will appear.
|
||||
pref_show_paid_chapter_summary_off=Only free chapters will be displayed.
|
||||
chapter_page_url_not_found=Page URL not found
|
||||
|
@ -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
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 9
|
||||
baseVersionCode = 8
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
@ -54,7 +55,7 @@ abstract class Keyoapp(
|
||||
protected val intl = Intl(
|
||||
language = lang,
|
||||
baseLanguage = "en",
|
||||
availableLanguages = setOf("ar", "en", "fr"),
|
||||
availableLanguages = setOf("en"),
|
||||
classLoader = this::class.java.classLoader!!,
|
||||
)
|
||||
|
||||
@ -258,11 +259,9 @@ abstract class Keyoapp(
|
||||
// Image list
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val cdnUrl = getCdnUrl(document)
|
||||
document.select("#pages > img")
|
||||
.map { it.attr("uid") }
|
||||
.filter { it.isNotEmpty() }
|
||||
.also { cdnUrl ?: throw Exception(intl["chapter_page_url_not_found"]) }
|
||||
.mapIndexed { index, img ->
|
||||
Page(index, document.location(), "$cdnUrl/$img")
|
||||
}
|
||||
@ -278,16 +277,7 @@ abstract class Keyoapp(
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun getCdnUrl(document: Document): String? {
|
||||
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"
|
||||
}
|
||||
}
|
||||
protected open val cdnUrl = "https://2xffbs-cn8.is1.buzz/uploads"
|
||||
|
||||
private val oldImgCdnRegex = Regex("""^(https?:)?//cdn\d*\.keyoapp\.com""")
|
||||
|
||||
@ -307,7 +297,12 @@ abstract class Keyoapp(
|
||||
|
||||
protected open fun Element.getImageUrl(selector: String): String? {
|
||||
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 {
|
||||
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 37
|
||||
baseVersionCode = 36
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:cryptoaes"))
|
||||
|
@ -767,21 +767,12 @@ abstract class Madara(
|
||||
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("srcset") -> element.attr("abs:srcset").substringBefore(" ")
|
||||
element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc")
|
||||
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
|
||||
* fetch the manga chapters instead of the old admin-ajax.php one.
|
||||
@ -1115,7 +1106,6 @@ abstract class Madara(
|
||||
|
||||
companion object {
|
||||
const val URL_SEARCH_PREFIX = "slug:"
|
||||
val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)${'$'}""".toRegex()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -221,9 +221,9 @@ abstract class MangaBox(
|
||||
val value = date.split(' ')[0].toIntOrNull()
|
||||
val cal = Calendar.getInstance()
|
||||
when {
|
||||
value != null && "min" in date -> cal.apply { add(Calendar.MINUTE, -value) }
|
||||
value != null && "hour" in date -> cal.apply { add(Calendar.HOUR_OF_DAY, -value) }
|
||||
value != null && "day" in date -> cal.apply { add(Calendar.DATE, -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 * -1) }
|
||||
value != null && "day" in date -> cal.apply { add(Calendar.DATE, value * -1) }
|
||||
else -> null
|
||||
}?.timeInMillis
|
||||
} else {
|
||||
|
@ -7,7 +7,6 @@ sort_by_filter_views=Views
|
||||
sort_by_filter_updated=Updated
|
||||
sort_by_filter_added=Added
|
||||
status_filter_title=Status
|
||||
status_filter_all=All
|
||||
status_filter_ongoing=Ongoing
|
||||
status_filter_hiatus=Hiatus
|
||||
status_filter_dropped=Dropped
|
||||
|
@ -7,7 +7,6 @@ sort_by_filter_views=Vistas
|
||||
sort_by_filter_updated=Actualización
|
||||
sort_by_filter_added=Agregado
|
||||
status_filter_title=Estado
|
||||
status_filter_all=Todos
|
||||
status_filter_ongoing=En curso
|
||||
status_filter_hiatus=En pausa
|
||||
status_filter_dropped=Abandonado
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 2
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
@ -127,9 +127,7 @@ abstract class MangaEsp(
|
||||
val statusFilter = filterList.firstInstanceOrNull<StatusFilter>()
|
||||
|
||||
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>()
|
||||
@ -218,7 +216,6 @@ abstract class MangaEsp(
|
||||
)
|
||||
|
||||
protected open fun getStatusList() = arrayOf(
|
||||
Pair(intl["status_filter_all"], 0),
|
||||
Pair(intl["status_filter_ongoing"], 1),
|
||||
Pair(intl["status_filter_hiatus"], 2),
|
||||
Pair(intl["status_filter_dropped"], 3),
|
||||
@ -249,7 +246,7 @@ abstract class MangaEsp(
|
||||
companion object {
|
||||
private val UNESCAPE_REGEX = """\\(.)""".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
|
||||
}
|
||||
}
|
||||
|
@ -292,7 +292,7 @@ abstract class MangaThemesia(
|
||||
listOf("canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné")
|
||||
.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
|
||||
|
||||
else -> SManga.UNKNOWN
|
||||
|
@ -5,5 +5,5 @@ plugins {
|
||||
baseVersionCode = 9
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:zipinterceptor"))
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
||||
}
|
||||
|
@ -1,7 +1,12 @@
|
||||
package eu.kanade.tachiyomi.multisrc.peachscan
|
||||
|
||||
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.asObservableSuccess
|
||||
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.jsonPrimitive
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
abstract class PeachScan(
|
||||
@ -41,7 +53,7 @@ abstract class PeachScan(
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
.newBuilder()
|
||||
.addInterceptor(ZipInterceptor()::zipImageInterceptor)
|
||||
.addInterceptor(::zipImageInterceptor)
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
@ -180,6 +192,90 @@ abstract class PeachScan(
|
||||
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 {
|
||||
const val URL_SEARCH_PREFIX = "slug:"
|
||||
}
|
||||
|
@ -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.Canvas
|
||||
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 uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
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 newInstanceMethod: Method
|
||||
|
||||
private var classSignature = ClassSignature.Newest
|
||||
|
||||
private enum class ClassSignature {
|
||||
@ -38,6 +35,7 @@ object ImageDecoderWrapper {
|
||||
val inputStreamClass = InputStream::class.java
|
||||
|
||||
try {
|
||||
// Mihon Preview r6595+
|
||||
classSignature = ClassSignature.Newest
|
||||
|
||||
// decode(region, sampleSize)
|
||||
@ -56,6 +54,7 @@ object ImageDecoderWrapper {
|
||||
)
|
||||
} catch (_: NoSuchMethodException) {
|
||||
try {
|
||||
// Mihon Stable & forks
|
||||
classSignature = ClassSignature.New
|
||||
|
||||
// decode(region, rgb565, sampleSize, applyColorManagement, displayProfile)
|
||||
@ -75,6 +74,7 @@ object ImageDecoderWrapper {
|
||||
booleanClass,
|
||||
)
|
||||
} catch (_: NoSuchMethodException) {
|
||||
// Tachiyomi J2k
|
||||
classSignature = ClassSignature.Old
|
||||
|
||||
// decode(region, rgb565, sampleSize)
|
||||
@ -122,97 +122,3 @@ object ImageDecoderWrapper {
|
||||
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
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -152,25 +152,25 @@ abstract class Zbulu(
|
||||
val value = date.split(' ')[0].toInt()
|
||||
when {
|
||||
"second" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.SECOND, -value)
|
||||
add(Calendar.SECOND, value * -1)
|
||||
}.timeInMillis
|
||||
"minute" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, -value)
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
}.timeInMillis
|
||||
"hour" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, -value)
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
}.timeInMillis
|
||||
"day" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -value)
|
||||
add(Calendar.DATE, value * -1)
|
||||
}.timeInMillis
|
||||
"week" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -value * 7)
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
}.timeInMillis
|
||||
"month" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -value)
|
||||
add(Calendar.MONTH, value * -1)
|
||||
}.timeInMillis
|
||||
"year" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, -value)
|
||||
add(Calendar.YEAR, value * -1)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
0L
|
||||
|
@ -5,7 +5,6 @@ import android.util.Base64
|
||||
import java.security.MessageDigest
|
||||
import java.util.Arrays
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
plugins {
|
||||
id("lib-android")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Bato.to'
|
||||
extClass = '.BatoToFactory'
|
||||
extVersionCode = 43
|
||||
extVersionCode = 41
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,6 @@ import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
@ -117,7 +116,7 @@ open class BatoTo(
|
||||
.build()
|
||||
|
||||
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 {
|
||||
@ -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 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()
|
||||
@ -324,7 +323,7 @@ open class BatoTo(
|
||||
return super.mangaDetailsRequest(manga)
|
||||
}
|
||||
private var titleRegex: Regex =
|
||||
Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|《[^》]*》|⌜.+?⌝|⟨[^⟩]*⟩|/.+)")
|
||||
Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|/.+?)\\s*|([|/~].*)|-.*-")
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div#mainer div.container-fluid")
|
||||
@ -363,55 +362,44 @@ open class BatoTo(
|
||||
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())
|
||||
.select("channel > item").map { item ->
|
||||
SChapter.create().apply {
|
||||
url = item.selectFirst("guid")!!.text()
|
||||
name = item.selectFirst("title")!!.text()
|
||||
date_upload = parseAltChapterDate(item.selectFirst("pubDate")!!.text())
|
||||
name = item.selectFirst("title")!!.text().substringAfter(title).trim()
|
||||
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 {
|
||||
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 {
|
||||
return if (getAltChapterListPref()) {
|
||||
val id = manga.url.substringBeforeLast("/").substringAfterLast("/").trim()
|
||||
|
||||
GET("$baseUrl/rss/series/$id.xml", headers)
|
||||
} else if (manga.url.startsWith("http")) {
|
||||
GET(manga.url, headers)
|
||||
} else {
|
||||
super.chapterListRequest(manga)
|
||||
if (manga.url.startsWith("http")) {
|
||||
return GET(manga.url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return super.chapterListRequest(manga)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.main div.p-2"
|
||||
@ -440,46 +428,46 @@ open class BatoTo(
|
||||
|
||||
return when {
|
||||
"secs" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.SECOND, -value)
|
||||
add(Calendar.SECOND, value * -1)
|
||||
}.timeInMillis
|
||||
"mins" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, -value)
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
}.timeInMillis
|
||||
"hours" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, -value)
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
}.timeInMillis
|
||||
"days" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -value)
|
||||
add(Calendar.DATE, value * -1)
|
||||
}.timeInMillis
|
||||
"weeks" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -value * 7)
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
}.timeInMillis
|
||||
"months" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -value)
|
||||
add(Calendar.MONTH, value * -1)
|
||||
}.timeInMillis
|
||||
"years" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, -value)
|
||||
add(Calendar.YEAR, value * -1)
|
||||
}.timeInMillis
|
||||
"sec" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.SECOND, -value)
|
||||
add(Calendar.SECOND, value * -1)
|
||||
}.timeInMillis
|
||||
"min" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, -value)
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
}.timeInMillis
|
||||
"hour" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, -value)
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
}.timeInMillis
|
||||
"day" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -value)
|
||||
add(Calendar.DATE, value * -1)
|
||||
}.timeInMillis
|
||||
"week" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -value * 7)
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
}.timeInMillis
|
||||
"month" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -value)
|
||||
add(Calendar.MONTH, value * -1)
|
||||
}.timeInMillis
|
||||
"year" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, -value)
|
||||
add(Calendar.YEAR, value * -1)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
return 0
|
||||
|
@ -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_on=More specific, but might contain spoilers!
|
||||
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_on=Keep cover updated
|
||||
update_cover_off=Prefer first cover
|
||||
|
@ -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_on=Mais detalhadas, mas podem conter spoilers
|
||||
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_on=Manter capas atualizadas
|
||||
update_cover_off=Usar apenas a primeira capa
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Comick'
|
||||
extClass = '.ComickFactory'
|
||||
extVersionCode = 50
|
||||
extVersionCode = 48
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -97,20 +97,6 @@ abstract class Comick(
|
||||
}
|
||||
}.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 {
|
||||
key = FIRST_COVER_PREF
|
||||
title = intl["update_cover_title"]
|
||||
@ -163,9 +149,6 @@ abstract class Comick(
|
||||
private val SharedPreferences.includeMuTags: Boolean
|
||||
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
|
||||
get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT)
|
||||
|
||||
@ -396,23 +379,22 @@ abstract class Comick(
|
||||
val coversUrl =
|
||||
"$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true"
|
||||
val covers = client.newCall(GET(coversUrl)).execute()
|
||||
.parseAs<Covers>().mdCovers.reversed()
|
||||
val firstVol = covers.filter { it.vol == "1" }.ifEmpty { covers }
|
||||
val originalCovers = firstVol
|
||||
.filter { mangaData.comic.isoLang.orEmpty().startsWith(it.locale.orEmpty()) }
|
||||
val localCovers = firstVol
|
||||
.filter { comickLang.startsWith(it.locale.orEmpty()) }
|
||||
.parseAs<Covers>().mdCovers.reversed().toMutableList()
|
||||
if (covers.any { it.vol == "1" }) covers.retainAll { it.vol == "1" }
|
||||
if (
|
||||
covers.any { it.locale == comickLang.split('-').first() }
|
||||
) {
|
||||
covers.retainAll { it.locale == comickLang.split('-').first() }
|
||||
}
|
||||
return mangaData.toSManga(
|
||||
includeMuTags = preferences.includeMuTags,
|
||||
scorePosition = preferences.scorePosition,
|
||||
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol },
|
||||
groupTags = preferences.groupTags,
|
||||
covers = covers,
|
||||
)
|
||||
}
|
||||
return mangaData.toSManga(
|
||||
includeMuTags = preferences.includeMuTags,
|
||||
scorePosition = preferences.scorePosition,
|
||||
groupTags = preferences.groupTags,
|
||||
)
|
||||
}
|
||||
|
||||
@ -466,10 +448,9 @@ abstract class Comick(
|
||||
.map { it.toSChapter(mangaUrl) }
|
||||
}
|
||||
|
||||
private val publishedDateFormat =
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
private val publishedDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
return "$baseUrl${chapter.url}"
|
||||
@ -532,8 +513,6 @@ abstract class Comick(
|
||||
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
||||
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
||||
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 FIRST_COVER_PREF = "DefaultCover"
|
||||
private const val FIRST_COVER_DEFAULT = true
|
||||
|
@ -1,6 +1,5 @@
|
||||
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.SCORE_POSITION_DEFAULT
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
@ -30,14 +29,13 @@ class Manga(
|
||||
val comic: Comic,
|
||||
private val artists: 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,
|
||||
) {
|
||||
fun toSManga(
|
||||
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
|
||||
scorePosition: String = SCORE_POSITION_DEFAULT,
|
||||
covers: List<MDcovers>? = null,
|
||||
groupTags: Boolean = GROUP_TAGS_DEFAULT,
|
||||
) =
|
||||
SManga.create().apply {
|
||||
// appennding # at end as part of migration from slug to hid
|
||||
@ -77,23 +75,19 @@ class Manga(
|
||||
artist = artists.joinToString { it.name.trim() }
|
||||
author = authors.joinToString { it.name.trim() }
|
||||
genre = buildList {
|
||||
comic.origination?.let { add(Genre("Origination", it.name)) }
|
||||
demographic?.let { add(Genre("Demographic", it)) }
|
||||
addAll(
|
||||
comic.mdGenres.mapNotNull { it.genre }.sortedBy { it.group }
|
||||
.sortedBy { it.name },
|
||||
)
|
||||
addAll(genres.sortedBy { it.group }.sortedBy { it.name })
|
||||
comic.origination?.let(::add)
|
||||
demographic?.let { add(Name(it)) }
|
||||
addAll(genres)
|
||||
addAll(comic.mdGenres.mapNotNull { it.name })
|
||||
if (includeMuTags) {
|
||||
addAll(
|
||||
comic.muGenres.categories.mapNotNull { it?.category?.title }.sorted()
|
||||
.map { Genre("Category", it) },
|
||||
)
|
||||
comic.muGenres.categories.forEach { category ->
|
||||
category?.category?.title?.let { add(Name(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
.distinctBy { it.name }
|
||||
.filterNot { it.name.isNullOrBlank() || it.group.isNullOrBlank() }
|
||||
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" }
|
||||
.filter { it.name.isNotBlank() }
|
||||
.joinToString { it.name.trim() }
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +106,6 @@ class Comic(
|
||||
@SerialName("md_comic_md_genres") val mdGenres: List<MdGenres>,
|
||||
@SerialName("mu_comics") val muGenres: MuComicCategories = MuComicCategories(emptyList()),
|
||||
@SerialName("bayesian_rating") val score: String? = null,
|
||||
@SerialName("iso639_1") val isoLang: String? = null,
|
||||
) {
|
||||
val origination = when (country) {
|
||||
"jp" -> Name("Manga")
|
||||
@ -135,13 +128,7 @@ class Comic(
|
||||
|
||||
@Serializable
|
||||
class MdGenres(
|
||||
@SerialName("md_genres") val genre: Genre? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Genre(
|
||||
val group: String? = null,
|
||||
val name: String? = null,
|
||||
@SerialName("md_genres") val name: Name? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'E-Hentai'
|
||||
extClass = '.EHFactory'
|
||||
extVersionCode = 22
|
||||
extVersionCode = 20
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -4,17 +4,17 @@ import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.webkit.CookieManager
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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.Text
|
||||
import eu.kanade.tachiyomi.source.model.Filter.TriState
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
@ -39,22 +39,13 @@ abstract class EHentai(
|
||||
private val ehLang: String,
|
||||
) : ConfigurableSource, HttpSource() {
|
||||
|
||||
override val name = "E-Hentai"
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() }
|
||||
private val memberId: String by lazy { getMemberIdPref() }
|
||||
private val passHash: String by lazy { getPassHashPref() }
|
||||
override val name = "E-Hentai"
|
||||
|
||||
override val baseUrl: String
|
||||
get() = when {
|
||||
System.getenv("CI") == "true" -> "https://e-hentai.org"
|
||||
memberId.isNotEmpty() && passHash.isNotEmpty() -> "https://exhentai.org"
|
||||
else -> "https://e-hentai.org"
|
||||
}
|
||||
override val baseUrl = "https://e-hentai.org"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
@ -82,7 +73,7 @@ abstract class EHentai(
|
||||
val manga = mangaElements[i].let {
|
||||
SManga.create().apply {
|
||||
// Get title
|
||||
it.selectFirst("a")?.apply {
|
||||
it.select("a")?.first()?.apply {
|
||||
title = this.select(".glink").text()
|
||||
url = ExGalleryMetadata.normalizeUrl(attr("href"))
|
||||
if (i == mangaElements.lastIndex) {
|
||||
@ -140,9 +131,9 @@ abstract class EHentai(
|
||||
}
|
||||
|
||||
private fun parseChapterPage(response: Element) = with(response) {
|
||||
select("#gdt a").map {
|
||||
it.attr("href")
|
||||
}
|
||||
select(".gdtm a").map {
|
||||
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()
|
||||
@ -170,23 +161,10 @@ abstract class EHentai(
|
||||
query.isBlank() -> languageTag(enforceLanguageFilter)
|
||||
else -> languageTag(enforceLanguageFilter).let { if (it.isNotEmpty()) "$query,$it" else query }
|
||||
}
|
||||
filters.filterIsInstance<TextFilter>().forEach { it ->
|
||||
if (it.state.isNotEmpty()) {
|
||||
val splitted = it.state.split(",").filter(String::isNotBlank)
|
||||
if (splitted.size < 2 && it.type != "tags") {
|
||||
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(" ", "+")}\""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
modifiedQuery += filters.filterIsInstance<TagFilter>()
|
||||
.flatMap { it.markedTags() }
|
||||
.joinToString(",")
|
||||
.let { if (it.isNotEmpty()) ",$it" else it }
|
||||
uri.appendQueryParameter("f_search", modifiedQuery)
|
||||
// when attempting to search with no genres selected, will auto select all genres
|
||||
filters.filterIsInstance<GenreGroup>().firstOrNull()?.state?.let {
|
||||
@ -374,12 +352,6 @@ abstract class EHentai(
|
||||
// Bypass "Offensive For Everyone" content warning
|
||||
cookies["nw"] = "1"
|
||||
|
||||
cookies["ipb_member_id"] = memberId
|
||||
|
||||
cookies["ipb_pass_hash"] = passHash
|
||||
|
||||
cookies["igneous"] = ""
|
||||
|
||||
buildCookies(cookies)
|
||||
}
|
||||
|
||||
@ -416,17 +388,12 @@ abstract class EHentai(
|
||||
EnforceLanguageFilter(getEnforceLanguagePref()),
|
||||
Watched(),
|
||||
GenreGroup(),
|
||||
Filter.Header("Separate tags with commas (,)"),
|
||||
Filter.Header("Prepend with dash (-) to exclude"),
|
||||
Filter.Header("Use 'Female Tags' or 'Male Tags' for specific categories. 'Tags' searches all categories."),
|
||||
TextFilter("Tags", "tag"),
|
||||
TextFilter("Female Tags", "female"),
|
||||
TextFilter("Male Tags", "male"),
|
||||
TagFilter("Misc Tags", triStateBoxesFrom(miscTags), "other"),
|
||||
TagFilter("Female Tags", triStateBoxesFrom(femaleTags), "female"),
|
||||
TagFilter("Male Tags", triStateBoxesFrom(maleTags), "male"),
|
||||
AdvancedGroup(),
|
||||
)
|
||||
|
||||
internal open class TextFilter(name: String, val type: String, val specific: String = "") : Filter.Text(name)
|
||||
|
||||
class Watched : CheckBox("Watched List"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state) {
|
||||
@ -520,6 +487,17 @@ abstract class EHentai(
|
||||
|
||||
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
|
||||
private val languageMappings = listOf(
|
||||
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_SUMMARY = "If checked, forces browsing of manga matching a language tag"
|
||||
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
|
||||
@ -577,56 +545,8 @@ abstract class EHentai(
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ ext {
|
||||
extClass = '.EternalMangasFactory'
|
||||
themePkg = 'mangaesp'
|
||||
baseUrl = 'https://eternalmangas.com'
|
||||
overrideVersionCode = 1
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -7,17 +7,11 @@ import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
open class EternalMangas(
|
||||
lang: String,
|
||||
@ -61,62 +55,9 @@ open class EternalMangas(
|
||||
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> {
|
||||
val doc = Jsoup.parse(jsRedirect(response))
|
||||
return doc.select("main > img").mapIndexed { i, img ->
|
||||
Page(i, imageUrl = img.imgAttr())
|
||||
}
|
||||
}
|
||||
var document = response.asJsoup()
|
||||
|
||||
private fun jsRedirect(response: Response): String {
|
||||
var body = response.body.string()
|
||||
val document = Jsoup.parse(body)
|
||||
document.selectFirst("body > form[method=post]")?.let {
|
||||
val action = it.attr("action")
|
||||
val inputs = it.select("input")
|
||||
@ -126,9 +67,12 @@ open class EternalMangas(
|
||||
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
|
||||
|
@ -1,8 +0,0 @@
|
||||
ext {
|
||||
extName = 'EveriaClub (unoriginal)'
|
||||
extClass = '.EveriaClubCom'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 35 KiB |
@ -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")
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'FoamGirl'
|
||||
extClass = '.FoamGirl'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,6 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
@ -67,43 +65,22 @@ class FoamGirl() : ParsedHttpSource() {
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
val imageCount = document.select(".post_title_topimg").text().substringAfter("(").substringBefore("P").toInt()
|
||||
val imageUrl = document.select(".imageclick-imgbox").attr("href").toHttpUrl()
|
||||
val baseIndex = imageUrl.pathSegments.last().substringBefore(".")
|
||||
|
||||
return if (baseIndex.isNumber()) {
|
||||
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(
|
||||
index,
|
||||
imageUrl = imageUrl.newBuilder().apply {
|
||||
removePathSegment(imageUrl.pathSize - 1)
|
||||
addPathSegment("${imagePrefix}${index + 2}.jpg")
|
||||
}.build().toString(),
|
||||
val imagePrefix = imageUrl.pathSegments.last().substringBefore(".").toLong() / 10
|
||||
for (i in 0 until imageCount) {
|
||||
pages.add(
|
||||
Page(
|
||||
i,
|
||||
imageUrl = imageUrl.newBuilder().apply {
|
||||
removePathSegment(imageUrl.pathSize - 1)
|
||||
addPathSegment("${imagePrefix}${i + 2}.jpg")
|
||||
}.build().toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
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 {
|
||||
val HAS_NEXT_PAGE_REGEX = """(\d+_\d+)""".toRegex()
|
||||
private val DATE_FORMAT by lazy {
|
||||
SimpleDateFormat("yyyy.M.d", Locale.ENGLISH)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Hennojin'
|
||||
extClass = '.HennojinFactory'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,32 +1,30 @@
|
||||
package eu.kanade.tachiyomi.extension.all.hennojin
|
||||
|
||||
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.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Evaluator
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Hennojin(override val lang: String) : ParsedHttpSource() {
|
||||
override val baseUrl = "https://hennojin.com"
|
||||
class Hennojin(override val lang: String, suffix: String) : ParsedHttpSource() {
|
||||
override val baseUrl = "https://hennojin.com/home/$suffix"
|
||||
|
||||
override val name = "Hennojin"
|
||||
|
||||
// Popular is latest
|
||||
override val supportsLatest = false
|
||||
|
||||
private val httpUrl by lazy { "$baseUrl/home".toHttpUrl() }
|
||||
private val httpUrl by lazy { baseUrl.toHttpUrl() }
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
@ -43,23 +41,15 @@ class Hennojin(override val lang: String) : ParsedHttpSource() {
|
||||
override fun popularMangaNextPageSelector() = ".paginate .next"
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
httpUrl.request {
|
||||
when (lang) {
|
||||
"ja" -> {
|
||||
addEncodedPathSegments("page/$page/")
|
||||
addQueryParameter("archive", "raw")
|
||||
}
|
||||
else -> addEncodedPathSegments("page/$page")
|
||||
}
|
||||
}
|
||||
httpUrl.request { addEncodedPathSegments("page/$page") }
|
||||
|
||||
override fun popularMangaFromElement(element: Element) =
|
||||
SManga.create().apply {
|
||||
element.selectFirst(".title_link > a").let {
|
||||
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()
|
||||
@ -76,68 +66,46 @@ class Hennojin(override val lang: String) : ParsedHttpSource() {
|
||||
override fun searchMangaFromElement(element: Element) =
|
||||
popularMangaFromElement(element)
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) =
|
||||
GET("https://hennojin.com" + manga.url, headers)
|
||||
|
||||
override fun mangaDetailsParse(document: Document) =
|
||||
SManga.create().apply {
|
||||
description = document.select(
|
||||
description = document.selectFirst(
|
||||
".manga-subtitle + p + p",
|
||||
).joinToString("\n") {
|
||||
it
|
||||
.apply { select(Evaluator.Tag("br")).prepend("\\n") }
|
||||
.text()
|
||||
.replace("\\n", "\n")
|
||||
.replace("\n ", "\n")
|
||||
}.trim()
|
||||
)?.html()?.replace("<br> ", "\n")
|
||||
genre = document.select(
|
||||
".tags-list a[href*=/parody/]," +
|
||||
".tags-list a[href*=/tags/]," +
|
||||
".tags-list a[href*=/character/]",
|
||||
).joinToString { it.text() }
|
||||
artist = document.selectFirst(
|
||||
)?.joinToString { it.text() }
|
||||
artist = document.select(
|
||||
".tags-list a[href*=/artist/]",
|
||||
)?.text()
|
||||
author = document.selectFirst(
|
||||
)?.joinToString { it.text() }
|
||||
author = document.select(
|
||||
".tags-list a[href*=/group/]",
|
||||
)?.text() ?: artist
|
||||
)?.joinToString { it.text() } ?: artist
|
||||
status = SManga.COMPLETED
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup(response.body.string())
|
||||
val date = document
|
||||
.selectFirst(".manga-thumbnail > img")
|
||||
?.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 {
|
||||
setUrlWithoutDomain(
|
||||
it
|
||||
?.absUrl("href")
|
||||
?.toHttpUrlOrNull()
|
||||
?.newBuilder()
|
||||
?.removeAllQueryParameters("view")
|
||||
?.addQueryParameter("view", "multi")
|
||||
?.build()
|
||||
?.toString()
|
||||
?: it.absUrl("href"),
|
||||
)
|
||||
name = "Chapter"
|
||||
date?.run { date_upload = this }
|
||||
chapter_number = -1f
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun fetchChapterList(manga: SManga) =
|
||||
Request.Builder().url(manga.thumbnail_url!!)
|
||||
.head().build().run(client::newCall)
|
||||
.asObservableSuccess().map { res ->
|
||||
SChapter.create().apply {
|
||||
name = "Chapter"
|
||||
url = manga.reader
|
||||
date_upload = res.date
|
||||
chapter_number = -1f
|
||||
}.let(::listOf)
|
||||
}!!
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) =
|
||||
GET("https://hennojin.com" + chapter.url, headers)
|
||||
|
||||
override fun pageListParse(document: Document) =
|
||||
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(
|
||||
block: HttpUrl.Builder.() -> HttpUrl.Builder,
|
||||
@ -146,6 +114,9 @@ class Hennojin(override val lang: String) : ParsedHttpSource() {
|
||||
private inline val Response.date: Long
|
||||
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() =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
|
@ -4,7 +4,7 @@ import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class HennojinFactory : SourceFactory {
|
||||
override fun createSources() = listOf(
|
||||
Hennojin("en"),
|
||||
Hennojin("ja"),
|
||||
Hennojin("en", ""),
|
||||
Hennojin("ja", "?archive=raw"),
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
@ -1,8 +0,0 @@
|
||||
ext {
|
||||
extName = 'SchaleNetwork'
|
||||
extClass = '.KoharuFactory'
|
||||
extVersionCode = 11
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
@ -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"),
|
||||
)
|
||||
}
|
@ -76,8 +76,6 @@ original_language=Original language
|
||||
original_language_filter_chinese=%s (Manhua)
|
||||
original_language_filter_japanese=%s (Manga)
|
||||
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_josei=Josei
|
||||
publication_demographic_none=None
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'MangaDex'
|
||||
extClass = '.MangaDexFactory'
|
||||
extVersionCode = 196
|
||||
extVersionCode = 194
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -138,11 +138,6 @@ object MDConstants {
|
||||
return "${altTitlesInDescPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val preferExtensionLangTitlePref = "preferExtensionLangTitle"
|
||||
fun getPreferExtensionLangTitlePrefKey(dexLang: String): String {
|
||||
return "${preferExtensionLangTitlePref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val tagGroupContent = "content"
|
||||
private const val tagGroupFormat = "format"
|
||||
private const val tagGroupGenre = "genre"
|
||||
|
@ -113,7 +113,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
||||
.firstInstanceOrNull<CoverArtDto>()
|
||||
?.attributes?.fileName
|
||||
}
|
||||
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang, preferences.preferExtensionLangTitle)
|
||||
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
|
||||
}
|
||||
|
||||
return MangasPage(mangaList, mangaListDto.hasNextPage)
|
||||
@ -177,7 +177,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
||||
.firstInstanceOrNull<CoverArtDto>()
|
||||
?.attributes?.fileName
|
||||
}
|
||||
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang, preferences.preferExtensionLangTitle)
|
||||
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
|
||||
}
|
||||
|
||||
return MangasPage(mangaList, chapterListDto.hasNextPage)
|
||||
@ -360,7 +360,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
||||
.firstInstanceOrNull<CoverArtDto>()
|
||||
?.attributes?.fileName
|
||||
}
|
||||
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang, preferences.preferExtensionLangTitle)
|
||||
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
|
||||
}
|
||||
|
||||
return mangaList
|
||||
@ -423,7 +423,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
||||
dexLang,
|
||||
preferences.coverQuality,
|
||||
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(tryUsingFirstVolumeCoverPref)
|
||||
screen.addPreference(dataSaverPref)
|
||||
screen.addPreference(standardHttpsPortPref)
|
||||
screen.addPreference(altTitlesInDescPref)
|
||||
screen.addPreference(preferExtensionLangTitlePref)
|
||||
screen.addPreference(contentRatingPref)
|
||||
screen.addPreference(originalLanguagePref)
|
||||
screen.addPreference(blockedGroupsPref)
|
||||
@ -857,9 +840,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
||||
private val SharedPreferences.altTitlesInDesc
|
||||
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
|
||||
* preferences. This method clear invalid UUIDs in case the user have updated from
|
||||
|
@ -6,19 +6,15 @@ import eu.kanade.tachiyomi.source.SourceFactory
|
||||
class MangaDexFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
MangaDexEnglish(),
|
||||
MangadexAfrikaans(),
|
||||
MangaDexAlbanian(),
|
||||
MangaDexArabic(),
|
||||
MangaDexAzerbaijani(),
|
||||
MangaDexBasque(),
|
||||
MangaDexBelarusian(),
|
||||
MangaDexBengali(),
|
||||
MangaDexBulgarian(),
|
||||
MangaDexBurmese(),
|
||||
MangaDexCatalan(),
|
||||
MangaDexChineseSimplified(),
|
||||
MangaDexChineseTraditional(),
|
||||
MangaDexChuvash(),
|
||||
MangaDexCroatian(),
|
||||
MangaDexCzech(),
|
||||
MangaDexDanish(),
|
||||
@ -34,10 +30,8 @@ class MangaDexFactory : SourceFactory {
|
||||
MangaDexHebrew(),
|
||||
MangaDexHindi(),
|
||||
MangaDexHungarian(),
|
||||
MangaDexIrish(),
|
||||
MangaDexIndonesian(),
|
||||
MangaDexItalian(),
|
||||
MangaDexJavanese(),
|
||||
MangaDexJapanese(),
|
||||
MangaDexKazakh(),
|
||||
MangaDexKorean(),
|
||||
@ -63,25 +57,19 @@ class MangaDexFactory : SourceFactory {
|
||||
MangaDexThai(),
|
||||
MangaDexTurkish(),
|
||||
MangaDexUkrainian(),
|
||||
MangaDexUrdu(),
|
||||
MangaDexUzbek(),
|
||||
MangaDexVietnamese(),
|
||||
)
|
||||
}
|
||||
|
||||
class MangadexAfrikaans : MangaDex("af")
|
||||
class MangaDexAlbanian : MangaDex("sq")
|
||||
class MangaDexArabic : MangaDex("ar")
|
||||
class MangaDexAzerbaijani : MangaDex("az")
|
||||
class MangaDexBasque : MangaDex("eu")
|
||||
class MangaDexBelarusian : MangaDex("be")
|
||||
class MangaDexBengali : MangaDex("bn")
|
||||
class MangaDexBulgarian : MangaDex("bg")
|
||||
class MangaDexBurmese : MangaDex("my")
|
||||
class MangaDexCatalan : MangaDex("ca")
|
||||
class MangaDexChineseSimplified : MangaDex("zh-Hans", "zh")
|
||||
class MangaDexChineseTraditional : MangaDex("zh-Hant", "zh-hk")
|
||||
class MangaDexChuvash : MangaDex("cv")
|
||||
class MangaDexCroatian : MangaDex("hr")
|
||||
class MangaDexCzech : MangaDex("cs")
|
||||
class MangaDexDanish : MangaDex("da")
|
||||
@ -98,11 +86,9 @@ class MangaDexGreek : MangaDex("el")
|
||||
class MangaDexHebrew : MangaDex("he")
|
||||
class MangaDexHindi : MangaDex("hi")
|
||||
class MangaDexHungarian : MangaDex("hu")
|
||||
class MangaDexIrish : MangaDex("ga")
|
||||
class MangaDexIndonesian : MangaDex("id")
|
||||
class MangaDexItalian : MangaDex("it")
|
||||
class MangaDexJapanese : MangaDex("ja")
|
||||
class MangaDexJavanese : MangaDex("jv")
|
||||
class MangaDexKazakh : MangaDex("kk")
|
||||
class MangaDexKorean : MangaDex("ko")
|
||||
class MangaDexLatin : MangaDex("la")
|
||||
@ -127,6 +113,4 @@ class MangaDexTelugu : MangaDex("te")
|
||||
class MangaDexThai : MangaDex("th")
|
||||
class MangaDexTurkish : MangaDex("tr")
|
||||
class MangaDexUkrainian : MangaDex("uk")
|
||||
class MangaDexUrdu : MangaDex("ur")
|
||||
class MangaDexUzbek : MangaDex("uz")
|
||||
class MangaDexVietnamese : MangaDex("vi")
|
||||
|
@ -275,9 +275,6 @@ class MangaDexHelper(lang: String) {
|
||||
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.
|
||||
*/
|
||||
@ -286,24 +283,15 @@ class MangaDexHelper(lang: String) {
|
||||
coverFileName: String?,
|
||||
coverSuffix: String?,
|
||||
lang: String,
|
||||
preferExtensionLangTitle: Boolean,
|
||||
): SManga = SManga.create().apply {
|
||||
url = "/manga/${mangaDataDto.id}"
|
||||
|
||||
val titleMap = mangaDataDto.attributes!!.title
|
||||
title = with(mangaDataDto.attributes) {
|
||||
titleMap[lang] ?: altTitles.run {
|
||||
val mainTitle = titleMap.values.firstOrNull()
|
||||
val langTitle = findTitleByLang(lang)
|
||||
val enTitle = findTitleByLang("en")
|
||||
|
||||
if (preferExtensionLangTitle) {
|
||||
listOf(langTitle, mainTitle, enTitle)
|
||||
} else {
|
||||
listOf(mainTitle, langTitle, enTitle)
|
||||
}.firstNotNullOfOrNull { it }
|
||||
}
|
||||
}?.removeEntities().orEmpty()
|
||||
val dirtyTitle =
|
||||
titleMap.values.firstOrNull() // use literally anything from title as first resort
|
||||
?: mangaDataDto.attributes.altTitles
|
||||
.find { (it[lang] ?: it["en"]) !== null }
|
||||
?.values?.singleOrNull() // find something else from alt titles
|
||||
title = dirtyTitle?.removeEntities().orEmpty()
|
||||
|
||||
coverFileName?.let {
|
||||
thumbnail_url = when (!coverSuffix.isNullOrEmpty()) {
|
||||
@ -323,7 +311,6 @@ class MangaDexHelper(lang: String) {
|
||||
lang: String,
|
||||
coverSuffix: String?,
|
||||
altTitlesInDesc: Boolean,
|
||||
preferExtensionLangTitle: Boolean,
|
||||
): SManga {
|
||||
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
|
||||
author = authors.joinToString()
|
||||
artist = artists.joinToString()
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'MANGA Plus by SHUEISHA'
|
||||
extClass = '.MangaPlusFactory'
|
||||
extVersionCode = 54
|
||||
extVersionCode = 53
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -210,7 +210,6 @@ class Label(val label: LabelCode? = LabelCode.WEEKLY_SHOUNEN_JUMP) {
|
||||
LabelCode.SHOUNEN_JUMP_PLUS -> "Shounen Jump+"
|
||||
LabelCode.MANGA_PLUS_CREATORS -> "MANGA Plus Creators"
|
||||
LabelCode.SAIKYOU_JUMP -> "Saikyou Jump"
|
||||
LabelCode.ULTRA_JUMP -> "Ultra Jump"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@ -247,9 +246,6 @@ enum class LabelCode {
|
||||
|
||||
@SerialName("WSJ")
|
||||
WEEKLY_SHOUNEN_JUMP,
|
||||
|
||||
@SerialName("UJ")
|
||||
ULTRA_JUMP,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
@ -3,7 +3,7 @@ ext {
|
||||
extClass = '.MangaReaderFactory'
|
||||
themePkg = 'mangareader'
|
||||
baseUrl = 'https://mangareader.to'
|
||||
overrideVersionCode = 4
|
||||
overrideVersionCode = 3
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -22,11 +22,8 @@ import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
|
||||
open class MangaReader(
|
||||
val language: Language,
|
||||
override val lang: String,
|
||||
) : MangaReader() {
|
||||
|
||||
override val lang = language.code
|
||||
|
||||
override val name = "MangaReader"
|
||||
|
||||
override val baseUrl = "https://mangareader.to"
|
||||
@ -36,10 +33,10 @@ open class MangaReader(
|
||||
.build()
|
||||
|
||||
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) =
|
||||
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 {
|
||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||
@ -50,7 +47,7 @@ open class MangaReader(
|
||||
}
|
||||
} else {
|
||||
urlBuilder.addPathSegment("filter").apply {
|
||||
addQueryParameter("language", language.infix)
|
||||
addQueryParameter("language", lang)
|
||||
addQueryParameter("page", page.toString())
|
||||
filters.ifEmpty(::getFilterList).forEach { filter ->
|
||||
when (filter) {
|
||||
@ -145,7 +142,7 @@ open class MangaReader(
|
||||
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
||||
val container = response.parseHtmlProperty().run {
|
||||
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()
|
||||
}
|
||||
|
@ -4,19 +4,5 @@ import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class MangaReaderFactory : SourceFactory {
|
||||
override fun createSources() =
|
||||
arrayOf(
|
||||
Language("en"),
|
||||
Language("es", chapterInfix = "es-mx"),
|
||||
Language("fr"),
|
||||
Language("ja"),
|
||||
Language("ko"),
|
||||
Language("pt-BR", infix = "pt"),
|
||||
Language("zh"),
|
||||
).map(::MangaReader)
|
||||
arrayOf("en", "fr", "ja", "ko", "zh").map(::MangaReader)
|
||||
}
|
||||
|
||||
data class Language(
|
||||
val code: String,
|
||||
val infix: String = code,
|
||||
val chapterInfix: String = code.lowercase(),
|
||||
)
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 5.5 KiB |
@ -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) }
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'NHentai'
|
||||
extClass = '.NHFactory'
|
||||
extVersionCode = 50
|
||||
extVersionCode = 46
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
)
|
@ -1,36 +1,63 @@
|
||||
package eu.kanade.tachiyomi.extension.all.nhentai
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
object NHUtils {
|
||||
fun getArtists(data: Hentai): String {
|
||||
val artists = data.tags.filter { it.type == "artist" }
|
||||
return artists.joinToString(", ") { it.name }
|
||||
fun getArtists(document: Document): String {
|
||||
val artists = document.select("#tags > div:nth-child(4) > span > a .name")
|
||||
return artists.joinToString(", ") { it.cleanTag() }
|
||||
}
|
||||
|
||||
fun getGroups(data: Hentai): String? {
|
||||
val groups = data.tags.filter { it.type == "group" }
|
||||
return groups.joinToString(", ") { it.name }.takeIf { it.isBlank() }
|
||||
}
|
||||
|
||||
fun getTagDescription(data: Hentai): String {
|
||||
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 getGroups(document: Document): String? {
|
||||
val groups = document.select("#tags > div:nth-child(5) > span > a .name")
|
||||
return if (groups.isNotEmpty()) {
|
||||
groups.joinToString(", ") { it.cleanTag() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getTags(data: Hentai): String {
|
||||
val artists = data.tags.filter { it.type == "tag" }
|
||||
return artists.joinToString(", ") { it.name }
|
||||
fun getTagDescription(document: Document): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
|
||||
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()
|
||||
|
@ -6,8 +6,10 @@ import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
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.getNumPages
|
||||
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.getTime
|
||||
import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen
|
||||
import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA
|
||||
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.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@ -36,7 +36,6 @@ import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
open class NHentai(
|
||||
override val lang: String,
|
||||
@ -51,8 +50,6 @@ open class NHentai(
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
@ -74,8 +71,6 @@ open class NHentai(
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
@ -108,7 +103,7 @@ open class NHentai(
|
||||
title = element.select("a > div").text().replace("\"", "").let {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@ -212,26 +207,22 @@ open class NHentai(
|
||||
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
|
||||
|
||||
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 {
|
||||
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")
|
||||
status = SManga.COMPLETED
|
||||
artist = getArtists(data)
|
||||
author = getGroups(data) ?: getArtists(data)
|
||||
artist = getArtists(document)
|
||||
author = getGroups(document)
|
||||
// Some people want these additional details in description
|
||||
description = "Full English and Japanese titles:\n"
|
||||
.plus("${data.title.english ?: data.title.japanese ?: data.title.pretty ?: ""}\n")
|
||||
.plus(data.title.japanese ?: "")
|
||||
.plus("\n\n")
|
||||
.plus("Pages: ${data.images.pages.size}\n")
|
||||
.plus("Favorited by: ${data.num_favorites}\n")
|
||||
.plus(getTagDescription(data))
|
||||
genre = getTags(data)
|
||||
.plus("$fullTitle\n")
|
||||
.plus("${document.select("div#info h2").text()}\n\n")
|
||||
.plus("Pages: ${getNumPages(document)}\n")
|
||||
.plus("Favorited by: ${document.select("div#info i.fa-heart ~ span span").text().removeSurrounding("(", ")")}\n")
|
||||
.plus(getTagDescription(document))
|
||||
genre = getTags(document)
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
}
|
||||
}
|
||||
@ -240,16 +231,11 @@ open class NHentai(
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val script = document.selectFirst(hentaiSelector)!!.data()
|
||||
|
||||
val json = dataRegex.find(script)?.groupValues!![1]
|
||||
|
||||
val data = json.parseAs<Hentai>()
|
||||
return listOf(
|
||||
SChapter.create().apply {
|
||||
name = "Chapter"
|
||||
scanlator = getGroups(data)
|
||||
date_upload = data.upload_date * 1000
|
||||
scanlator = getGroups(document)
|
||||
date_upload = getTime(document)
|
||||
setUrlWithoutDomain(response.request.url.encodedPath)
|
||||
},
|
||||
)
|
||||
@ -260,24 +246,11 @@ open class NHentai(
|
||||
override fun chapterListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val script = document.selectFirst("script:containsData(media_server)")!!.data()
|
||||
val script2 = document.selectFirst(hentaiSelector)!!.data()
|
||||
|
||||
val script = document.select("script:containsData(media_server)").first()!!.data()
|
||||
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 data.images.pages.mapIndexed { i, image ->
|
||||
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"
|
||||
},
|
||||
)
|
||||
return document.select("div.thumbs a > img").mapIndexed { i, img ->
|
||||
Page(i, "", img.attr("abs:data-src").replace("t.nh", "i.nh").replace("t\\d+.nh".toRegex(), "i$mediaServer.nh").replace("t.", "."))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
|
@ -2,8 +2,8 @@ ext {
|
||||
extName = 'Otaku Sanctuary'
|
||||
extClass = '.OtakuSanctuaryFactory'
|
||||
themePkg = 'otakusanctuary'
|
||||
baseUrl = 'https://otakusan.me'
|
||||
overrideVersionCode = 2
|
||||
baseUrl = 'https://otakusan.net'
|
||||
overrideVersionCode = 0
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -5,11 +5,11 @@ import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class OtakuSanctuaryFactory : SourceFactory {
|
||||
override fun createSources() = listOf(
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "all"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "vi"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "en"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "it"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "fr"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.me", "es"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "all"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "vi"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "en"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "it"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "fr"),
|
||||
OtakuSanctuary("Otaku Sanctuary", "https://otakusan.net", "es"),
|
||||
)
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
ext {
|
||||
extName = 'Pururin'
|
||||
extClass = '.PururinFactory'
|
||||
extVersionCode = 10
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
ext {
|
||||
extName = 'Pururin'
|
||||
extClass = '.PururinFactory'
|
||||
extVersionCode = 9
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -1,271 +1,264 @@
|
||||
package eu.kanade.tachiyomi.extension.all.pururin
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class Pururin(
|
||||
override val lang: String = "all",
|
||||
private val searchLang: Pair<String, String>? = null,
|
||||
private val langPath: String = "",
|
||||
) : ParsedHttpSource() {
|
||||
override val name = "Pururin"
|
||||
|
||||
final override val baseUrl = "https://pururin.me"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// Popular
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector(): String = "a.card"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
title = element.attr("title")
|
||||
setUrlWithoutDomain(element.attr("abs:href"))
|
||||
thumbnail_url = element.select("img").attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/browse$langPath?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
|
||||
|
||||
// Search
|
||||
|
||||
private fun List<Pair<String, String>>.toValue(): String {
|
||||
return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]"
|
||||
}
|
||||
|
||||
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
|
||||
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
|
||||
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
|
||||
|
||||
if (num < 0) return minPages to maxPages
|
||||
return when (query.firstOrNull()) {
|
||||
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
|
||||
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
|
||||
'=' -> when (query[1]) {
|
||||
'>' -> limitedNum() to maxPages
|
||||
'<' -> 1 to limitedNum(maxPages)
|
||||
else -> limitedNum() to limitedNum()
|
||||
}
|
||||
else -> limitedNum() to limitedNum()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Tag(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
private fun findTagByNameSubstring(tags: List<Tag>, substring: String): Pair<String, String>? {
|
||||
val tag = tags.find { it.name.contains(substring, ignoreCase = true) }
|
||||
return tag?.let { Pair(tag.id.toString(), tag.name) }
|
||||
}
|
||||
|
||||
private fun tagSearch(tag: String, type: String): Pair<String, String>? {
|
||||
val requestBody = FormBody.Builder()
|
||||
.add("text", tag)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/api/get/tags/search")
|
||||
.headers(headers)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
return findTagByNameSubstring(response.parseAs<List<Tag>>(), type)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val includeTags = mutableListOf<Pair<String, String>>()
|
||||
val excludeTags = mutableListOf<Pair<String, String>>()
|
||||
var pagesMin = 1
|
||||
var pagesMax = 9999
|
||||
var sortBy = "newest"
|
||||
|
||||
if (searchLang != null) includeTags.add(searchLang)
|
||||
|
||||
filters.forEach {
|
||||
when (it) {
|
||||
is SelectFilter -> sortBy = it.getValue()
|
||||
|
||||
is TypeFilter -> {
|
||||
val (_, inactiveFilters) = it.state.partition { stIt -> stIt.state }
|
||||
excludeTags += inactiveFilters.map { fil -> Pair(fil.value, "${fil.name} [Category]") }
|
||||
}
|
||||
|
||||
is PageFilter -> {
|
||||
if (it.state.isNotEmpty()) {
|
||||
val (min, max) = parsePageRange(it.state)
|
||||
pagesMin = min
|
||||
pagesMax = max
|
||||
}
|
||||
}
|
||||
|
||||
is TextFilter -> {
|
||||
if (it.state.isNotEmpty()) {
|
||||
it.state.split(",").filter(String::isNotBlank).map { tag ->
|
||||
val trimmed = tag.trim()
|
||||
if (trimmed.startsWith('-')) {
|
||||
tagSearch(trimmed.lowercase().removePrefix("-"), it.type)?.let { tagInfo ->
|
||||
excludeTags.add(tagInfo)
|
||||
}
|
||||
} else {
|
||||
tagSearch(trimmed.lowercase(), it.type)?.let { tagInfo ->
|
||||
includeTags.add(tagInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
// Searching with just one tag usually gives wrong results
|
||||
if (query.isEmpty()) {
|
||||
when {
|
||||
excludeTags.size == 1 && includeTags.isEmpty() -> excludeTags.addAll(excludeTags)
|
||||
includeTags.size == 1 && excludeTags.isEmpty() -> {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("browse")
|
||||
addPathSegment("tags")
|
||||
addPathSegment("content")
|
||||
addPathSegment(includeTags[0].first)
|
||||
addQueryParameter("sort", sortBy)
|
||||
addQueryParameter("start_page", pagesMin.toString())
|
||||
addQueryParameter("last_page", pagesMax.toString())
|
||||
if (page > 1) addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("search")
|
||||
addQueryParameter("q", query)
|
||||
addQueryParameter("sort", sortBy)
|
||||
addQueryParameter("start_page", pagesMin.toString())
|
||||
addQueryParameter("last_page", pagesMax.toString())
|
||||
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
|
||||
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
|
||||
if (page > 1) addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
document.select(".box-gallery").let { e ->
|
||||
initialized = true
|
||||
title = e.select(".title").text()
|
||||
author = e.select("a[href*=/circle/]").eachText().joinToString().ifEmpty { e.select("[itemprop=author]").text() }
|
||||
artist = e.select("[itemprop=author]").eachText().joinToString()
|
||||
genre = e.select("a[href*=/content/]").eachText().joinToString()
|
||||
description = e.select(".box-gallery .table-info tr")
|
||||
.filter { tr ->
|
||||
tr.select("td").let { td ->
|
||||
td.isNotEmpty() &&
|
||||
td.none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
.joinToString("\n") { tr ->
|
||||
tr.select("td").let { td ->
|
||||
var a = td.select("a").toList()
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListSelector(): String = ".table-collection tbody tr a"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
name = element.text()
|
||||
setUrlWithoutDomain(element.attr("abs:href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return response.asJsoup().select(chapterListSelector())
|
||||
.map { chapterFromElement(it) }
|
||||
.reversed()
|
||||
.let { list ->
|
||||
list.ifEmpty {
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(response.request.url.toString())
|
||||
name = "Chapter"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(".gallery-preview a img")
|
||||
.mapIndexed { i, img ->
|
||||
Page(i, "", (if (img.hasAttr("abs:src")) img.attr("abs:src") else img.attr("abs:data-src")).replace("t.", "."))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
override fun getFilterList() = getFilters()
|
||||
}
|
||||
package eu.kanade.tachiyomi.extension.all.pururin
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class Pururin(
|
||||
override val lang: String = "all",
|
||||
private val searchLang: Pair<String, String>? = null,
|
||||
private val langPath: String = "",
|
||||
) : ParsedHttpSource() {
|
||||
override val name = "Pururin"
|
||||
|
||||
final override val baseUrl = "https://pururin.to"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// Popular
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector(): String = "a.card"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
title = element.attr("title")
|
||||
setUrlWithoutDomain(element.attr("abs:href"))
|
||||
thumbnail_url = element.select("img").attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/browse$langPath?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
|
||||
|
||||
// Search
|
||||
|
||||
private fun List<Pair<String, String>>.toValue(): String {
|
||||
return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]"
|
||||
}
|
||||
|
||||
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
|
||||
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
|
||||
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
|
||||
|
||||
if (num < 0) return minPages to maxPages
|
||||
return when (query.firstOrNull()) {
|
||||
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
|
||||
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
|
||||
'=' -> when (query[1]) {
|
||||
'>' -> limitedNum() to maxPages
|
||||
'<' -> 1 to limitedNum(maxPages)
|
||||
else -> limitedNum() to limitedNum()
|
||||
}
|
||||
else -> limitedNum() to limitedNum()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Tag(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
private fun findTagByNameSubstring(tags: List<Tag>, substring: String): Pair<String, String>? {
|
||||
val tag = tags.find { it.name.contains(substring, ignoreCase = true) }
|
||||
return tag?.let { Pair(tag.id.toString(), tag.name) }
|
||||
}
|
||||
|
||||
private fun tagSearch(tag: String, type: String): Pair<String, String>? {
|
||||
val requestBody = FormBody.Builder()
|
||||
.add("text", tag)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/api/get/tags/search")
|
||||
.headers(headers)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
return findTagByNameSubstring(response.parseAs<List<Tag>>(), type)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val includeTags = mutableListOf<Pair<String, String>>()
|
||||
val excludeTags = mutableListOf<Pair<String, String>>()
|
||||
var pagesMin = 1
|
||||
var pagesMax = 9999
|
||||
var sortBy = "newest"
|
||||
|
||||
if (searchLang != null) includeTags.add(searchLang)
|
||||
|
||||
filters.forEach {
|
||||
when (it) {
|
||||
is SelectFilter -> sortBy = it.getValue()
|
||||
|
||||
is TypeFilter -> {
|
||||
val (_, inactiveFilters) = it.state.partition { stIt -> stIt.state }
|
||||
excludeTags += inactiveFilters.map { fil -> Pair(fil.value, "${fil.name} [Category]") }
|
||||
}
|
||||
|
||||
is PageFilter -> {
|
||||
if (it.state.isNotEmpty()) {
|
||||
val (min, max) = parsePageRange(it.state)
|
||||
pagesMin = min
|
||||
pagesMax = max
|
||||
}
|
||||
}
|
||||
|
||||
is TextFilter -> {
|
||||
if (it.state.isNotEmpty()) {
|
||||
it.state.split(",").filter(String::isNotBlank).map { tag ->
|
||||
val trimmed = tag.trim()
|
||||
if (trimmed.startsWith('-')) {
|
||||
tagSearch(trimmed.lowercase().removePrefix("-"), it.type)?.let { tagInfo ->
|
||||
excludeTags.add(tagInfo)
|
||||
}
|
||||
} else {
|
||||
tagSearch(trimmed.lowercase(), it.type)?.let { tagInfo ->
|
||||
includeTags.add(tagInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
// Searching with just one tag usually gives wrong results
|
||||
if (query.isEmpty()) {
|
||||
when {
|
||||
excludeTags.size == 1 && includeTags.isEmpty() -> excludeTags.addAll(excludeTags)
|
||||
includeTags.size == 1 && excludeTags.isEmpty() -> {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("browse")
|
||||
addPathSegment("tags")
|
||||
addPathSegment("content")
|
||||
addPathSegment(includeTags[0].first)
|
||||
addQueryParameter("sort", sortBy)
|
||||
addQueryParameter("start_page", pagesMin.toString())
|
||||
addQueryParameter("last_page", pagesMax.toString())
|
||||
if (page > 1) addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("search")
|
||||
addQueryParameter("q", query)
|
||||
addQueryParameter("sort", sortBy)
|
||||
addQueryParameter("start_page", pagesMin.toString())
|
||||
addQueryParameter("last_page", pagesMax.toString())
|
||||
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
|
||||
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
|
||||
if (page > 1) addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
document.select(".box-gallery").let { e ->
|
||||
initialized = true
|
||||
title = e.select(".title").text()
|
||||
author = e.select("a[href*=/circle/]").text().ifEmpty { e.select("[itemprop=author]").text() }
|
||||
artist = e.select("[itemprop=author]").text()
|
||||
genre = e.select("a[href*=/content/]").text()
|
||||
description = e.select(".box-gallery .table-info tr")
|
||||
.filter { tr ->
|
||||
tr.select("td").none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
|
||||
}
|
||||
.joinToString("\n") { tr ->
|
||||
tr.select("td")
|
||||
.joinToString(": ") { it.text() }
|
||||
}
|
||||
thumbnail_url = e.select("img").attr("abs:src")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListSelector(): String = ".table-collection tbody tr a"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
name = element.text()
|
||||
setUrlWithoutDomain(element.attr("abs:href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return response.asJsoup().select(chapterListSelector())
|
||||
.map { chapterFromElement(it) }
|
||||
.reversed()
|
||||
.let { list ->
|
||||
list.ifEmpty {
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(response.request.url.toString())
|
||||
name = "Chapter"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(".gallery-preview a img")
|
||||
.mapIndexed { i, img ->
|
||||
Page(i, "", (if (img.hasAttr("abs:src")) img.attr("abs:src") else img.attr("abs:data-src")).replace("t.", "."))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
override fun getFilterList() = getFilters()
|
||||
}
|
||||
|
@ -1,24 +1,24 @@
|
||||
package eu.kanade.tachiyomi.extension.all.pururin
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class PururinFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
PururinAll(),
|
||||
PururinEN(),
|
||||
PururinJA(),
|
||||
)
|
||||
}
|
||||
|
||||
class PururinAll : Pururin()
|
||||
class PururinEN : Pururin(
|
||||
"en",
|
||||
Pair("13010", "english"),
|
||||
"/tags/language/13010/english",
|
||||
)
|
||||
class PururinJA : Pururin(
|
||||
"ja",
|
||||
Pair("13011", "japanese"),
|
||||
"/tags/language/13011/japanese",
|
||||
)
|
||||
package eu.kanade.tachiyomi.extension.all.pururin
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class PururinFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
PururinAll(),
|
||||
PururinEN(),
|
||||
PururinJA(),
|
||||
)
|
||||
}
|
||||
|
||||
class PururinAll : Pururin()
|
||||
class PururinEN : Pururin(
|
||||
"en",
|
||||
Pair("13010", "english"),
|
||||
"/tags/language/13010/english",
|
||||
)
|
||||
class PururinJA : Pururin(
|
||||
"ja",
|
||||
Pair("13011", "japanese"),
|
||||
"/tags/language/13011/japanese",
|
||||
)
|
||||
|