Compare commits

..

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

788 changed files with 3904 additions and 10258 deletions

View File

@ -105,15 +105,3 @@ body:
required: true
- 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -269,32 +269,32 @@ abstract class FMReader(
// languages: en, vi, es, tr
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

View File

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

View File

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

View File

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

View File

@ -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>()

View File

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

View File

@ -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)) {

View File

@ -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

View File

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

View File

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

View File

@ -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) }
}

View File

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

View File

@ -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 {

View File

@ -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() =

View File

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

View File

@ -1,4 +1,3 @@
pref_show_paid_chapter_title=Display paid chapters
pref_show_paid_chapter_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

View File

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

View File

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

View File

@ -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()
}
}

View File

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

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

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

View File

@ -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
}
}

View File

@ -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

View File

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

View File

@ -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:"
}

View File

@ -1,29 +1,26 @@
package eu.kanade.tachiyomi.lib.zipinterceptor
package eu.kanade.tachiyomi.multisrc.peachscan
import android.app.ActivityManager
import android.app.Application
import android.graphics.Bitmap
import android.graphics.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
}
}

View File

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

View File

@ -152,25 +152,25 @@ abstract class Zbulu(
val value = date.split(' ')[0].toInt()
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

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

@ -3,9 +3,6 @@ ignored_groups_summary=Chapters from these groups won't be shown.\nOne group nam
include_tags_title=Include Tags
include_tags_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

View File

@ -3,9 +3,6 @@ ignored_groups_summary=Capítulos desses grupos não aparecerão.\nUm grupo por
include_tags_title=Incluir Tags
include_tags_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

View File

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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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)
}
}

View File

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

View File

@ -7,17 +7,11 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

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

View File

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

View File

@ -6,8 +6,6 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.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)
}

View File

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

View File

@ -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()

View File

@ -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"),
)
}

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.koharu.KoharuUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:pathPattern="/g/..*/..*"/>
<data android:host="koharu.to" />
<data android:host="schale.network" />
<data android:host="gehenna.jp" />
<data android:host="niyaniya.moe" />
<data android:host="seia.to" />
<data android:host="shupogaki.moe" />
<data android:host="hoshino.one" />
</intent-filter>
</activity>
</application>
</manifest>

View File

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

View File

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

View File

@ -76,8 +76,6 @@ original_language=Original language
original_language_filter_chinese=%s (Manhua)
original_language_filter_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

View File

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

View File

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

View File

@ -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

View File

@ -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")

View File

@ -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()

View File

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

View File

@ -210,7 +210,6 @@ class Label(val label: LabelCode? = LabelCode.WEEKLY_SHOUNEN_JUMP) {
LabelCode.SHOUNEN_JUMP_PLUS -> "Shounen Jump+"
LabelCode.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

View File

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

View File

@ -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()
}

View File

@ -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(),
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

View File

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

View File

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

View File

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

View File

@ -1,36 +1,63 @@
package eu.kanade.tachiyomi.extension.all.nhentai
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()

View File

@ -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

View File

@ -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
}

View File

@ -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"),
)
}

View File

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

View File

@ -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()
}

View File

@ -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",
)

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