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 json
|
||||||
import os
|
import os
|
||||||
import re
|
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:
|
with (REPO_DIR / "index.min.json").open("w", encoding="utf-8") as f:
|
||||||
json.dump(index_min_data, f, ensure_ascii=False, separators=(",", ":"))
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 7
|
baseVersionCode = 8
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:speedbinb"))
|
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 {
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
url = element.attr("href").toOldChapterUrl()
|
url = element.attr("href").toOldChapterUrl()
|
||||||
val number = url.removeSuffix("/").substringAfterLast('/').replace('_', '.')
|
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.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
@ -8,10 +8,9 @@ import android.util.Log
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Springboard that accepts https://imhentai.xxx/gallery/xxxxxx intents and redirects them to
|
* Springboard that accepts https://<domain>/g/xxxxxx intents and redirects them to main app process.
|
||||||
* the main Tachiyomi process.
|
|
||||||
*/
|
*/
|
||||||
class IMHentaiUrlActivity : Activity() {
|
class GalleryAdultsUrlActivity : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val pathSegments = intent?.data?.pathSegments
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
@ -19,17 +18,17 @@ class IMHentaiUrlActivity : Activity() {
|
||||||
val id = pathSegments[1]
|
val id = pathSegments[1]
|
||||||
val mainIntent = Intent().apply {
|
val mainIntent = Intent().apply {
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "id:$id")
|
putExtra("query", "${GalleryAdults.PREFIX_ID_SEARCH}$id")
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startActivity(mainIntent)
|
startActivity(mainIntent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Log.e("IMHentaiUrlActivity", e.toString())
|
Log.e("GalleryAdultsUrl", e.toString())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e("IMHentaiUrlActivity", "could not parse uri from intent $intent")
|
Log.e("GalleryAdultsUrl", "could not parse uri from intent $intent")
|
||||||
}
|
}
|
||||||
|
|
||||||
finish()
|
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")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 23
|
baseVersionCode = 24
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
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)
|
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("Origin", baseUrl)
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 2
|
baseVersionCode = 4
|
||||||
|
|
|
@ -223,42 +223,38 @@ abstract class Keyoapp(
|
||||||
// Image list
|
// Image list
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
return document.select("#pages > img").map {
|
return document.select("#pages > img")
|
||||||
val index = it.attr("count").toInt()
|
.map { it.imgAttr() }
|
||||||
Page(index, document.location(), it.imgAttr("150"))
|
.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) = ""
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
|
|
||||||
// From mangathemesia
|
// From mangathemesia
|
||||||
private fun Element.imgAttr(width: String): String {
|
private fun Element.imgAttr(): String {
|
||||||
val url = when {
|
val url = when {
|
||||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||||
hasAttr("data-src") -> attr("abs:data-src")
|
hasAttr("data-src") -> attr("abs:data-src")
|
||||||
else -> attr("abs:src")
|
else -> attr("abs:src")
|
||||||
}
|
}
|
||||||
return url.toHttpUrl()
|
return url
|
||||||
.newBuilder()
|
|
||||||
.addQueryParameter("w", width)
|
|
||||||
.build()
|
|
||||||
.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Element.getImageUrl(selector: String): String? {
|
private fun Element.getImageUrl(selector: String): String? {
|
||||||
return this.selectFirst(selector)?.let {
|
return this.selectFirst(selector)?.let { element ->
|
||||||
it.attr("style")
|
element.attr("style")
|
||||||
.substringAfter(":url(", "")
|
.substringAfter(":url(", "")
|
||||||
.substringBefore(")", "")
|
.substringBefore(")", "")
|
||||||
.takeIf { it.isNotEmpty() }
|
.takeIf { it.isNotEmpty() }
|
||||||
?.toHttpUrlOrNull()?.let {
|
?.toHttpUrlOrNull()?.newBuilder()?.setQueryParameter("w", "480")?.build()
|
||||||
it.newBuilder()
|
?.toString()
|
||||||
.setQueryParameter("w", "480")
|
|
||||||
.build()
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 35
|
baseVersionCode = 36
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:cryptoaes"))
|
api(project(":lib:cryptoaes"))
|
||||||
|
|
|
@ -612,6 +612,9 @@ abstract class Madara(
|
||||||
"مكتمل",
|
"مكتمل",
|
||||||
"已完结",
|
"已完结",
|
||||||
"Tamamlandı",
|
"Tamamlandı",
|
||||||
|
"Đã hoàn thành",
|
||||||
|
"Завершено",
|
||||||
|
"Tamamlanan",
|
||||||
)
|
)
|
||||||
|
|
||||||
protected val ongoingStatusList: Array<String> = arrayOf(
|
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",
|
"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",
|
"Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision",
|
||||||
"Curso", "En marcha", "Publicandose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
|
"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(
|
protected val hiatusStatusList: Array<String> = arrayOf(
|
||||||
|
@ -626,12 +630,22 @@ abstract class Madara(
|
||||||
"Pausado",
|
"Pausado",
|
||||||
"En espera",
|
"En espera",
|
||||||
"Durduruldu",
|
"Durduruldu",
|
||||||
|
"Beklemede",
|
||||||
|
"Đang chờ",
|
||||||
|
"متوقف",
|
||||||
|
"En Pause",
|
||||||
|
"Заморожено",
|
||||||
)
|
)
|
||||||
|
|
||||||
protected val canceledStatusList: Array<String> = arrayOf(
|
protected val canceledStatusList: Array<String> = arrayOf(
|
||||||
"Canceled",
|
"Canceled",
|
||||||
"Cancelado",
|
"Cancelado",
|
||||||
"İptal Edildi",
|
"İptal Edildi",
|
||||||
|
"Güncel",
|
||||||
|
"Đã hủy",
|
||||||
|
"ملغي",
|
||||||
|
"Abandonné",
|
||||||
|
"Заброшено",
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
@ -946,7 +960,11 @@ abstract class Madara(
|
||||||
val imageUrl = element.selectFirst("img")?.let { imageFromElement(it) }
|
val imageUrl = element.selectFirst("img")?.let { imageFromElement(it) }
|
||||||
Page(index, document.location(), imageUrl)
|
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
|
val password = chapterProtectorHtml
|
||||||
.substringAfter("wpmangaprotectornonce='")
|
.substringAfter("wpmangaprotectornonce='")
|
||||||
.substringBefore("';")
|
.substringBefore("';")
|
||||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 13
|
baseVersionCode = 14
|
||||||
|
|
|
@ -21,6 +21,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
@ -29,24 +30,25 @@ import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
abstract class MadTheme(
|
abstract class MadTheme(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
override val lang: 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() {
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.rateLimit(1, 1)
|
.rateLimit(1, 1, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// TODO: better cookie sharing
|
// TODO: better cookie sharing
|
||||||
// TODO: don't count cached responses against rate limit
|
// TODO: don't count cached responses against rate limit
|
||||||
private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder()
|
private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||||
.rateLimit(1, 12)
|
.rateLimit(1, 12, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override fun headersBuilder() = Headers.Builder().apply {
|
override fun headersBuilder() = Headers.Builder().apply {
|
||||||
|
@ -55,6 +57,8 @@ abstract class MadTheme(
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private var genreKey = "genre[]"
|
||||||
|
|
||||||
// Popular
|
// Popular
|
||||||
override fun popularMangaRequest(page: Int): Request =
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
searchMangaRequest(page, "", FilterList(OrderFilter(0)))
|
searchMangaRequest(page, "", FilterList(OrderFilter(0)))
|
||||||
|
@ -100,7 +104,7 @@ abstract class MadTheme(
|
||||||
.filter { it.state }
|
.filter { it.state }
|
||||||
.let { list ->
|
.let { list ->
|
||||||
if (list.isNotEmpty()) {
|
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 searchMangaSelector(): String = ".book-detailed-item"
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
|
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
|
||||||
title = element.select("a").first()!!.attr("title")
|
title = element.selectFirst("a")!!.attr("title")
|
||||||
description = element.select(".summary").first()?.text()
|
element.selectFirst(".summary")?.text()?.let { description = it }
|
||||||
genre = element.select(".genres > *").joinToString { it.text() }
|
element.select(".genres > *").joinToString { it.text() }.takeIf { it.isNotEmpty() }?.let { genre = it }
|
||||||
thumbnail_url = element.select("img").first()!!.attr("abs:data-src")
|
thumbnail_url = element.selectFirst("img")!!.attr("abs:data-src")
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -135,23 +139,25 @@ abstract class MadTheme(
|
||||||
|
|
||||||
// Details
|
// Details
|
||||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
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(',', ' ') }
|
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(',', ' ') }
|
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(',', ';')
|
?.split(',', ';')
|
||||||
?.mapNotNull { it.trim().takeIf { it != title } }
|
?.mapNotNull { it.trim().takeIf { it != title } }
|
||||||
?: listOf()
|
?: 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()}" } ?: "")
|
(altNames.takeIf { it.isNotEmpty() }?.let { "\n\nAlt name(s): ${it.joinToString()}" } ?: "")
|
||||||
|
|
||||||
val statusText = document.select(".detail .meta > p > strong:contains(Status) ~ a").first()!!.text()
|
val statusText = document.selectFirst(".detail .meta > p > strong:contains(Status) ~ a")!!.text()
|
||||||
status = when (statusText.lowercase(Locale.US)) {
|
status = when (statusText.lowercase(Locale.ENGLISH)) {
|
||||||
"ongoing" -> SManga.ONGOING
|
"ongoing" -> SManga.ONGOING
|
||||||
"completed" -> SManga.COMPLETED
|
"completed" -> SManga.COMPLETED
|
||||||
|
"on-hold" -> SManga.ON_HIATUS
|
||||||
|
"canceled" -> SManga.CANCELLED
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,7 +193,14 @@ abstract class MadTheme(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request =
|
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 {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
if (genresList == null) {
|
if (genresList == null) {
|
||||||
|
@ -204,16 +217,25 @@ abstract class MadTheme(
|
||||||
.absUrl("href")
|
.absUrl("href")
|
||||||
.removePrefix(baseUrl)
|
.removePrefix(baseUrl)
|
||||||
|
|
||||||
name = element.select(".chapter-title").first()!!.text()
|
name = element.selectFirst(".chapter-title")!!.text()
|
||||||
date_upload = parseChapterDate(element.select(".chapter-update").first()?.text())
|
date_upload = parseChapterDate(element.selectFirst(".chapter-update")?.text())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
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 = \"")) {
|
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
|
// 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
|
// the rest to be lazily(?) loaded by javascript. Let's extract `chapImages` and compare
|
||||||
|
@ -292,7 +314,7 @@ abstract class MadTheme(
|
||||||
}
|
}
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
"ago".endsWith(date) -> {
|
" ago" in date -> {
|
||||||
parseRelativeDate(date)
|
parseRelativeDate(date)
|
||||||
}
|
}
|
||||||
else -> dateFormat.tryParse(date)
|
else -> dateFormat.tryParse(date)
|
||||||
|
@ -300,10 +322,12 @@ abstract class MadTheme(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseRelativeDate(date: String): Long {
|
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()
|
val cal = Calendar.getInstance()
|
||||||
|
|
||||||
return when {
|
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("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||||
date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||||
date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||||
|
@ -314,13 +338,21 @@ abstract class MadTheme(
|
||||||
|
|
||||||
// Dynamic genres
|
// Dynamic genres
|
||||||
private fun parseGenres(document: Document): List<Genre>? {
|
private fun parseGenres(document: Document): List<Genre>? {
|
||||||
return document.select(".checkbox-group.genres").first()?.select("label")?.map {
|
return document.selectFirst(".checkbox-group.genres")?.select(".checkbox-wrapper")?.run {
|
||||||
Genre(it.select(".radio__label").first()!!.text(), it.select("input").`val`())
|
firstOrNull()?.selectFirst("input")?.attr("name")?.takeIf { it.isNotEmpty() }?.let { genreKey = it }
|
||||||
|
map {
|
||||||
|
Genre(it.selectFirst(".radio__label")!!.text(), it.selectFirst("input")!!.`val`())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
override fun getFilterList() = FilterList(
|
override fun getFilterList() = FilterList(
|
||||||
|
// TODO: Filters for sites that support it:
|
||||||
|
// excluded genres
|
||||||
|
// genre inclusion mode
|
||||||
|
// bookmarks
|
||||||
|
// author
|
||||||
GenreFilter(getGenreList()),
|
GenreFilter(getGenreList()),
|
||||||
StatusFilter(),
|
StatusFilter(),
|
||||||
OrderFilter(),
|
OrderFilter(),
|
||||||
|
@ -352,6 +384,7 @@ abstract class MadTheme(
|
||||||
Pair("Updated", "updated_at"),
|
Pair("Updated", "updated_at"),
|
||||||
Pair("Created", "created_at"),
|
Pair("Created", "created_at"),
|
||||||
Pair("Name A-Z", "name"),
|
Pair("Name A-Z", "name"),
|
||||||
|
// Pair("Number of Chapters", "total_chapters"),
|
||||||
Pair("Rating", "rating"),
|
Pair("Rating", "rating"),
|
||||||
),
|
),
|
||||||
state,
|
state,
|
||||||
|
@ -365,4 +398,10 @@ abstract class MadTheme(
|
||||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
|
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
|
||||||
fun toUriPart() = vals[state].second
|
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")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 1
|
baseVersionCode = 2
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
|
|
@ -73,7 +73,7 @@ abstract class MangaEsp(
|
||||||
return MangasPage(mangas, false)
|
return MangasPage(mangas, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var comicsList = mutableListOf<SeriesDto>()
|
protected var comicsList = mutableListOf<SeriesDto>()
|
||||||
|
|
||||||
override fun fetchSearchManga(
|
override fun fetchSearchManga(
|
||||||
page: Int,
|
page: Int,
|
||||||
|
@ -93,7 +93,7 @@ abstract class MangaEsp(
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
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 document = response.asJsoup()
|
||||||
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
||||||
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
||||||
|
@ -105,7 +105,7 @@ abstract class MangaEsp(
|
||||||
|
|
||||||
private var filteredList = mutableListOf<SeriesDto>()
|
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) {
|
if (page == 1) {
|
||||||
filteredList.clear()
|
filteredList.clear()
|
||||||
|
|
||||||
|
@ -228,21 +228,21 @@ abstract class MangaEsp(
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
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-lazy-src") -> attr("abs:data-lazy-src")
|
||||||
hasAttr("data-src") -> attr("abs:data-src")
|
hasAttr("data-src") -> attr("abs:data-src")
|
||||||
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
|
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
|
||||||
else -> attr("abs:src")
|
else -> attr("abs:src")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.unescape(): String {
|
fun String.unescape(): String {
|
||||||
return UNESCAPE_REGEX.replace(this, "$1")
|
return UNESCAPE_REGEX.replace(this, "$1")
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val UNESCAPE_REGEX = """\\(.)""".toRegex()
|
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 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,
|
val trending: TrendingDto? = null,
|
||||||
@SerialName("autors") private val authors: List<AuthorDto> = emptyList(),
|
@SerialName("autors") private val authors: List<AuthorDto> = emptyList(),
|
||||||
private val artists: List<ArtistDto> = emptyList(),
|
private val artists: List<ArtistDto> = emptyList(),
|
||||||
|
@Suppress("unused") // Used in some sources
|
||||||
|
@SerialName("idioma")
|
||||||
|
val language: String? = null,
|
||||||
) {
|
) {
|
||||||
fun toSManga(): SManga {
|
fun toSManga(): SManga {
|
||||||
return SManga.create().apply {
|
return SManga.create().apply {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".en.anchira.AnchiraUrlActivity"
|
android:name="eu.kanade.tachiyomi.multisrc.peachscan.PeachScanUrlActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
@ -12,10 +12,10 @@
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data
|
||||||
<data android:scheme="https" />
|
android:host="${SOURCEHOST}"
|
||||||
<data android:host="anchira.to" />
|
android:pathPattern="/..*"
|
||||||
<data android:pathPattern="/g/.*/..*" />
|
android:scheme="${SOURCESCHEME}" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
|
@ -2,7 +2,7 @@ plugins {
|
||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 9
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
@ -28,6 +29,7 @@ import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
@ -82,6 +84,18 @@ abstract class PeachScan(
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = null
|
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 {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
addPathSegments("auto-complete/")
|
addPathSegments("auto-complete/")
|
||||||
|
@ -153,21 +167,31 @@ abstract class PeachScan(
|
||||||
}.getOrDefault(0L)
|
}.getOrDefault(0L)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val urlsRegex = """const\s+urls\s*=\s*\[(.*?)]\s*;""".toRegex()
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
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 ->
|
?: 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 ->
|
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 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 val dataUriRegex = Regex("""base64,([0-9a-zA-Z/+=\s]+)""")
|
||||||
|
|
||||||
private fun zipImageInterceptor(chain: Interceptor.Chain): Response {
|
private fun zipImageInterceptor(chain: Interceptor.Chain): Response {
|
||||||
|
@ -188,7 +212,7 @@ abstract class PeachScan(
|
||||||
val entryIndex = splitEntryName.first().toInt()
|
val entryIndex = splitEntryName.first().toInt()
|
||||||
val entryType = splitEntryName.last()
|
val entryType = splitEntryName.last()
|
||||||
|
|
||||||
val imageData = if (entryType == "avif") {
|
val imageData = if (entryType == "avif" || splitEntryName.size == 1) {
|
||||||
zis.readBytes()
|
zis.readBytes()
|
||||||
} else {
|
} else {
|
||||||
val svgBytes = zis.readBytes()
|
val svgBytes = zis.readBytes()
|
||||||
|
@ -251,4 +275,8 @@ abstract class PeachScan(
|
||||||
|
|
||||||
memInfo.totalMem < 3L * 1024 * 1024 * 1024
|
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 useNewChapterFeed = false
|
||||||
protected open val useOldChapterFeed = false
|
protected open val useOldChapterFeed = false
|
||||||
|
|
||||||
private val chapterFeedRegex = """clwd\.run\(["'](.*?)["']\)""".toRegex()
|
protected open val chapterFeedRegex = """clwd\.run\(["'](.*?)["']\)""".toRegex()
|
||||||
private val scriptSelector = "#clwd > script"
|
protected open val scriptSelector = "#clwd > script"
|
||||||
|
|
||||||
open fun getChapterFeedUrl(doc: Document): String {
|
open fun getChapterFeedUrl(doc: Document): String {
|
||||||
if (useNewChapterFeed) return newChapterFeedUrl(doc)
|
if (useNewChapterFeed) return newChapterFeedUrl(doc)
|
||||||
|
@ -434,6 +434,6 @@ abstract class ZeistManga(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val maxMangaResults = 20
|
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 {
|
ext {
|
||||||
extName = 'AsmHentai'
|
extName = 'AsmHentai'
|
||||||
extClass = '.ASMHFactory'
|
extClass = '.ASMHFactory'
|
||||||
extVersionCode = 1
|
themePkg = 'galleryadults'
|
||||||
|
baseUrl = 'https://asmhentai.com'
|
||||||
|
overrideVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.asmhentai
|
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.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
class ASMHFactory : SourceFactory {
|
class ASMHFactory : SourceFactory {
|
||||||
override fun createSources(): List<Source> = listOf(
|
override fun createSources(): List<Source> = listOf(
|
||||||
AsmHentai("en", "english"),
|
AsmHentai("en", GalleryAdults.LANGUAGE_ENGLISH),
|
||||||
AsmHentai("ja", "japanese"),
|
AsmHentai("ja", GalleryAdults.LANGUAGE_JAPANESE),
|
||||||
AsmHentai("zh", "chinese"),
|
AsmHentai("zh", GalleryAdults.LANGUAGE_CHINESE),
|
||||||
AsmHentai("all", ""),
|
AsmHentai("all", GalleryAdults.LANGUAGE_MULTI),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,274 +1,103 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.asmhentai
|
package eu.kanade.tachiyomi.extension.all.asmhentai
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.multisrc.galleryadults.Genre
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.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.FormBody
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
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 {
|
override fun Element.getInfo(tag: String): String {
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
return select(".tags:contains($tag:) .tag_list a")
|
||||||
if (tlTag.isNotEmpty()) addPathSegments("language/$tlTag/")
|
.joinToString {
|
||||||
if (page > 1) addQueryParameter("page", page.toString())
|
val name = it.selectFirst(".tag")?.ownText() ?: ""
|
||||||
|
if (tag.contains(regexTag)) {
|
||||||
|
genres[name] = it.attr("href")
|
||||||
|
.removeSuffix("/").substringAfterLast('/')
|
||||||
}
|
}
|
||||||
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) }
|
|
||||||
}
|
|
||||||
query.toIntOrNull() != null -> {
|
|
||||||
client.newCall(searchMangaByIdRequest(query))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response -> searchMangaByIdParse(response, query) }
|
|
||||||
}
|
|
||||||
else -> super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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("/"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.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(
|
listOf(
|
||||||
SChapter.create().apply {
|
name,
|
||||||
name = "Chapter"
|
it.select(".split_tag").text()
|
||||||
url = manga.url
|
.removePrefix("| ")
|
||||||
},
|
.trim(),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
.filter { s -> s.isNotBlank() }
|
||||||
|
.joinToString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListSelector(): String {
|
override fun Element.getInfoPages(document: Document?) =
|
||||||
throw UnsupportedOperationException()
|
selectFirst(".book_page .pages h3")?.ownText()
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
override val mangaDetailInfoSelector = ".book_page"
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages
|
/**
|
||||||
|
* [totalPagesSelector] only exists if pages > 10
|
||||||
|
*/
|
||||||
|
override val totalPagesSelector = "t_pages"
|
||||||
|
|
||||||
// convert thumbnail URLs to full image URLs
|
override val galleryIdSelector = "load_id"
|
||||||
private fun String.full(): String {
|
override val thumbnailSelector = ".preview_thumb"
|
||||||
val fType = substringAfterLast("t")
|
|
||||||
return replace("t$fType", fType)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Document.inputIdValueOf(string: String): String {
|
override val idPrefixUri = "g"
|
||||||
return select("input[id=$string]").attr("value")
|
override val pageUri = "gallery"
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageRequestForm(document: Document, totalPages: String, loadedPages: Int): FormBody {
|
||||||
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 token = document.select("[name=csrf-token]").attr("content")
|
||||||
|
|
||||||
val form = FormBody.Builder()
|
return FormBody.Builder()
|
||||||
.add("_token", token)
|
.add("id", document.inputIdValueOf(loadIdSelector))
|
||||||
.add("id", document.inputIdValueOf("load_id"))
|
.add("dir", document.inputIdValueOf(loadDirSelector))
|
||||||
.add("dir", document.inputIdValueOf("load_dir"))
|
.add("visible_pages", loadedPages.toString())
|
||||||
.add("visible_pages", "10")
|
|
||||||
.add("t_pages", totalPages)
|
.add("t_pages", totalPages)
|
||||||
.add("type", "2") // 1 would be "more", 2 is "all remaining"
|
.add("type", "2") // 1 would be "more", 2 is "all remaining"
|
||||||
.build()
|
.apply {
|
||||||
|
if (token.isNotBlank()) add("_token", token)
|
||||||
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()) }
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
override fun tagsParser(document: Document): List<Genre> {
|
||||||
|
return document.select(".tags_page .tags a.tag")
|
||||||
// Filters
|
.mapNotNull {
|
||||||
|
Genre(
|
||||||
override fun getFilterList(): FilterList = FilterList(
|
it.ownText(),
|
||||||
Filter.Header("Separate tags with commas (,)"),
|
it.attr("href")
|
||||||
TagFilter(),
|
.removeSuffix("/").substringAfterLast('/'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class TagFilter : Filter.Text("Tags")
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val PREFIX_ID_SEARCH = "id:"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -47,6 +47,9 @@
|
||||||
<data
|
<data
|
||||||
android:pathPattern="/subject-overview/..*"
|
android:pathPattern="/subject-overview/..*"
|
||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:pathPattern="/title/..*"
|
||||||
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Bato.to'
|
extName = 'Bato.to'
|
||||||
extClass = '.BatoToFactory'
|
extClass = '.BatoToFactory'
|
||||||
extVersionCode = 35
|
extVersionCode = 36
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,10 +42,27 @@ class BatoToUrlActivity : Activity() {
|
||||||
|
|
||||||
private fun fromBatoTo(pathSegments: MutableList<String>): String? {
|
private fun fromBatoTo(pathSegments: MutableList<String>): String? {
|
||||||
return if (pathSegments.size >= 2) {
|
return if (pathSegments.size >= 2) {
|
||||||
val id = pathSegments[1]
|
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"
|
"ID:$id"
|
||||||
} else {
|
} else {
|
||||||
null
|
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 {
|
ext {
|
||||||
extName = 'Comick'
|
extName = 'Comick'
|
||||||
extClass = '.ComickFactory'
|
extClass = '.ComickFactory'
|
||||||
extVersionCode = 42
|
extVersionCode = 46
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,11 +21,17 @@ import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
abstract class Comick(
|
abstract class Comick(
|
||||||
|
@ -155,9 +161,31 @@ abstract class Comick(
|
||||||
}
|
}
|
||||||
|
|
||||||
override val client = network.client.newBuilder()
|
override val client = network.client.newBuilder()
|
||||||
.rateLimit(3, 1)
|
.addNetworkInterceptor(::errorInterceptor)
|
||||||
|
.rateLimit(3, 1, TimeUnit.SECONDS)
|
||||||
.build()
|
.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 **/
|
/** Popular Manga **/
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
val url = "$apiUrl/v1.0/search?sort=follow&limit=$LIMIT&page=$page&tachiyomi=true"
|
val url = "$apiUrl/v1.0/search?sort=follow&limit=$LIMIT&page=$page&tachiyomi=true"
|
||||||
|
@ -301,7 +329,7 @@ abstract class Comick(
|
||||||
is TagFilter -> {
|
is TagFilter -> {
|
||||||
if (it.state.isNotEmpty()) {
|
if (it.state.isNotEmpty()) {
|
||||||
it.state.split(",").forEach {
|
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")
|
.substringBefore("/chapters")
|
||||||
.substringAfter(apiUrl)
|
.substringAfter(apiUrl)
|
||||||
|
|
||||||
|
val currentTimestamp = System.currentTimeMillis()
|
||||||
|
|
||||||
return chapterListResponse.chapters
|
return chapterListResponse.chapters
|
||||||
.filter {
|
.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) }
|
.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 {
|
override fun getChapterUrl(chapter: SChapter): String {
|
||||||
return "$baseUrl${chapter.url}"
|
return "$baseUrl${chapter.url}"
|
||||||
}
|
}
|
||||||
|
@ -453,6 +499,7 @@ abstract class Comick(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SLUG_SEARCH_PREFIX = "id:"
|
const val SLUG_SEARCH_PREFIX = "id:"
|
||||||
|
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
||||||
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
||||||
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
||||||
private const val INCLUDE_MU_TAGS_DEFAULT = false
|
private const val INCLUDE_MU_TAGS_DEFAULT = false
|
||||||
|
|
|
@ -170,7 +170,8 @@ class Chapter(
|
||||||
private val hid: String,
|
private val hid: String,
|
||||||
private val lang: String = "",
|
private val lang: String = "",
|
||||||
private val title: 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 chap: String = "",
|
||||||
private val vol: String = "",
|
private val vol: String = "",
|
||||||
@SerialName("group_name") val groups: List<String> = emptyList(),
|
@SerialName("group_name") val groups: List<String> = emptyList(),
|
||||||
|
@ -197,3 +198,9 @@ class ChapterPageData(
|
||||||
class Page(
|
class Page(
|
||||||
val url: String? = null,
|
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
|
import java.util.TimeZone
|
||||||
|
|
||||||
private val dateFormat by lazy {
|
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")
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Danbooru'
|
extName = 'Danbooru'
|
||||||
extClass = '.Danbooru'
|
extClass = '.Danbooru'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,54 +47,52 @@ class Danbooru : ParsedHttpSource() {
|
||||||
override fun popularMangaSelector(): String =
|
override fun popularMangaSelector(): String =
|
||||||
searchMangaSelector()
|
searchMangaSelector()
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = Request(
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder().run {
|
val url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder()
|
||||||
setEncodedQueryParameter("search[category]", "series")
|
|
||||||
|
url.setEncodedQueryParameter("search[category]", "series")
|
||||||
|
|
||||||
filters.forEach {
|
filters.forEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
is FilterTags -> if (it.state.isNotBlank()) {
|
is FilterTags -> if (it.state.isNotBlank()) {
|
||||||
addQueryParameter("search[post_tags_match]", it.state)
|
url.addQueryParameter("search[post_tags_match]", it.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
is FilterDescription -> if (it.state.isNotBlank()) {
|
is FilterDescription -> if (it.state.isNotBlank()) {
|
||||||
addQueryParameter("search[description_matches]", it.state)
|
url.addQueryParameter("search[description_matches]", it.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
is FilterIsDeleted -> if (it.state) {
|
is FilterIsDeleted -> if (it.state) {
|
||||||
addEncodedQueryParameter("search[is_deleted]", "true")
|
url.addEncodedQueryParameter("search[is_deleted]", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
is FilterCategory -> {
|
is FilterCategory -> {
|
||||||
setEncodedQueryParameter("search[category]", it.selected)
|
url.setEncodedQueryParameter("search[category]", it.selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
is FilterOrder -> if (it.selected != null) {
|
is FilterOrder -> if (it.selected != null) {
|
||||||
addEncodedQueryParameter("search[order]", it.selected)
|
url.addEncodedQueryParameter("search[order]", it.selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> throw IllegalStateException("Unrecognized filter")
|
else -> throw IllegalStateException("Unrecognized filter")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addEncodedQueryParameter("page", page.toString())
|
url.addEncodedQueryParameter("page", page.toString())
|
||||||
|
|
||||||
if (query.isNotBlank()) {
|
if (query.isNotBlank()) {
|
||||||
addQueryParameter("search[name_contains]", query)
|
url.addQueryParameter("search[name_contains]", query)
|
||||||
}
|
}
|
||||||
|
|
||||||
build()
|
return GET(url.build(), headers)
|
||||||
},
|
}
|
||||||
|
|
||||||
headers = headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun searchMangaSelector(): String =
|
override fun searchMangaSelector(): String =
|
||||||
".post-preview"
|
"article.post-preview"
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
url = element.selectFirst(".post-preview-link")?.attr("href")!!
|
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")
|
thumbnail_url = element.selectFirst("source")?.attr("srcset")
|
||||||
?.substringAfterLast(',')?.trim()
|
?.substringAfterLast(',')?.trim()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'EternalMangas'
|
extName = 'EternalMangas'
|
||||||
extClass = '.EternalMangas'
|
extClass = '.EternalMangasFactory'
|
||||||
themePkg = 'mangaesp'
|
themePkg = 'mangaesp'
|
||||||
baseUrl = 'https://eternalmangas.com'
|
baseUrl = 'https://eternalmangas.com'
|
||||||
overrideVersionCode = 0
|
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 {
|
ext {
|
||||||
extName = 'Hitomi'
|
extName = 'Hitomi'
|
||||||
extClass = '.HitomiFactory'
|
extClass = '.HitomiFactory'
|
||||||
extVersionCode = 26
|
extVersionCode = 28
|
||||||
isNsfw = true
|
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
|
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.GET
|
||||||
import eu.kanade.tachiyomi.network.await
|
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
@ -22,6 +26,8 @@ import okhttp3.Call
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
|
@ -35,7 +41,7 @@ import kotlin.math.min
|
||||||
class Hitomi(
|
class Hitomi(
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
private val nozomiLang: String,
|
private val nozomiLang: String,
|
||||||
) : HttpSource() {
|
) : ConfigurableSource, HttpSource() {
|
||||||
|
|
||||||
override val name = "Hitomi"
|
override val name = "Hitomi"
|
||||||
|
|
||||||
|
@ -51,6 +57,12 @@ class Hitomi(
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
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()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("referer", "$baseUrl/")
|
.set("referer", "$baseUrl/")
|
||||||
.set("origin", baseUrl)
|
.set("origin", baseUrl)
|
||||||
|
@ -76,11 +88,13 @@ class Hitomi(
|
||||||
private lateinit var searchResponse: List<Int>
|
private lateinit var searchResponse: List<Int>
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.fromCallable {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.fromCallable {
|
||||||
|
val parsedFilter = parseFilters(filters)
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
if (page == 1) {
|
if (page == 1) {
|
||||||
searchResponse = hitomiSearch(
|
searchResponse = hitomiSearch(
|
||||||
query.trim(),
|
"$query${parsedFilter.first}".trim(),
|
||||||
filters.filterIsInstance<SortFilter>().firstOrNull()?.state == 1,
|
parsedFilter.second,
|
||||||
nozomiLang,
|
nozomiLang,
|
||||||
).toList()
|
).toList()
|
||||||
}
|
}
|
||||||
|
@ -93,11 +107,7 @@ class Hitomi(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SortFilter : Filter.Select<String>("Sort By", arrayOf("Updated", "Popularity"))
|
override fun getFilterList(): FilterList = getFilterListInternal()
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
return FilterList(SortFilter())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Int.nextPageRange(): LongRange {
|
private fun Int.nextPageRange(): LongRange {
|
||||||
val byteOffset = ((this - 1) * 25) * 4L
|
val byteOffset = ((this - 1) * 25) * 4L
|
||||||
|
@ -117,7 +127,7 @@ class Hitomi(
|
||||||
|
|
||||||
private suspend fun hitomiSearch(
|
private suspend fun hitomiSearch(
|
||||||
query: String,
|
query: String,
|
||||||
sortByPopularity: Boolean = false,
|
order: OrderType,
|
||||||
language: String = "all",
|
language: String = "all",
|
||||||
): Set<Int> =
|
): Set<Int> =
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
|
@ -126,9 +136,6 @@ class Hitomi(
|
||||||
.replace(Regex("""^\?"""), "")
|
.replace(Regex("""^\?"""), "")
|
||||||
.lowercase()
|
.lowercase()
|
||||||
.split(Regex("\\s+"))
|
.split(Regex("\\s+"))
|
||||||
.map {
|
|
||||||
it.replace('_', ' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
val positiveTerms = LinkedList<String>()
|
val positiveTerms = LinkedList<String>()
|
||||||
val negativeTerms = LinkedList<String>()
|
val negativeTerms = LinkedList<String>()
|
||||||
|
@ -144,7 +151,7 @@ class Hitomi(
|
||||||
val positiveResults = positiveTerms.map {
|
val positiveResults = positiveTerms.map {
|
||||||
async {
|
async {
|
||||||
runCatching {
|
runCatching {
|
||||||
getGalleryIDsForQuery(it, language)
|
getGalleryIDsForQuery(it, language, order)
|
||||||
}.getOrDefault(emptySet())
|
}.getOrDefault(emptySet())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,14 +159,13 @@ class Hitomi(
|
||||||
val negativeResults = negativeTerms.map {
|
val negativeResults = negativeTerms.map {
|
||||||
async {
|
async {
|
||||||
runCatching {
|
runCatching {
|
||||||
getGalleryIDsForQuery(it, language)
|
getGalleryIDsForQuery(it, language, order)
|
||||||
}.getOrDefault(emptySet())
|
}.getOrDefault(emptySet())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val results = when {
|
val results = when {
|
||||||
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", language)
|
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(order.first, order.second, language)
|
||||||
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", language)
|
|
||||||
else -> emptySet()
|
else -> emptySet()
|
||||||
}.toMutableSet()
|
}.toMutableSet()
|
||||||
|
|
||||||
|
@ -191,6 +197,7 @@ class Hitomi(
|
||||||
private suspend fun getGalleryIDsForQuery(
|
private suspend fun getGalleryIDsForQuery(
|
||||||
query: String,
|
query: String,
|
||||||
language: String = "all",
|
language: String = "all",
|
||||||
|
order: OrderType,
|
||||||
): Set<Int> {
|
): Set<Int> {
|
||||||
query.replace("_", " ").let {
|
query.replace("_", " ").let {
|
||||||
if (it.indexOf(':') > -1) {
|
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)
|
return getGalleryIDsFromNozomi(area, tag, lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,7 +450,7 @@ class Hitomi(
|
||||||
url = galleryurl
|
url = galleryurl
|
||||||
author = groups?.joinToString { it.formatted }
|
author = groups?.joinToString { it.formatted }
|
||||||
artist = artists?.joinToString { it.formatted }
|
artist = artists?.joinToString { it.formatted }
|
||||||
genre = tags?.joinToString { it.formatted }
|
genre = tags?.joinToString { it.getFormatted(iconified) }
|
||||||
thumbnail_url = files.first().let {
|
thumbnail_url = files.first().let {
|
||||||
val hash = it.hash
|
val hash = it.hash
|
||||||
val imageId = imageIdFromHash(hash)
|
val imageId = imageIdFromHash(hash)
|
||||||
|
@ -444,7 +465,8 @@ class Hitomi(
|
||||||
parodys?.joinToString { it.formatted }?.let {
|
parodys?.joinToString { it.formatted }?.let {
|
||||||
append("Parodies: ", it, "\n")
|
append("Parodies: ", it, "\n")
|
||||||
}
|
}
|
||||||
append("Pages: ", files.size)
|
append("Pages: ", files.size, "\n")
|
||||||
|
append("Language: ", language)
|
||||||
}
|
}
|
||||||
status = SManga.COMPLETED
|
status = SManga.COMPLETED
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
|
@ -598,9 +620,28 @@ class Hitomi(
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun popularMangaRequest(page: Int) = 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 latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
|
||||||
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun imageUrlParse(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 title: String,
|
||||||
val date: String,
|
val date: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
|
val language: String,
|
||||||
val tags: List<Tag>?,
|
val tags: List<Tag>?,
|
||||||
val artists: List<Artist>?,
|
val artists: List<Artist>?,
|
||||||
val groups: List<Group>?,
|
val groups: List<Group>?,
|
||||||
|
@ -28,10 +29,10 @@ data class Tag(
|
||||||
val male: JsonPrimitive?,
|
val male: JsonPrimitive?,
|
||||||
val tag: String,
|
val tag: String,
|
||||||
) {
|
) {
|
||||||
val formatted get() = if (female?.content == "1") {
|
fun getFormatted(iconified: Boolean) = if (female?.content == "1") {
|
||||||
"${tag.toCamelCase()} (Female)"
|
tag.toCamelCase() + if (iconified) " ♀" else " (Female)"
|
||||||
} else if (male?.content == "1") {
|
} else if (male?.content == "1") {
|
||||||
"${tag.toCamelCase()} (Male)"
|
tag.toCamelCase() + if (iconified) " ♂" else " (Male)"
|
||||||
} else {
|
} else {
|
||||||
tag.toCamelCase()
|
tag.toCamelCase()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'IMHentai'
|
extName = 'IMHentai'
|
||||||
extClass = '.IMHentaiFactory'
|
extClass = '.IMHentaiFactory'
|
||||||
extVersionCode = 14
|
themePkg = 'galleryadults'
|
||||||
|
baseUrl = 'https://imhentai.xxx'
|
||||||
|
overrideVersionCode = 15
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.imhentai
|
package eu.kanade.tachiyomi.extension.all.imhentai
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
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.online.ParsedHttpSource
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.select.Elements
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class IMHentai(override val lang: String, private val imhLang: String) : ParsedHttpSource() {
|
class IMHentai(
|
||||||
|
lang: String = "all",
|
||||||
override val baseUrl: String = "https://imhentai.xxx"
|
override val mangaLang: String = LANGUAGE_MULTI,
|
||||||
override val name: String = "IMHentai"
|
) : GalleryAdults(
|
||||||
|
"IMHentai",
|
||||||
|
"https://imhentai.xxx",
|
||||||
|
lang = lang,
|
||||||
|
) {
|
||||||
override val supportsLatest = true
|
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
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
|
@ -57,271 +57,32 @@ class IMHentai(override val lang: String, private val imhLang: String) : ParsedH
|
||||||
},
|
},
|
||||||
).build()
|
).build()
|
||||||
|
|
||||||
// Popular
|
/* Details */
|
||||||
|
override fun Element.getInfo(tag: String): String {
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
return select("li:has(.tags_text:contains($tag:)) a.tag")
|
||||||
return SManga.create().apply {
|
.joinToString {
|
||||||
thumbnail_url = element.selectFirst(".inner_thumb img")?.let {
|
val name = it.ownText()
|
||||||
it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src")
|
if (tag.contains(regexTag)) {
|
||||||
|
genres[name] = it.attr("href")
|
||||||
|
.removeSuffix("/").substringAfterLast('/')
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
private fun Elements.csvText(splitTagSeparator: String = ", "): String {
|
|
||||||
return this.joinToString {
|
|
||||||
listOf(
|
listOf(
|
||||||
it.ownText(),
|
name,
|
||||||
it.select(".split_tag").text()
|
it.select(".split_tag").text()
|
||||||
.trim()
|
.trim()
|
||||||
.removePrefix("| "),
|
.removePrefix("| "),
|
||||||
)
|
)
|
||||||
.filter { s -> !s.isNullOrBlank() }
|
.filter { s -> s.isNotBlank() }
|
||||||
.joinToString(splitTagSeparator)
|
.joinToString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
override fun Element.getCover() =
|
||||||
title = document.selectFirst("div.right_details > h1")!!.text()
|
selectFirst(".left_cover img")?.imgAttr()
|
||||||
|
|
||||||
thumbnail_url = document.selectFirst("div.left_cover img")?.let {
|
override val mangaDetailInfoSelector = ".gallery_first"
|
||||||
it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src")
|
|
||||||
}
|
/* Pages */
|
||||||
|
override val thumbnailSelector = ".gthumb"
|
||||||
val mangaInfoElement = document.select(".galleries_info")
|
override val pageUri = "view"
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.imhentai
|
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.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
class IMHentaiFactory : SourceFactory {
|
class IMHentaiFactory : SourceFactory {
|
||||||
|
|
||||||
override fun createSources(): List<Source> = listOf(
|
override fun createSources(): List<Source> = listOf(
|
||||||
IMHentai("en", IMHentai.LANGUAGE_ENGLISH),
|
IMHentai("en", GalleryAdults.LANGUAGE_ENGLISH),
|
||||||
IMHentai("ja", IMHentai.LANGUAGE_JAPANESE),
|
IMHentai("ja", GalleryAdults.LANGUAGE_JAPANESE),
|
||||||
IMHentai("es", IMHentai.LANGUAGE_SPANISH),
|
IMHentai("es", GalleryAdults.LANGUAGE_SPANISH),
|
||||||
IMHentai("fr", IMHentai.LANGUAGE_FRENCH),
|
IMHentai("fr", GalleryAdults.LANGUAGE_FRENCH),
|
||||||
IMHentai("ko", IMHentai.LANGUAGE_KOREAN),
|
IMHentai("ko", GalleryAdults.LANGUAGE_KOREAN),
|
||||||
IMHentai("de", IMHentai.LANGUAGE_GERMAN),
|
IMHentai("de", GalleryAdults.LANGUAGE_GERMAN),
|
||||||
IMHentai("ru", IMHentai.LANGUAGE_RUSSIAN),
|
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 {
|
ext {
|
||||||
extName = 'MangaDex'
|
extName = 'MangaDex'
|
||||||
extClass = '.MangaDexFactory'
|
extClass = '.MangaDexFactory'
|
||||||
extVersionCode = 192
|
extVersionCode = 193
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import androidx.preference.MultiSelectListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import eu.kanade.tachiyomi.AppInfo
|
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.AggregateDto
|
||||||
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume
|
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume
|
||||||
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
|
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 {
|
final override fun headersBuilder(): Headers.Builder {
|
||||||
val extraHeader = "Android/${Build.VERSION.RELEASE} " +
|
val extraHeader = "Android/${Build.VERSION.RELEASE} " +
|
||||||
"Tachiyomi/${AppInfo.getVersionName()} " +
|
"Tachiyomi/${AppInfo.getVersionName()} " +
|
||||||
"MangaDex/1.4.190"
|
"MangaDex/1.4.${BuildConfig.VERSION_CODE} " +
|
||||||
|
"Keiyoushi"
|
||||||
|
|
||||||
val builder = super.headersBuilder().apply {
|
val builder = super.headersBuilder().apply {
|
||||||
set("Referer", "$baseUrl/")
|
set("Referer", "$baseUrl/")
|
||||||
|
set("Origin", baseUrl)
|
||||||
set("Extra", extraHeader)
|
set("Extra", extraHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
||||||
extClass = '.Manhwa18CcFactory'
|
extClass = '.Manhwa18CcFactory'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://manhwa18.cc'
|
baseUrl = 'https://manhwa18.cc'
|
||||||
overrideVersionCode = 4
|
overrideVersionCode = 5
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|