Compare commits
320 Commits
9b7a772823
...
feb6c74f5c
Author | SHA1 | Date |
---|---|---|
renovate[bot] | feb6c74f5c | |
bapeey | e2d97e0860 | |
bapeey | ba7836b0a1 | |
Chopper | df5abd131b | |
bapeey | 7d55908956 | |
ZIDOUZI | 60f5f67479 | |
Chopper | 258578c413 | |
Norsze | 7348576150 | |
Chopper | 9e8bbb7f0b | |
Smol Ame | 4dba7261af | |
bapeey | 8a8f22583b | |
Chopper | c2ffc63cc4 | |
Smol Ame | ed05461925 | |
stevenyomi | 92bcce4f0d | |
AwkwardPeak7 | 4ede181f9d | |
GoonGooner | 93d6a9b328 | |
Vetle Ledaal | e277149bc5 | |
Chopper | 70302c2d30 | |
Chopper | 3402c77336 | |
bapeey | 3310e274d5 | |
stevenyomi | 08ad68a631 | |
Vetle Ledaal | 71ebbabdeb | |
Vetle Ledaal | 6892001319 | |
Chopper | 1e85171288 | |
AwkwardPeak7 | 17e79b4b79 | |
Chopper | 81827ef0d1 | |
AwkwardPeak7 | be806a0771 | |
KenjieDec | b335be2148 | |
bapeey | 9a4258a47e | |
kana-shii | adfd31c749 | |
bapeey | 1b34ce2ddd | |
kana-shii | b514456b03 | |
Chopper | 611cf69a39 | |
Smol Ame | 29b2b23a86 | |
Chopper | be2daf0c4a | |
Chopper | c900e6c0a0 | |
Chopper | d7b79fcc0f | |
Chopper | dc236041b1 | |
bapeey | f7b2ff60ac | |
Chopper | 5966f9699b | |
Chopper | 02af60dcf5 | |
Vetle Ledaal | e2bca12ccd | |
sinkableShip | 2c00628e87 | |
Smol Ame | 89f53742bb | |
Chopper | 9c5e1b3482 | |
bapeey | c33c22062c | |
bapeey | 642e90ccc7 | |
2Loong6 | 6a1cd83b51 | |
bapeey | f860926491 | |
Chopper | 6770a0672a | |
Chopper | f1a59ba618 | |
kana-shii | 241578b0a2 | |
kana-shii | 5228acc900 | |
Chopper | fd9d8d39f0 | |
Chopper | a8c69a300d | |
Chopper | 4cb500b888 | |
Smol Ame | 0fe5108c9a | |
bapeey | f99fc3cce6 | |
Chopper | 34ee8297d2 | |
bapeey | ad0b1a041d | |
Chopper | d5c8b1dd1f | |
Chopper | cc7cc7c9f7 | |
Chopper | e1879e0528 | |
Chopper | ca386889d5 | |
Chopper | c7c002322d | |
bapeey | 04a60a51eb | |
Smol Ame | 0cbbe84fbb | |
Smol Ame | c97b4e5e9e | |
Vetle Ledaal | 943ac6e891 | |
AwkwardPeak7 | d430eb8286 | |
Vetle Ledaal | 608a2c5d2a | |
Chopper | 986683e93d | |
mohamedotaku | 410977c9df | |
bapeey | 850c5dce7e | |
Chopper | cf75a1f995 | |
Chopper | f497bddc71 | |
sinkableShip | febc0ba112 | |
Vetle Ledaal | a4e879bba9 | |
Chopper | 6d3f09e1df | |
Chopper | ceb953ff96 | |
Chopper | f4d1f0d244 | |
bapeey | 72812dc898 | |
bapeey | 934ca4a97f | |
Vetle Ledaal | 86e45b2678 | |
Chopper | 7b2e00811f | |
Secozzi | 1d6a48c189 | |
bapeey | 9440fc3b1a | |
Chopper | 065c277c40 | |
Chopper | 5c9980dec9 | |
Chopper | 4085d94f9a | |
Fermín Cirella | 871c4d7a22 | |
Vetle Ledaal | 6bbb9d0da9 | |
Vetle Ledaal | 5f5e160cfb | |
Vetle Ledaal | fb6ae7f7c0 | |
Vetle Ledaal | e0bffdb80c | |
bapeey | d9e487bfb3 | |
bapeey | bc808a6bc4 | |
bapeey | 51dba16677 | |
bapeey | 76faf0ab3d | |
AwkwardPeak7 | 8ac86c6e8e | |
aaronisles | 312bb8c3b0 | |
Vetle Ledaal | 025c675714 | |
are-are-are | 3c4c79eea4 | |
Secozzi | 769280bbb6 | |
AwkwardPeak7 | 6c5462309d | |
are-are-are | b1c691394c | |
Vetle Ledaal | 1ef30094ca | |
Vetle Ledaal | 4b8fd246c4 | |
bapeey | d4191959e4 | |
are-are-are | f09a232033 | |
Fermín Cirella | 3cb723b12b | |
KirinRaikage | 47af581f6d | |
bapeey | 9e6f5efa92 | |
Vetle Ledaal | 73a5340c58 | |
Davide | 0a7ed30e02 | |
Vetle Ledaal | bf8a3bf3ce | |
bapeey | db035c7ad3 | |
bapeey | dbe3eaa77c | |
bapeey | 49ccc61b6a | |
Chaos Pjeles | 271898bd58 | |
Vetle Ledaal | a41eeebc50 | |
bapeey | 75db04eca2 | |
Vetle Ledaal | 78847a26e6 | |
Vetle Ledaal | 34beb05922 | |
Vetle Ledaal | 8a5578250d | |
stevenyomi | fbffcbb557 | |
mohamedotaku | a3f9894b5c | |
Chopper | f3314852bd | |
Chopper | 1ef8153eca | |
mohamedotaku | 9fe6e06574 | |
bapeey | 65f9de384e | |
Vetle Ledaal | 6e21329ada | |
anenasa | af4b442cc0 | |
AwkwardPeak7 | 5a229cd8cf | |
Bartu Özen | dc225dba18 | |
Vetle Ledaal | dbeb67b3e9 | |
Cuong M. Tran | 6abded47de | |
Chopper | 87157d8aa9 | |
Chopper | 58aec56373 | |
Vetle Ledaal | 3873976886 | |
Vetle Ledaal | 5d6d9463b7 | |
bapeey | 87548ae9ee | |
Chopper | caf462e781 | |
Chopper | a9b5fbd2aa | |
Vetle Ledaal | 6423049771 | |
mohamedotaku | 3df4684c88 | |
bapeey | 6ed5e31f37 | |
Vetle Ledaal | 307540a2aa | |
Vetle Ledaal | 43a3249da3 | |
Vetle Ledaal | 56a10df09d | |
Vetle Ledaal | b81469035f | |
Vetle Ledaal | 15aacb0eb9 | |
Vetle Ledaal | 154b4c50f5 | |
Vetle Ledaal | be5622f93d | |
Vetle Ledaal | a7178fc20e | |
Vetle Ledaal | 64fe0ecdf5 | |
Vetle Ledaal | cdb5b6f488 | |
Vetle Ledaal | 14c1195ff3 | |
Vetle Ledaal | 008f348d38 | |
Chopper | 39e153cc67 | |
Chopper | 6a329074e1 | |
Vetle Ledaal | c1e5450946 | |
Vetle Ledaal | ffa8ba4a45 | |
Vetle Ledaal | 43c934d1f8 | |
Vetle Ledaal | daecf41724 | |
Vetle Ledaal | b389d4089e | |
Vetle Ledaal | 78f01bf837 | |
Vetle Ledaal | 804208d4f8 | |
bapeey | 9af69afccd | |
Vetle Ledaal | 22efdcc2a8 | |
Vetle Ledaal | a6caf6dabe | |
Vetle Ledaal | 5727e8a8aa | |
Eshlender | ad275eb542 | |
Fermín Cirella | edad8aed4b | |
bapeey | bb912b9570 | |
Trevor Paley | 21f03c9454 | |
bapeey | 20b50323b9 | |
Chopper | 483a9bbc70 | |
Vetle Ledaal | f767aa8b6d | |
Chopper | 4161c3f51d | |
Chopper | 437ee8b4bc | |
Chopper | e257636ded | |
Chopper | 47aadb3873 | |
Chopper | 61e0817e90 | |
Chopper | fd0b45a71e | |
Chopper | a58e72f4ec | |
Chopper | 631f2fbdb7 | |
Vetle Ledaal | 552c349632 | |
Vetle Ledaal | ab6e43b1df | |
Vetle Ledaal | 4d76106169 | |
Vetle Ledaal | 1c50b086a1 | |
mohamedotaku | 2b23047ee0 | |
Vetle Ledaal | b531e1fc39 | |
Vetle Ledaal | c84a1acd0b | |
mohamedotaku | 512c1c4b8e | |
Vetle Ledaal | cc45142058 | |
Vetle Ledaal | 18ff934880 | |
Vetle Ledaal | 8c2d6c3e3e | |
Vetle Ledaal | d2a6999b3e | |
Vetle Ledaal | 44964e777d | |
Vetle Ledaal | 496cc82ee4 | |
Vetle Ledaal | 4fa30fc8cb | |
Vetle Ledaal | 5249c98336 | |
Vetle Ledaal | 313b43f78c | |
Vetle Ledaal | 486914692d | |
Vetle Ledaal | 90b8740e32 | |
Vetle Ledaal | d3aa8adb8a | |
Vetle Ledaal | 0c137fb39c | |
Vetle Ledaal | 91ed215f0b | |
bapeey | 38cf9505d2 | |
Chopper | 1e46d2fdc7 | |
Chopper | d2bc63650b | |
Chopper | 2aa1e1f76e | |
Vetle Ledaal | 4251468894 | |
Secozzi | cfc2fcbf4b | |
Vetle Ledaal | a848d1bb2e | |
Vetle Ledaal | 0622230f4b | |
Vetle Ledaal | fd0ad36ac4 | |
Vetle Ledaal | 9c13009267 | |
Vetle Ledaal | a91cda1806 | |
bapeey | f189ddfa55 | |
Vetle Ledaal | 9c152bb303 | |
Vetle Ledaal | 0eb3298f82 | |
Vetle Ledaal | d96d692209 | |
Vetle Ledaal | d7929ab6be | |
Vetle Ledaal | 94ee09037d | |
Vetle Ledaal | f436ebf6ab | |
Vetle Ledaal | bd8979d4b8 | |
Vetle Ledaal | 7cd7706644 | |
Vetle Ledaal | 2f91187312 | |
Vetle Ledaal | b50abc61c6 | |
Vetle Ledaal | 50ef399d3a | |
Vetle Ledaal | 01e2f9db9f | |
Vetle Ledaal | 158402d0db | |
Vetle Ledaal | 64bd4df45c | |
Vetle Ledaal | 9572c9f863 | |
Vetle Ledaal | 325ec1421c | |
bapeey | 0d7c58d326 | |
Chopper | 82ddc7daab | |
Chopper | 2cf4736da0 | |
Vetle Ledaal | cd602e0fcc | |
KenjieDec | 9936531d44 | |
Vetle Ledaal | 5549c629a3 | |
Chopper | 4c8950ffb2 | |
Chopper | aa221b1adc | |
Vetle Ledaal | 4516e2f208 | |
Chopper | 02a6fb32ba | |
Vetle Ledaal | 31cca03e4e | |
Vetle Ledaal | a6439e6316 | |
Chopper | e84664029d | |
Vetle Ledaal | 126a586a21 | |
Vetle Ledaal | 4fa0b2b320 | |
Vetle Ledaal | 622146d821 | |
Vetle Ledaal | 713363838a | |
bapeey | 71e6e4b10c | |
Pedro Azevedo | a5d5c805fd | |
Vetle Ledaal | 0fd72be73b | |
Vetle Ledaal | caab4716b4 | |
Vetle Ledaal | e93b41ce7f | |
Vetle Ledaal | 4058612bca | |
Vetle Ledaal | e867689cc5 | |
Vetle Ledaal | 0c27089d2f | |
Vetle Ledaal | e807908b9f | |
Vetle Ledaal | 14ad5b9027 | |
mohamedotaku | 5b5657ff14 | |
Vetle Ledaal | 6ce176226a | |
Vetle Ledaal | feeec15597 | |
Vetle Ledaal | fe705dab68 | |
Vetle Ledaal | c50d959323 | |
Vetle Ledaal | df3cdcf1fc | |
Vetle Ledaal | a0e0b81bda | |
Vetle Ledaal | 5670bb6899 | |
Vetle Ledaal | 2ffb3930d2 | |
Vetle Ledaal | b653340c6e | |
Vetle Ledaal | 48cb2265c1 | |
kana-shii | 19532c2f33 | |
kana-shii | 97cab8fc8f | |
AwkwardPeak7 | 81a3a9375f | |
AwkwardPeak7 | 5797b9638d | |
Vetle Ledaal | c3045a2d81 | |
bapeey | 6a498537ce | |
bapeey | 3f6a8b9fd0 | |
bapeey | 0522aaf29e | |
bapeey | d4bdbbe059 | |
Vetle Ledaal | 07c41e78fb | |
Vetle Ledaal | 5db5f710dd | |
Vetle Ledaal | 094ef457b0 | |
Vetle Ledaal | 5a7386cd6c | |
Vetle Ledaal | f191400e7d | |
mohamedotaku | ca26ce4167 | |
Cuong M. Tran | 935bd089fc | |
Vetle Ledaal | 1077a96940 | |
Vetle Ledaal | 071342ecbe | |
Hasan | 964adc3624 | |
Vetle Ledaal | a6baf39b2e | |
Vetle Ledaal | c6e6fc9aaa | |
Vetle Ledaal | 1c07987326 | |
Chopper | 2d6fd42c9d | |
mohamedotaku | 3a1c58f65e | |
Chopper | cabf481d6f | |
Chaos Pjeles | 239c1634cf | |
Vetle Ledaal | 7e52eac427 | |
Vetle Ledaal | bb6221038e | |
Vetle Ledaal | e3fca411f3 | |
Vetle Ledaal | 1b456b875a | |
Vetle Ledaal | 411c3218fd | |
Vetle Ledaal | b54330f88f | |
bapeey | dd5afb8bfc | |
Mon | 4f18cd9fb1 | |
bapeey | 41c95e1d14 | |
Chopper | 5334569924 | |
Chopper | 3845041ef4 | |
Chopper | c1a0d2af2d | |
KenjieDec | 8e9e4f02f6 | |
KirinRaikage | 22469d8a51 | |
KirinRaikage | 4abf193718 | |
KirinRaikage | 226766fdd0 | |
nomaxsnx | 6f8cae7f96 | |
Vetle Ledaal | b2d5c77ee6 | |
Akshar Kalathiya | 1da73af727 |
|
@ -1,3 +1,4 @@
|
|||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
@ -107,3 +108,10 @@ with (REPO_DIR / "index.json").open("w", encoding="utf-8") as f:
|
|||
with (REPO_DIR / "index.min.json").open("w", encoding="utf-8") as f:
|
||||
json.dump(index_min_data, f, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
with (REPO_DIR / "index.html").open("w", encoding="utf-8") as f:
|
||||
f.write('<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title>apks</title>\n</head>\n<body>\n<pre>\n')
|
||||
for entry in index_data:
|
||||
apk_escaped = 'apk/' + html.escape(entry["apk"])
|
||||
name_escaped = html.escape(entry["name"])
|
||||
f.write(f'<a href="{apk_escaped}">{name_escaped}</a>\n')
|
||||
f.write('</pre>\n</body>\n</html>\n')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-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.7-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 7
|
||||
baseVersionCode = 8
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:speedbinb"))
|
||||
|
|
|
@ -84,7 +84,7 @@ open class ComicGamma(
|
|||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".read__area > .read__outer > a:not([href=#comics])"
|
||||
override fun chapterListSelector() = ".read__area .read__outer > a:not([href=#comics])"
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
url = element.attr("href").toOldChapterUrl()
|
||||
val number = url.removeSuffix("/").substringAfterLast('/').replace('_', '.')
|
||||
|
|
|
@ -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.galleryadults.GalleryAdultsUrlActivity"
|
||||
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="/g.*/..*/"
|
||||
android:scheme="${SOURCESCHEME}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,5 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 11 KiB |
|
@ -0,0 +1,941 @@
|
|||
package eu.kanade.tachiyomi.multisrc.galleryadults
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
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.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.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
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.text.SimpleDateFormat
|
||||
|
||||
abstract class GalleryAdults(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String = "all",
|
||||
protected open val mangaLang: String = LANGUAGE_MULTI,
|
||||
protected val simpleDateFormat: SimpleDateFormat? = null,
|
||||
) : ConfigurableSource, ParsedHttpSource() {
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
protected open val xhrHeaders = headers.newBuilder()
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
/* Preferences */
|
||||
protected val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
protected open val useShortTitlePreference = true
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_SHORT_TITLE
|
||||
title = "Display Short Titles"
|
||||
summaryOff = "Showing Long Titles"
|
||||
summaryOn = "Showing short Titles"
|
||||
setDefaultValue(false)
|
||||
setVisible(useShortTitlePreference)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
protected val SharedPreferences.shortTitle
|
||||
get() = getBoolean(PREF_SHORT_TITLE, false)
|
||||
|
||||
/* List detail */
|
||||
protected class SMangaDto(
|
||||
val title: String,
|
||||
val url: String,
|
||||
val thumbnail: String?,
|
||||
val lang: String,
|
||||
)
|
||||
|
||||
protected open fun Element.mangaTitle(selector: String = ".caption"): String? =
|
||||
mangaFullTitle(selector).let {
|
||||
if (preferences.shortTitle) it?.shortenTitle() else it
|
||||
}
|
||||
|
||||
protected open fun Element.mangaFullTitle(selector: String) =
|
||||
selectFirst(selector)?.text()
|
||||
|
||||
protected open fun String.shortenTitle() = this.replace(shortenTitleRegex, "").trim()
|
||||
|
||||
protected open val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""")
|
||||
|
||||
protected open fun Element.mangaUrl() =
|
||||
selectFirst(".inner_thumb a")?.attr("abs:href")
|
||||
|
||||
protected open fun Element.mangaThumbnail() =
|
||||
selectFirst(".inner_thumb img")?.imgAttr()
|
||||
|
||||
// Overwrite this to filter other languages' manga from search result.
|
||||
// Default to [mangaLang] won't filter anything
|
||||
protected open fun Element.mangaLang() = mangaLang
|
||||
|
||||
protected open fun HttpUrl.Builder.addPageUri(page: Int): HttpUrl.Builder {
|
||||
val url = toString()
|
||||
if (!url.endsWith('/') && !url.contains('?')) {
|
||||
addPathSegment("") // trailing slash (/)
|
||||
}
|
||||
addQueryParameter("page", page.toString())
|
||||
return this
|
||||
}
|
||||
|
||||
/* Popular */
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
if (mangaLang.isNotBlank()) addPathSegments("language/$mangaLang")
|
||||
if (supportsLatest) addPathSegment("popular")
|
||||
addPageUri(page)
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.thumb"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
title = element.mangaTitle()!!
|
||||
setUrlWithoutDomain(element.mangaUrl()!!)
|
||||
thumbnail_url = element.mangaThumbnail()
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = ".pagination li.active + li:not(.disabled)"
|
||||
|
||||
/* Latest */
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
if (mangaLang.isNotBlank()) addPathSegments("language/$mangaLang")
|
||||
addPageUri(page)
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
/* Search */
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val randomEntryFilter = filters.filterIsInstance<RandomEntryFilter>().firstOrNull()
|
||||
|
||||
return when {
|
||||
randomEntryFilter?.state == true -> {
|
||||
client.newCall(randomEntryRequest())
|
||||
.asObservableSuccess()
|
||||
.map { response -> randomEntryParse(response) }
|
||||
}
|
||||
query.startsWith(PREFIX_ID_SEARCH) -> {
|
||||
val id = query.removePrefix(PREFIX_ID_SEARCH)
|
||||
client.newCall(searchMangaByIdRequest(id))
|
||||
.asObservableSuccess()
|
||||
.map { response -> searchMangaByIdParse(response, id) }
|
||||
}
|
||||
query.toIntOrNull() != null -> {
|
||||
client.newCall(searchMangaByIdRequest(query))
|
||||
.asObservableSuccess()
|
||||
.map { response -> searchMangaByIdParse(response, query) }
|
||||
}
|
||||
else -> {
|
||||
client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response -> searchMangaParse(response) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun randomEntryRequest(): Request = GET("$baseUrl/random/", headers)
|
||||
|
||||
protected open fun randomEntryParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val url = response.request.url.toString()
|
||||
val id = url.removeSuffix("/").substringAfterLast('/')
|
||||
return MangasPage(
|
||||
listOf(
|
||||
SManga.create().apply {
|
||||
title = document.mangaTitle("h1")!!
|
||||
setUrlWithoutDomain("$baseUrl/$idPrefixUri/$id/")
|
||||
thumbnail_url = document.getCover()
|
||||
},
|
||||
),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Manga URL: $baseUrl/$idPrefixUri/<id>/
|
||||
*/
|
||||
protected open val idPrefixUri = "gallery"
|
||||
|
||||
protected open fun searchMangaByIdRequest(id: String): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment(idPrefixUri)
|
||||
addPathSegments("$id/")
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
protected open fun searchMangaByIdParse(response: Response, id: String): MangasPage {
|
||||
val details = mangaDetailsParse(response.asJsoup())
|
||||
details.url = "/$idPrefixUri/$id/"
|
||||
return MangasPage(listOf(details), false)
|
||||
}
|
||||
|
||||
protected open val useIntermediateSearch: Boolean = false
|
||||
protected open val supportAdvancedSearch: Boolean = false
|
||||
protected open val supportSpeechless: Boolean = false
|
||||
protected open val useBasicSearch: Boolean
|
||||
get() = !useIntermediateSearch
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// Basic search
|
||||
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
|
||||
val genresFilter = filters.filterIsInstance<GenresFilter>().firstOrNull()
|
||||
val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList()
|
||||
val favoriteFilter = filters.filterIsInstance<FavoriteFilter>().firstOrNull()
|
||||
|
||||
// Speechless
|
||||
val speechlessFilter = filters.filterIsInstance<SpeechlessFilter>().firstOrNull()
|
||||
|
||||
// Advanced search
|
||||
val advancedSearchFilters = filters.filterIsInstance<AdvancedTextFilter>()
|
||||
|
||||
return when {
|
||||
favoriteFilter?.state == true ->
|
||||
favoriteFilterSearchRequest(page, query, filters)
|
||||
supportSpeechless && speechlessFilter?.state == true ->
|
||||
speechlessFilterSearchRequest(page, query, filters)
|
||||
supportAdvancedSearch && advancedSearchFilters.any { it.state.isNotBlank() } ->
|
||||
advancedSearchRequest(page, query, filters)
|
||||
selectedGenres.size == 1 && query.isBlank() ->
|
||||
tagBrowsingSearchRequest(page, query, filters)
|
||||
useIntermediateSearch ->
|
||||
intermediateSearchRequest(page, query, filters)
|
||||
useBasicSearch && (selectedGenres.size > 1 || query.isNotBlank()) ->
|
||||
basicSearchRequest(page, query, filters)
|
||||
sortOrderFilter?.state == 1 ->
|
||||
latestUpdatesRequest(page)
|
||||
else ->
|
||||
popularMangaRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
protected open val basicSearchKey = "q"
|
||||
|
||||
/**
|
||||
* Basic Search: support query string with multiple-genres filter by adding genres to query string.
|
||||
*/
|
||||
protected open fun basicSearchRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// Basic search
|
||||
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
|
||||
val genresFilter = filters.filterIsInstance<GenresFilter>().firstOrNull()
|
||||
val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList()
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegments("search/")
|
||||
addEncodedQueryParameter(basicSearchKey, buildQueryString(selectedGenres.map { it.name }, query))
|
||||
if (sortOrderFilter?.state == 0) addQueryParameter("sort", "popular")
|
||||
addPageUri(page)
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
protected open val intermediateSearchKey = "key"
|
||||
|
||||
/**
|
||||
* This supports filter query search with languages, categories (manga, doujinshi...)
|
||||
* with additional sort orders.
|
||||
*/
|
||||
protected open fun intermediateSearchRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// Basic search
|
||||
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
|
||||
val genresFilter = filters.filterIsInstance<GenresFilter>().firstOrNull()
|
||||
val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList()
|
||||
|
||||
// Intermediate search
|
||||
val categoryFilters = filters.filterIsInstance<CategoryFilters>().firstOrNull()
|
||||
|
||||
// Only for query string or multiple tags
|
||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
||||
getSortOrderURIs().forEachIndexed { index, pair ->
|
||||
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
|
||||
}
|
||||
categoryFilters?.state?.forEach {
|
||||
addQueryParameter(it.uri, toBinary(it.state))
|
||||
}
|
||||
getLanguageURIs().forEach { pair ->
|
||||
addQueryParameter(
|
||||
pair.second,
|
||||
toBinary(mangaLang == pair.first || mangaLang == LANGUAGE_MULTI),
|
||||
)
|
||||
}
|
||||
addEncodedQueryParameter(intermediateSearchKey, buildQueryString(selectedGenres.map { it.name }, query))
|
||||
addPageUri(page)
|
||||
}
|
||||
return GET(url.build())
|
||||
}
|
||||
|
||||
protected open val advancedSearchKey = "key"
|
||||
protected open val advancedSearchUri = "advsearch"
|
||||
|
||||
/**
|
||||
* Advanced Search normally won't support search for string but allow include/exclude specific
|
||||
* tags/artists/groups/parodies/characters
|
||||
*/
|
||||
protected open fun advancedSearchRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// Basic search
|
||||
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
|
||||
val genresFilter = filters.filterIsInstance<GenresFilter>().firstOrNull()
|
||||
val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList()
|
||||
|
||||
// Intermediate search
|
||||
val categoryFilters = filters.filterIsInstance<CategoryFilters>().firstOrNull()
|
||||
// Advanced search
|
||||
val advancedSearchFilters = filters.filterIsInstance<AdvancedTextFilter>()
|
||||
|
||||
val url = "$baseUrl/$advancedSearchUri".toHttpUrl().newBuilder().apply {
|
||||
getSortOrderURIs().forEachIndexed { index, pair ->
|
||||
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
|
||||
}
|
||||
categoryFilters?.state?.forEach {
|
||||
addQueryParameter(it.uri, toBinary(it.state))
|
||||
}
|
||||
getLanguageURIs().forEach { pair ->
|
||||
addQueryParameter(
|
||||
pair.second,
|
||||
toBinary(
|
||||
mangaLang == pair.first ||
|
||||
mangaLang == LANGUAGE_MULTI,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Build this query string: +tag:"bat+man"+-tag:"cat"+artist:"Joe"...
|
||||
// +tag must be encoded into %2Btag while the rest are not needed to encode
|
||||
val keys = emptyList<String>().toMutableList()
|
||||
keys.addAll(selectedGenres.map { "%2Btag:\"${it.name}\"" })
|
||||
advancedSearchFilters.forEach { filter ->
|
||||
val key = when (filter) {
|
||||
is TagsFilter -> "tag"
|
||||
is ParodiesFilter -> "parody"
|
||||
is ArtistsFilter -> "artist"
|
||||
is CharactersFilter -> "character"
|
||||
is GroupsFilter -> "group"
|
||||
else -> null
|
||||
}
|
||||
if (key != null) {
|
||||
keys.addAll(
|
||||
filter.state.trim()
|
||||
.replace(regexSpaceNotAfterComma, "+")
|
||||
.replace(" ", "")
|
||||
.split(',')
|
||||
.mapNotNull {
|
||||
val match = regexExcludeTerm.find(it)
|
||||
match?.groupValues?.let { groups ->
|
||||
"${if (groups[1].isNotBlank()) "-" else "%2B"}$key:\"${groups[2]}\""
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
addEncodedQueryParameter(advancedSearchKey, keys.joinToString("+"))
|
||||
addPageUri(page)
|
||||
}
|
||||
return GET(url.build())
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert space( ) typed in search-box into plus(+) in URL. Then:
|
||||
* - uses plus(+) to search for exact match
|
||||
* - use comma(,) for separate terms, as AND condition.
|
||||
* Plus(+) after comma(,) doesn't have any effect.
|
||||
*/
|
||||
protected open fun buildQueryString(tags: List<String>, query: String): String {
|
||||
return (tags + query).filterNot { it.isBlank() }.joinToString(",") {
|
||||
// any space except after a comma (we're going to replace spaces only between words)
|
||||
it.trim()
|
||||
.replace(regexSpaceNotAfterComma, "+")
|
||||
.replace(" ", "")
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun tagBrowsingSearchRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// Basic search
|
||||
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
|
||||
val genresFilter = filters.filterIsInstance<GenresFilter>().firstOrNull()
|
||||
val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList()
|
||||
|
||||
// Browsing single tag's catalog
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("tag")
|
||||
addPathSegment(selectedGenres.single().uri)
|
||||
if (sortOrderFilter?.state == 0) addPathSegment("popular")
|
||||
addPageUri(page)
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Browsing speechless titles. Some sites exclude speechless titles from normal search and
|
||||
* allow browsing separately.
|
||||
*/
|
||||
protected open fun speechlessFilterSearchRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// Basic search
|
||||
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("language")
|
||||
addPathSegment(LANGUAGE_SPEECHLESS)
|
||||
if (sortOrderFilter?.state == 0) addPathSegment("popular")
|
||||
addPageUri(page)
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Browsing user's personal favorites saved on site. This requires login in view WebView.
|
||||
*/
|
||||
protected open fun favoriteFilterSearchRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/$favoritePath".toHttpUrl().newBuilder()
|
||||
return POST(
|
||||
url.build().toString(),
|
||||
xhrHeaders,
|
||||
FormBody.Builder()
|
||||
.add("page", page.toString())
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
protected open val favoritePath = "user/fav_pags.php"
|
||||
|
||||
protected open fun loginRequired(document: Document, url: String): Boolean {
|
||||
return (
|
||||
url.contains("/login/") &&
|
||||
document.select("input[value=Login]").isNotEmpty()
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
if (loginRequired(document, response.request.url.toString())) {
|
||||
throw Exception("Log in via WebView to view favorites")
|
||||
} else {
|
||||
val hasNextPage = document.select(searchMangaNextPageSelector()).isNotEmpty()
|
||||
val mangas = document.select(searchMangaSelector())
|
||||
.map {
|
||||
SMangaDto(
|
||||
title = it.mangaTitle()!!,
|
||||
url = it.mangaUrl()!!,
|
||||
thumbnail = it.mangaThumbnail(),
|
||||
lang = it.mangaLang(),
|
||||
)
|
||||
}
|
||||
.let { unfiltered ->
|
||||
val results = unfiltered.filter { mangaLang.isBlank() || it.lang == mangaLang }
|
||||
// return at least 1 title if all mangas in current page is of other languages
|
||||
if (results.isEmpty() && hasNextPage) listOf(unfiltered[0]) else results
|
||||
}
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
title = it.title
|
||||
setUrlWithoutDomain(it.url)
|
||||
thumbnail_url = it.thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
/* Details */
|
||||
protected open val mangaDetailInfoSelector = ".gallery_top"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return document.selectFirst(mangaDetailInfoSelector)!!.run {
|
||||
SManga.create().apply {
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
status = SManga.COMPLETED
|
||||
title = mangaTitle("h1")!!
|
||||
thumbnail_url = getCover()
|
||||
genre = getInfo("Tags")
|
||||
author = getInfo("Artists")
|
||||
description = getDescription(document)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun Element.getCover() =
|
||||
selectFirst(".cover img")?.imgAttr()
|
||||
|
||||
protected val regexTag = Regex("Tags?")
|
||||
|
||||
/**
|
||||
* Parsing document to extract info related to [tag].
|
||||
*/
|
||||
protected abstract fun Element.getInfo(tag: String): String
|
||||
|
||||
protected open fun Element.getDescription(document: Document? = null): String = (
|
||||
listOf("Parodies", "Characters", "Languages", "Categories", "Category")
|
||||
.mapNotNull { tag ->
|
||||
getInfo(tag)
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { "$tag: $it" }
|
||||
} +
|
||||
listOfNotNull(
|
||||
getInfoPages(document),
|
||||
getInfoAlternativeTitle(),
|
||||
getInfoFullTitle(),
|
||||
)
|
||||
)
|
||||
.joinToString("\n\n")
|
||||
|
||||
protected open fun Element.getInfoPages(document: Document? = null): String? =
|
||||
document?.inputIdValueOf(totalPagesSelector)
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { "Pages: $it" }
|
||||
|
||||
protected open fun Element.getInfoAlternativeTitle(): String? =
|
||||
selectFirst("h1 + h2, .subtitle")?.ownText()
|
||||
.takeIf { !it.isNullOrBlank() }
|
||||
?.let { "Alternative title: $it" }
|
||||
|
||||
protected open fun Element.getInfoFullTitle(): String? =
|
||||
if (preferences.shortTitle) "Full title: ${mangaFullTitle("h1")}" else null
|
||||
|
||||
protected open fun Element.getTime(): Long =
|
||||
selectFirst(".uploaded")
|
||||
?.ownText()
|
||||
.toDate(simpleDateFormat)
|
||||
|
||||
/* Chapters */
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return listOf(
|
||||
SChapter.create().apply {
|
||||
name = "Chapter"
|
||||
scanlator = document.selectFirst(mangaDetailInfoSelector)
|
||||
?.getInfo("Groups")
|
||||
date_upload = document.getTime()
|
||||
setUrlWithoutDomain(response.request.url.encodedPath)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
|
||||
|
||||
/* Pages */
|
||||
protected open fun Element.inputIdValueOf(string: String): String {
|
||||
return select("input[id=$string]").attr("value")
|
||||
}
|
||||
|
||||
protected open val pagesRequest = "inc/thumbs_loader.php"
|
||||
protected open val galleryIdSelector = "gallery_id"
|
||||
protected open val loadIdSelector = "load_id"
|
||||
protected open val loadDirSelector = "load_dir"
|
||||
protected open val totalPagesSelector = "load_pages"
|
||||
protected open val serverSelector = "load_server"
|
||||
|
||||
protected open fun pageRequestForm(document: Document, totalPages: String, loadedPages: Int): FormBody {
|
||||
val token = document.select("[name=csrf-token]").attr("content")
|
||||
val serverNumber = document.serverNumber()
|
||||
|
||||
return FormBody.Builder()
|
||||
.add("u_id", document.inputIdValueOf(galleryIdSelector))
|
||||
.add("g_id", document.inputIdValueOf(loadIdSelector))
|
||||
.add("img_dir", document.inputIdValueOf(loadDirSelector))
|
||||
.add("visible_pages", loadedPages.toString())
|
||||
.add("total_pages", totalPages)
|
||||
.add("type", "2") // 1 would be "more", 2 is "all remaining"
|
||||
.apply {
|
||||
if (token.isNotBlank()) add("_token", token)
|
||||
if (serverNumber != null) add("server", serverNumber)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
protected open val thumbnailSelector = ".gallery_thumb"
|
||||
|
||||
private val jsonFormat: Json by injectLazy()
|
||||
|
||||
protected open fun Element.getServer(): String {
|
||||
val domain = baseUrl.toHttpUrl().host
|
||||
return serverNumber()
|
||||
?.let { "m$it.$domain" }
|
||||
?: getCover()!!.toHttpUrl().host
|
||||
}
|
||||
|
||||
protected open fun Element.serverNumber(): String? =
|
||||
inputIdValueOf(serverSelector)
|
||||
.takeIf { it.isNotBlank() }
|
||||
|
||||
protected open fun Element.parseJson(): String? =
|
||||
selectFirst("script:containsData(parseJSON)")?.data()
|
||||
?.substringAfter("$.parseJSON('")
|
||||
?.substringBefore("');")?.trim()
|
||||
|
||||
/**
|
||||
* Page URL: $baseUrl/$pageUri/<id>/<page>
|
||||
*/
|
||||
protected open val pageUri = "g"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val json = document.parseJson()
|
||||
|
||||
if (json != null) {
|
||||
val loadDir = document.inputIdValueOf(loadDirSelector)
|
||||
val loadId = document.inputIdValueOf(loadIdSelector)
|
||||
val galleryId = document.inputIdValueOf(galleryIdSelector)
|
||||
val pageUrl = "$baseUrl/$pageUri/$galleryId"
|
||||
|
||||
val server = document.getServer()
|
||||
val imagesUri = "https://$server/$loadDir/$loadId"
|
||||
|
||||
try {
|
||||
val pages = mutableListOf<Page>()
|
||||
val images = jsonFormat.parseToJsonElement(json).jsonObject
|
||||
|
||||
// JSON string in this form: {"1":"j,1100,1148","2":"j,728,689",...
|
||||
for (image in images) {
|
||||
val ext = image.value.toString().replace("\"", "").split(",")[0]
|
||||
val imageExt = when (ext) {
|
||||
"p" -> "png"
|
||||
"b" -> "bmp"
|
||||
"g" -> "gif"
|
||||
else -> "jpg"
|
||||
}
|
||||
val idx = image.key.toInt()
|
||||
pages.add(
|
||||
Page(
|
||||
index = idx,
|
||||
imageUrl = "$imagesUri/${image.key}.$imageExt",
|
||||
url = "$pageUrl/$idx/",
|
||||
),
|
||||
)
|
||||
}
|
||||
return pages
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("GalleryAdults", "Failed to decode JSON")
|
||||
return this.pageListParseAlternative(document)
|
||||
}
|
||||
} else {
|
||||
return this.pageListParseAlternative(document)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite this to force extension not blindly converting thumbnails to full image
|
||||
* by simply removing the trailing "t" from file name. Instead, it will open each page,
|
||||
* one by one, then parsing for actual image's URL.
|
||||
* This will be much slower but guaranteed work.
|
||||
*
|
||||
* This only apply if site doesn't provide 'parseJSON'.
|
||||
*/
|
||||
protected open val parsingImagePageByPage: Boolean = false
|
||||
|
||||
/**
|
||||
* Either:
|
||||
* - Load all thumbnails then convert thumbnails to full images.
|
||||
* - Or request then parse for a list of manga's page's URL,
|
||||
* which will then request one by one to parse for page's image's URL using [imageUrlParse].
|
||||
*/
|
||||
protected open fun pageListParseAlternative(document: Document): List<Page> {
|
||||
val totalPages = document.inputIdValueOf(totalPagesSelector)
|
||||
val galleryId = document.inputIdValueOf(galleryIdSelector)
|
||||
val pageUrl = "$baseUrl/$pageUri/$galleryId"
|
||||
|
||||
val pages = document.select("$thumbnailSelector a")
|
||||
.map {
|
||||
if (parsingImagePageByPage) {
|
||||
it.absUrl("href")
|
||||
} else {
|
||||
it.selectFirst("img")!!.imgAttr()
|
||||
}
|
||||
}
|
||||
.toMutableList()
|
||||
|
||||
if (totalPages.isNotBlank() && totalPages.toInt() > pages.size) {
|
||||
val form = pageRequestForm(document, totalPages, pages.size)
|
||||
|
||||
val morePages = client.newCall(POST("$baseUrl/$pagesRequest", xhrHeaders, form))
|
||||
.execute()
|
||||
.asJsoup()
|
||||
.select("a")
|
||||
.map {
|
||||
if (parsingImagePageByPage) {
|
||||
it.absUrl("href")
|
||||
} else {
|
||||
it.selectFirst("img")!!.imgAttr()
|
||||
}
|
||||
}
|
||||
if (morePages.isNotEmpty()) {
|
||||
pages.addAll(morePages)
|
||||
} else {
|
||||
return pageListParseDummy(document)
|
||||
}
|
||||
}
|
||||
|
||||
return pages.mapIndexed { idx, url ->
|
||||
if (parsingImagePageByPage) {
|
||||
Page(idx, url)
|
||||
} else {
|
||||
Page(
|
||||
index = idx,
|
||||
imageUrl = url.thumbnailToFull(),
|
||||
url = "$pageUrl/$idx/",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all images using `totalPages`. Supposedly they are sequential.
|
||||
* Use in case any extension doesn't know how to request for "All thumbnails"
|
||||
*/
|
||||
protected open fun pageListParseDummy(document: Document): List<Page> {
|
||||
val loadDir = document.inputIdValueOf(loadDirSelector)
|
||||
val loadId = document.inputIdValueOf(loadIdSelector)
|
||||
val galleryId = document.inputIdValueOf(galleryIdSelector)
|
||||
val pageUrl = "$baseUrl/$pageUri/$galleryId"
|
||||
|
||||
val server = document.getServer()
|
||||
val imagesUri = "https://$server/$loadDir/$loadId"
|
||||
|
||||
val images = document.select("$thumbnailSelector img")
|
||||
val thumbUrls = images.map { it.imgAttr() }.toMutableList()
|
||||
|
||||
val totalPages = document.inputIdValueOf(totalPagesSelector)
|
||||
|
||||
if (totalPages.isNotBlank() && totalPages.toInt() > thumbUrls.size) {
|
||||
val imagesExt = images.first()?.imgAttr()!!
|
||||
.substringAfterLast('.')
|
||||
|
||||
thumbUrls.addAll(
|
||||
listOf((images.size + 1)..totalPages.toInt()).flatten().map {
|
||||
"$imagesUri/${it}t.$imagesExt"
|
||||
},
|
||||
)
|
||||
}
|
||||
return thumbUrls.mapIndexed { idx, url ->
|
||||
Page(
|
||||
index = idx,
|
||||
imageUrl = url.thumbnailToFull(),
|
||||
url = "$pageUrl/$idx/",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String {
|
||||
return document.selectFirst("img#gimg, img#fimg")?.imgAttr()!!
|
||||
}
|
||||
|
||||
/* Filters */
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
private fun launchIO(block: () -> Unit) = scope.launch { block() }
|
||||
private var tagsFetched = false
|
||||
private var tagsFetchAttempt = 0
|
||||
|
||||
/**
|
||||
* List of tags in <name, uri> pairs
|
||||
*/
|
||||
protected var genres: MutableMap<String, String> = mutableMapOf()
|
||||
|
||||
protected open fun tagsRequest(page: Int): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegments("tags/popular")
|
||||
addPageUri(page)
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsing [document] to return a list of tags in <name, uri> pairs.
|
||||
*/
|
||||
protected open fun tagsParser(document: Document): List<Genre> {
|
||||
return document.select("a.tag_btn")
|
||||
.mapNotNull {
|
||||
Genre(
|
||||
it.select(".list_tag, .tag_name").text(),
|
||||
it.attr("href")
|
||||
.removeSuffix("/").substringAfterLast('/'),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun requestTags() {
|
||||
if (!tagsFetched && tagsFetchAttempt < 3) {
|
||||
launchIO {
|
||||
val tags = mutableListOf<Genre>()
|
||||
runBlocking {
|
||||
val jobsPool = mutableListOf<Job>()
|
||||
// Get first 3 pages
|
||||
(1..3).forEach { page ->
|
||||
jobsPool.add(
|
||||
launchIO {
|
||||
runCatching {
|
||||
tags.addAll(
|
||||
client.newCall(tagsRequest(page))
|
||||
.execute().asJsoup().let { tagsParser(it) },
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
jobsPool.joinAll()
|
||||
tags.sortedWith(compareBy { it.name })
|
||||
.forEach {
|
||||
genres[it.name] = it.uri
|
||||
}
|
||||
tagsFetched = true
|
||||
}
|
||||
|
||||
tagsFetchAttempt++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
requestTags()
|
||||
val filters = emptyList<Filter<*>>().toMutableList()
|
||||
if (useIntermediateSearch) {
|
||||
filters.add(Filter.Header("HINT: Separate search term with comma (,)"))
|
||||
}
|
||||
|
||||
filters.add(SortOrderFilter(getSortOrderURIs()))
|
||||
|
||||
if (genres.isEmpty()) {
|
||||
filters.add(Filter.Header("Press 'reset' to attempt to load tags"))
|
||||
} else {
|
||||
filters.add(GenresFilter(genres))
|
||||
}
|
||||
|
||||
if (useIntermediateSearch || supportAdvancedSearch) {
|
||||
filters.addAll(
|
||||
listOf(
|
||||
Filter.Separator(),
|
||||
CategoryFilters(getCategoryURIs()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (supportAdvancedSearch) {
|
||||
filters.addAll(
|
||||
listOf(
|
||||
Filter.Separator(),
|
||||
Filter.Header("Advanced filters will ignore query search. Separate terms by comma (,) and precede term with minus (-) to exclude."),
|
||||
TagsFilter(),
|
||||
ParodiesFilter(),
|
||||
ArtistsFilter(),
|
||||
CharactersFilter(),
|
||||
GroupsFilter(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
filters.add(Filter.Separator())
|
||||
|
||||
if (supportSpeechless) {
|
||||
filters.add(SpeechlessFilter())
|
||||
}
|
||||
filters.add(FavoriteFilter())
|
||||
|
||||
filters.add(RandomEntryFilter())
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
protected open fun getSortOrderURIs() = listOf(
|
||||
Pair("Popular", "pp"),
|
||||
Pair("Latest", "lt"),
|
||||
) + if (useIntermediateSearch || supportAdvancedSearch) {
|
||||
listOf(
|
||||
Pair("Downloads", "dl"),
|
||||
Pair("Top Rated", "tr"),
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
protected open fun getCategoryURIs() = listOf(
|
||||
SearchFlagFilter("Manga", "m"),
|
||||
SearchFlagFilter("Doujinshi", "d"),
|
||||
SearchFlagFilter("Western", "w"),
|
||||
SearchFlagFilter("Image Set", "i"),
|
||||
SearchFlagFilter("Artist CG", "a"),
|
||||
SearchFlagFilter("Game CG", "g"),
|
||||
)
|
||||
|
||||
protected open fun getLanguageURIs() = listOf(
|
||||
Pair(LANGUAGE_ENGLISH, "en"),
|
||||
Pair(LANGUAGE_JAPANESE, "jp"),
|
||||
Pair(LANGUAGE_SPANISH, "es"),
|
||||
Pair(LANGUAGE_FRENCH, "fr"),
|
||||
Pair(LANGUAGE_KOREAN, "kr"),
|
||||
Pair(LANGUAGE_GERMAN, "de"),
|
||||
Pair(LANGUAGE_RUSSIAN, "ru"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val PREFIX_ID_SEARCH = "id:"
|
||||
|
||||
private const val PREF_SHORT_TITLE = "pref_short_title"
|
||||
|
||||
// references to be used in factory
|
||||
const val LANGUAGE_MULTI = ""
|
||||
const val LANGUAGE_ENGLISH = "english"
|
||||
const val LANGUAGE_JAPANESE = "japanese"
|
||||
const val LANGUAGE_CHINESE = "chinese"
|
||||
const val LANGUAGE_KOREAN = "korean"
|
||||
const val LANGUAGE_SPANISH = "spanish"
|
||||
const val LANGUAGE_FRENCH = "french"
|
||||
const val LANGUAGE_GERMAN = "german"
|
||||
const val LANGUAGE_RUSSIAN = "russian"
|
||||
const val LANGUAGE_SPEECHLESS = "speechless"
|
||||
const val LANGUAGE_TRANSLATED = "translated"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package eu.kanade.tachiyomi.multisrc.galleryadults
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class Genre(name: String, val uri: String) : Filter.CheckBox(name)
|
||||
class GenresFilter(genres: Map<String, String>) : Filter.Group<Genre>(
|
||||
"Tags",
|
||||
genres.map { Genre(it.key, it.value) },
|
||||
)
|
||||
|
||||
class SortOrderFilter(sortOrderURIs: List<Pair<String, String>>) :
|
||||
Filter.Select<String>("Sort By", sortOrderURIs.map { it.first }.toTypedArray())
|
||||
|
||||
class FavoriteFilter : Filter.CheckBox("Show favorites only (login via WebView)", false)
|
||||
|
||||
class RandomEntryFilter : Filter.CheckBox("Random manga", false)
|
||||
|
||||
// Speechless
|
||||
class SpeechlessFilter : Filter.CheckBox("Show speechless items only", false)
|
||||
|
||||
// Intermediate search
|
||||
class SearchFlagFilter(name: String, val uri: String, state: Boolean = true) : Filter.CheckBox(name, state)
|
||||
class CategoryFilters(flags: List<SearchFlagFilter>) : Filter.Group<SearchFlagFilter>("Categories", flags)
|
||||
|
||||
// Advance search
|
||||
abstract class AdvancedTextFilter(name: String) : Filter.Text(name)
|
||||
class TagsFilter : AdvancedTextFilter("Tags")
|
||||
class ParodiesFilter : AdvancedTextFilter("Parodies")
|
||||
class ArtistsFilter : AdvancedTextFilter("Artists")
|
||||
class CharactersFilter : AdvancedTextFilter("Characters")
|
||||
class GroupsFilter : AdvancedTextFilter("Groups")
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.all.imhentai
|
||||
package eu.kanade.tachiyomi.multisrc.galleryadults
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
|
@ -8,10 +8,9 @@ import android.util.Log
|
|||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Springboard that accepts https://imhentai.xxx/gallery/xxxxxx intents and redirects them to
|
||||
* the main Tachiyomi process.
|
||||
* Springboard that accepts https://<domain>/g/xxxxxx intents and redirects them to main app process.
|
||||
*/
|
||||
class IMHentaiUrlActivity : Activity() {
|
||||
class GalleryAdultsUrlActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
|
@ -19,17 +18,17 @@ class IMHentaiUrlActivity : Activity() {
|
|||
val id = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "id:$id")
|
||||
putExtra("query", "${GalleryAdults.PREFIX_ID_SEARCH}$id")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("IMHentaiUrlActivity", e.toString())
|
||||
Log.e("GalleryAdultsUrl", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("IMHentaiUrlActivity", "could not parse uri from intent $intent")
|
||||
Log.e("GalleryAdultsUrl", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
|
@ -0,0 +1,144 @@
|
|||
package eu.kanade.tachiyomi.multisrc.galleryadults
|
||||
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
|
||||
// any space except after a comma (we're going to replace spaces only between words)
|
||||
val regexSpaceNotAfterComma = Regex("""(?<!,)\s+""")
|
||||
|
||||
// extract preceding minus (-) and term
|
||||
val regexExcludeTerm = Regex("""^(-?)"?(.+)"?""")
|
||||
|
||||
val regexTagCountNumber = Regex("\\([0-9,]*\\)")
|
||||
val regexDateSuffix = Regex("""\d(st|nd|rd|th)""")
|
||||
val regexDate = Regex("""\d\D\D""")
|
||||
val regexNotNumber = Regex("""\D""")
|
||||
val regexRelativeDateTime = Regex("""\d*[^0-9]*(\d+)""")
|
||||
|
||||
fun Element.imgAttr() = when {
|
||||
hasAttr("data-cfsrc") -> absUrl("data-cfsrc")
|
||||
hasAttr("data-src") -> absUrl("data-src")
|
||||
hasAttr("data-lazy-src") -> absUrl("data-lazy-src")
|
||||
hasAttr("srcset") -> absUrl("srcset").substringBefore(" ")
|
||||
else -> absUrl("src")
|
||||
}
|
||||
|
||||
fun Element.cleanTag(): String = text().cleanTag()
|
||||
fun String.cleanTag(): String = replace(regexTagCountNumber, "").trim()
|
||||
|
||||
// convert thumbnail URLs to full image URLs
|
||||
fun String.thumbnailToFull(): String {
|
||||
val ext = substringAfterLast(".")
|
||||
return replace("t.$ext", ".$ext")
|
||||
}
|
||||
|
||||
fun String?.toDate(simpleDateFormat: SimpleDateFormat?): Long {
|
||||
this ?: return 0L
|
||||
|
||||
return if (simpleDateFormat != null) {
|
||||
if (contains(regexDateSuffix)) {
|
||||
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
|
||||
split(" ").map {
|
||||
if (it.contains(regexDate)) {
|
||||
it.replace(regexNotNumber, "")
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.let { simpleDateFormat.tryParse(it.joinToString(" ")) }
|
||||
} else {
|
||||
simpleDateFormat.tryParse(this)
|
||||
}
|
||||
} else {
|
||||
parseDate(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(date: String?): Long {
|
||||
date ?: return 0L
|
||||
|
||||
return when {
|
||||
// Handle 'yesterday' and 'today', using midnight
|
||||
WordSet("yesterday", "يوم واحد").startsWith(date) -> {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DAY_OF_MONTH, -1) // yesterday
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
}
|
||||
WordSet("today", "just now").startsWith(date) -> {
|
||||
Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
}
|
||||
WordSet("يومين").startsWith(date) -> {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DAY_OF_MONTH, -2) // day before yesterday
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
}
|
||||
WordSet("ago", "atrás", "önce", "قبل").endsWith(date) -> {
|
||||
parseRelativeDate(date)
|
||||
}
|
||||
WordSet("hace").startsWith(date) -> {
|
||||
parseRelativeDate(date)
|
||||
}
|
||||
else -> 0L
|
||||
}
|
||||
}
|
||||
|
||||
// Parses dates in this form: 21 hours ago OR "2 days ago (Updated 19 hours ago)"
|
||||
private fun parseRelativeDate(date: String): Long {
|
||||
val number = regexRelativeDateTime.find(date)?.value?.toIntOrNull()
|
||||
?: date.split(" ").firstOrNull()
|
||||
?.replace("one", "1")
|
||||
?.replace("a", "1")
|
||||
?.toIntOrNull()
|
||||
?: return 0L
|
||||
val now = Calendar.getInstance()
|
||||
|
||||
// Sort by order
|
||||
return when {
|
||||
WordSet("detik", "segundo", "second", "วินาที").anyWordIn(date) ->
|
||||
now.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||
WordSet("menit", "dakika", "min", "minute", "minuto", "นาที", "دقائق").anyWordIn(date) ->
|
||||
now.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||
WordSet("jam", "saat", "heure", "hora", "hour", "ชั่วโมง", "giờ", "ore", "ساعة", "小时").anyWordIn(date) ->
|
||||
now.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||
WordSet("hari", "gün", "jour", "día", "dia", "day", "วัน", "ngày", "giorni", "أيام", "天").anyWordIn(date) ->
|
||||
now.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||
WordSet("week", "semana").anyWordIn(date) ->
|
||||
now.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
|
||||
WordSet("month", "mes").anyWordIn(date) ->
|
||||
now.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||
WordSet("year", "año").anyWordIn(date) ->
|
||||
now.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||
else -> 0L
|
||||
}
|
||||
}
|
||||
|
||||
private fun SimpleDateFormat.tryParse(string: String): Long {
|
||||
return try {
|
||||
parse(string)?.time ?: 0L
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
class WordSet(private vararg val words: String) {
|
||||
fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) }
|
||||
fun startsWith(dateString: String): Boolean = words.any { dateString.startsWith(it, ignoreCase = true) }
|
||||
fun endsWith(dateString: String): Boolean = words.any { dateString.endsWith(it, ignoreCase = true) }
|
||||
}
|
||||
|
||||
fun toBinary(boolean: Boolean) = if (boolean) "1" else "0"
|
|
@ -0,0 +1,5 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 6.7 KiB |
|
@ -0,0 +1,178 @@
|
|||
package eu.kanade.tachiyomi.multisrc.goda
|
||||
|
||||
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.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.Entities
|
||||
import rx.Observable
|
||||
|
||||
open class GoDa(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
) : HttpSource() {
|
||||
|
||||
override val supportsLatest get() = true
|
||||
|
||||
private val enableGenres = true
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private fun getKey(link: String): String {
|
||||
return link.substringAfter("/manga/").removeSuffix("/")
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/hots/page/$page", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup().also(::parseGenres)
|
||||
val mangas = document.select(".cardlist .pb-2 a").map { element ->
|
||||
SManga.create().apply {
|
||||
val imgSrc = element.selectFirst("img")!!.attr("src")
|
||||
url = getKey(element.attr("href"))
|
||||
title = element.selectFirst("h3")!!.ownText()
|
||||
thumbnail_url = if ("url=" in imgSrc) imgSrc.toHttpUrl().queryParameter("url")!! else imgSrc
|
||||
}
|
||||
}
|
||||
val nextPage = if (lang == "zh") "下一頁" else "NEXT"
|
||||
val hasNextPage = document.selectFirst("a[aria-label=$nextPage] button") != null
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/newss/page/$page", headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (query.isNotEmpty()) {
|
||||
val url = "$baseUrl/s".toHttpUrl().newBuilder()
|
||||
.addPathSegment(query)
|
||||
.addEncodedQueryParameter("page", "$page")
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
for (filter in filters) {
|
||||
if (filter is UriPartFilter) return GET(baseUrl + filter.toUriPart() + "/page/$page", headers)
|
||||
}
|
||||
return popularMangaRequest(page)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = "$baseUrl/manga/${manga.url}"
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(getMangaUrl(manga), headers)
|
||||
}
|
||||
|
||||
private fun Element.getMangaId() = selectFirst("#mangachapters")!!.attr("data-mid")
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val document = response.asJsoup().selectFirst("main")!!
|
||||
val titleElement = document.selectFirst("h1")!!
|
||||
val elements = titleElement.parent()!!.parent()!!.children()
|
||||
check(elements.size == 6)
|
||||
|
||||
title = titleElement.ownText()
|
||||
status = when (titleElement.child(0).text()) {
|
||||
"連載中", "Ongoing" -> SManga.ONGOING
|
||||
"完結" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
author = Entities.unescape(elements[1].children().drop(1).joinToString { it.text().removeSuffix(" ,") })
|
||||
genre = buildList {
|
||||
elements[2].children().drop(1).mapTo(this) { it.text().removeSuffix(" ,") }
|
||||
elements[3].children().mapTo(this) { it.text().removePrefix("#") }
|
||||
}.joinToString()
|
||||
description = (elements[4].text() + "\n\nID: ${document.getMangaId()}").trim()
|
||||
thumbnail_url = document.selectFirst("img.object-cover")!!.attr("src")
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
|
||||
val mangaId = manga.description
|
||||
?.substringAfterLast("ID: ", "")
|
||||
?.takeIf { it.toIntOrNull() != null }
|
||||
?: client.newCall(mangaDetailsRequest(manga)).execute().asJsoup().getMangaId()
|
||||
|
||||
fetchChapterList(mangaId)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
open fun fetchChapterList(mangaId: String): List<SChapter> {
|
||||
val response = client.newCall(GET("$baseUrl/manga/get?mid=$mangaId&mode=all", headers)).execute()
|
||||
|
||||
return response.asJsoup().select(".chapteritem").asReversed().map { element ->
|
||||
val anchor = element.selectFirst("a")!!
|
||||
SChapter.create().apply {
|
||||
url = getKey(anchor.attr("href")) + "#$mangaId/" + anchor.attr("data-cs")
|
||||
name = anchor.attr("data-ct")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/manga/" + chapter.url.substringBeforeLast('#')
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val id = chapter.url.substringAfterLast('#', "")
|
||||
val mangaId = id.substringBefore('/', "")
|
||||
val chapterId = id.substringAfter('/', "")
|
||||
return pageListRequest(mangaId, chapterId)
|
||||
}
|
||||
|
||||
open fun pageListRequest(mangaId: String, chapterId: String) = GET("$baseUrl/chapter/getcontent?m=$mangaId&c=$chapterId", headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
return document.select("#chapcontent > div > img").mapIndexed { index, element ->
|
||||
Page(index, imageUrl = element.attr("data-src").ifEmpty { element.attr("src") })
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
private var genres: Array<Pair<String, String>> = emptyArray()
|
||||
|
||||
private fun parseGenres(document: Document) {
|
||||
if (!enableGenres || genres.isNotEmpty()) return
|
||||
val box = document.selectFirst("h2")?.parent()?.parent() ?: return
|
||||
val items = box.select("a")
|
||||
genres = Array(items.size) { i ->
|
||||
val item = items[i]
|
||||
Pair(item.text().removePrefix("#"), item.attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList =
|
||||
if (!enableGenres) {
|
||||
FilterList()
|
||||
} else if (genres.isEmpty()) {
|
||||
FilterList(listOf(Filter.Header(if (lang == "zh") "点击“重置”刷新分类" else "Tap 'Reset' to load genres")))
|
||||
} else {
|
||||
val list = listOf(
|
||||
Filter.Header(if (lang == "zh") "分类(搜索文本时无效)" else "Filters are ignored when using text search."),
|
||||
UriPartFilter(if (lang == "zh") "分类" else "Genre", genres),
|
||||
)
|
||||
FilterList(list)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 23
|
||||
baseVersionCode = 24
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
|
|
@ -76,7 +76,7 @@ abstract class HeanCms(
|
|||
|
||||
protected open val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ", Locale.US)
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Origin", baseUrl)
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 4
|
||||
|
|
|
@ -223,42 +223,38 @@ abstract class Keyoapp(
|
|||
// Image list
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("#pages > img").map {
|
||||
val index = it.attr("count").toInt()
|
||||
Page(index, document.location(), it.imgAttr("150"))
|
||||
}
|
||||
return document.select("#pages > img")
|
||||
.map { it.imgAttr() }
|
||||
.filter { it.contains(imgCdnRegex) }
|
||||
.mapIndexed { index, img ->
|
||||
Page(index, document.location(), img)
|
||||
}
|
||||
}
|
||||
|
||||
private val imgCdnRegex = Regex("""^(https?:)?//cdn\d*\.keyoapp\.com""")
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
// Utilities
|
||||
|
||||
// From mangathemesia
|
||||
private fun Element.imgAttr(width: String): String {
|
||||
private fun Element.imgAttr(): String {
|
||||
val url = when {
|
||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||
hasAttr("data-src") -> attr("abs:data-src")
|
||||
else -> attr("abs:src")
|
||||
}
|
||||
return url.toHttpUrl()
|
||||
.newBuilder()
|
||||
.addQueryParameter("w", width)
|
||||
.build()
|
||||
.toString()
|
||||
return url
|
||||
}
|
||||
|
||||
private fun Element.getImageUrl(selector: String): String? {
|
||||
return this.selectFirst(selector)?.let {
|
||||
it.attr("style")
|
||||
return this.selectFirst(selector)?.let { element ->
|
||||
element.attr("style")
|
||||
.substringAfter(":url(", "")
|
||||
.substringBefore(")", "")
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.toHttpUrlOrNull()?.let {
|
||||
it.newBuilder()
|
||||
.setQueryParameter("w", "480")
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
?.toHttpUrlOrNull()?.newBuilder()?.setQueryParameter("w", "480")?.build()
|
||||
?.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 35
|
||||
baseVersionCode = 36
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:cryptoaes"))
|
||||
|
|
|
@ -612,6 +612,9 @@ abstract class Madara(
|
|||
"مكتمل",
|
||||
"已完结",
|
||||
"Tamamlandı",
|
||||
"Đã hoàn thành",
|
||||
"Завершено",
|
||||
"Tamamlanan",
|
||||
)
|
||||
|
||||
protected val ongoingStatusList: Array<String> = arrayOf(
|
||||
|
@ -619,6 +622,7 @@ abstract class Madara(
|
|||
"Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor",
|
||||
"Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision",
|
||||
"Curso", "En marcha", "Publicandose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
|
||||
"Đang làm", "Em postagem", "Devam Eden", "Em progresso",
|
||||
)
|
||||
|
||||
protected val hiatusStatusList: Array<String> = arrayOf(
|
||||
|
@ -626,12 +630,22 @@ abstract class Madara(
|
|||
"Pausado",
|
||||
"En espera",
|
||||
"Durduruldu",
|
||||
"Beklemede",
|
||||
"Đang chờ",
|
||||
"متوقف",
|
||||
"En Pause",
|
||||
"Заморожено",
|
||||
)
|
||||
|
||||
protected val canceledStatusList: Array<String> = arrayOf(
|
||||
"Canceled",
|
||||
"Cancelado",
|
||||
"İptal Edildi",
|
||||
"Güncel",
|
||||
"Đã hủy",
|
||||
"ملغي",
|
||||
"Abandonné",
|
||||
"Заброшено",
|
||||
)
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
|
@ -946,7 +960,11 @@ abstract class Madara(
|
|||
val imageUrl = element.selectFirst("img")?.let { imageFromElement(it) }
|
||||
Page(index, document.location(), imageUrl)
|
||||
}
|
||||
val chapterProtectorHtml = chapterProtector.html()
|
||||
val chapterProtectorHtml = chapterProtector.attr("src")
|
||||
.takeIf { it.startsWith("data:text/javascript;base64,") }
|
||||
?.substringAfter("data:text/javascript;base64,")
|
||||
?.let { Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) }
|
||||
?: chapterProtector.html()
|
||||
val password = chapterProtectorHtml
|
||||
.substringAfter("wpmangaprotectornonce='")
|
||||
.substringBefore("';")
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 13
|
||||
baseVersionCode = 14
|
||||
|
|
|
@ -21,6 +21,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
@ -29,24 +30,25 @@ import java.text.ParseException
|
|||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class MadTheme(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM dd, yyy", Locale.US),
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH),
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(1, 1)
|
||||
.rateLimit(1, 1, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
// TODO: better cookie sharing
|
||||
// TODO: don't count cached responses against rate limit
|
||||
private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(1, 12)
|
||||
.rateLimit(1, 12, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
|
@ -55,6 +57,8 @@ abstract class MadTheme(
|
|||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private var genreKey = "genre[]"
|
||||
|
||||
// Popular
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
searchMangaRequest(page, "", FilterList(OrderFilter(0)))
|
||||
|
@ -100,7 +104,7 @@ abstract class MadTheme(
|
|||
.filter { it.state }
|
||||
.let { list ->
|
||||
if (list.isNotEmpty()) {
|
||||
list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) }
|
||||
list.forEach { genre -> url.addQueryParameter(genreKey, genre.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,11 +124,11 @@ abstract class MadTheme(
|
|||
override fun searchMangaSelector(): String = ".book-detailed-item"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
|
||||
title = element.select("a").first()!!.attr("title")
|
||||
description = element.select(".summary").first()?.text()
|
||||
genre = element.select(".genres > *").joinToString { it.text() }
|
||||
thumbnail_url = element.select("img").first()!!.attr("abs:data-src")
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
|
||||
title = element.selectFirst("a")!!.attr("title")
|
||||
element.selectFirst(".summary")?.text()?.let { description = it }
|
||||
element.select(".genres > *").joinToString { it.text() }.takeIf { it.isNotEmpty() }?.let { genre = it }
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("abs:data-src")
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -135,23 +139,25 @@ abstract class MadTheme(
|
|||
|
||||
// Details
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
title = document.select(".detail h1").first()!!.text()
|
||||
title = document.selectFirst(".detail h1")!!.text()
|
||||
author = document.select(".detail .meta > p > strong:contains(Authors) ~ a").joinToString { it.text().trim(',', ' ') }
|
||||
genre = document.select(".detail .meta > p > strong:contains(Genres) ~ a").joinToString { it.text().trim(',', ' ') }
|
||||
thumbnail_url = document.select("#cover img").first()!!.attr("abs:data-src")
|
||||
thumbnail_url = document.selectFirst("#cover img")!!.attr("abs:data-src")
|
||||
|
||||
val altNames = document.select(".detail h2").first()?.text()
|
||||
val altNames = document.selectFirst(".detail h2")?.text()
|
||||
?.split(',', ';')
|
||||
?.mapNotNull { it.trim().takeIf { it != title } }
|
||||
?: listOf()
|
||||
|
||||
description = document.select(".summary .content").first()?.text() +
|
||||
description = document.select(".summary .content, .summary .content ~ p").text() +
|
||||
(altNames.takeIf { it.isNotEmpty() }?.let { "\n\nAlt name(s): ${it.joinToString()}" } ?: "")
|
||||
|
||||
val statusText = document.select(".detail .meta > p > strong:contains(Status) ~ a").first()!!.text()
|
||||
status = when (statusText.lowercase(Locale.US)) {
|
||||
val statusText = document.selectFirst(".detail .meta > p > strong:contains(Status) ~ a")!!.text()
|
||||
status = when (statusText.lowercase(Locale.ENGLISH)) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
"on-hold" -> SManga.ON_HIATUS
|
||||
"canceled" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
@ -187,7 +193,14 @@ abstract class MadTheme(
|
|||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request =
|
||||
GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
|
||||
MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)?.let { mangaId ->
|
||||
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("manga_id", mangaId)
|
||||
.addQueryParameter("manga_name", manga.title)
|
||||
.build()
|
||||
|
||||
GET(url, headers)
|
||||
} ?: GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (genresList == null) {
|
||||
|
@ -204,16 +217,25 @@ abstract class MadTheme(
|
|||
.absUrl("href")
|
||||
.removePrefix(baseUrl)
|
||||
|
||||
name = element.select(".chapter-title").first()!!.text()
|
||||
date_upload = parseChapterDate(element.select(".chapter-update").first()?.text())
|
||||
name = element.selectFirst(".chapter-title")!!.text()
|
||||
date_upload = parseChapterDate(element.selectFirst(".chapter-update")?.text())
|
||||
}
|
||||
|
||||
// Pages
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val html = document.html()
|
||||
val mangaId = MANGA_ID_REGEX.find(document.location())?.groupValues?.get(1)
|
||||
val chapterId = CHAPTER_ID_REGEX.find(document.html())?.groupValues?.get(1)
|
||||
|
||||
val html = if (mangaId != null && chapterId != null) {
|
||||
val url = GET("$baseUrl/service/backend/chapterServer/?server_id=1&chapter_id=$chapterId", headers)
|
||||
client.newCall(url).execute().body.string()
|
||||
} else {
|
||||
document.html()
|
||||
}
|
||||
val realDocument = Jsoup.parse(html, document.location())
|
||||
|
||||
if (!html.contains("var mainServer = \"")) {
|
||||
val chapterImagesFromHtml = document.select("#chapter-images img")
|
||||
val chapterImagesFromHtml = realDocument.select("#chapter-images img, .chapter-image[data-src]")
|
||||
|
||||
// 17/03/2023: Certain hosts only embed two pages in their "#chapter-images" and leave
|
||||
// the rest to be lazily(?) loaded by javascript. Let's extract `chapImages` and compare
|
||||
|
@ -292,7 +314,7 @@ abstract class MadTheme(
|
|||
}
|
||||
|
||||
return when {
|
||||
"ago".endsWith(date) -> {
|
||||
" ago" in date -> {
|
||||
parseRelativeDate(date)
|
||||
}
|
||||
else -> dateFormat.tryParse(date)
|
||||
|
@ -300,10 +322,12 @@ abstract class MadTheme(
|
|||
}
|
||||
|
||||
private fun parseRelativeDate(date: String): Long {
|
||||
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
|
||||
val number = NUMBER_REGEX.find(date)?.groupValues?.getOrNull(0)?.toIntOrNull() ?: return 0
|
||||
val cal = Calendar.getInstance()
|
||||
|
||||
return when {
|
||||
date.contains("year") -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||
date.contains("month") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||
date.contains("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||
date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||
date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||
|
@ -314,13 +338,21 @@ abstract class MadTheme(
|
|||
|
||||
// Dynamic genres
|
||||
private fun parseGenres(document: Document): List<Genre>? {
|
||||
return document.select(".checkbox-group.genres").first()?.select("label")?.map {
|
||||
Genre(it.select(".radio__label").first()!!.text(), it.select("input").`val`())
|
||||
return document.selectFirst(".checkbox-group.genres")?.select(".checkbox-wrapper")?.run {
|
||||
firstOrNull()?.selectFirst("input")?.attr("name")?.takeIf { it.isNotEmpty() }?.let { genreKey = it }
|
||||
map {
|
||||
Genre(it.selectFirst(".radio__label")!!.text(), it.selectFirst("input")!!.`val`())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(
|
||||
// TODO: Filters for sites that support it:
|
||||
// excluded genres
|
||||
// genre inclusion mode
|
||||
// bookmarks
|
||||
// author
|
||||
GenreFilter(getGenreList()),
|
||||
StatusFilter(),
|
||||
OrderFilter(),
|
||||
|
@ -352,6 +384,7 @@ abstract class MadTheme(
|
|||
Pair("Updated", "updated_at"),
|
||||
Pair("Created", "created_at"),
|
||||
Pair("Name A-Z", "name"),
|
||||
// Pair("Number of Chapters", "total_chapters"),
|
||||
Pair("Rating", "rating"),
|
||||
),
|
||||
state,
|
||||
|
@ -365,4 +398,10 @@ abstract class MadTheme(
|
|||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val MANGA_ID_REGEX = """/manga/(\d+)-""".toRegex()
|
||||
private val CHAPTER_ID_REGEX = """chapterId\s*=\s*(\d+)""".toRegex()
|
||||
private val NUMBER_REGEX = """\d+""".toRegex()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
baseVersionCode = 2
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
|
|
@ -73,7 +73,7 @@ abstract class MangaEsp(
|
|||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
private var comicsList = mutableListOf<SeriesDto>()
|
||||
protected var comicsList = mutableListOf<SeriesDto>()
|
||||
|
||||
override fun fetchSearchManga(
|
||||
page: Int,
|
||||
|
@ -93,7 +93,7 @@ abstract class MangaEsp(
|
|||
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||
|
||||
private fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
|
||||
protected open fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
||||
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
||||
|
@ -105,7 +105,7 @@ abstract class MangaEsp(
|
|||
|
||||
private var filteredList = mutableListOf<SeriesDto>()
|
||||
|
||||
private fun parseComicsList(page: Int, query: String, filterList: FilterList): MangasPage {
|
||||
protected open fun parseComicsList(page: Int, query: String, filterList: FilterList): MangasPage {
|
||||
if (page == 1) {
|
||||
filteredList.clear()
|
||||
|
||||
|
@ -228,21 +228,21 @@ abstract class MangaEsp(
|
|||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
private fun Element.imgAttr(): String = when {
|
||||
protected open fun Element.imgAttr(): String = when {
|
||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||
hasAttr("data-src") -> attr("abs:data-src")
|
||||
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
|
||||
else -> attr("abs:src")
|
||||
}
|
||||
|
||||
private fun String.unescape(): String {
|
||||
fun String.unescape(): String {
|
||||
return UNESCAPE_REGEX.replace(this, "$1")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val UNESCAPE_REGEX = """\\(.)""".toRegex()
|
||||
private val MANGA_LIST_REGEX = """self\.__next_f\.push\(.*data\\":(\[.*trending.*])\}""".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()
|
||||
private const val MANGAS_PER_PAGE = 15
|
||||
const val MANGAS_PER_PAGE = 15
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,9 @@ class SeriesDto(
|
|||
val trending: TrendingDto? = null,
|
||||
@SerialName("autors") private val authors: List<AuthorDto> = emptyList(),
|
||||
private val artists: List<ArtistDto> = emptyList(),
|
||||
|
||||
@Suppress("unused") // Used in some sources
|
||||
@SerialName("idioma")
|
||||
val language: String? = null,
|
||||
) {
|
||||
fun toSManga(): SManga {
|
||||
return SManga.create().apply {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".en.anchira.AnchiraUrlActivity"
|
||||
android:name="eu.kanade.tachiyomi.multisrc.peachscan.PeachScanUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
|
@ -12,10 +12,10 @@
|
|||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="anchira.to" />
|
||||
<data android:pathPattern="/g/.*/..*" />
|
||||
<data
|
||||
android:host="${SOURCEHOST}"
|
||||
android:pathPattern="/..*"
|
||||
android:scheme="${SOURCESCHEME}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
||||
baseVersionCode = 9
|
||||
|
||||
dependencies {
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.graphics.Canvas
|
|||
import android.graphics.Rect
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -28,6 +29,7 @@ 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
|
||||
|
@ -82,6 +84,18 @@ abstract class PeachScan(
|
|||
|
||||
override fun latestUpdatesNextPageSelector() = null
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith(URL_SEARCH_PREFIX)) {
|
||||
val manga = SManga.create().apply { url = query.substringAfter(URL_SEARCH_PREFIX) }
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
MangasPage(listOf(mangaDetailsParse(it).apply { url = manga.url }), false)
|
||||
}
|
||||
}
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegments("auto-complete/")
|
||||
|
@ -153,21 +167,31 @@ abstract class PeachScan(
|
|||
}.getOrDefault(0L)
|
||||
}
|
||||
|
||||
private val urlsRegex = """const\s+urls\s*=\s*\[(.*?)]\s*;""".toRegex()
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val scriptElement = document.selectFirst("script:containsData(const urls =[)")
|
||||
val scriptElement = document.selectFirst("script:containsData(const urls)")
|
||||
?: return document.select("#imageContainer img").mapIndexed { i, it ->
|
||||
Page(i, imageUrl = it.attr("abs:src"))
|
||||
Page(i, document.location(), it.attr("abs:src"))
|
||||
}
|
||||
|
||||
val urls = scriptElement.html().substringAfter("const urls =[").substringBefore("];")
|
||||
val urls = urlsRegex.find(scriptElement.data())?.groupValues?.get(1)
|
||||
?: throw Exception("Could not find image URLs")
|
||||
|
||||
return urls.split(",").mapIndexed { i, it ->
|
||||
Page(i, imageUrl = baseUrl + it.trim().removeSurrounding("'") + "#page")
|
||||
Page(i, document.location(), baseUrl + it.trim().removeSurrounding("'") + "#page")
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val imgHeaders = headersBuilder()
|
||||
.add("Referer", page.url)
|
||||
.build()
|
||||
return GET(page.imageUrl!!, imgHeaders)
|
||||
}
|
||||
|
||||
private val dataUriRegex = Regex("""base64,([0-9a-zA-Z/+=\s]+)""")
|
||||
|
||||
private fun zipImageInterceptor(chain: Interceptor.Chain): Response {
|
||||
|
@ -188,7 +212,7 @@ abstract class PeachScan(
|
|||
val entryIndex = splitEntryName.first().toInt()
|
||||
val entryType = splitEntryName.last()
|
||||
|
||||
val imageData = if (entryType == "avif") {
|
||||
val imageData = if (entryType == "avif" || splitEntryName.size == 1) {
|
||||
zis.readBytes()
|
||||
} else {
|
||||
val svgBytes = zis.readBytes()
|
||||
|
@ -251,4 +275,8 @@ abstract class PeachScan(
|
|||
|
||||
memInfo.totalMem < 3L * 1024 * 1024 * 1024
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val URL_SEARCH_PREFIX = "slug:"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package eu.kanade.tachiyomi.multisrc.peachscan
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class PeachScanUrlActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val path = intent?.data?.path
|
||||
if (path != null) {
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${PeachScan.URL_SEARCH_PREFIX}$path")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("PeachScanUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("PeachScanUrlActivity", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
|
@ -212,8 +212,8 @@ abstract class ZeistManga(
|
|||
protected open val useNewChapterFeed = false
|
||||
protected open val useOldChapterFeed = false
|
||||
|
||||
private val chapterFeedRegex = """clwd\.run\(["'](.*?)["']\)""".toRegex()
|
||||
private val scriptSelector = "#clwd > script"
|
||||
protected open val chapterFeedRegex = """clwd\.run\(["'](.*?)["']\)""".toRegex()
|
||||
protected open val scriptSelector = "#clwd > script"
|
||||
|
||||
open fun getChapterFeedUrl(doc: Document): String {
|
||||
if (useNewChapterFeed) return newChapterFeedUrl(doc)
|
||||
|
@ -434,6 +434,6 @@ abstract class ZeistManga(
|
|||
|
||||
companion object {
|
||||
private const val maxMangaResults = 20
|
||||
private const val maxChapterResults = 999999
|
||||
const val maxChapterResults = 999999
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'AHottie'
|
||||
extClass = '.AHottie'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 22 KiB |
|
@ -0,0 +1,122 @@
|
|||
package eu.kanade.tachiyomi.extension.all.ahottie
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class AHottie() : ParsedHttpSource() {
|
||||
override val baseUrl = "https://ahottie.net"
|
||||
override val lang = "all"
|
||||
override val name = "AHottie"
|
||||
override val supportsLatest = false
|
||||
|
||||
// Popular
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
thumbnail_url = element.select(".relative img").attr("src")
|
||||
genre = element.select(".flex a").joinToString(", ") {
|
||||
it.text()
|
||||
}
|
||||
title = element.select("h2").text()
|
||||
setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
initialized = true
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a[rel=next]"
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "#main > div > div"
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET(
|
||||
baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("search")
|
||||
addQueryParameter("kw", query)
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build(),
|
||||
headers,
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
// Details
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
title = document.select("h1").text()
|
||||
genre = document.select("div.pl-3 > a").joinToString(", ") {
|
||||
it.text()
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
var doc = document
|
||||
while (true) {
|
||||
doc.select("#main img.block").map {
|
||||
pages.add(Page(pages.size, imageUrl = it.attr("src")))
|
||||
}
|
||||
val nextPageUrl = doc.select("a[rel=next]").attr("abs:href")
|
||||
if (nextPageUrl.isEmpty()) break
|
||||
doc = client.newCall(GET(nextPageUrl, headers)).execute().asJsoup()
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.select("link[rel=canonical]").attr("abs:href"))
|
||||
chapter_number = 0F
|
||||
name = "GALLERY"
|
||||
date_upload = getDate(element.select("time").text())
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "html"
|
||||
|
||||
// Pages
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector(): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private fun getDate(str: String): Long {
|
||||
return try {
|
||||
DATE_FORMAT.parse(str)?.time ?: 0L
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMAT by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
ext {
|
||||
extName = 'AsmHentai'
|
||||
extClass = '.ASMHFactory'
|
||||
extVersionCode = 1
|
||||
themePkg = 'galleryadults'
|
||||
baseUrl = 'https://asmhentai.com'
|
||||
overrideVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package eu.kanade.tachiyomi.extension.all.asmhentai
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class ASMHFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
AsmHentai("en", "english"),
|
||||
AsmHentai("ja", "japanese"),
|
||||
AsmHentai("zh", "chinese"),
|
||||
AsmHentai("all", ""),
|
||||
AsmHentai("en", GalleryAdults.LANGUAGE_ENGLISH),
|
||||
AsmHentai("ja", GalleryAdults.LANGUAGE_JAPANESE),
|
||||
AsmHentai("zh", GalleryAdults.LANGUAGE_CHINESE),
|
||||
AsmHentai("all", GalleryAdults.LANGUAGE_MULTI),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,274 +1,103 @@
|
|||
package eu.kanade.tachiyomi.extension.all.asmhentai
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.Genre
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
|
||||
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.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
open class AsmHentai(override val lang: String, private val tlTag: String) : ParsedHttpSource() {
|
||||
class AsmHentai(
|
||||
lang: String = "all",
|
||||
override val mangaLang: String = LANGUAGE_MULTI,
|
||||
) : GalleryAdults(
|
||||
"AsmHentai",
|
||||
"https://asmhentai.com",
|
||||
lang = lang,
|
||||
) {
|
||||
override val supportsLatest = mangaLang.isNotBlank()
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
override fun Element.mangaUrl() =
|
||||
selectFirst(".image a")?.attr("abs:href")
|
||||
|
||||
override val baseUrl = "https://asmhentai.com"
|
||||
override fun Element.mangaThumbnail() =
|
||||
selectFirst(".image img")?.imgAttr()
|
||||
|
||||
override val name = "AsmHentai"
|
||||
override fun Element.mangaLang() =
|
||||
select("a:has(.flag)").attr("href")
|
||||
.removeSuffix("/").substringAfterLast("/")
|
||||
|
||||
override val supportsLatest = false
|
||||
override fun popularMangaSelector() = ".preview_item"
|
||||
|
||||
// Popular
|
||||
override val favoritePath = "inc/user.php?act=favs"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
if (tlTag.isNotEmpty()) addPathSegments("language/$tlTag/")
|
||||
if (page > 1) addQueryParameter("page", page.toString())
|
||||
}
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector(): String = ".preview_item"
|
||||
|
||||
private fun Element.mangaTitle() = select("h2").text()
|
||||
|
||||
private fun Element.mangaUrl() = select(".image a").attr("abs:href")
|
||||
|
||||
private fun Element.mangaThumbnail() = select(".image img").attr("abs:src")
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
title = element.mangaTitle()
|
||||
setUrlWithoutDomain(element.mangaUrl())
|
||||
thumbnail_url = element.mangaThumbnail()
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String = "li.active + li:not(.disabled)"
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector(): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return when {
|
||||
query.startsWith(PREFIX_ID_SEARCH) -> {
|
||||
val id = query.removePrefix(PREFIX_ID_SEARCH)
|
||||
client.newCall(searchMangaByIdRequest(id))
|
||||
.asObservableSuccess()
|
||||
.map { response -> searchMangaByIdParse(response, id) }
|
||||
override fun Element.getInfo(tag: String): String {
|
||||
return select(".tags:contains($tag:) .tag_list a")
|
||||
.joinToString {
|
||||
val name = it.selectFirst(".tag")?.ownText() ?: ""
|
||||
if (tag.contains(regexTag)) {
|
||||
genres[name] = it.attr("href")
|
||||
.removeSuffix("/").substringAfterLast('/')
|
||||
}
|
||||
listOf(
|
||||
name,
|
||||
it.select(".split_tag").text()
|
||||
.removePrefix("| ")
|
||||
.trim(),
|
||||
)
|
||||
.filter { s -> s.isNotBlank() }
|
||||
.joinToString()
|
||||
}
|
||||
query.toIntOrNull() != null -> {
|
||||
client.newCall(searchMangaByIdRequest(query))
|
||||
.asObservableSuccess()
|
||||
.map { response -> searchMangaByIdParse(response, query) }
|
||||
}
|
||||
|
||||
override fun Element.getInfoPages(document: Document?) =
|
||||
selectFirst(".book_page .pages h3")?.ownText()
|
||||
|
||||
override val mangaDetailInfoSelector = ".book_page"
|
||||
|
||||
/**
|
||||
* [totalPagesSelector] only exists if pages > 10
|
||||
*/
|
||||
override val totalPagesSelector = "t_pages"
|
||||
|
||||
override val galleryIdSelector = "load_id"
|
||||
override val thumbnailSelector = ".preview_thumb"
|
||||
|
||||
override val idPrefixUri = "g"
|
||||
override val pageUri = "gallery"
|
||||
|
||||
override fun pageRequestForm(document: Document, totalPages: String, loadedPages: Int): FormBody {
|
||||
val token = document.select("[name=csrf-token]").attr("content")
|
||||
|
||||
return FormBody.Builder()
|
||||
.add("id", document.inputIdValueOf(loadIdSelector))
|
||||
.add("dir", document.inputIdValueOf(loadDirSelector))
|
||||
.add("visible_pages", loadedPages.toString())
|
||||
.add("t_pages", totalPages)
|
||||
.add("type", "2") // 1 would be "more", 2 is "all remaining"
|
||||
.apply {
|
||||
if (token.isNotBlank()) add("_token", token)
|
||||
}
|
||||
else -> super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
// any space except after a comma (we're going to replace spaces only between words)
|
||||
private val spaceRegex = Regex("""(?<!,)\s+""")
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val tags = (filters.last() as TagFilter).state
|
||||
|
||||
val q = when {
|
||||
tags.isBlank() -> query
|
||||
query.isBlank() -> tags
|
||||
else -> "$query,$tags"
|
||||
}.replace(spaceRegex, "+")
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegments("search/")
|
||||
addEncodedQueryParameter("q", q)
|
||||
if (page > 1) addQueryParameter("page", page.toString())
|
||||
}
|
||||
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
private class SMangaDto(
|
||||
val title: String,
|
||||
val url: String,
|
||||
val thumbnail: String,
|
||||
val lang: String,
|
||||
)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val doc = response.asJsoup()
|
||||
|
||||
val mangas = doc.select(searchMangaSelector())
|
||||
.map {
|
||||
SMangaDto(
|
||||
title = it.mangaTitle(),
|
||||
url = it.mangaUrl(),
|
||||
thumbnail = it.mangaThumbnail(),
|
||||
lang = it.select("a:has(.flag)").attr("href").removeSuffix("/").substringAfterLast("/"),
|
||||
override fun tagsParser(document: Document): List<Genre> {
|
||||
return document.select(".tags_page .tags a.tag")
|
||||
.mapNotNull {
|
||||
Genre(
|
||||
it.ownText(),
|
||||
it.attr("href")
|
||||
.removeSuffix("/").substringAfterLast('/'),
|
||||
)
|
||||
}
|
||||
.let { unfiltered ->
|
||||
if (tlTag.isNotEmpty()) unfiltered.filter { it.lang == tlTag } else unfiltered
|
||||
}
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
title = it.title
|
||||
setUrlWithoutDomain(it.url)
|
||||
thumbnail_url = it.thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, doc.select(searchMangaNextPageSelector()).isNotEmpty())
|
||||
}
|
||||
|
||||
private fun searchMangaByIdRequest(id: String) = GET("$baseUrl/g/$id/", headers)
|
||||
|
||||
private fun searchMangaByIdParse(response: Response, id: String): MangasPage {
|
||||
val details = mangaDetailsParse(response)
|
||||
details.url = "/g/$id/"
|
||||
return MangasPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Details
|
||||
|
||||
private fun Element.get(tag: String): String {
|
||||
return select(".tags:contains($tag) .tag").joinToString { it.ownText() }
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
document.select(".book_page").first()!!.let { element ->
|
||||
thumbnail_url = element.select(".cover img").attr("abs:src")
|
||||
title = element.select("h1").text()
|
||||
genre = element.get("Tags")
|
||||
artist = element.get("Artists")
|
||||
author = artist
|
||||
description = listOf("Parodies", "Groups", "Languages", "Category")
|
||||
.mapNotNull { tag ->
|
||||
element.get(tag).let { if (it.isNotEmpty()) "$tag: $it" else null }
|
||||
}
|
||||
.joinToString("\n", postfix = "\n") +
|
||||
element.select(".pages h3").text() +
|
||||
element.select("h1 + h2").text()
|
||||
.let { altTitle -> if (altTitle.isNotEmpty()) "\nAlternate Title: $altTitle" else "" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return Observable.just(
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
name = "Chapter"
|
||||
url = manga.url
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun chapterListSelector(): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
// convert thumbnail URLs to full image URLs
|
||||
private fun String.full(): String {
|
||||
val fType = substringAfterLast("t")
|
||||
return replace("t$fType", fType)
|
||||
}
|
||||
|
||||
private fun Document.inputIdValueOf(string: String): String {
|
||||
return select("input[id=$string]").attr("value")
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val thumbUrls = document.select(".preview_thumb img")
|
||||
.map { it.attr("abs:data-src") }
|
||||
.toMutableList()
|
||||
|
||||
// input only exists if pages > 10 and have to make a request to get the other thumbnails
|
||||
val totalPages = document.inputIdValueOf("t_pages")
|
||||
|
||||
if (totalPages.isNotEmpty()) {
|
||||
val token = document.select("[name=csrf-token]").attr("content")
|
||||
|
||||
val form = FormBody.Builder()
|
||||
.add("_token", token)
|
||||
.add("id", document.inputIdValueOf("load_id"))
|
||||
.add("dir", document.inputIdValueOf("load_dir"))
|
||||
.add("visible_pages", "10")
|
||||
.add("t_pages", totalPages)
|
||||
.add("type", "2") // 1 would be "more", 2 is "all remaining"
|
||||
.build()
|
||||
|
||||
val xhrHeaders = headers.newBuilder()
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
client.newCall(POST("$baseUrl/inc/thumbs_loader.php", xhrHeaders, form))
|
||||
.execute()
|
||||
.asJsoup()
|
||||
.select("img")
|
||||
.mapTo(thumbUrls) { it.attr("abs:data-src") }
|
||||
}
|
||||
return thumbUrls.mapIndexed { i, url -> Page(i, "", url.full()) }
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
Filter.Header("Separate tags with commas (,)"),
|
||||
TagFilter(),
|
||||
override fun getFilterList() = FilterList(
|
||||
listOf(
|
||||
Filter.Header("HINT: Separate search term with comma (,)"),
|
||||
Filter.Header("String query search doesn't support Sort"),
|
||||
) + super.getFilterList().list,
|
||||
)
|
||||
|
||||
class TagFilter : Filter.Text("Tags")
|
||||
|
||||
companion object {
|
||||
const val PREFIX_ID_SEARCH = "id:"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,9 @@
|
|||
<data
|
||||
android:pathPattern="/subject-overview/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:pathPattern="/title/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Bato.to'
|
||||
extClass = '.BatoToFactory'
|
||||
extVersionCode = 35
|
||||
extVersionCode = 36
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -42,8 +42,25 @@ class BatoToUrlActivity : Activity() {
|
|||
|
||||
private fun fromBatoTo(pathSegments: MutableList<String>): String? {
|
||||
return if (pathSegments.size >= 2) {
|
||||
val id = pathSegments[1]
|
||||
"ID:$id"
|
||||
val path = pathSegments[1] as java.lang.String?
|
||||
if (path != null) {
|
||||
var index = -1
|
||||
for (i in path.indices) {
|
||||
if (path[i] == '-') {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
val id = if (index == -1) {
|
||||
path
|
||||
} else {
|
||||
path.substring(0, index)
|
||||
}
|
||||
"ID:$id"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = '3600000 Beauty'
|
||||
extClass = '.Beauty3600000'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,175 @@
|
|||
package eu.kanade.tachiyomi.extension.all.beauty3600000
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
class Beauty3600000 : ParsedHttpSource() {
|
||||
override val baseUrl = "https://3600000.xyz"
|
||||
override val lang = "all"
|
||||
override val name = "3600000 Beauty"
|
||||
override val supportsLatest = false
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||
|
||||
// Popular
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
thumbnail_url = element.select("a img.ls_lazyimg").attr("file")
|
||||
title = element.select(".entry-title").text()
|
||||
setUrlWithoutDomain(element.select(".entry-title > a").attr("abs:href"))
|
||||
status = SManga.COMPLETED
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = ".next"
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/page/$page/", headers)
|
||||
override fun popularMangaSelector() = "#blog-entries > article"
|
||||
|
||||
// Search
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val tagFilter = filterList.findInstance<TagFilter>()!!
|
||||
val categoryFilter = filterList.findInstance<CategoryFilter>()!!
|
||||
var searchQuery = query
|
||||
val searchPath: String = when {
|
||||
tagFilter.state.isNotEmpty() -> "$baseUrl/tag/${tagFilter.state}/page/$page/"
|
||||
categoryFilter.state != 0 -> "$baseUrl/category/${categoryFilter.toUriPart()}/page/$page/"
|
||||
query.startsWith("tag:") -> {
|
||||
tagFilter.state = searchQuery.substringAfter("tag:")
|
||||
searchQuery = ""
|
||||
"$baseUrl/tag/${tagFilter.state}/page/$page/"
|
||||
}
|
||||
else -> "$baseUrl/page/$page/"
|
||||
}
|
||||
return when {
|
||||
searchQuery.isNotEmpty() -> GET(
|
||||
searchPath.toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("s", searchQuery)
|
||||
}.build(),
|
||||
headers,
|
||||
)
|
||||
else -> GET(searchPath, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
// Details
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val main = document.selectFirst("#main")!!
|
||||
title = main.select(".entry-title").text()
|
||||
description = main.select(".entry-title").text()
|
||||
genre = getGenres(document).joinToString(", ")
|
||||
thumbnail_url = main.select(".entry-content img.ls_lazyimg").attr("file")
|
||||
status = SManga.COMPLETED
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
}
|
||||
|
||||
private fun getGenres(element: Element): List<String> {
|
||||
val genres = mutableListOf<String>()
|
||||
element.select(".cat-links a").forEach {
|
||||
genres.add(it.text())
|
||||
}
|
||||
element.select(".tags-links a").forEach {
|
||||
val tag = it.attr("href").toHttpUrl().pathSegments[1]
|
||||
genres.add("tag:$tag")
|
||||
}
|
||||
return genres
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.select("link[rel=\"shortlink\"]").attr("href"))
|
||||
name = "Gallery"
|
||||
date_upload = getDate(element.select("#main time").attr("datetime"))
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "html"
|
||||
|
||||
// Pages
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("noscript").remove()
|
||||
document.select(".entry-content img").forEachIndexed { i, it ->
|
||||
val itUrl = it.select("img.ls_lazyimg").attr("file")
|
||||
pages.add(Page(i, imageUrl = itUrl))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
// Filters
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
Filter.Header("NOTE: Only one filter will be applied!"),
|
||||
Filter.Separator(),
|
||||
CategoryFilter(),
|
||||
TagFilter(),
|
||||
)
|
||||
|
||||
open class UriPartFilter(
|
||||
displayName: String,
|
||||
private val valuePair: Array<Pair<String, String>>,
|
||||
) : Filter.Select<String>(displayName, valuePair.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = valuePair[state].second
|
||||
}
|
||||
|
||||
class CategoryFilter : UriPartFilter(
|
||||
"Category",
|
||||
arrayOf(
|
||||
Pair("Any", ""),
|
||||
Pair("Gravure", "gravure"),
|
||||
Pair("Aidol", "aidol"),
|
||||
Pair("Magazine", "magazine"),
|
||||
Pair("Korea", "korea"),
|
||||
Pair("Thailand", "thailand"),
|
||||
Pair("Chinese", "chinese"),
|
||||
Pair("Japan", "japan"),
|
||||
Pair("China", "china"),
|
||||
Pair("Uncategorized", "uncategorized"),
|
||||
Pair("Magazine", "magazine"),
|
||||
Pair("Photobook", "photobook"),
|
||||
Pair("Western", "western"),
|
||||
),
|
||||
)
|
||||
|
||||
class TagFilter : Filter.Text("Tag")
|
||||
|
||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||
|
||||
private fun getDate(str: String): Long {
|
||||
return try {
|
||||
DATE_FORMAT.parse(str).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMAT by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Comick'
|
||||
extClass = '.ComickFactory'
|
||||
extVersionCode = 42
|
||||
extVersionCode = 46
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -21,11 +21,17 @@ import kotlinx.serialization.decodeFromString
|
|||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.min
|
||||
|
||||
abstract class Comick(
|
||||
|
@ -155,9 +161,31 @@ abstract class Comick(
|
|||
}
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.rateLimit(3, 1)
|
||||
.addNetworkInterceptor(::errorInterceptor)
|
||||
.rateLimit(3, 1, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private fun errorInterceptor(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
if (
|
||||
response.isSuccessful ||
|
||||
"application/json" !in response.header("Content-Type").orEmpty()
|
||||
) {
|
||||
return response
|
||||
}
|
||||
|
||||
val error = try {
|
||||
response.parseAs<Error>()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
error?.run {
|
||||
throw Exception("$name error $statusCode: $message")
|
||||
} ?: throw Exception("HTTP error ${response.code}")
|
||||
}
|
||||
|
||||
/** Popular Manga **/
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = "$apiUrl/v1.0/search?sort=follow&limit=$LIMIT&page=$page&tachiyomi=true"
|
||||
|
@ -301,7 +329,7 @@ abstract class Comick(
|
|||
is TagFilter -> {
|
||||
if (it.state.isNotEmpty()) {
|
||||
it.state.split(",").forEach {
|
||||
addQueryParameter("tags", it.trim())
|
||||
addQueryParameter("tags", it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-").replace("'-", "-and-039-").replace("'", "-and-039-"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -398,13 +426,31 @@ abstract class Comick(
|
|||
.substringBefore("/chapters")
|
||||
.substringAfter(apiUrl)
|
||||
|
||||
val currentTimestamp = System.currentTimeMillis()
|
||||
|
||||
return chapterListResponse.chapters
|
||||
.filter {
|
||||
it.groups.map { g -> g.lowercase() }.intersect(preferences.ignoredGroups).isEmpty()
|
||||
val publishTime = try {
|
||||
publishedDateFormat.parse(it.publishedAt)!!.time
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
|
||||
val publishedChapter = publishTime <= currentTimestamp
|
||||
|
||||
val noGroupBlock = it.groups.map { g -> g.lowercase() }
|
||||
.intersect(preferences.ignoredGroups)
|
||||
.isEmpty()
|
||||
|
||||
publishedChapter && noGroupBlock
|
||||
}
|
||||
.map { it.toSChapter(mangaUrl) }
|
||||
}
|
||||
|
||||
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}"
|
||||
}
|
||||
|
@ -453,6 +499,7 @@ abstract class Comick(
|
|||
|
||||
companion object {
|
||||
const val SLUG_SEARCH_PREFIX = "id:"
|
||||
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
||||
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
||||
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
||||
private const val INCLUDE_MU_TAGS_DEFAULT = false
|
||||
|
|
|
@ -170,7 +170,8 @@ class Chapter(
|
|||
private val hid: String,
|
||||
private val lang: String = "",
|
||||
private val title: String = "",
|
||||
@SerialName("created_at") val createdAt: String = "",
|
||||
@SerialName("created_at") private val createdAt: String = "",
|
||||
@SerialName("publish_at") val publishedAt: String = "",
|
||||
private val chap: String = "",
|
||||
private val vol: String = "",
|
||||
@SerialName("group_name") val groups: List<String> = emptyList(),
|
||||
|
@ -197,3 +198,9 @@ class ChapterPageData(
|
|||
class Page(
|
||||
val url: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Error(
|
||||
val statusCode: Int,
|
||||
val message: String,
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ import java.util.Locale
|
|||
import java.util.TimeZone
|
||||
|
||||
private val dateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH).apply {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.ENGLISH).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Danbooru'
|
||||
extClass = '.Danbooru'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -47,54 +47,52 @@ class Danbooru : ParsedHttpSource() {
|
|||
override fun popularMangaSelector(): String =
|
||||
searchMangaSelector()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = Request(
|
||||
url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder().run {
|
||||
setEncodedQueryParameter("search[category]", "series")
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder()
|
||||
|
||||
filters.forEach {
|
||||
when (it) {
|
||||
is FilterTags -> if (it.state.isNotBlank()) {
|
||||
addQueryParameter("search[post_tags_match]", it.state)
|
||||
}
|
||||
url.setEncodedQueryParameter("search[category]", "series")
|
||||
|
||||
is FilterDescription -> if (it.state.isNotBlank()) {
|
||||
addQueryParameter("search[description_matches]", it.state)
|
||||
}
|
||||
|
||||
is FilterIsDeleted -> if (it.state) {
|
||||
addEncodedQueryParameter("search[is_deleted]", "true")
|
||||
}
|
||||
|
||||
is FilterCategory -> {
|
||||
setEncodedQueryParameter("search[category]", it.selected)
|
||||
}
|
||||
|
||||
is FilterOrder -> if (it.selected != null) {
|
||||
addEncodedQueryParameter("search[order]", it.selected)
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unrecognized filter")
|
||||
filters.forEach {
|
||||
when (it) {
|
||||
is FilterTags -> if (it.state.isNotBlank()) {
|
||||
url.addQueryParameter("search[post_tags_match]", it.state)
|
||||
}
|
||||
|
||||
is FilterDescription -> if (it.state.isNotBlank()) {
|
||||
url.addQueryParameter("search[description_matches]", it.state)
|
||||
}
|
||||
|
||||
is FilterIsDeleted -> if (it.state) {
|
||||
url.addEncodedQueryParameter("search[is_deleted]", "true")
|
||||
}
|
||||
|
||||
is FilterCategory -> {
|
||||
url.setEncodedQueryParameter("search[category]", it.selected)
|
||||
}
|
||||
|
||||
is FilterOrder -> if (it.selected != null) {
|
||||
url.addEncodedQueryParameter("search[order]", it.selected)
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unrecognized filter")
|
||||
}
|
||||
}
|
||||
|
||||
addEncodedQueryParameter("page", page.toString())
|
||||
url.addEncodedQueryParameter("page", page.toString())
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
addQueryParameter("search[name_contains]", query)
|
||||
}
|
||||
if (query.isNotBlank()) {
|
||||
url.addQueryParameter("search[name_contains]", query)
|
||||
}
|
||||
|
||||
build()
|
||||
},
|
||||
|
||||
headers = headers,
|
||||
)
|
||||
return GET(url.build(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector(): String =
|
||||
".post-preview"
|
||||
"article.post-preview"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
url = element.selectFirst(".post-preview-link")?.attr("href")!!
|
||||
title = element.selectFirst(".desc")?.text() ?: ""
|
||||
title = element.selectFirst("div.text-center")?.text() ?: ""
|
||||
|
||||
thumbnail_url = element.selectFirst("source")?.attr("srcset")
|
||||
?.substringAfterLast(',')?.trim()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
ext {
|
||||
extName = 'EternalMangas'
|
||||
extClass = '.EternalMangas'
|
||||
extClass = '.EternalMangasFactory'
|
||||
themePkg = 'mangaesp'
|
||||
baseUrl = 'https://eternalmangas.com'
|
||||
overrideVersionCode = 0
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
@ -0,0 +1,82 @@
|
|||
package eu.kanade.tachiyomi.extension.all.eternalmangas
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp
|
||||
import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto
|
||||
import eu.kanade.tachiyomi.multisrc.mangaesp.TopSeriesDto
|
||||
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.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Response
|
||||
|
||||
open class EternalMangas(
|
||||
lang: String,
|
||||
private val internalLang: String,
|
||||
) : MangaEsp(
|
||||
"EternalMangas",
|
||||
"https://eternalmangas.com",
|
||||
lang,
|
||||
) {
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val body = response.body.string()
|
||||
val responseData = json.decodeFromString<TopSeriesDto>(body)
|
||||
|
||||
val topDaily = responseData.response.topDaily.flatten().map { it.data }
|
||||
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
|
||||
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
|
||||
|
||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
|
||||
.filter { it.language == internalLang }
|
||||
.map { it.toSManga() }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
|
||||
val mangas = responseData.updates[internalLang]?.flatten()?.map { it.toSManga() } ?: emptyList()
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
||||
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
||||
?: throw Exception(intl["comics_list_error"])
|
||||
val unescapedJson = jsonString.unescape()
|
||||
comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson)
|
||||
.filter { it.language == internalLang }
|
||||
.toMutableList()
|
||||
return parseComicsList(page, query, filters)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
var document = response.asJsoup()
|
||||
|
||||
document.selectFirst("body > form[method=post]")?.let {
|
||||
val action = it.attr("action")
|
||||
val inputs = it.select("input")
|
||||
|
||||
val form = FormBody.Builder()
|
||||
inputs.forEach { input ->
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class LatestUpdatesDto(
|
||||
val updates: Map<String, List<List<SeriesDto>>>,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package eu.kanade.tachiyomi.extension.all.eternalmangas
|
||||
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class EternalMangasFactory : SourceFactory {
|
||||
override fun createSources() = listOf(
|
||||
EternalMangasES(),
|
||||
EternalMangasEN(),
|
||||
EternalMangasPTBR(),
|
||||
)
|
||||
}
|
||||
|
||||
class EternalMangasES : EternalMangas("es", "es")
|
||||
class EternalMangasEN : EternalMangas("en", "en")
|
||||
class EternalMangasPTBR : EternalMangas("pt-BR", "pt")
|
|
@ -0,0 +1,10 @@
|
|||
ext {
|
||||
extName = 'HentaiFox'
|
||||
extClass = '.HentaiFoxFactory'
|
||||
themePkg = 'galleryadults'
|
||||
baseUrl = 'https://hentaifox.com'
|
||||
overrideVersionCode = 6
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,103 @@
|
|||
package eu.kanade.tachiyomi.extension.all.hentaifox
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.toDate
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import okhttp3.HttpUrl
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class HentaiFox(
|
||||
lang: String = "all",
|
||||
override val mangaLang: String = LANGUAGE_MULTI,
|
||||
) : GalleryAdults(
|
||||
"HentaiFox",
|
||||
"https://hentaifox.com",
|
||||
lang = lang,
|
||||
) {
|
||||
override val supportsLatest = mangaLang.isNotBlank()
|
||||
|
||||
private val languages: List<Pair<String, String>> = listOf(
|
||||
Pair(LANGUAGE_ENGLISH, "1"),
|
||||
Pair(LANGUAGE_TRANSLATED, "2"),
|
||||
Pair(LANGUAGE_JAPANESE, "5"),
|
||||
Pair(LANGUAGE_CHINESE, "6"),
|
||||
Pair(LANGUAGE_KOREAN, "11"),
|
||||
)
|
||||
private val langCode = languages.firstOrNull { lang -> lang.first == mangaLang }?.second
|
||||
|
||||
override fun Element.mangaLang() = attr("data-languages")
|
||||
.split(' ').let {
|
||||
when {
|
||||
it.contains(langCode) -> mangaLang
|
||||
// search result doesn't have "data-languages" which will return a list with 1 blank element
|
||||
it.size > 1 || (it.size == 1 && it.first().isNotBlank()) -> "other"
|
||||
// if we don't know which language to filter then set to mangaLang to not filter at all
|
||||
else -> mangaLang
|
||||
}
|
||||
}
|
||||
|
||||
override val useShortTitlePreference = false
|
||||
override fun Element.mangaTitle(selector: String): String? = mangaFullTitle(selector)
|
||||
|
||||
override fun Element.getInfo(tag: String): String {
|
||||
return select("ul.${tag.lowercase()} a")
|
||||
.joinToString {
|
||||
val name = it.ownText()
|
||||
if (tag.contains(regexTag)) {
|
||||
genres[name] = it.attr("href")
|
||||
.removeSuffix("/").substringAfterLast('/')
|
||||
}
|
||||
listOf(
|
||||
name,
|
||||
it.select(".split_tag").text()
|
||||
.removePrefix("| ")
|
||||
.trim(),
|
||||
)
|
||||
.filter { s -> s.isNotBlank() }
|
||||
.joinToString()
|
||||
}
|
||||
}
|
||||
|
||||
override fun Element.getTime(): Long =
|
||||
selectFirst(".pages:contains(Posted:)")?.ownText()
|
||||
?.removePrefix("Posted: ")
|
||||
.toDate(simpleDateFormat)
|
||||
|
||||
override fun HttpUrl.Builder.addPageUri(page: Int): HttpUrl.Builder {
|
||||
val url = toString()
|
||||
when {
|
||||
url == "$baseUrl/" && page == 2 ->
|
||||
addPathSegments("page/$page")
|
||||
url.contains('?') ->
|
||||
addQueryParameter("page", page.toString())
|
||||
else ->
|
||||
addPathSegments("pag/$page")
|
||||
}
|
||||
addPathSegment("") // trailing slash (/)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert space( ) typed in search-box into plus(+) in URL. Then:
|
||||
* - ignore the word preceding by a special character (e.g. 'school-girl' will ignore 'girl')
|
||||
* => replace to plus(+),
|
||||
* - use plus(+) for separate terms, as AND condition.
|
||||
* - use double quote(") to search for exact match.
|
||||
*/
|
||||
override fun buildQueryString(tags: List<String>, query: String): String {
|
||||
val regexSpecialCharacters = Regex("""[^a-zA-Z0-9"]+(?=[a-zA-Z0-9"])""")
|
||||
return (tags + query + mangaLang).filterNot { it.isBlank() }.joinToString("+") {
|
||||
it.trim().replace(regexSpecialCharacters, "+")
|
||||
}
|
||||
}
|
||||
|
||||
override val favoritePath = "includes/user_favs.php"
|
||||
override val pagesRequest = "includes/thumbs_loader.php"
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
listOf(
|
||||
Filter.Header("HINT: Use double quote (\") for exact match"),
|
||||
) + super.getFilterList().list,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package eu.kanade.tachiyomi.extension.all.hentaifox
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class HentaiFoxFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
HentaiFox("en", GalleryAdults.LANGUAGE_ENGLISH),
|
||||
HentaiFox("ja", GalleryAdults.LANGUAGE_JAPANESE),
|
||||
HentaiFox("zh", GalleryAdults.LANGUAGE_CHINESE),
|
||||
HentaiFox("ko", GalleryAdults.LANGUAGE_KOREAN),
|
||||
HentaiFox("all", GalleryAdults.LANGUAGE_MULTI),
|
||||
)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Hitomi'
|
||||
extClass = '.HitomiFactory'
|
||||
extVersionCode = 26
|
||||
extVersionCode = 28
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
package eu.kanade.tachiyomi.extension.all.hitomi
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
typealias OrderType = Pair<String?, String>
|
||||
typealias ParsedFilter = Pair<String, OrderType>
|
||||
|
||||
private fun parseFilter(query: StringBuilder, area: String, filterState: String) {
|
||||
filterState
|
||||
.trim()
|
||||
.split(',')
|
||||
.filter { it.isNotBlank() }
|
||||
.forEach {
|
||||
val trimmed = it.trim()
|
||||
val negativePrefix = if (trimmed.startsWith("-")) "-" else ""
|
||||
query.append(" $negativePrefix$area:${trimmed.removePrefix("-").replace(" ", "_")}")
|
||||
}
|
||||
}
|
||||
|
||||
fun parseFilters(filters: FilterList): ParsedFilter {
|
||||
val query = StringBuilder()
|
||||
var order: OrderType = Pair("date", "added")
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> {
|
||||
order = filter.getOrder
|
||||
}
|
||||
is AreaFilter -> {
|
||||
parseFilter(query, filter.getAreaName, filter.state)
|
||||
}
|
||||
else -> { /* Do Nothing */ }
|
||||
}
|
||||
}
|
||||
return Pair(query.toString(), order)
|
||||
}
|
||||
|
||||
private class OrderFilter(val name: String, val order: OrderType) {
|
||||
val getFilterName: String
|
||||
get() = name
|
||||
val getOrder: OrderType
|
||||
get() = order
|
||||
}
|
||||
|
||||
private class SortFilter : UriPartFilter(
|
||||
"Sort By",
|
||||
arrayOf(
|
||||
OrderFilter("Date Added", Pair(null, "index")),
|
||||
OrderFilter("Date Published", Pair("date", "published")),
|
||||
OrderFilter("Popular: Today", Pair("popular", "today")),
|
||||
OrderFilter("Popular: Week", Pair("popular", "week")),
|
||||
OrderFilter("Popular: Month", Pair("popular", "month")),
|
||||
OrderFilter("Popular: Year", Pair("popular", "year")),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<OrderFilter>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.getFilterName }.toTypedArray()) {
|
||||
val getOrder: OrderType
|
||||
get() = vals[state].getOrder
|
||||
}
|
||||
|
||||
private class AreaFilter(displayName: String, val areaName: String) :
|
||||
Filter.Text(displayName) {
|
||||
val getAreaName: String
|
||||
get() = areaName
|
||||
}
|
||||
|
||||
fun getFilterListInternal(): FilterList = FilterList(
|
||||
SortFilter(),
|
||||
Filter.Header("Separate tags with commas (,)"),
|
||||
Filter.Header("Prepend with dash (-) to exclude"),
|
||||
AreaFilter("Artist(s)", "artist"),
|
||||
AreaFilter("Character(s)", "character"),
|
||||
AreaFilter("Group(s)", "group"),
|
||||
AreaFilter("Series", "series"),
|
||||
AreaFilter("Female Tag(s)", "female"),
|
||||
AreaFilter("Male Tag(s)", "male"),
|
||||
Filter.Header("Don't put Female/Male tags here, they won't work!"),
|
||||
AreaFilter("Tag(s)", "tag"),
|
||||
)
|
|
@ -1,8 +1,12 @@
|
|||
package eu.kanade.tachiyomi.extension.all.hitomi
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -22,6 +26,8 @@ import okhttp3.Call
|
|||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
@ -35,7 +41,7 @@ import kotlin.math.min
|
|||
class Hitomi(
|
||||
override val lang: String,
|
||||
private val nozomiLang: String,
|
||||
) : HttpSource() {
|
||||
) : ConfigurableSource, HttpSource() {
|
||||
|
||||
override val name = "Hitomi"
|
||||
|
||||
|
@ -51,6 +57,12 @@ class Hitomi(
|
|||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private var iconified = preferences.getBoolean(PREF_TAG_GENDER_ICON, false)
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("referer", "$baseUrl/")
|
||||
.set("origin", baseUrl)
|
||||
|
@ -76,11 +88,13 @@ class Hitomi(
|
|||
private lateinit var searchResponse: List<Int>
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.fromCallable {
|
||||
val parsedFilter = parseFilters(filters)
|
||||
|
||||
runBlocking {
|
||||
if (page == 1) {
|
||||
searchResponse = hitomiSearch(
|
||||
query.trim(),
|
||||
filters.filterIsInstance<SortFilter>().firstOrNull()?.state == 1,
|
||||
"$query${parsedFilter.first}".trim(),
|
||||
parsedFilter.second,
|
||||
nozomiLang,
|
||||
).toList()
|
||||
}
|
||||
|
@ -93,11 +107,7 @@ class Hitomi(
|
|||
}
|
||||
}
|
||||
|
||||
private class SortFilter : Filter.Select<String>("Sort By", arrayOf("Updated", "Popularity"))
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
return FilterList(SortFilter())
|
||||
}
|
||||
override fun getFilterList(): FilterList = getFilterListInternal()
|
||||
|
||||
private fun Int.nextPageRange(): LongRange {
|
||||
val byteOffset = ((this - 1) * 25) * 4L
|
||||
|
@ -117,7 +127,7 @@ class Hitomi(
|
|||
|
||||
private suspend fun hitomiSearch(
|
||||
query: String,
|
||||
sortByPopularity: Boolean = false,
|
||||
order: OrderType,
|
||||
language: String = "all",
|
||||
): Set<Int> =
|
||||
coroutineScope {
|
||||
|
@ -126,9 +136,6 @@ class Hitomi(
|
|||
.replace(Regex("""^\?"""), "")
|
||||
.lowercase()
|
||||
.split(Regex("\\s+"))
|
||||
.map {
|
||||
it.replace('_', ' ')
|
||||
}
|
||||
|
||||
val positiveTerms = LinkedList<String>()
|
||||
val negativeTerms = LinkedList<String>()
|
||||
|
@ -144,7 +151,7 @@ class Hitomi(
|
|||
val positiveResults = positiveTerms.map {
|
||||
async {
|
||||
runCatching {
|
||||
getGalleryIDsForQuery(it, language)
|
||||
getGalleryIDsForQuery(it, language, order)
|
||||
}.getOrDefault(emptySet())
|
||||
}
|
||||
}
|
||||
|
@ -152,14 +159,13 @@ class Hitomi(
|
|||
val negativeResults = negativeTerms.map {
|
||||
async {
|
||||
runCatching {
|
||||
getGalleryIDsForQuery(it, language)
|
||||
getGalleryIDsForQuery(it, language, order)
|
||||
}.getOrDefault(emptySet())
|
||||
}
|
||||
}
|
||||
|
||||
val results = when {
|
||||
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", language)
|
||||
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", language)
|
||||
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(order.first, order.second, language)
|
||||
else -> emptySet()
|
||||
}.toMutableSet()
|
||||
|
||||
|
@ -191,6 +197,7 @@ class Hitomi(
|
|||
private suspend fun getGalleryIDsForQuery(
|
||||
query: String,
|
||||
language: String = "all",
|
||||
order: OrderType,
|
||||
): Set<Int> {
|
||||
query.replace("_", " ").let {
|
||||
if (it.indexOf(':') > -1) {
|
||||
|
@ -213,6 +220,20 @@ class Hitomi(
|
|||
}
|
||||
}
|
||||
|
||||
if (area != null) {
|
||||
if (order.first != null) {
|
||||
area = "$area/${order.first}"
|
||||
if (tag.isBlank()) {
|
||||
tag = order.second
|
||||
} else {
|
||||
area = "$area/${order.second}"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
area = order.first
|
||||
tag = order.second
|
||||
}
|
||||
|
||||
return getGalleryIDsFromNozomi(area, tag, lang)
|
||||
}
|
||||
|
||||
|
@ -429,7 +450,7 @@ class Hitomi(
|
|||
url = galleryurl
|
||||
author = groups?.joinToString { it.formatted }
|
||||
artist = artists?.joinToString { it.formatted }
|
||||
genre = tags?.joinToString { it.formatted }
|
||||
genre = tags?.joinToString { it.getFormatted(iconified) }
|
||||
thumbnail_url = files.first().let {
|
||||
val hash = it.hash
|
||||
val imageId = imageIdFromHash(hash)
|
||||
|
@ -444,7 +465,8 @@ class Hitomi(
|
|||
parodys?.joinToString { it.formatted }?.let {
|
||||
append("Parodies: ", it, "\n")
|
||||
}
|
||||
append("Pages: ", files.size)
|
||||
append("Pages: ", files.size, "\n")
|
||||
append("Language: ", language)
|
||||
}
|
||||
status = SManga.COMPLETED
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
|
@ -598,9 +620,28 @@ class Hitomi(
|
|||
|
||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_TAG_GENDER_ICON
|
||||
title = "Show gender as text or icon in tags (requires refresh)"
|
||||
summaryOff = "Show gender as text"
|
||||
summaryOn = "Show gender as icon"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
iconified = newValue == true
|
||||
true
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
companion object {
|
||||
private const val PREF_TAG_GENDER_ICON = "pref_tag_gender_icon"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ data class Gallery(
|
|||
val title: String,
|
||||
val date: String,
|
||||
val type: String,
|
||||
val language: String,
|
||||
val tags: List<Tag>?,
|
||||
val artists: List<Artist>?,
|
||||
val groups: List<Group>?,
|
||||
|
@ -28,10 +29,10 @@ data class Tag(
|
|||
val male: JsonPrimitive?,
|
||||
val tag: String,
|
||||
) {
|
||||
val formatted get() = if (female?.content == "1") {
|
||||
"${tag.toCamelCase()} (Female)"
|
||||
fun getFormatted(iconified: Boolean) = if (female?.content == "1") {
|
||||
tag.toCamelCase() + if (iconified) " ♀" else " (Female)"
|
||||
} else if (male?.content == "1") {
|
||||
"${tag.toCamelCase()} (Male)"
|
||||
tag.toCamelCase() + if (iconified) " ♂" else " (Male)"
|
||||
} else {
|
||||
tag.toCamelCase()
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
ext {
|
||||
extName = 'IMHentai'
|
||||
extClass = '.IMHentaiFactory'
|
||||
extVersionCode = 14
|
||||
themePkg = 'galleryadults'
|
||||
baseUrl = 'https://imhentai.xxx'
|
||||
overrideVersionCode = 15
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
package eu.kanade.tachiyomi.extension.all.imhentai
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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 kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
|
||||
class IMHentai(override val lang: String, private val imhLang: String) : ParsedHttpSource() {
|
||||
|
||||
override val baseUrl: String = "https://imhentai.xxx"
|
||||
override val name: String = "IMHentai"
|
||||
class IMHentai(
|
||||
lang: String = "all",
|
||||
override val mangaLang: String = LANGUAGE_MULTI,
|
||||
) : GalleryAdults(
|
||||
"IMHentai",
|
||||
"https://imhentai.xxx",
|
||||
lang = lang,
|
||||
) {
|
||||
override val supportsLatest = true
|
||||
override val useIntermediateSearch: Boolean = true
|
||||
override val supportAdvancedSearch: Boolean = true
|
||||
override val supportSpeechless: Boolean = true
|
||||
|
||||
override fun Element.mangaLang() =
|
||||
select("a:has(.thumb_flag)").attr("href")
|
||||
.removeSuffix("/").substringAfterLast("/")
|
||||
.let {
|
||||
// Include Speechless in search results
|
||||
if (it == LANGUAGE_SPEECHLESS) mangaLang else it
|
||||
}
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
.newBuilder()
|
||||
|
@ -57,271 +57,32 @@ class IMHentai(override val lang: String, private val imhLang: String) : ParsedH
|
|||
},
|
||||
).build()
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
thumbnail_url = element.selectFirst(".inner_thumb img")?.let {
|
||||
it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src")
|
||||
}
|
||||
with(element.select(".caption a")) {
|
||||
url = this.attr("href")
|
||||
title = this.text()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String = ".pagination li a:contains(Next):not([tabindex])"
|
||||
|
||||
override fun popularMangaSelector(): String = ".thumbs_container .thumb"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = searchMangaRequest(page, "", getFilterList(SORT_ORDER_POPULAR))
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(page, "", getFilterList(SORT_ORDER_LATEST))
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularMangaSelector()
|
||||
|
||||
// Search
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith("id:")) {
|
||||
val id = query.substringAfter("id:")
|
||||
return client.newCall(GET("$baseUrl/gallery/$id/"))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val manga = mangaDetailsParse(response)
|
||||
manga.url = "/gallery/$id/"
|
||||
MangasPage(listOf(manga), false)
|
||||
/* Details */
|
||||
override fun Element.getInfo(tag: String): String {
|
||||
return select("li:has(.tags_text:contains($tag:)) a.tag")
|
||||
.joinToString {
|
||||
val name = it.ownText()
|
||||
if (tag.contains(regexTag)) {
|
||||
genres[name] = it.attr("href")
|
||||
.removeSuffix("/").substringAfterLast('/')
|
||||
}
|
||||
}
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
|
||||
|
||||
private fun toBinary(boolean: Boolean) = if (boolean) "1" else "0"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (filters.any { it is LanguageFilters && it.state.any { it.name == LANGUAGE_SPEECHLESS && it.state } }) { // edge case for language = speechless
|
||||
val url = "$baseUrl/language/speechless/".toHttpUrl().newBuilder()
|
||||
|
||||
if ((if (filters.isEmpty()) getFilterList() else filters).filterIsInstance<SortOrderFilter>()[0].state == 0) {
|
||||
url.addPathSegment("popular")
|
||||
listOf(
|
||||
name,
|
||||
it.select(".split_tag").text()
|
||||
.trim()
|
||||
.removePrefix("| "),
|
||||
)
|
||||
.filter { s -> s.isNotBlank() }
|
||||
.joinToString()
|
||||
}
|
||||
return GET(url.build())
|
||||
} else {
|
||||
val url = "$baseUrl/search".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("key", query)
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter(getLanguageURIByName(imhLang).uri, toBinary(true)) // main language always enabled
|
||||
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is LanguageFilters -> {
|
||||
filter.state.forEach {
|
||||
url.addQueryParameter(it.uri, toBinary(it.state))
|
||||
}
|
||||
}
|
||||
is CategoryFilters -> {
|
||||
filter.state.forEach {
|
||||
url.addQueryParameter(it.uri, toBinary(it.state))
|
||||
}
|
||||
}
|
||||
is SortOrderFilter -> {
|
||||
getSortOrderURIs().forEachIndexed { index, pair ->
|
||||
url.addQueryParameter(pair.second, toBinary(filter.state == index))
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return GET(url.build())
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
||||
override fun Element.getCover() =
|
||||
selectFirst(".left_cover img")?.imgAttr()
|
||||
|
||||
// Details
|
||||
override val mangaDetailInfoSelector = ".gallery_first"
|
||||
|
||||
private fun Elements.csvText(splitTagSeparator: String = ", "): String {
|
||||
return this.joinToString {
|
||||
listOf(
|
||||
it.ownText(),
|
||||
it.select(".split_tag").text()
|
||||
.trim()
|
||||
.removePrefix("| "),
|
||||
)
|
||||
.filter { s -> !s.isNullOrBlank() }
|
||||
.joinToString(splitTagSeparator)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
title = document.selectFirst("div.right_details > h1")!!.text()
|
||||
|
||||
thumbnail_url = document.selectFirst("div.left_cover img")?.let {
|
||||
it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src")
|
||||
}
|
||||
|
||||
val mangaInfoElement = document.select(".galleries_info")
|
||||
val infoMap = mangaInfoElement.select("li:not(.pages)").associate {
|
||||
it.select("span.tags_text").text().removeSuffix(":") to it.select(".tag")
|
||||
}
|
||||
|
||||
artist = infoMap["Artists"]?.csvText(" | ")
|
||||
|
||||
author = artist
|
||||
|
||||
genre = infoMap["Tags"]?.csvText()
|
||||
|
||||
status = SManga.COMPLETED
|
||||
|
||||
val pages = mangaInfoElement.select("li.pages").text().substringAfter("Pages: ")
|
||||
val altTitle = document.select(".subtitle").text().ifBlank { null }
|
||||
|
||||
description = listOf(
|
||||
"Parodies",
|
||||
"Characters",
|
||||
"Groups",
|
||||
"Languages",
|
||||
"Category",
|
||||
).map { it to infoMap[it]?.csvText() }
|
||||
.let { listOf(Pair("Alternate Title", altTitle)) + it + listOf(Pair("Pages", pages)) }
|
||||
.filter { !it.second.isNullOrEmpty() }
|
||||
.joinToString("\n\n") { "${it.first}:\n${it.second}" }
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return listOf(
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(response.request.url.toString().replace("gallery", "view") + "1")
|
||||
name = "Chapter"
|
||||
chapter_number = 1f
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
|
||||
|
||||
override fun chapterListSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
// Pages
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val imageDir = document.select("#image_dir").`val`()
|
||||
val galleryId = document.select("#gallery_id").`val`()
|
||||
val uId = document.select("#u_id").`val`().toInt()
|
||||
|
||||
val randomServer = when (uId) {
|
||||
in 1..274825 -> "m1.imhentai.xxx"
|
||||
in 274826..403818 -> "m2.imhentai.xxx"
|
||||
in 403819..527143 -> "m3.imhentai.xxx"
|
||||
in 527144..632481 -> "m4.imhentai.xxx"
|
||||
in 632482..816010 -> "m5.imhentai.xxx"
|
||||
in 816011..970098 -> "m6.imhentai.xxx"
|
||||
in 970099..1121113 -> "m7.imhentai.xxx"
|
||||
else -> "m8.imhentai.xxx"
|
||||
}
|
||||
|
||||
val images = json.parseToJsonElement(
|
||||
document.selectFirst("script:containsData(var g_th)")!!.data()
|
||||
.substringAfter("$.parseJSON('").substringBefore("');").trim(),
|
||||
).jsonObject
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
for (image in images) {
|
||||
val iext = image.value.toString().replace("\"", "").split(",")[0]
|
||||
val iextPr = when (iext) {
|
||||
"p" -> "png"
|
||||
"b" -> "bmp"
|
||||
"g" -> "gif"
|
||||
else -> "jpg"
|
||||
}
|
||||
pages.add(Page(image.key.toInt() - 1, "", "https://$randomServer/$imageDir/$galleryId/${image.key}.$iextPr"))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
// Filters
|
||||
|
||||
private class SortOrderFilter(sortOrderURIs: List<Pair<String, String>>, state: Int) :
|
||||
Filter.Select<String>("Sort By", sortOrderURIs.map { it.first }.toTypedArray(), state)
|
||||
|
||||
private open class SearchFlagFilter(name: String, val uri: String, state: Boolean = true) : Filter.CheckBox(name, state)
|
||||
private class LanguageFilter(name: String, uri: String = name) : SearchFlagFilter(name, uri, false)
|
||||
private class LanguageFilters(flags: List<LanguageFilter>) : Filter.Group<LanguageFilter>("Other Languages", flags)
|
||||
private class CategoryFilters(flags: List<SearchFlagFilter>) : Filter.Group<SearchFlagFilter>("Categories", flags)
|
||||
|
||||
override fun getFilterList() = getFilterList(SORT_ORDER_DEFAULT)
|
||||
|
||||
private fun getFilterList(sortOrderState: Int) = FilterList(
|
||||
SortOrderFilter(getSortOrderURIs(), sortOrderState),
|
||||
CategoryFilters(getCategoryURIs()),
|
||||
LanguageFilters(getLanguageURIs().filter { it.name != imhLang }), // exclude main lang
|
||||
Filter.Header("Speechless language: ignores all filters except \"Popular\" and \"Latest\" in Sorting Filter"),
|
||||
)
|
||||
|
||||
private fun getCategoryURIs() = listOf(
|
||||
SearchFlagFilter("Manga", "manga"),
|
||||
SearchFlagFilter("Doujinshi", "doujinshi"),
|
||||
SearchFlagFilter("Western", "western"),
|
||||
SearchFlagFilter("Image Set", "imageset"),
|
||||
SearchFlagFilter("Artist CG", "artistcg"),
|
||||
SearchFlagFilter("Game CG", "gamecg"),
|
||||
)
|
||||
|
||||
// update sort order indices in companion object if order is changed
|
||||
private fun getSortOrderURIs() = listOf(
|
||||
Pair("Popular", "pp"),
|
||||
Pair("Latest", "lt"),
|
||||
Pair("Downloads", "dl"),
|
||||
Pair("Top Rated", "tr"),
|
||||
)
|
||||
|
||||
private fun getLanguageURIs() = listOf(
|
||||
LanguageFilter(LANGUAGE_ENGLISH, "en"),
|
||||
LanguageFilter(LANGUAGE_JAPANESE, "jp"),
|
||||
LanguageFilter(LANGUAGE_SPANISH, "es"),
|
||||
LanguageFilter(LANGUAGE_FRENCH, "fr"),
|
||||
LanguageFilter(LANGUAGE_KOREAN, "kr"),
|
||||
LanguageFilter(LANGUAGE_GERMAN, "de"),
|
||||
LanguageFilter(LANGUAGE_RUSSIAN, "ru"),
|
||||
LanguageFilter(LANGUAGE_SPEECHLESS, ""),
|
||||
)
|
||||
|
||||
private fun getLanguageURIByName(name: String): LanguageFilter {
|
||||
return getLanguageURIs().first { it.name == name }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
// references to sort order indices
|
||||
private const val SORT_ORDER_POPULAR = 0
|
||||
private const val SORT_ORDER_LATEST = 1
|
||||
private const val SORT_ORDER_DEFAULT = SORT_ORDER_POPULAR
|
||||
|
||||
// references to be used in factory
|
||||
const val LANGUAGE_ENGLISH = "English"
|
||||
const val LANGUAGE_JAPANESE = "Japanese"
|
||||
const val LANGUAGE_SPANISH = "Spanish"
|
||||
const val LANGUAGE_FRENCH = "French"
|
||||
const val LANGUAGE_KOREAN = "Korean"
|
||||
const val LANGUAGE_GERMAN = "German"
|
||||
const val LANGUAGE_RUSSIAN = "Russian"
|
||||
const val LANGUAGE_SPEECHLESS = "Speechless"
|
||||
}
|
||||
/* Pages */
|
||||
override val thumbnailSelector = ".gthumb"
|
||||
override val pageUri = "view"
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
package eu.kanade.tachiyomi.extension.all.imhentai
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class IMHentaiFactory : SourceFactory {
|
||||
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
IMHentai("en", IMHentai.LANGUAGE_ENGLISH),
|
||||
IMHentai("ja", IMHentai.LANGUAGE_JAPANESE),
|
||||
IMHentai("es", IMHentai.LANGUAGE_SPANISH),
|
||||
IMHentai("fr", IMHentai.LANGUAGE_FRENCH),
|
||||
IMHentai("ko", IMHentai.LANGUAGE_KOREAN),
|
||||
IMHentai("de", IMHentai.LANGUAGE_GERMAN),
|
||||
IMHentai("ru", IMHentai.LANGUAGE_RUSSIAN),
|
||||
IMHentai("en", GalleryAdults.LANGUAGE_ENGLISH),
|
||||
IMHentai("ja", GalleryAdults.LANGUAGE_JAPANESE),
|
||||
IMHentai("es", GalleryAdults.LANGUAGE_SPANISH),
|
||||
IMHentai("fr", GalleryAdults.LANGUAGE_FRENCH),
|
||||
IMHentai("ko", GalleryAdults.LANGUAGE_KOREAN),
|
||||
IMHentai("de", GalleryAdults.LANGUAGE_GERMAN),
|
||||
IMHentai("ru", GalleryAdults.LANGUAGE_RUSSIAN),
|
||||
IMHentai("all", GalleryAdults.LANGUAGE_MULTI),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
ext {
|
||||
extName = 'Manga18Me'
|
||||
extClass = '.M18MFactory'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,84 @@
|
|||
package eu.kanade.tachiyomi.extension.all.manga18me
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
fun getFilters(): FilterList {
|
||||
return FilterList(
|
||||
Filter.Header(name = "The filter is ignored when using text search."),
|
||||
GenreFilter("Genre", getGenresList),
|
||||
SortFilter("Sort", getSortsList),
|
||||
RawFilter("Raw"),
|
||||
CompletedFilter("Completed"),
|
||||
)
|
||||
}
|
||||
|
||||
/** Filters **/
|
||||
|
||||
internal class GenreFilter(name: String, genreList: List<Pair<String, String>>, state: Int = 0) :
|
||||
SelectFilter(name, genreList, state)
|
||||
|
||||
internal class SortFilter(name: String, sortList: List<Pair<String, String>>, state: Int = 0) :
|
||||
SelectFilter(name, sortList, state)
|
||||
|
||||
internal class CompletedFilter(name: String) : CheckBoxFilter(name)
|
||||
|
||||
internal class RawFilter(name: String) : CheckBoxFilter(name)
|
||||
|
||||
internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name)
|
||||
|
||||
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0) :
|
||||
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
||||
fun getValue() = vals[state].second
|
||||
}
|
||||
|
||||
/** Filters Data **/
|
||||
private val getGenresList: List<Pair<String, String>> = listOf(
|
||||
Pair("Manga", "manga"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Mature", "mature"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Adult", "adult"),
|
||||
Pair("Hentai", "hentai"),
|
||||
Pair("Comedy", "comedy"),
|
||||
Pair("Ecchi", "ecchi"),
|
||||
Pair("School Life", "school-life"),
|
||||
Pair("Shounen", "shounen"),
|
||||
Pair("Slice of Life", "slice-of-life"),
|
||||
Pair("Seinen", "seinen"),
|
||||
Pair("Yuri", "yuri"),
|
||||
Pair("Action", "action"),
|
||||
Pair("Fantasy", "fantasy"),
|
||||
Pair("Harem", "harem"),
|
||||
Pair("Supernatural", "supernatural"),
|
||||
Pair("Sci-Fi", "sci-fi"),
|
||||
Pair("Isekai", "isekai"),
|
||||
Pair("Shoujo", "shoujo"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Psychological", "psychological"),
|
||||
Pair("Smut", "smut"),
|
||||
Pair("Tragedy", "tragedy"),
|
||||
Pair("Raw", "raw"),
|
||||
Pair("Historical", "historical"),
|
||||
Pair("Adventure", "adventure"),
|
||||
Pair("Martial Arts", "martial-arts"),
|
||||
Pair("Manhwa", "manhwa"),
|
||||
Pair("Manhua", "manhua"),
|
||||
Pair("Mystery", "mystery"),
|
||||
Pair("BL", "bl"),
|
||||
Pair("Yaoi", "yaoi"),
|
||||
Pair("Gender Bender", "gender-bender"),
|
||||
Pair("Thriller", "thriller"),
|
||||
Pair("Josei", "josei"),
|
||||
Pair("Sports", "sports"),
|
||||
Pair("GL", "gl"),
|
||||
Pair("Family", "family"),
|
||||
Pair("Magic", "magic"),
|
||||
)
|
||||
|
||||
private val getSortsList: List<Pair<String, String>> = listOf(
|
||||
Pair("Latest", "latest"),
|
||||
Pair("A-Z", "alphabet"),
|
||||
Pair("Rating", "rating"),
|
||||
Pair("Trending", "trending"),
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
package eu.kanade.tachiyomi.extension.all.manga18me
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class M18MFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> =
|
||||
listOf(
|
||||
Manga18Me("all"),
|
||||
Manga18Me("en"),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
package eu.kanade.tachiyomi.extension.all.manga18me
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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 eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.lang.Exception
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
open class Manga18Me(override val lang: String) : ParsedHttpSource() {
|
||||
|
||||
override val name = "Manga18.me"
|
||||
|
||||
override val baseUrl = "https://manga18.me"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("Referer", "$baseUrl/")
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/manga/$page?orderby=trending", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val entries = document.select(popularMangaSelector())
|
||||
val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null
|
||||
|
||||
if (lang == "en") {
|
||||
val searchText = document.selectFirst("div.section-heading h1")?.text() ?: ""
|
||||
val raw = document.selectFirst("div.canonical")?.attr("href") ?: ""
|
||||
return MangasPage(
|
||||
entries
|
||||
.filter { it ->
|
||||
val title = it.selectFirst("div.item-thumb.wleft a")?.attr("href") ?: ""
|
||||
|
||||
searchText.lowercase().contains("raw") ||
|
||||
raw.contains("raw") ||
|
||||
!title.contains("raw")
|
||||
}
|
||||
.map(::popularMangaFromElement),
|
||||
hasNextPage,
|
||||
)
|
||||
}
|
||||
|
||||
return MangasPage(entries.map(::popularMangaFromElement), hasNextPage)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.page-item-detail"
|
||||
override fun popularMangaNextPageSelector() = ".next"
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
title = element.selectFirst("div.item-thumb.wleft a")!!.attr("title")
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/manga/$page?orderby=latest", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
if (query.isEmpty()) {
|
||||
var completed = false
|
||||
var raw = false
|
||||
var genre = ""
|
||||
filters.forEach {
|
||||
when (it) {
|
||||
is GenreFilter -> {
|
||||
genre = it.getValue()
|
||||
}
|
||||
|
||||
is CompletedFilter -> {
|
||||
completed = it.state
|
||||
}
|
||||
|
||||
is RawFilter -> {
|
||||
raw = it.state
|
||||
}
|
||||
|
||||
is SortFilter -> {
|
||||
addQueryParameter("orderby", it.getValue())
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (raw) {
|
||||
addPathSegment("raw")
|
||||
} else if (completed) {
|
||||
addPathSegment("completed")
|
||||
} else {
|
||||
if (genre != "manga") addPathSegment("genre")
|
||||
addPathSegment(genre)
|
||||
}
|
||||
addPathSegment(page.toString())
|
||||
} else {
|
||||
addPathSegment("search")
|
||||
addQueryParameter("q", query)
|
||||
addQueryParameter("page", page.toString())
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun getFilterList() = getFilters()
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val info = document.selectFirst("div.post_content")!!
|
||||
|
||||
title = document.select("div.post-title.wleft > h1").text()
|
||||
description = buildString {
|
||||
document.select("div.ss-manga > p")
|
||||
.eachText().onEach {
|
||||
append(it.trim())
|
||||
append("\n\n")
|
||||
}
|
||||
|
||||
info.selectFirst("div.post-content_item.wleft:contains(Alternative) div.summary-content")
|
||||
?.text()
|
||||
?.takeIf { it != "Updating" && it.isNotEmpty() }
|
||||
?.let {
|
||||
append("Alternative Names:\n")
|
||||
append(it.trim())
|
||||
}
|
||||
}
|
||||
status = when (info.select("div.post-content_item.wleft:contains(Status) div.summary-content").text()) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
author = info.selectFirst("div.href-content.artist-content > a")?.text()?.takeIf { it != "Updating" }
|
||||
artist = info.selectFirst("div.href-content.artist-content > a")?.text()?.takeIf { it != "Updating" }
|
||||
genre = info.select("div.href-content.genres-content > a[href*=/manga-list/]").eachText().joinToString()
|
||||
thumbnail_url = document.selectFirst("div.summary_image > img")?.absUrl("src")
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "ul.row-content-chapter.wleft .a-h.wleft"
|
||||
|
||||
private val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH)
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
element.selectFirst("a")!!.run {
|
||||
setUrlWithoutDomain(absUrl("href"))
|
||||
name = text()
|
||||
}
|
||||
date_upload = try {
|
||||
dateFormat.parse(element.selectFirst("span")!!.text())!!.time
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val contents = document.select("div.read-content.wleft img")
|
||||
if (contents.isEmpty()) {
|
||||
throw Exception("Unable to find script with image data")
|
||||
}
|
||||
|
||||
return contents.mapIndexed { idx, image ->
|
||||
val imageUrl = image.attr("src")
|
||||
Page(idx, imageUrl = imageUrl)
|
||||
}
|
||||
}
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MangaDex'
|
||||
extClass = '.MangaDexFactory'
|
||||
extVersionCode = 192
|
||||
extVersionCode = 193
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.preference.MultiSelectListPreference
|
|||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.AppInfo
|
||||
import eu.kanade.tachiyomi.extension.BuildConfig
|
||||
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateDto
|
||||
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume
|
||||
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
|
||||
|
@ -62,10 +63,12 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
|||
final override fun headersBuilder(): Headers.Builder {
|
||||
val extraHeader = "Android/${Build.VERSION.RELEASE} " +
|
||||
"Tachiyomi/${AppInfo.getVersionName()} " +
|
||||
"MangaDex/1.4.190"
|
||||
"MangaDex/1.4.${BuildConfig.VERSION_CODE} " +
|
||||
"Keiyoushi"
|
||||
|
||||
val builder = super.headersBuilder().apply {
|
||||
set("Referer", "$baseUrl/")
|
||||
set("Origin", baseUrl)
|
||||
set("Extra", extraHeader)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.Manhwa18CcFactory'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://manhwa18.cc'
|
||||
overrideVersionCode = 4
|
||||
overrideVersionCode = 5
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|