Compare commits
209 Commits
cb27948307
...
e91f02af02
Author | SHA1 | Date |
---|---|---|
Draff | e91f02af02 | |
AwkwardPeak7 | 44b820eca0 | |
Michał Marszałek | 21d51725e5 | |
Chopper | 5df41c9b5f | |
AlphaBoom | eb41438fb3 | |
TheKingTermux | 4cc9b24137 | |
AlphaBoom | b3f22ebf80 | |
AlphaBoom | 3ecfb8529c | |
are-are-are | 3a05d381e5 | |
xdwil | a39a339c8e | |
Chopper | a900c6cdfc | |
Chopper | d434d470f3 | |
Matthias Ahouansou | ac8b118d90 | |
Vetle Ledaal | 614165b64c | |
Vetle Ledaal | b23bd214ff | |
Cuong-Tran | e11aafbd0f | |
hatozuki-programmer | 7abb7e3c16 | |
Chopper | 9ae0fcfc3e | |
hatozuki-programmer | b4f081e28b | |
Creepler13 | 8719d05ee4 | |
are-are-are | e339eb3797 | |
hatozuki-programmer | da6869d9b4 | |
Fioren | dcf3bb75d5 | |
Bui Dai | 3b8e3cf5bd | |
Cuong-Tran | 23a4a22ad1 | |
Creepler13 | d04dc0e876 | |
dngonz | 95900b50fa | |
dngonz | 98ed77b3c7 | |
dngonz | 635fdcaf9e | |
are-are-are | 40d11c7278 | |
Creepler13 | b91a9c90af | |
Emanuel Nibizi | dc905e5f6d | |
Nguyen Ngoc Duy Bao | 737a141bad | |
Bui Dai | 21aedafb7d | |
KenjieDec | 5f9d4e1dc6 | |
KirinRaikage | 0d1ac7b51e | |
Lucas Bernardineli de Queiroz | 489c6a3fc0 | |
Creepler13 | 816133a4a8 | |
Creepler13 | 023b928462 | |
Creepler13 | 1c31319402 | |
Creepler13 | 54bcdce9de | |
BrutuZ | 90ffe087db | |
kana-shii | c66131c809 | |
kana-shii | ab800d59c8 | |
Creepler13 | e3933d6f22 | |
Vetle Ledaal | f9566c55e4 | |
Vetle Ledaal | f80a060670 | |
Artem Kuznetcov | 9ee5e17b37 | |
Vetle Ledaal | bac2cb25fb | |
Vetle Ledaal | 7e0c565a4d | |
Vetle Ledaal | 1e09043247 | |
are-are-are | 348c373bc0 | |
Creepler13 | abcea6a91a | |
Nguyen Ngoc Duy Bao | b462b1429b | |
bapeey | aa5804b858 | |
Michał Marszałek | bcf51e8138 | |
Chopper | 5911343a9a | |
renovate[bot] | 827fbace8d | |
are-are-are | 91b673efbb | |
dngonz | 050efe196b | |
dngonz | aea608013b | |
dngonz | d4e8394524 | |
Emanuel Nibizi | f4c64f4386 | |
dngonz | 5e34b3aaee | |
Fioren | 0b29192c10 | |
anenasa | c8e113a524 | |
Bui Dai | 83bdb4d9b4 | |
dngonz | 989279dca6 | |
Yush0DAN | 7e226dbaa8 | |
dngonz | b21cc255c8 | |
bapeey | 4d8111b5a0 | |
dngonz | a8aaf331b2 | |
bapeey | 361081d840 | |
dngonz | 639c9821ae | |
dngonz | a424ae756f | |
lamaxama | ea644d7697 | |
dngonz | 2def743f23 | |
dngonz | 7e3a434fc0 | |
dngonz | fe4fa84e8a | |
Vetle Ledaal | 67c6278662 | |
Vetle Ledaal | dc1f6d22da | |
Vetle Ledaal | 49cd082a5e | |
Vetle Ledaal | 297cb4c6e4 | |
are-are-are | e1f897f6c8 | |
Bui Dai | a420b0e1f6 | |
Vetle Ledaal | e762d3ecb2 | |
Vetle Ledaal | 7dcf085233 | |
dngonz | 5713b09ff2 | |
dngonz | bb362356c2 | |
dngonz | 151548c0f9 | |
dngonz | 5b2ccea602 | |
are-are-are | bcf57d6f73 | |
Chopper | ea99590719 | |
dngonz | 855a25518b | |
duongtra | fa484f2b78 | |
dngonz | 2e4c3010c0 | |
dngonz | e69b2141e8 | |
KenjieDec | f60a0d37a7 | |
anenasa | 8271cbd637 | |
AwkwardPeak7 | 3e2dee3690 | |
bapeey | ae5842be87 | |
dngonz | ad8db484bd | |
Samuel Pereira da Silva | d4d400a52c | |
Vetle Ledaal | 3f57305313 | |
Bui Dai | 345c6a09e2 | |
AlphaBoom | e7b5987ed2 | |
KenjieDec | ab8ea5a743 | |
Chopper | 3f74c33e2c | |
Chopper | e875e266c5 | |
Chopper | bd97eb008b | |
kana-shii | 8253a0ae7a | |
AwkwardPeak7 | 155a2d34a4 | |
kana-shii | e1b8dd745e | |
Michał Marszałek | 7288ec8122 | |
Chopper | 516790a053 | |
are-are-are | 3fc2029917 | |
Chopper | e062d5d30d | |
Cuong-Tran | a3f8524300 | |
Roman | 07a466617d | |
Vetle Ledaal | 2760945800 | |
bapeey | 7365776e5e | |
KenjieDec | d4fcb880c4 | |
Cuong-Tran | 9419e9b07a | |
Chopper | 4e49ac42e7 | |
Chopper | 0ec37d30aa | |
Cuong-Tran | b7cf468ffe | |
Chopper | f42e5fa6de | |
Chopper | f84ec7c418 | |
are-are-are | dbfe27e7a3 | |
Chopper | 875dbcde3a | |
Chopper | 51c238bb6e | |
Chopper | 3784abee0c | |
Chopper | 11eaf8789e | |
AlphaBoom | 79f097eae3 | |
AlphaBoom | 1e7b9b3a73 | |
Chopper | e435f0f3b8 | |
Chopper | 3843573b45 | |
Chopper | c1a88e2b19 | |
Chopper | 21beafbf88 | |
Chopper | c70d6a710b | |
Vetle Ledaal | f841e5de70 | |
Chopper | 1f7095dd08 | |
Vetle Ledaal | 6c28238769 | |
AwkwardPeak7 | 098b3133b0 | |
Michał Marszałek | ce3bced7e1 | |
Chopper | 6b73e1dfec | |
Yush0DAN | 0c3a9dc8aa | |
Chopper | 829076f604 | |
Chopper | 220221deec | |
Chopper | 81e02d01b3 | |
Chopper | 097ad52daa | |
Chopper | 49e35d5223 | |
Chopper | d07881f0b6 | |
Chopper | 50cce6c943 | |
Deivid Gabriel Pereira de Oliveira | 00b01225db | |
Chopper | 0fc23e3cb0 | |
are-are-are | a038b718b1 | |
KenjieDec | 85d977e407 | |
are-are-are | 158ddc6c91 | |
Lefan | bcda90b107 | |
are-are-are | 6aedf4e884 | |
eientei95 | 3205b7d3f7 | |
eientei95 | 9ebdcdd53f | |
are-are-are | 0c4cea69e6 | |
eientei95 | 7720d8ac57 | |
eientei95 | 9807d6c478 | |
Chopper | 4ab8d2b615 | |
Chopper | cc48c9be5c | |
Chopper | 7d4082dffb | |
Lefan | cf86915f48 | |
Chopper | 69e7bc75ef | |
are-are-are | f871cdaf67 | |
Forza06 | df8f486b8e | |
stevenyomi | 1ad22ed47d | |
Forza06 | bdc4f38a6f | |
Chocobo Latos | fefd4b85c3 | |
Lefan | 76532be103 | |
Maxim Molochkov | 5ab2cea54b | |
Chopper | 578ef4dda4 | |
Chopper | a7600082ab | |
are-are-are | df88c6535c | |
AwkwardPeak7 | ebf18fcd96 | |
mr-brune | 995240cb32 | |
kana-shii | ff3e4dff8f | |
bapeey | 1457da270e | |
Chopper | fd2bf762da | |
Chopper | 562304145d | |
Chopper | be5bf59d77 | |
Chopper | 03cad6d580 | |
Chopper | 5e42af04cb | |
bapeey | 404c86ac09 | |
BrutuZ | 684a9a0972 | |
Chopper | bd37268e7f | |
duongtra | 4064b06759 | |
bapeey | 96e3f93416 | |
bapeey | da96de83ea | |
bapeey | 874ca2af62 | |
Chopper | cf602b2fcb | |
Chopper | 15a01b7baf | |
Chopper | d570d9ca83 | |
zhongfly | eed4186aa4 | |
anenasa | 02e4201f7c | |
Lefan | 3a7bfbbd6a | |
Lefan | e3ddf94daf | |
are-are-are | c59a7ac0b7 | |
dangobruh | 613ec7b37a | |
bapeey | 666fb51186 | |
Chopper | 38de421bba | |
Chaos Pjeles | 618b173d30 |
|
@ -105,3 +105,15 @@ 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
|
||||
|
|
|
@ -57,3 +57,15 @@ 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
|
||||
|
|
|
@ -55,3 +55,15 @@ 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
|
||||
|
|
|
@ -61,3 +61,15 @@ 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
|
||||
|
|
|
@ -57,3 +57,15 @@ 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
|
||||
|
|
|
@ -39,3 +39,15 @@ body:
|
|||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: <!-- footer -->
|
||||
description: Do **not** modify. This is a reminder for other users to vote.
|
||||
value: |
|
||||
---
|
||||
|
||||
Add a :+1: [reaction] to [issues you find important].
|
||||
|
||||
[reaction]: https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/
|
||||
[issues you find important]: https://github.com/keiyoushi/extensions-source/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
||||
baseVersionCode = 5
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:synchrony"))
|
||||
|
|
|
@ -153,7 +153,7 @@ abstract class ColaManga(
|
|||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
title = document.selectFirst("h1.fed-part-eone")!!.text()
|
||||
thumbnail_url = document.selectFirst("a.fed-list-pics")?.absUrl("data-orignal")
|
||||
thumbnail_url = document.selectFirst("a.fed-list-pics")?.absUrl("data-original")
|
||||
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
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.multisrc.etoshore.EtoshoreUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="${SOURCEHOST}"
|
||||
android:pathPattern="/.*/..*"
|
||||
android:scheme="${SOURCESCHEME}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,5 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
|
@ -0,0 +1,242 @@
|
|||
package eu.kanade.tachiyomi.multisrc.etoshore
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
abstract class Etoshore(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
final override val lang: String,
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
// ============================== Popular ==============================
|
||||
|
||||
open val popularFilter = FilterList(
|
||||
SelectionList("", listOf(Tag(value = "views", query = "sort"))),
|
||||
)
|
||||
|
||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun popularMangaSelector() = throw UnsupportedOperationException()
|
||||
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
|
||||
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
// ============================== Latest ===============================
|
||||
|
||||
open val latestFilter = FilterList(
|
||||
SelectionList("", listOf(Tag(value = "date", query = "sort"))),
|
||||
)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
|
||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
// ============================== Search ===============================
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/page/$page".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("s", query)
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SelectionList -> {
|
||||
val selected = filter.selected()
|
||||
url.addQueryParameter(selected.query, selected.value)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith(PREFIX_SEARCH)) {
|
||||
val slug = query.substringAfter(PREFIX_SEARCH)
|
||||
return fetchMangaDetails(SManga.create().apply { url = "/manga/$slug/" })
|
||||
.map { manga -> MangasPage(listOf(manga), false) }
|
||||
}
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = ".search-posts .chapter-box .poster a"
|
||||
|
||||
override fun searchMangaNextPageSelector() = ".navigation .naviright:has(a)"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
title = element.attr("title")
|
||||
thumbnail_url = element.selectFirst("img")?.let(::imageFromElement)
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (filterList.isEmpty()) {
|
||||
filterParse(response)
|
||||
}
|
||||
return super.searchMangaParse(response)
|
||||
}
|
||||
|
||||
// ============================== Details ===============================
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
title = document.selectFirst("h1")!!.text()
|
||||
description = document.selectFirst(".excerpt p")?.text()
|
||||
document.selectFirst(".details-right-con img")?.let { thumbnail_url = imageFromElement(it) }
|
||||
genre = document.select("div.meta-item span.meta-title:contains(Genres) + span a")
|
||||
.joinToString { it.text() }
|
||||
author = document.selectFirst("div.meta-item span.meta-title:contains(Author) + span a")
|
||||
?.text()
|
||||
document.selectFirst(".status")?.text()?.let {
|
||||
status = it.toMangaStatus()
|
||||
}
|
||||
|
||||
setUrlWithoutDomain(document.location())
|
||||
}
|
||||
|
||||
protected open fun imageFromElement(element: Element): String? {
|
||||
return when {
|
||||
element.hasAttr("data-src") -> element.attr("abs:data-src")
|
||||
element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src")
|
||||
element.hasAttr("srcset") -> element.attr("abs:srcset").getSrcSetImage()
|
||||
element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc")
|
||||
else -> element.attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun String.getSrcSetImage(): String? {
|
||||
return this.split(" ")
|
||||
.filter(URL_REGEX::matches)
|
||||
.maxOfOrNull(String::toString)
|
||||
}
|
||||
|
||||
protected val completedStatusList: Array<String> = arrayOf(
|
||||
"Finished",
|
||||
"Completo",
|
||||
)
|
||||
|
||||
protected open val ongoingStatusList: Array<String> = arrayOf(
|
||||
"Publishing",
|
||||
"Ativo",
|
||||
)
|
||||
|
||||
protected val hiatusStatusList: Array<String> = arrayOf(
|
||||
"on hiatus",
|
||||
)
|
||||
|
||||
protected val canceledStatusList: Array<String> = arrayOf(
|
||||
"Canceled",
|
||||
"Discontinued",
|
||||
)
|
||||
|
||||
open fun String.toMangaStatus(): Int {
|
||||
return when {
|
||||
containsIn(completedStatusList) -> SManga.COMPLETED
|
||||
containsIn(ongoingStatusList) -> SManga.ONGOING
|
||||
containsIn(hiatusStatusList) -> SManga.ON_HIATUS
|
||||
containsIn(canceledStatusList) -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Chapters ============================
|
||||
|
||||
override fun chapterListSelector() = ".chapter-list li a"
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
name = element.selectFirst(".title")!!.text()
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
}
|
||||
|
||||
// ============================== Pages ===============================
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(".chapter-images .chapter-item > img").mapIndexed { index, element ->
|
||||
Page(index, imageUrl = imageFromElement(element))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
// ============================= Filters ==============================
|
||||
|
||||
private var filterList = emptyList<Pair<String, List<Tag>>>()
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
val filters = mutableListOf<Filter<*>>()
|
||||
|
||||
filters += if (filterList.isNotEmpty()) {
|
||||
filterList.map { SelectionList(it.first, it.second) }
|
||||
} else {
|
||||
listOf(Filter.Header("Aperte 'Redefinir' para tentar mostrar os filtros"))
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
protected open fun parseSelection(document: Document, selector: String): Pair<String, List<Tag>>? {
|
||||
val selectorFilter = "#filter-form $selector .select-item-head .text"
|
||||
return document.selectFirst(selectorFilter)?.text()?.let { displayName ->
|
||||
displayName to document.select("#filter-form $selector li").map { element ->
|
||||
element.selectFirst("input")!!.let { input ->
|
||||
Tag(
|
||||
name = element.selectFirst(".text")!!.text(),
|
||||
value = input.attr("value"),
|
||||
query = input.attr("name"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open val filterListSelector: List<String> = listOf(
|
||||
".filter-genre",
|
||||
".filter-status",
|
||||
".filter-type",
|
||||
".filter-year",
|
||||
".filter-sort",
|
||||
)
|
||||
|
||||
open fun filterParse(response: Response) {
|
||||
val document = Jsoup.parseBodyFragment(response.peekBody(Long.MAX_VALUE).string())
|
||||
filterList = filterListSelector.mapNotNull { selector -> parseSelection(document, selector) }
|
||||
}
|
||||
|
||||
protected data class Tag(val name: String = "", val value: String = "", val query: String = "")
|
||||
|
||||
private open class SelectionList(displayName: String, private val vals: List<Tag>, state: Int = 0) :
|
||||
Filter.Select<String>(displayName, vals.map { it.name }.toTypedArray(), state) {
|
||||
fun selected() = vals[state]
|
||||
}
|
||||
|
||||
// ============================= Utils ==============================
|
||||
|
||||
private fun String.containsIn(array: Array<String>): Boolean {
|
||||
return this.lowercase() in array.map { it.lowercase() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREFIX_SEARCH = "id:"
|
||||
val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)${'$'}""".toRegex()
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.en.ninehentai
|
||||
package eu.kanade.tachiyomi.multisrc.etoshore
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
|
@ -7,29 +7,28 @@ import android.os.Bundle
|
|||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Springboard that accepts https://9hentai.com/g/xxxxxx intents and redirects them to
|
||||
* the main Tachiyomi process.
|
||||
*/
|
||||
class NineHentaiUrlActivity : Activity() {
|
||||
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 id = pathSegments[1]
|
||||
val item = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "id:$id")
|
||||
putExtra("query", "${Etoshore.PREFIX_SEARCH}$item")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("NineHentaiUrlActivity", e.toString())
|
||||
Log.e(tag, e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("NineHentaiUrlActivity", "could not parse uri from intent $intent")
|
||||
Log.e(tag, "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
|
@ -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 * -1)
|
||||
add(Calendar.MINUTE, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"hour", "giờ", "hora", "saat" -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
add(Calendar.HOUR_OF_DAY, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"day", "ngày", "día", "gün" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * -1)
|
||||
add(Calendar.DATE, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"week", "tuần", "semana", "hafta" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
add(Calendar.DATE, -value * 7)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"month", "tháng", "mes", "ay" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, value * -1)
|
||||
add(Calendar.MONTH, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"year", "năm", "año", "yıl" -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, value * -1)
|
||||
add(Calendar.YEAR, -value)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
|
|
@ -647,6 +647,7 @@ abstract class GalleryAdults(
|
|||
"p" -> "png"
|
||||
"b" -> "bmp"
|
||||
"g" -> "gif"
|
||||
"w" -> "webp"
|
||||
else -> "jpg"
|
||||
}
|
||||
val idx = image.key.toInt()
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
||||
baseVersionCode = 6
|
||||
|
|
|
@ -22,7 +22,6 @@ 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
|
||||
|
@ -137,19 +136,22 @@ abstract class GigaViewer(
|
|||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
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 aggregateId = document.selectFirst("script.js-valve")!!.attr("data-giga_series")
|
||||
|
||||
val newHeaders = headers.newBuilder()
|
||||
.set("Referer", response.request.url.toString())
|
||||
.build()
|
||||
var readMoreEndpoint = firstListEndpoint.newBuilder()
|
||||
.setQueryParameter("number_since", numberSince.toString())
|
||||
|
||||
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()
|
||||
.toString()
|
||||
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 24
|
||||
baseVersionCode = 26
|
||||
|
|
|
@ -65,7 +65,8 @@ abstract class GroupLe(
|
|||
}
|
||||
.build()
|
||||
|
||||
private var uagent: String = preferences.getString(UAGENT_TITLE, UAGENT_DEFAULT)!!
|
||||
private var uagent = preferences.getString(UAGENT_TITLE, UAGENT_DEFAULT)!!
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", uagent)
|
||||
add("Referer", baseUrl)
|
||||
|
@ -206,28 +207,44 @@ 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").isNullOrEmpty() && document.toString().contains("current_user_country_code = 'RU'")) || (document.select("img.logo").first()?.attr("title")?.contains("Allhentai") == true && document.select(".user-avatar").isNullOrEmpty())) {
|
||||
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()
|
||||
)
|
||||
) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, manga) }
|
||||
|
||||
val chapterSearchParams = getChapterSearchParams(document)
|
||||
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, manga, chapterSearchParams) }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() =
|
||||
"tr.item-row:has(td > a):has(td.date:not(.text-info))"
|
||||
|
||||
private fun chapterFromElement(element: Element, manga: SManga): SChapter {
|
||||
private fun chapterFromElement(element: Element, manga: SManga, chapterSearchParams: String): 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") + "?mtr=true") // mtr is 18+ fractional skip
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + chapterSearchParams)
|
||||
|
||||
val translatorElement = urlElement.attr("title")
|
||||
|
||||
chapter.scanlator = if (!translatorElement.isNullOrBlank()) {
|
||||
chapter.scanlator = if (translatorElement.isNotBlank()) {
|
||||
translatorElement
|
||||
.replace("(Переводчик),", "&")
|
||||
.removeSuffix(" (Переводчик)")
|
||||
|
@ -251,10 +268,14 @@ abstract class GroupLe(
|
|||
chapter.chapter_number = chapterInf.attr("data-num").toFloat() / 10
|
||||
|
||||
chapter.date_upload = element.select("td.d-none").last()?.text()?.let {
|
||||
try {
|
||||
SimpleDateFormat("dd.MM.yy", Locale.US).parse(it)?.time ?: 0L
|
||||
} catch (e: ParseException) {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it)?.time ?: 0L
|
||||
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
|
||||
}
|
||||
}
|
||||
} ?: 0
|
||||
return chapter
|
||||
|
@ -292,15 +313,15 @@ abstract class GroupLe(
|
|||
|
||||
val html = document.html()
|
||||
|
||||
var readerMark = "rm_h.readerDoInit(["
|
||||
|
||||
// allhentai necessary
|
||||
if (!html.contains(readerMark)) {
|
||||
readerMark = "rm_h.readerInit( 0,["
|
||||
}
|
||||
val readerMark = "rm_h.readerDoInit(["
|
||||
|
||||
if (!html.contains(readerMark)) {
|
||||
if (document.select(".input-lg").isNotEmpty() || (document.select(".user-avatar").isNullOrEmpty() && document.select("img.logo").first()?.attr("title")?.contains("Allhentai") == true)) {
|
||||
if (document.select(".input-lg").isNotEmpty() || (
|
||||
document.select(".user-avatar")
|
||||
.isEmpty() && document.select("img.logo").first()?.attr("title")
|
||||
?.contains("Allhentai") == true
|
||||
)
|
||||
) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
if (!response.request.url.toString().contains(baseUrl)) {
|
||||
|
|
|
@ -232,7 +232,7 @@ abstract class HentaiHand(
|
|||
val date = it.jsonObject["added_at"]!!.jsonPrimitive.content
|
||||
date_upload = if (date.contains("day")) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, date.filter { it.isDigit() }.toInt() * -1)
|
||||
add(Calendar.DATE, -date.filter { it.isDigit() }.toInt())
|
||||
}.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() * -1)
|
||||
add(Calendar.DATE, -date.filter { it.isDigit() }.toInt())
|
||||
}.timeInMillis
|
||||
} else {
|
||||
DATE_FORMAT.parse(obj.jsonObject["uploaded_at"]!!.jsonPrimitive.content)?.time ?: 0
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
||||
baseVersionCode = 5
|
||||
|
|
|
@ -96,10 +96,13 @@ class Chapter(
|
|||
private val createdBy: Name,
|
||||
private val createdAt: String,
|
||||
private val chapterStatus: String,
|
||||
private val isAccessible: Boolean,
|
||||
private val mangaPost: ChapterPostDetails,
|
||||
) {
|
||||
fun isPublic() = chapterStatus == "PUBLIC"
|
||||
|
||||
fun isAccessible() = isAccessible
|
||||
|
||||
fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
|
||||
val seriesSlug = mangaSlug ?: mangaPost.slug
|
||||
url = "/series/$seriesSlug/$slug#$id"
|
||||
|
|
|
@ -128,7 +128,7 @@ abstract class Iken(
|
|||
assert(!data.post.isNovel) { "Novels are unsupported" }
|
||||
|
||||
return data.post.chapters
|
||||
.filter { it.isPublic() }
|
||||
.filter { it.isPublic() && it.isAccessible() }
|
||||
.map { it.toSChapter(data.post.slug) }
|
||||
}
|
||||
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 14
|
||||
baseVersionCode = 16
|
||||
|
|
|
@ -233,8 +233,8 @@ open class Kemono(
|
|||
GET("$baseUrl/$apiPath${chapter.url}", headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val post: KemonoPostDto = response.parseAs()
|
||||
return post.images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) }
|
||||
val postData: KemonoPostDtoWrapped = response.parseAs()
|
||||
return postData.post.images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) }
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
|
|
|
@ -51,6 +51,11 @@ class KemonoCreatorDto(
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class KemonoPostDtoWrapped(
|
||||
val post: KemonoPostDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class KemonoPostDto(
|
||||
private val id: String,
|
||||
|
@ -65,7 +70,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()) {
|
||||
|
@ -101,8 +106,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(val name: String, val path: String) {
|
||||
override fun toString() = "$path?f=$name"
|
||||
class KemonoAttachmentDto(var name: String? = null, val path: String) {
|
||||
override fun toString() = path + if (name != null) "?f=$name" else ""
|
||||
}
|
||||
|
||||
private fun getApiDateFormat() =
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
pref_show_paid_chapter_title=عرض الفصول المدفوعة
|
||||
pref_show_paid_chapter_summary_on=سيتم عرض الفصول المدفوعة
|
||||
pref_show_paid_chapter_summary_off=سيتم عرض الفصول المجانية فقط.
|
||||
chapter_page_url_not_found=رابط الصفحة غير موجود
|
|
@ -1,3 +1,4 @@
|
|||
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
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
pref_show_paid_chapter_title=Afficher les chapitres payants
|
||||
pref_show_paid_chapter_summary_on=Les chapitres payants apparaitront.
|
||||
pref_show_paid_chapter_summary_off=Seuls les chapitres gratuits apparaitront.
|
||||
chapter_page_url_not_found=Page URL non trouvée
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 8
|
||||
baseVersionCode = 9
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
|
|
@ -18,7 +18,6 @@ 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
|
||||
|
@ -55,7 +54,7 @@ abstract class Keyoapp(
|
|||
protected val intl = Intl(
|
||||
language = lang,
|
||||
baseLanguage = "en",
|
||||
availableLanguages = setOf("en"),
|
||||
availableLanguages = setOf("ar", "en", "fr"),
|
||||
classLoader = this::class.java.classLoader!!,
|
||||
)
|
||||
|
||||
|
@ -259,9 +258,11 @@ 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")
|
||||
}
|
||||
|
@ -277,7 +278,16 @@ abstract class Keyoapp(
|
|||
}
|
||||
}
|
||||
|
||||
protected open val cdnUrl = "https://2xffbs-cn8.is1.buzz/uploads"
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
private val oldImgCdnRegex = Regex("""^(https?:)?//cdn\d*\.keyoapp\.com""")
|
||||
|
||||
|
@ -297,12 +307,7 @@ abstract class Keyoapp(
|
|||
|
||||
protected open fun Element.getImageUrl(selector: String): String? {
|
||||
return this.selectFirst(selector)?.let { element ->
|
||||
element.attr("style")
|
||||
.substringAfter(":url(", "")
|
||||
.substringBefore(")", "")
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.toHttpUrlOrNull()?.newBuilder()?.setQueryParameter("w", "480")?.build()
|
||||
?.toString()
|
||||
IMG_REGEX.find(element.attr("style"))?.groups?.get("url")?.value
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -360,5 +365,8 @@ abstract class Keyoapp(
|
|||
companion object {
|
||||
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
|
||||
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
|
||||
val CDN_HOST_REGEX = """realUrl\s*=\s*`[^`]+//(?<host>[^/]+)""".toRegex()
|
||||
val CDN_CLEAN_REGEX = """\$\{[^}]*\}""".toRegex()
|
||||
val IMG_REGEX = """url\(['"]?(?<url>[^(['"\)])]+)""".toRegex()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 36
|
||||
baseVersionCode = 37
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:cryptoaes"))
|
||||
|
|
|
@ -767,12 +767,21 @@ 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").substringBefore(" ")
|
||||
element.hasAttr("srcset") -> element.attr("abs:srcset").getSrcSetImage()
|
||||
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.
|
||||
|
@ -1106,6 +1115,7 @@ abstract class Madara(
|
|||
|
||||
companion object {
|
||||
const val URL_SEARCH_PREFIX = "slug:"
|
||||
val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)${'$'}""".toRegex()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -221,9 +221,9 @@ abstract class MangaBox(
|
|||
val value = date.split(' ')[0].toIntOrNull()
|
||||
val cal = Calendar.getInstance()
|
||||
when {
|
||||
value != null && "min" in date -> cal.apply { add(Calendar.MINUTE, value * -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) }
|
||||
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) }
|
||||
else -> null
|
||||
}?.timeInMillis
|
||||
} else {
|
||||
|
|
|
@ -7,6 +7,7 @@ sort_by_filter_views=Views
|
|||
sort_by_filter_updated=Updated
|
||||
sort_by_filter_added=Added
|
||||
status_filter_title=Status
|
||||
status_filter_all=All
|
||||
status_filter_ongoing=Ongoing
|
||||
status_filter_hiatus=Hiatus
|
||||
status_filter_dropped=Dropped
|
||||
|
|
|
@ -7,6 +7,7 @@ sort_by_filter_views=Vistas
|
|||
sort_by_filter_updated=Actualización
|
||||
sort_by_filter_added=Agregado
|
||||
status_filter_title=Estado
|
||||
status_filter_all=Todos
|
||||
status_filter_ongoing=En curso
|
||||
status_filter_hiatus=En pausa
|
||||
status_filter_dropped=Abandonado
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
|
|
@ -127,7 +127,9 @@ abstract class MangaEsp(
|
|||
val statusFilter = filterList.firstInstanceOrNull<StatusFilter>()
|
||||
|
||||
if (statusFilter != null) {
|
||||
filteredList = filteredList.filter { it.status == statusFilter.toUriPart() }.toMutableList()
|
||||
if (statusFilter.toUriPart() != 0) {
|
||||
filteredList = filteredList.filter { it.status == statusFilter.toUriPart() }.toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
val sortByFilter = filterList.firstInstanceOrNull<SortByFilter>()
|
||||
|
@ -216,6 +218,7 @@ 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),
|
||||
|
@ -246,7 +249,7 @@ abstract class MangaEsp(
|
|||
companion object {
|
||||
private val UNESCAPE_REGEX = """\\(.)""".toRegex()
|
||||
val MANGA_LIST_REGEX = """self\.__next_f\.push\(.*data\\":(\[.*trending.*])\}""".toRegex()
|
||||
private val MANGA_DETAILS_REGEX = """self\.__next_f\.push\(.*data\\":(\{.*lastChapters.*\}).*\\"numFollow""".toRegex()
|
||||
val MANGA_DETAILS_REGEX = """self\.__next_f\.push\(.*data\\":(\{.*lastChapters.*\}).*\\"numFollow""".toRegex()
|
||||
const val MANGAS_PER_PAGE = 15
|
||||
}
|
||||
}
|
||||
|
|
|
@ -292,7 +292,7 @@ abstract class MangaThemesia(
|
|||
listOf("canceled", "cancelled", "cancelado", "cancellato", "cancelados", "dropped", "discontinued", "abandonné")
|
||||
.any { this.contains(it, ignoreCase = true) } -> SManga.CANCELLED
|
||||
|
||||
listOf("hiatus", "on hold", "pausado", "en espera", "en pause", "en attente")
|
||||
listOf("hiatus", "on hold", "pausado", "en espera", "en pause", "en attente", "hiato")
|
||||
.any { this.contains(it, ignoreCase = true) } -> SManga.ON_HIATUS
|
||||
|
||||
else -> SManga.UNKNOWN
|
||||
|
|
|
@ -5,5 +5,5 @@ plugins {
|
|||
baseVersionCode = 9
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
||||
implementation(project(":lib:zipinterceptor"))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
package eu.kanade.tachiyomi.multisrc.peachscan
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.lib.zipinterceptor.ZipInterceptor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
@ -21,23 +16,16 @@ 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(
|
||||
|
@ -53,7 +41,7 @@ abstract class PeachScan(
|
|||
|
||||
override val client = network.cloudflareClient
|
||||
.newBuilder()
|
||||
.addInterceptor(::zipImageInterceptor)
|
||||
.addInterceptor(ZipInterceptor()::zipImageInterceptor)
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
@ -192,90 +180,6 @@ 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:"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.es.vcpvmp
|
||||
package eu.kanade.tachiyomi.multisrc.vercomics
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
@ -8,45 +8,79 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
open class VCPVMP(override val name: String, override val baseUrl: String) : ParsedHttpSource() {
|
||||
|
||||
override val lang = "es"
|
||||
abstract class VerComics(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest: Boolean = false
|
||||
|
||||
override fun headersBuilder(): Headers.Builder {
|
||||
return Headers.Builder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
}
|
||||
protected open val urlSuffix = ""
|
||||
protected open val genreSuffix = ""
|
||||
protected open val useSuffixOnSearch = true
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$urlSuffix/page/$page", headers)
|
||||
|
||||
override fun popularMangaSelector() = "div.blog-list-items > div.entry"
|
||||
override fun popularMangaSelector() = "header:has(h1) ~ * .entry"
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.wp-pagenavi > span.current + a"
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
element.select("a.popimg").first()!!.let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.select("img").attr("alt")
|
||||
thumbnail_url = it.select("img:not(noscript img)").attr("abs:data-src")
|
||||
thumbnail_url = it.selectFirst("img:not(noscript img)")?.imgAttr()
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.wp-pagenavi > span.current + a"
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
var url = baseUrl.toHttpUrl().newBuilder()
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
url = baseUrl.toHttpUrl().newBuilder()
|
||||
if (useSuffixOnSearch) {
|
||||
url.addPathSegments(urlSuffix)
|
||||
}
|
||||
url.addPathSegments("page")
|
||||
url.addPathSegments(page.toString())
|
||||
url.addQueryParameter("s", query)
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is Genre -> {
|
||||
if (filter.toUriPart().isNotEmpty()) {
|
||||
url.addPathSegments(genreSuffix)
|
||||
url.addPathSegments(filter.toUriPart())
|
||||
|
||||
url.addPathSegments("page")
|
||||
url.addPathSegments(page.toString())
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
document.select("div.tax_post").let {
|
||||
|
@ -81,50 +115,13 @@ open class VCPVMP(override val name: String, override val baseUrl: String) : Par
|
|||
override fun chapterListSelector() = throw UnsupportedOperationException()
|
||||
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
protected open val pageListSelector = "div.wp-content p > img:not(noscript img)"
|
||||
protected open val pageListSelector =
|
||||
"div.wp-content p > img:not(noscript img), " +
|
||||
"div.wp-content div#lector > img:not(noscript img), " +
|
||||
"div.wp-content > figure img:not(noscript img)"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> = document.select(pageListSelector)
|
||||
.mapIndexed { i, img -> Page(i, "", img.attr("abs:data-src")) }
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
protected open val urlSuffix = ""
|
||||
protected open val genreSuffix = ""
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
var url = baseUrl.toHttpUrl().newBuilder()
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
url = "$baseUrl/$urlSuffix".toHttpUrl().newBuilder()
|
||||
url.addPathSegments("page")
|
||||
url.addPathSegments(page.toString())
|
||||
url.addQueryParameter("s", query)
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is Genre -> {
|
||||
if (filter.toUriPart().isNotEmpty()) {
|
||||
url.addPathSegments(genreSuffix)
|
||||
url.addPathSegments(filter.toUriPart())
|
||||
|
||||
url.addPathSegments("page")
|
||||
url.addPathSegments(page.toString())
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
.mapIndexed { i, img -> Page(i, imageUrl = img.imgAttr()) }
|
||||
|
||||
protected open var genres = arrayOf(Pair("Ver todos", ""))
|
||||
|
||||
|
@ -138,11 +135,45 @@ open class VCPVMP(override val name: String, override val baseUrl: String) : Par
|
|||
return FilterList(filters)
|
||||
}
|
||||
|
||||
// Array.from(document.querySelectorAll('div.tagcloud a.tag-cloud-link')).map(a => `Pair("${a.innerText}", "${a.href.replace('https://vercomicsporno.com/etiquetas/', '')}")`).join(',\n')
|
||||
// from https://vercomicsporno.com/
|
||||
protected open fun Element.imgAttr(): String? {
|
||||
return when {
|
||||
this.hasAttr("data-src") -> this.attr("abs:data-src")
|
||||
this.hasAttr("data-lazy-src") -> this.attr("abs:data-lazy-src")
|
||||
this.hasAttr("srcset") -> this.attr("abs:srcset").getSrcSetImage()
|
||||
this.hasAttr("data-cfsrc") -> this.attr("abs:data-cfsrc")
|
||||
else -> this.attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
private class Genre(genres: Array<Pair<String, String>>) : UriPartFilter(
|
||||
private fun String.getSrcSetImage(): String? {
|
||||
return this.split(" ")
|
||||
.filter(URL_REGEX::matches)
|
||||
.maxOfOrNull(String::toString)
|
||||
}
|
||||
|
||||
// Replace the baseUrl and genreSuffix in the following string
|
||||
// Array.from(document.querySelectorAll('div.tagcloud a.tag-cloud-link')).map(a => `Pair("${a.innerText}", "${a.href.replace('$baseUrl/genreSuffix/', '')}")`).join(',\n')
|
||||
class Genre(genres: Array<Pair<String, String>>) : UriPartFilter(
|
||||
"Filtrar por género",
|
||||
genres,
|
||||
)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
companion object {
|
||||
val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)${'$'}""".toRegex()
|
||||
}
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
}
|
|
@ -152,25 +152,25 @@ abstract class Zbulu(
|
|||
val value = date.split(' ')[0].toInt()
|
||||
when {
|
||||
"second" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.SECOND, value * -1)
|
||||
add(Calendar.SECOND, -value)
|
||||
}.timeInMillis
|
||||
"minute" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
add(Calendar.MINUTE, -value)
|
||||
}.timeInMillis
|
||||
"hour" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
add(Calendar.HOUR_OF_DAY, -value)
|
||||
}.timeInMillis
|
||||
"day" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * -1)
|
||||
add(Calendar.DATE, -value)
|
||||
}.timeInMillis
|
||||
"week" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
add(Calendar.DATE, -value * 7)
|
||||
}.timeInMillis
|
||||
"month" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, value * -1)
|
||||
add(Calendar.MONTH, -value)
|
||||
}.timeInMillis
|
||||
"year" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, value * -1)
|
||||
add(Calendar.YEAR, -value)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
0L
|
||||
|
|
|
@ -5,6 +5,7 @@ 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
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id("lib-android")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
||||
}
|
|
@ -1,26 +1,29 @@
|
|||
package eu.kanade.tachiyomi.multisrc.peachscan
|
||||
package eu.kanade.tachiyomi.lib.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 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
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
object ImageDecoderWrapper {
|
||||
private var decodeMethod: Method
|
||||
private var newInstanceMethod: Method
|
||||
|
||||
private var classSignature = ClassSignature.Newest
|
||||
|
||||
private enum class ClassSignature {
|
||||
|
@ -35,7 +38,6 @@ object PeachScanUtils {
|
|||
val inputStreamClass = InputStream::class.java
|
||||
|
||||
try {
|
||||
// Mihon Preview r6595+
|
||||
classSignature = ClassSignature.Newest
|
||||
|
||||
// decode(region, sampleSize)
|
||||
|
@ -54,7 +56,6 @@ object PeachScanUtils {
|
|||
)
|
||||
} catch (_: NoSuchMethodException) {
|
||||
try {
|
||||
// Mihon Stable & forks
|
||||
classSignature = ClassSignature.New
|
||||
|
||||
// decode(region, rgb565, sampleSize, applyColorManagement, displayProfile)
|
||||
|
@ -74,7 +75,6 @@ object PeachScanUtils {
|
|||
booleanClass,
|
||||
)
|
||||
} catch (_: NoSuchMethodException) {
|
||||
// Tachiyomi J2k
|
||||
classSignature = ClassSignature.Old
|
||||
|
||||
// decode(region, rgb565, sampleSize)
|
||||
|
@ -122,3 +122,97 @@ object PeachScanUtils {
|
|||
return bitmap
|
||||
}
|
||||
}
|
||||
|
||||
open class ZipInterceptor {
|
||||
private val dataUriRegex = Regex("""base64,([0-9a-zA-Z/+=\s]+)""")
|
||||
|
||||
open fun zipGetByteStream(request: Request, response: Response): InputStream {
|
||||
return response.body.byteStream()
|
||||
}
|
||||
|
||||
open fun requestIsZipImage(request: Request): Boolean {
|
||||
return request.url.fragment == "page" && request.url.pathSegments.last().contains(".zip")
|
||||
}
|
||||
|
||||
fun zipImageInterceptor(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
val filename = request.url.pathSegments.last()
|
||||
|
||||
if (requestIsZipImage(request).not()) {
|
||||
return response
|
||||
}
|
||||
|
||||
val zis = ZipInputStream(zipGetByteStream(request, response))
|
||||
|
||||
val images = generateSequence { zis.nextEntry }
|
||||
.mapNotNull {
|
||||
val entryName = it.name
|
||||
val splitEntryName = entryName.split('.')
|
||||
val entryIndex = splitEntryName.first().toInt()
|
||||
val entryType = splitEntryName.last()
|
||||
|
||||
val imageData = if (entryType == "avif" || splitEntryName.size == 1) {
|
||||
zis.readBytes()
|
||||
} else {
|
||||
val svgBytes = zis.readBytes()
|
||||
val svgContent = svgBytes.toString(Charsets.UTF_8)
|
||||
val b64 = dataUriRegex.find(svgContent)?.groupValues?.get(1)
|
||||
?: return@mapNotNull null
|
||||
|
||||
Base64.decode(b64, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
entryIndex to ImageDecoderWrapper.decodeImage(imageData, isLowRamDevice, filename, entryName)
|
||||
}
|
||||
.sortedBy { it.first }
|
||||
.toList()
|
||||
|
||||
zis.closeEntry()
|
||||
zis.close()
|
||||
|
||||
val totalWidth = images.maxOf { it.second.width }
|
||||
val totalHeight = images.sumOf { it.second.height }
|
||||
|
||||
val result = Bitmap.createBitmap(totalWidth, totalHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(result)
|
||||
|
||||
var dy = 0
|
||||
|
||||
images.forEach {
|
||||
val srcRect = Rect(0, 0, it.second.width, it.second.height)
|
||||
val dstRect = Rect(0, dy, it.second.width, dy + it.second.height)
|
||||
|
||||
canvas.drawBitmap(it.second, srcRect, dstRect, null)
|
||||
|
||||
dy += it.second.height
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
|
||||
|
||||
val image = output.toByteArray()
|
||||
val body = image.toResponseBody("image/jpeg".toMediaType())
|
||||
|
||||
return response.newBuilder()
|
||||
.body(body)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* ActivityManager#isLowRamDevice is based on a system property, which isn't
|
||||
* necessarily trustworthy. 1GB is supposedly the regular threshold.
|
||||
*
|
||||
* Instead, we consider anything with less than 3GB of RAM as low memory
|
||||
* considering how heavy image processing can be.
|
||||
*/
|
||||
private val isLowRamDevice by lazy {
|
||||
val ctx = Injekt.get<Application>()
|
||||
val activityManager = ctx.getSystemService("activity") as ActivityManager
|
||||
val memInfo = ActivityManager.MemoryInfo()
|
||||
|
||||
activityManager.getMemoryInfo(memInfo)
|
||||
|
||||
memInfo.totalMem < 3L * 1024 * 1024 * 1024
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Bato.to'
|
||||
extClass = '.BatoToFactory'
|
||||
extVersionCode = 41
|
||||
extVersionCode = 43
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ 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
|
||||
|
@ -116,7 +117,7 @@ open class BatoTo(
|
|||
.build()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page")
|
||||
return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector(): String {
|
||||
|
@ -140,7 +141,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")
|
||||
return GET("$baseUrl/browse?langs=$siteLang&sort=views_a&page=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = latestUpdatesSelector()
|
||||
|
@ -323,7 +324,7 @@ open class BatoTo(
|
|||
return super.mangaDetailsRequest(manga)
|
||||
}
|
||||
private var titleRegex: Regex =
|
||||
Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|/.+?)\\s*|([|/~].*)|-.*-")
|
||||
Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|《[^》]*》|⌜.+?⌝|⟨[^⟩]*⟩|/.+)")
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div#mainer div.container-fluid")
|
||||
|
@ -362,44 +363,55 @@ open class BatoTo(
|
|||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
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> {
|
||||
private fun altChapterParse(response: Response): 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().substringAfter(title).trim()
|
||||
date_upload = SimpleDateFormat("E, dd MMM yyyy H:m:s Z", Locale.US).parse(item.selectFirst("pubDate")!!.text())?.time ?: 0L
|
||||
name = item.selectFirst("title")!!.text()
|
||||
date_upload = parseAltChapterDate(item.selectFirst("pubDate")!!.text())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if (manga.url.startsWith("http")) {
|
||||
return GET(manga.url, headers)
|
||||
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)
|
||||
}
|
||||
return super.chapterListRequest(manga)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
if (getAltChapterListPref()) {
|
||||
return altChapterParse(response)
|
||||
}
|
||||
|
||||
val document = response.asJsoup()
|
||||
|
||||
if (checkChapterLists(document)) {
|
||||
throw Exception("Deleted from site")
|
||||
}
|
||||
|
||||
return document.select(chapterListSelector())
|
||||
.map(::chapterFromElement)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.main div.p-2"
|
||||
|
@ -428,46 +440,46 @@ open class BatoTo(
|
|||
|
||||
return when {
|
||||
"secs" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.SECOND, value * -1)
|
||||
add(Calendar.SECOND, -value)
|
||||
}.timeInMillis
|
||||
"mins" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
add(Calendar.MINUTE, -value)
|
||||
}.timeInMillis
|
||||
"hours" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
add(Calendar.HOUR_OF_DAY, -value)
|
||||
}.timeInMillis
|
||||
"days" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * -1)
|
||||
add(Calendar.DATE, -value)
|
||||
}.timeInMillis
|
||||
"weeks" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
add(Calendar.DATE, -value * 7)
|
||||
}.timeInMillis
|
||||
"months" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, value * -1)
|
||||
add(Calendar.MONTH, -value)
|
||||
}.timeInMillis
|
||||
"years" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, value * -1)
|
||||
add(Calendar.YEAR, -value)
|
||||
}.timeInMillis
|
||||
"sec" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.SECOND, value * -1)
|
||||
add(Calendar.SECOND, -value)
|
||||
}.timeInMillis
|
||||
"min" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
add(Calendar.MINUTE, -value)
|
||||
}.timeInMillis
|
||||
"hour" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
add(Calendar.HOUR_OF_DAY, -value)
|
||||
}.timeInMillis
|
||||
"day" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * -1)
|
||||
add(Calendar.DATE, -value)
|
||||
}.timeInMillis
|
||||
"week" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
add(Calendar.DATE, -value * 7)
|
||||
}.timeInMillis
|
||||
"month" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, value * -1)
|
||||
add(Calendar.MONTH, -value)
|
||||
}.timeInMillis
|
||||
"year" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, value * -1)
|
||||
add(Calendar.YEAR, -value)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
return 0
|
||||
|
|
|
@ -3,6 +3,9 @@ ignored_groups_summary=Chapters from these groups won't be shown.\nOne group nam
|
|||
include_tags_title=Include Tags
|
||||
include_tags_on=More specific, but might contain spoilers!
|
||||
include_tags_off=Only the broader genres
|
||||
group_tags_title=Group Tags (fork must support grouping)
|
||||
group_tags_on=Will prefix tags with their type
|
||||
group_tags_off=List all tags together
|
||||
update_cover_title=Update Covers
|
||||
update_cover_on=Keep cover updated
|
||||
update_cover_off=Prefer first cover
|
||||
|
|
|
@ -3,6 +3,9 @@ ignored_groups_summary=Capítulos desses grupos não aparecerão.\nUm grupo por
|
|||
include_tags_title=Incluir Tags
|
||||
include_tags_on=Mais detalhadas, mas podem conter spoilers
|
||||
include_tags_off=Apenas os gêneros básicos
|
||||
group_tags_title=Agrupar Tags (necessário fork compatível)
|
||||
group_tags_on=Prefixar tags com o respectivo tipo
|
||||
group_tags_off=Listar todas as tags juntas
|
||||
update_cover_title=Atualizar Capas
|
||||
update_cover_on=Manter capas atualizadas
|
||||
update_cover_off=Usar apenas a primeira capa
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Comick'
|
||||
extClass = '.ComickFactory'
|
||||
extVersionCode = 48
|
||||
extVersionCode = 50
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -97,6 +97,20 @@ 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"]
|
||||
|
@ -149,6 +163,9 @@ 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)
|
||||
|
||||
|
@ -379,22 +396,23 @@ 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().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() }
|
||||
}
|
||||
.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()) }
|
||||
return mangaData.toSManga(
|
||||
includeMuTags = preferences.includeMuTags,
|
||||
scorePosition = preferences.scorePosition,
|
||||
covers = covers,
|
||||
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol },
|
||||
groupTags = preferences.groupTags,
|
||||
)
|
||||
}
|
||||
return mangaData.toSManga(
|
||||
includeMuTags = preferences.includeMuTags,
|
||||
scorePosition = preferences.scorePosition,
|
||||
groupTags = preferences.groupTags,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -448,9 +466,10 @@ 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}"
|
||||
|
@ -513,6 +532,8 @@ abstract class Comick(
|
|||
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
||||
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
||||
const val INCLUDE_MU_TAGS_DEFAULT = false
|
||||
private const val GROUP_TAGS_PREF = "GroupTags"
|
||||
const val GROUP_TAGS_DEFAULT = false
|
||||
private const val MIGRATED_IGNORED_GROUPS = "MigratedIgnoredGroups"
|
||||
private const val FIRST_COVER_PREF = "DefaultCover"
|
||||
private const val FIRST_COVER_DEFAULT = true
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
|
@ -29,13 +30,14 @@ class Manga(
|
|||
val comic: Comic,
|
||||
private val artists: List<Name> = emptyList(),
|
||||
private val authors: List<Name> = emptyList(),
|
||||
private val genres: List<Name> = emptyList(),
|
||||
private val genres: List<Genre> = 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
|
||||
|
@ -75,19 +77,23 @@ class Manga(
|
|||
artist = artists.joinToString { it.name.trim() }
|
||||
author = authors.joinToString { it.name.trim() }
|
||||
genre = buildList {
|
||||
comic.origination?.let(::add)
|
||||
demographic?.let { add(Name(it)) }
|
||||
addAll(genres)
|
||||
addAll(comic.mdGenres.mapNotNull { it.name })
|
||||
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 })
|
||||
if (includeMuTags) {
|
||||
comic.muGenres.categories.forEach { category ->
|
||||
category?.category?.title?.let { add(Name(it)) }
|
||||
}
|
||||
addAll(
|
||||
comic.muGenres.categories.mapNotNull { it?.category?.title }.sorted()
|
||||
.map { Genre("Category", it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctBy { it.name }
|
||||
.filter { it.name.isNotBlank() }
|
||||
.joinToString { it.name.trim() }
|
||||
.filterNot { it.name.isNullOrBlank() || it.group.isNullOrBlank() }
|
||||
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,6 +112,7 @@ 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")
|
||||
|
@ -128,7 +135,13 @@ class Comic(
|
|||
|
||||
@Serializable
|
||||
class MdGenres(
|
||||
@SerialName("md_genres") val name: Name? = null,
|
||||
@SerialName("md_genres") val genre: Genre? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Genre(
|
||||
val group: String? = null,
|
||||
val name: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'E-Hentai'
|
||||
extClass = '.EHFactory'
|
||||
extVersionCode = 20
|
||||
extVersionCode = 22
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -4,17 +4,17 @@ import android.annotation.SuppressLint
|
|||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.webkit.CookieManager
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter.CheckBox
|
||||
import eu.kanade.tachiyomi.source.model.Filter.Group
|
||||
import eu.kanade.tachiyomi.source.model.Filter.Select
|
||||
import eu.kanade.tachiyomi.source.model.Filter.Text
|
||||
import eu.kanade.tachiyomi.source.model.Filter.TriState
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -39,13 +39,22 @@ 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)
|
||||
}
|
||||
|
||||
override val name = "E-Hentai"
|
||||
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() }
|
||||
private val memberId: String by lazy { getMemberIdPref() }
|
||||
private val passHash: String by lazy { getPassHashPref() }
|
||||
|
||||
override val baseUrl = "https://e-hentai.org"
|
||||
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 supportsLatest = true
|
||||
|
||||
|
@ -73,7 +82,7 @@ abstract class EHentai(
|
|||
val manga = mangaElements[i].let {
|
||||
SManga.create().apply {
|
||||
// Get title
|
||||
it.select("a")?.first()?.apply {
|
||||
it.selectFirst("a")?.apply {
|
||||
title = this.select(".glink").text()
|
||||
url = ExGalleryMetadata.normalizeUrl(attr("href"))
|
||||
if (i == mangaElements.lastIndex) {
|
||||
|
@ -131,9 +140,9 @@ abstract class EHentai(
|
|||
}
|
||||
|
||||
private fun parseChapterPage(response: Element) = with(response) {
|
||||
select(".gdtm a").map {
|
||||
Pair(it.child(0).attr("alt").toInt(), it.attr("href"))
|
||||
}.sortedBy(Pair<Int, String>::first).map { it.second }
|
||||
select("#gdt a").map {
|
||||
it.attr("href")
|
||||
}
|
||||
}
|
||||
|
||||
private fun chapterPageCall(np: String) = client.newCall(chapterPageRequest(np)).asObservableSuccess()
|
||||
|
@ -161,10 +170,23 @@ abstract class EHentai(
|
|||
query.isBlank() -> languageTag(enforceLanguageFilter)
|
||||
else -> languageTag(enforceLanguageFilter).let { if (it.isNotEmpty()) "$query,$it" else query }
|
||||
}
|
||||
modifiedQuery += filters.filterIsInstance<TagFilter>()
|
||||
.flatMap { it.markedTags() }
|
||||
.joinToString(",")
|
||||
.let { if (it.isNotEmpty()) ",$it" else it }
|
||||
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(" ", "+")}\""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
uri.appendQueryParameter("f_search", modifiedQuery)
|
||||
// when attempting to search with no genres selected, will auto select all genres
|
||||
filters.filterIsInstance<GenreGroup>().firstOrNull()?.state?.let {
|
||||
|
@ -352,6 +374,12 @@ 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)
|
||||
}
|
||||
|
||||
|
@ -388,12 +416,17 @@ abstract class EHentai(
|
|||
EnforceLanguageFilter(getEnforceLanguagePref()),
|
||||
Watched(),
|
||||
GenreGroup(),
|
||||
TagFilter("Misc Tags", triStateBoxesFrom(miscTags), "other"),
|
||||
TagFilter("Female Tags", triStateBoxesFrom(femaleTags), "female"),
|
||||
TagFilter("Male Tags", triStateBoxesFrom(maleTags), "male"),
|
||||
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"),
|
||||
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) {
|
||||
|
@ -487,17 +520,6 @@ 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")),
|
||||
|
@ -529,6 +551,16 @@ 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
|
||||
|
@ -545,8 +577,56 @@ abstract class EHentai(
|
|||
preferences.edit().putBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", checkValue).commit()
|
||||
}
|
||||
}
|
||||
|
||||
val memberIdPref = EditTextPreference(screen.context).apply {
|
||||
key = MEMBER_ID_PREF_KEY
|
||||
title = MEMBER_ID_PREF_TITLE
|
||||
summary = MEMBER_ID_PREF_SUMMARY
|
||||
|
||||
setDefaultValue(MEMBER_ID_PREF_DEFAULT_VALUE)
|
||||
}
|
||||
|
||||
val passHashPref = EditTextPreference(screen.context).apply {
|
||||
key = PASS_HASH_PREF_KEY
|
||||
title = PASS_HASH_PREF_TITLE
|
||||
summary = PASS_HASH_PREF_SUMMARY
|
||||
|
||||
setDefaultValue(PASS_HASH_PREF_DEFAULT_VALUE)
|
||||
}
|
||||
screen.addPreference(memberIdPref)
|
||||
screen.addPreference(passHashPref)
|
||||
screen.addPreference(enforceLanguagePref)
|
||||
}
|
||||
|
||||
private fun getEnforceLanguagePref(): Boolean = preferences.getBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
|
||||
|
||||
private fun getCookieValue(cookieTitle: String, defaultValue: String, prefKey: String): String {
|
||||
val cookies = webViewCookieManager.getCookie("https://forums.e-hentai.org")
|
||||
var value: String? = null
|
||||
|
||||
if (cookies != null) {
|
||||
val cookieArray = cookies.split("; ")
|
||||
for (cookie in cookieArray) {
|
||||
if (cookie.startsWith("$cookieTitle=")) {
|
||||
value = cookie.split("=")[1]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
value = preferences.getString(prefKey, defaultValue) ?: defaultValue
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private fun getPassHashPref(): String {
|
||||
return getCookieValue(PASS_HASH_PREF_TITLE, PASS_HASH_PREF_DEFAULT_VALUE, PASS_HASH_PREF_KEY)
|
||||
}
|
||||
|
||||
private fun getMemberIdPref(): String {
|
||||
return getCookieValue(MEMBER_ID_PREF_TITLE, MEMBER_ID_PREF_DEFAULT_VALUE, MEMBER_ID_PREF_KEY)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.EternalMangasFactory'
|
||||
themePkg = 'mangaesp'
|
||||
baseUrl = 'https://eternalmangas.com'
|
||||
overrideVersionCode = 0
|
||||
overrideVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -7,11 +7,17 @@ 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,
|
||||
|
@ -55,9 +61,62 @@ open class EternalMangas(
|
|||
return parseComicsList(page, query, filters)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
var document = response.asJsoup()
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
@ -67,12 +126,9 @@ open class EternalMangas(
|
|||
form.add(input.attr("name"), input.attr("value"))
|
||||
}
|
||||
|
||||
document = client.newCall(POST(action, headers, form.build())).execute().asJsoup()
|
||||
}
|
||||
|
||||
return document.select("main > img").mapIndexed { i, img ->
|
||||
Page(i, imageUrl = img.imgAttr())
|
||||
body = client.newCall(POST(action, headers, form.build())).execute().body.string()
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'EveriaClub (unoriginal)'
|
||||
extClass = '.EveriaClubCom'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 35 KiB |
|
@ -0,0 +1,152 @@
|
|||
package eu.kanade.tachiyomi.extension.all.everiaclubcom
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
class EveriaClubCom() : HttpSource() {
|
||||
override val baseUrl = "https://www.everiaclub.com"
|
||||
override val lang = "all"
|
||||
override val name = "EveriaClub (unoriginal)"
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
private val Element.imgSrc: String?
|
||||
get() = when {
|
||||
hasAttr("data-original") -> attr("data-original")
|
||||
hasAttr("data-lazy-src") -> attr("data-lazy-src")
|
||||
hasAttr("data-src") -> attr("data-src")
|
||||
hasAttr("src") -> attr("src")
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun mangaFromElement(it: Element) = SManga.create().apply {
|
||||
setUrlWithoutDomain(it.attr("abs:href").removePrefix(baseUrl))
|
||||
with(it.selectFirst("img")!!) {
|
||||
thumbnail_url = imgSrc
|
||||
title = attr("title")
|
||||
}
|
||||
}
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/?page=$page", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val mangas = document.select(".mainleft .leftp > a").map {
|
||||
mangaFromElement(it)
|
||||
}
|
||||
val isLastPage = document.selectFirst("li:has(span.current) + li > a")
|
||||
return MangasPage(mangas, isLastPage != null)
|
||||
}
|
||||
|
||||
// Popular
|
||||
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val mangas = document.select(".mainright li a").map {
|
||||
mangaFromElement(it)
|
||||
}
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
// Search
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val tagFilter = filters.filterIsInstance<TagFilter>().first()
|
||||
val categoryFilter = filters.filterIsInstance<CategoryFilter>().first()
|
||||
val url = when {
|
||||
tagFilter.state.isNotBlank() -> baseUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegment("tags")
|
||||
.addPathSegment(tagFilter.state)
|
||||
.addPathSegment(page.toString())
|
||||
categoryFilter.state != 0 -> "$baseUrl/${categoryFilter.toUriPart()}?page=$page".toHttpUrl().newBuilder()
|
||||
query.isNotBlank() -> baseUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegment("search")
|
||||
.addPathSegment("")
|
||||
.addQueryParameter("keyword", query)
|
||||
.addQueryParameter("page", page.toString())
|
||||
else -> "$baseUrl/?page=$page".toHttpUrl().newBuilder()
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
||||
|
||||
// Details
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
return SManga.create().apply {
|
||||
genre = document.select("div.end span:contains(Tags:) ~ a > p.tags").joinToString {
|
||||
it.ownText()
|
||||
}
|
||||
status = SManga.COMPLETED
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val chapter = SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Gallery"
|
||||
chapter_number = 1f
|
||||
date_upload = 0L
|
||||
}
|
||||
return Observable.just(listOf(chapter))
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val images = document.select(".mainleft img")
|
||||
return images.mapIndexed { index, image ->
|
||||
Page(index, imageUrl = image.imgSrc)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
// Filters
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
Filter.Header("NOTE: Only one filter will be applied!"),
|
||||
Filter.Separator(),
|
||||
TagFilter(),
|
||||
CategoryFilter(),
|
||||
)
|
||||
|
||||
open class UriPartFilter(
|
||||
displayName: String,
|
||||
private val valuePair: Array<Pair<String, String>>,
|
||||
) : Filter.Select<String>(displayName, valuePair.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = valuePair[state].second
|
||||
}
|
||||
|
||||
class CategoryFilter : UriPartFilter(
|
||||
"Category",
|
||||
arrayOf(
|
||||
Pair("Any", ""),
|
||||
Pair("Gravure", "Gravure.html"),
|
||||
Pair("Japan", "Japan.html"),
|
||||
Pair("Korea", "Korea.html"),
|
||||
Pair("Thailand", "Thailand.html"),
|
||||
Pair("Chinese", "Chinese.html"),
|
||||
Pair("Cosplay", "Cosplay.html"),
|
||||
),
|
||||
)
|
||||
|
||||
class TagFilter : Filter.Text("Tag")
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'FoamGirl'
|
||||
extClass = '.FoamGirl'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ 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
|
||||
|
@ -65,22 +67,43 @@ 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 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(),
|
||||
),
|
||||
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(),
|
||||
)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
private fun getPageListByDocument(document: Document): List<Page> {
|
||||
val pages = document.select("#image_div img").mapIndexed { index, element ->
|
||||
Page(index, imageUrl = element.absUrl("src"))
|
||||
}.toList()
|
||||
|
||||
val nextPageUrl = document.selectFirst(".page-numbers[title=Next]")
|
||||
?.absUrl("href")
|
||||
?.takeIf { HAS_NEXT_PAGE_REGEX in it }
|
||||
?: return pages
|
||||
|
||||
return client.newCall(GET(nextPageUrl, headers)).execute().asJsoup().let {
|
||||
pages + getPageListByDocument(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
|
@ -119,7 +142,10 @@ class FoamGirl() : ParsedHttpSource() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun String.isNumber() = isNotEmpty() && all { it.isDigit() }
|
||||
|
||||
companion object {
|
||||
val HAS_NEXT_PAGE_REGEX = """(\d+_\d+)""".toRegex()
|
||||
private val DATE_FORMAT by lazy {
|
||||
SimpleDateFormat("yyyy.M.d", Locale.ENGLISH)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Hennojin'
|
||||
extClass = '.HennojinFactory'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
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, suffix: String) : ParsedHttpSource() {
|
||||
override val baseUrl = "https://hennojin.com/home/$suffix"
|
||||
class Hennojin(override val lang: String) : ParsedHttpSource() {
|
||||
override val baseUrl = "https://hennojin.com"
|
||||
|
||||
override val name = "Hennojin"
|
||||
|
||||
// Popular is latest
|
||||
override val supportsLatest = false
|
||||
|
||||
private val httpUrl by lazy { baseUrl.toHttpUrl() }
|
||||
private val httpUrl by lazy { "$baseUrl/home".toHttpUrl() }
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
|
@ -41,15 +43,23 @@ class Hennojin(override val lang: String, suffix: String) : ParsedHttpSource() {
|
|||
override fun popularMangaNextPageSelector() = ".paginate .next"
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
httpUrl.request { addEncodedPathSegments("page/$page") }
|
||||
httpUrl.request {
|
||||
when (lang) {
|
||||
"ja" -> {
|
||||
addEncodedPathSegments("page/$page/")
|
||||
addQueryParameter("archive", "raw")
|
||||
}
|
||||
else -> addEncodedPathSegments("page/$page")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element) =
|
||||
SManga.create().apply {
|
||||
element.selectFirst(".title_link > a").let {
|
||||
title = it!!.text()
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
setUrlWithoutDomain(it.absUrl("href"))
|
||||
}
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
@ -66,46 +76,68 @@ class Hennojin(override val lang: String, suffix: 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.selectFirst(
|
||||
description = document.select(
|
||||
".manga-subtitle + p + p",
|
||||
)?.html()?.replace("<br> ", "\n")
|
||||
).joinToString("\n") {
|
||||
it
|
||||
.apply { select(Evaluator.Tag("br")).prepend("\\n") }
|
||||
.text()
|
||||
.replace("\\n", "\n")
|
||||
.replace("\n ", "\n")
|
||||
}.trim()
|
||||
genre = document.select(
|
||||
".tags-list a[href*=/parody/]," +
|
||||
".tags-list a[href*=/tags/]," +
|
||||
".tags-list a[href*=/character/]",
|
||||
)?.joinToString { it.text() }
|
||||
artist = document.select(
|
||||
).joinToString { it.text() }
|
||||
artist = document.selectFirst(
|
||||
".tags-list a[href*=/artist/]",
|
||||
)?.joinToString { it.text() }
|
||||
author = document.select(
|
||||
)?.text()
|
||||
author = document.selectFirst(
|
||||
".tags-list a[href*=/group/]",
|
||||
)?.joinToString { it.text() } ?: artist
|
||||
)?.text() ?: artist
|
||||
status = SManga.COMPLETED
|
||||
}
|
||||
|
||||
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 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 pageListParse(document: Document) =
|
||||
document.select(".slideshow-container > img")
|
||||
.mapIndexed { idx, img -> Page(idx, "", img.absUrl("src")) }
|
||||
.mapIndexed { idx, img -> Page(idx, imageUrl = img.absUrl("src")) }
|
||||
|
||||
private inline fun HttpUrl.request(
|
||||
block: HttpUrl.Builder.() -> HttpUrl.Builder,
|
||||
|
@ -114,9 +146,6 @@ class Hennojin(override val lang: String, suffix: String) : ParsedHttpSource() {
|
|||
private inline val Response.date: Long
|
||||
get() = headers["Last-Modified"]?.run(httpDate::parse)?.time ?: 0L
|
||||
|
||||
private inline val SManga.reader: String
|
||||
get() = "/home/manga-reader/?manga=$title&view=multi"
|
||||
|
||||
override fun chapterListSelector() =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import eu.kanade.tachiyomi.source.SourceFactory
|
|||
|
||||
class HennojinFactory : SourceFactory {
|
||||
override fun createSources() = listOf(
|
||||
Hennojin("en", ""),
|
||||
Hennojin("ja", "?archive=raw"),
|
||||
Hennojin("en"),
|
||||
Hennojin("ja"),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?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>
|
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'SchaleNetwork'
|
||||
extClass = '.KoharuFactory'
|
||||
extVersionCode = 11
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.en.koharu
|
||||
package eu.kanade.tachiyomi.extension.all.koharu
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
@ -28,17 +29,21 @@ import uy.kohesive.injekt.injectLazy
|
|||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Koharu : HttpSource(), ConfigurableSource {
|
||||
override val name = "Koharu"
|
||||
class Koharu(
|
||||
override val lang: String = "all",
|
||||
private val searchLang: String = "",
|
||||
) : HttpSource(), ConfigurableSource {
|
||||
|
||||
override val baseUrl = "https://koharu.to"
|
||||
override val name = "SchaleNetwork"
|
||||
|
||||
override val baseUrl = "https://schale.network"
|
||||
|
||||
override val id = if (lang == "en") 1484902275639232927 else super.id
|
||||
|
||||
private val apiUrl = baseUrl.replace("://", "://api.")
|
||||
|
||||
private val apiBooksUrl = "$apiUrl/books"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
|
@ -58,9 +63,25 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
|
||||
private fun remadd() = preferences.getBoolean(PREF_REM_ADD, false)
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.add("Origin", baseUrl)
|
||||
private fun getDomain(): String {
|
||||
try {
|
||||
val noRedirectClient = client.newBuilder().followRedirects(false).build()
|
||||
val host = noRedirectClient.newCall(GET(baseUrl, headers)).execute()
|
||||
.headers["Location"]?.toHttpUrlOrNull()?.host
|
||||
?: return baseUrl
|
||||
return "https://$host"
|
||||
} catch (_: Exception) {
|
||||
return baseUrl
|
||||
}
|
||||
}
|
||||
|
||||
private val lazyHeaders by lazy {
|
||||
val domain = getDomain()
|
||||
headersBuilder()
|
||||
.set("Referer", "$domain/")
|
||||
.set("Origin", domain)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getManga(book: Entry) = SManga.create().apply {
|
||||
setUrlWithoutDomain("${book.id}/${book.public_key}")
|
||||
|
@ -70,7 +91,6 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
|
||||
private fun getImagesByMangaEntry(entry: MangaEntry): Pair<ImagesInfo, String> {
|
||||
val data = entry.data
|
||||
val quality = 0
|
||||
fun getIPK(
|
||||
ori: DataKey?,
|
||||
alt1: DataKey?,
|
||||
|
@ -103,19 +123,19 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
else -> "0"
|
||||
}
|
||||
|
||||
val imagesResponse = client.newCall(GET("$apiBooksUrl/data/${entry.id}/${entry.public_key}/$id/$public_key?v=${entry.updated_at ?: entry.created_at}&w=$realQuality", headers)).execute()
|
||||
val imagesResponse = client.newCall(GET("$apiBooksUrl/data/${entry.id}/${entry.public_key}/$id/$public_key?v=${entry.updated_at ?: entry.created_at}&w=$realQuality", lazyHeaders)).execute()
|
||||
val images = imagesResponse.parseAs<ImagesInfo>() to realQuality
|
||||
return images
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$apiBooksUrl?page=$page", headers)
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$apiBooksUrl?page=$page" + if (searchLang.isNotBlank()) "&s=language!:\"$searchLang\"" else "", lazyHeaders)
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$apiBooksUrl?sort=8&page=$page", headers)
|
||||
override fun popularMangaRequest(page: Int) = GET("$apiBooksUrl?sort=8&page=$page" + if (searchLang.isNotBlank()) "&s=language!:\"$searchLang\"" else "", lazyHeaders)
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<Books>()
|
||||
|
||||
|
@ -130,7 +150,7 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
return when {
|
||||
query.startsWith(PREFIX_ID_KEY_SEARCH) -> {
|
||||
val ipk = query.removePrefix(PREFIX_ID_KEY_SEARCH)
|
||||
val response = client.newCall(GET("$apiBooksUrl/detail/$ipk", headers)).execute()
|
||||
val response = client.newCall(GET("$apiBooksUrl/detail/$ipk", lazyHeaders)).execute()
|
||||
Observable.just(searchMangaParse2(response))
|
||||
}
|
||||
else -> super.fetchSearchManga(page, query, filters)
|
||||
|
@ -141,6 +161,7 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
val url = apiBooksUrl.toHttpUrl().newBuilder().apply {
|
||||
val terms: MutableList<String> = mutableListOf()
|
||||
|
||||
if (lang != "all") terms += "language!:\"$searchLang\""
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> addQueryParameter("sort", filter.getValue())
|
||||
|
@ -156,7 +177,7 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
if (filter.state.isNotEmpty()) {
|
||||
val tags = filter.state.split(",").filter(String::isNotBlank).joinToString(",")
|
||||
if (tags.isNotBlank()) {
|
||||
terms += "${filter.type}!:" + '"' + tags + '"'
|
||||
terms += "${filter.type}!:" + if (filter.type == "pages") tags else '"' + tags + '"'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,7 +189,7 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
return GET(url, lazyHeaders)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
@ -190,7 +211,7 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
// Details
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET("$apiBooksUrl/detail/${manga.url}", headers)
|
||||
return GET("$apiBooksUrl/detail/${manga.url}", lazyHeaders)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
|
@ -291,7 +312,7 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
// Chapter
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return GET("$apiBooksUrl/detail/${manga.url}", headers)
|
||||
return GET("$apiBooksUrl/detail/${manga.url}", lazyHeaders)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
|
@ -310,7 +331,7 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
// Page List
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET("$apiBooksUrl/detail/${chapter.url}", headers)
|
||||
return GET("$apiBooksUrl/detail/${chapter.url}", lazyHeaders)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
|
@ -322,6 +343,10 @@ class Koharu : HttpSource(), ConfigurableSource {
|
|||
}
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, lazyHeaders)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
// Settings
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.en.koharu
|
||||
package eu.kanade.tachiyomi.extension.all.koharu
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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"),
|
||||
)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.en.koharu
|
||||
package eu.kanade.tachiyomi.extension.all.koharu
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.en.koharu
|
||||
package eu.kanade.tachiyomi.extension.all.koharu
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
|
@ -76,6 +76,8 @@ original_language=Original language
|
|||
original_language_filter_chinese=%s (Manhua)
|
||||
original_language_filter_japanese=%s (Manga)
|
||||
original_language_filter_korean=%s (Manhwa)
|
||||
prefer_title_in_extension_language=Use Alternate Titles
|
||||
prefer_title_in_extension_language_summary=If there is an alternate title available which matches the extension language, it will be used
|
||||
publication_demographic=Publication demographic
|
||||
publication_demographic_josei=Josei
|
||||
publication_demographic_none=None
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MangaDex'
|
||||
extClass = '.MangaDexFactory'
|
||||
extVersionCode = 194
|
||||
extVersionCode = 196
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -138,6 +138,11 @@ object MDConstants {
|
|||
return "${altTitlesInDescPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val preferExtensionLangTitlePref = "preferExtensionLangTitle"
|
||||
fun getPreferExtensionLangTitlePrefKey(dexLang: String): String {
|
||||
return "${preferExtensionLangTitlePref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val tagGroupContent = "content"
|
||||
private const val tagGroupFormat = "format"
|
||||
private const val tagGroupGenre = "genre"
|
||||
|
|
|
@ -113,7 +113,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
|||
.firstInstanceOrNull<CoverArtDto>()
|
||||
?.attributes?.fileName
|
||||
}
|
||||
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang)
|
||||
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang, preferences.preferExtensionLangTitle)
|
||||
}
|
||||
|
||||
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)
|
||||
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang, preferences.preferExtensionLangTitle)
|
||||
}
|
||||
|
||||
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)
|
||||
helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang, preferences.preferExtensionLangTitle)
|
||||
}
|
||||
|
||||
return mangaList
|
||||
|
@ -423,6 +423,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
|||
dexLang,
|
||||
preferences.coverQuality,
|
||||
preferences.altTitlesInDesc,
|
||||
preferences.preferExtensionLangTitle,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -757,11 +758,27 @@ 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)
|
||||
|
@ -840,6 +857,9 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
|||
private val SharedPreferences.altTitlesInDesc
|
||||
get() = getBoolean(MDConstants.getAltTitlesInDescPrefKey(dexLang), false)
|
||||
|
||||
private val SharedPreferences.preferExtensionLangTitle
|
||||
get() = getBoolean(MDConstants.getPreferExtensionLangTitlePrefKey(dexLang), true)
|
||||
|
||||
/**
|
||||
* Previous versions of the extension allowed invalid UUID values to be stored in the
|
||||
* preferences. This method clear invalid UUIDs in case the user have updated from
|
||||
|
|
|
@ -6,15 +6,19 @@ 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(),
|
||||
|
@ -30,8 +34,10 @@ class MangaDexFactory : SourceFactory {
|
|||
MangaDexHebrew(),
|
||||
MangaDexHindi(),
|
||||
MangaDexHungarian(),
|
||||
MangaDexIrish(),
|
||||
MangaDexIndonesian(),
|
||||
MangaDexItalian(),
|
||||
MangaDexJavanese(),
|
||||
MangaDexJapanese(),
|
||||
MangaDexKazakh(),
|
||||
MangaDexKorean(),
|
||||
|
@ -57,19 +63,25 @@ 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")
|
||||
|
@ -86,9 +98,11 @@ 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")
|
||||
|
@ -113,4 +127,6 @@ class MangaDexTelugu : MangaDex("te")
|
|||
class MangaDexThai : MangaDex("th")
|
||||
class MangaDexTurkish : MangaDex("tr")
|
||||
class MangaDexUkrainian : MangaDex("uk")
|
||||
class MangaDexUrdu : MangaDex("ur")
|
||||
class MangaDexUzbek : MangaDex("uz")
|
||||
class MangaDexVietnamese : MangaDex("vi")
|
||||
|
|
|
@ -275,6 +275,9 @@ 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.
|
||||
*/
|
||||
|
@ -283,15 +286,24 @@ 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
|
||||
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()
|
||||
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()
|
||||
|
||||
coverFileName?.let {
|
||||
thumbnail_url = when (!coverSuffix.isNullOrEmpty()) {
|
||||
|
@ -311,6 +323,7 @@ class MangaDexHelper(lang: String) {
|
|||
lang: String,
|
||||
coverSuffix: String?,
|
||||
altTitlesInDesc: Boolean,
|
||||
preferExtensionLangTitle: Boolean,
|
||||
): SManga {
|
||||
val attr = mangaDataDto.attributes!!
|
||||
|
||||
|
@ -370,7 +383,7 @@ class MangaDexHelper(lang: String) {
|
|||
}
|
||||
}
|
||||
|
||||
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply {
|
||||
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang, preferExtensionLangTitle).apply {
|
||||
description = desc
|
||||
author = authors.joinToString()
|
||||
artist = artists.joinToString()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MANGA Plus by SHUEISHA'
|
||||
extClass = '.MangaPlusFactory'
|
||||
extVersionCode = 53
|
||||
extVersionCode = 54
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -210,6 +210,7 @@ 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
|
||||
}
|
||||
}
|
||||
|
@ -246,6 +247,9 @@ enum class LabelCode {
|
|||
|
||||
@SerialName("WSJ")
|
||||
WEEKLY_SHOUNEN_JUMP,
|
||||
|
||||
@SerialName("UJ")
|
||||
ULTRA_JUMP,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.MangaReaderFactory'
|
||||
themePkg = 'mangareader'
|
||||
baseUrl = 'https://mangareader.to'
|
||||
overrideVersionCode = 3
|
||||
overrideVersionCode = 4
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -22,8 +22,11 @@ import org.jsoup.select.Evaluator
|
|||
import rx.Observable
|
||||
|
||||
open class MangaReader(
|
||||
override val lang: String,
|
||||
val language: Language,
|
||||
) : MangaReader() {
|
||||
|
||||
override val lang = language.code
|
||||
|
||||
override val name = "MangaReader"
|
||||
|
||||
override val baseUrl = "https://mangareader.to"
|
||||
|
@ -33,10 +36,10 @@ open class MangaReader(
|
|||
.build()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/filter?sort=latest-updated&language=$lang&page=$page", headers)
|
||||
GET("$baseUrl/filter?sort=latest-updated&language=${language.infix}&page=$page", headers)
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
GET("$baseUrl/filter?sort=most-viewed&language=$lang&page=$page", headers)
|
||||
GET("$baseUrl/filter?sort=most-viewed&language=${language.infix}&page=$page", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||
|
@ -47,7 +50,7 @@ open class MangaReader(
|
|||
}
|
||||
} else {
|
||||
urlBuilder.addPathSegment("filter").apply {
|
||||
addQueryParameter("language", lang)
|
||||
addQueryParameter("language", language.infix)
|
||||
addQueryParameter("page", page.toString())
|
||||
filters.ifEmpty(::getFilterList).forEach { filter ->
|
||||
when (filter) {
|
||||
|
@ -142,7 +145,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("$lang-$type")) ?: return emptyList()
|
||||
selectFirst(Evaluator.Id("${language.chapterInfix}-$type")) ?: return emptyList()
|
||||
}
|
||||
return container.children()
|
||||
}
|
||||
|
|
|
@ -4,5 +4,19 @@ import eu.kanade.tachiyomi.source.SourceFactory
|
|||
|
||||
class MangaReaderFactory : SourceFactory {
|
||||
override fun createSources() =
|
||||
arrayOf("en", "fr", "ja", "ko", "zh").map(::MangaReader)
|
||||
arrayOf(
|
||||
Language("en"),
|
||||
Language("es", chapterInfix = "es-mx"),
|
||||
Language("fr"),
|
||||
Language("ja"),
|
||||
Language("ko"),
|
||||
Language("pt-BR", infix = "pt"),
|
||||
Language("zh"),
|
||||
).map(::MangaReader)
|
||||
}
|
||||
|
||||
data class Language(
|
||||
val code: String,
|
||||
val infix: String = code,
|
||||
val chapterInfix: String = code.lowercase(),
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
ext {
|
||||
extName = 'Koharu'
|
||||
extClass = '.Koharu'
|
||||
extName = 'Meitua.top'
|
||||
extClass = '.MeituaTop'
|
||||
extVersionCode = 8
|
||||
isNsfw = true
|
||||
}
|
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 4.3 KiB |