Compare commits
273 Commits
fc828972d8
...
cbfdd982ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbfdd982ff | ||
|
|
9a738a668f | ||
|
|
5fdd51e172 | ||
|
|
b6a4f40609 | ||
|
|
744c401773 | ||
|
|
d0e9279214 | ||
|
|
08b627c8e7 | ||
|
|
0ec1a28ed7 | ||
|
|
1ec8554fe2 | ||
|
|
9c695d0e65 | ||
|
|
93ba18cd9c | ||
|
|
ac069dd2ec | ||
|
|
8c74ea91cd | ||
|
|
fe47d20f67 | ||
|
|
41e64ac576 | ||
|
|
7d62c04507 | ||
|
|
33f4d5f8c0 | ||
|
|
2fd2613bd0 | ||
|
|
2259f1bc8b | ||
|
|
c0129e532f | ||
|
|
d3b715a4be | ||
|
|
6d63d49b16 | ||
|
|
728dd0de50 | ||
|
|
ff7a95faa0 | ||
|
|
e70acec541 | ||
|
|
d824aa0a17 | ||
|
|
d22070e11c | ||
|
|
3d97ceb3eb | ||
|
|
9b49a7d4f1 | ||
|
|
1e32eb651c | ||
|
|
09ebe226b7 | ||
|
|
af9e1adb51 | ||
|
|
1905a3c8dc | ||
|
|
2a5d28df53 | ||
|
|
a687048749 | ||
|
|
e2895bad8e | ||
|
|
250ab83b35 | ||
|
|
588b4d8bf4 | ||
|
|
27d5cf14b3 | ||
|
|
dc999bacca | ||
|
|
b90af30496 | ||
|
|
6501c051aa | ||
|
|
fca279bb58 | ||
|
|
d7620c1576 | ||
|
|
846d2d92bd | ||
|
|
ea0a3fded0 | ||
|
|
2d450add12 | ||
|
|
c47e2c1024 | ||
|
|
f1f374f773 | ||
|
|
84da2bf59f | ||
|
|
a45d427b92 | ||
|
|
7b843a9396 | ||
|
|
8da4bc0fd0 | ||
|
|
384206b02e | ||
|
|
12b907aee8 | ||
|
|
2580c2392d | ||
|
|
49fd5387e9 | ||
|
|
66ac931044 | ||
|
|
212a442610 | ||
|
|
d42a931714 | ||
|
|
69d2d65fed | ||
|
|
344b380d65 | ||
|
|
95e79f8690 | ||
|
|
23543e9fb8 | ||
|
|
955b86567b | ||
|
|
3835cb30f5 | ||
|
|
9c0fefbd7f | ||
|
|
ba2f0c6271 | ||
|
|
128c4a4194 | ||
|
|
a7408115ed | ||
|
|
b9c1a7afca | ||
|
|
c25f0e4c0f | ||
|
|
12ac787f8f | ||
|
|
64990abd92 | ||
|
|
96c72c7029 | ||
|
|
c6680c6559 | ||
|
|
f2d12068a0 | ||
|
|
75e1c1016a | ||
|
|
69ae2a2373 | ||
|
|
901630551a | ||
|
|
8ad92cc053 | ||
|
|
585e4e3b53 | ||
|
|
ea50657f8a | ||
|
|
b1adb8d810 | ||
|
|
347ef46ffc | ||
|
|
acbd615590 | ||
|
|
04493e878a | ||
|
|
8fded6cbf7 | ||
|
|
ae0992fc73 | ||
|
|
325905f741 | ||
|
|
582848455c | ||
|
|
f3835ed243 | ||
|
|
5dc3fc0cf2 | ||
|
|
87647ac04f | ||
|
|
3829a2df51 | ||
|
|
9cb0134130 | ||
|
|
b39f94faae | ||
|
|
adbc96dfea | ||
|
|
361f7e2c92 | ||
|
|
be6eab070b | ||
|
|
8a84ce8d8b | ||
|
|
859d5c5007 | ||
|
|
076f114469 | ||
|
|
96f2aa0344 | ||
|
|
c1d6d93ba8 | ||
|
|
2185b445f2 | ||
|
|
b9dd8b2de4 | ||
|
|
a999e665de | ||
|
|
eb51001d0a | ||
|
|
9dcb904c21 | ||
|
|
df9da07535 | ||
|
|
3e65d19929 | ||
|
|
105e329c47 | ||
|
|
0fae25ac43 | ||
|
|
08cf9260e8 | ||
|
|
9113f87e1e | ||
|
|
321cfbee03 | ||
|
|
3e14b9b697 | ||
|
|
14b5edc771 | ||
|
|
314b8f3848 | ||
|
|
0defe7773b | ||
|
|
a62e17736c | ||
|
|
efe09f539b | ||
|
|
79bdda34b2 | ||
|
|
b6bce67308 | ||
|
|
804fd752e8 | ||
|
|
af70c4b12c | ||
|
|
ef7a5f6faa | ||
|
|
6c255f4658 | ||
|
|
e49d76ff14 | ||
|
|
196b76805c | ||
|
|
7dea021357 | ||
|
|
f990ba581a | ||
|
|
76c7d2a0a7 | ||
|
|
0d51803f91 | ||
|
|
b59f8b5c53 | ||
|
|
c514b4fc04 | ||
|
|
c6bad74c45 | ||
|
|
8fe8ca4fd1 | ||
|
|
c796e33925 | ||
|
|
c74fe07813 | ||
|
|
d1fe20ff09 | ||
|
|
c0e22429bb | ||
|
|
0e9e55b945 | ||
|
|
f50bec002b | ||
|
|
e4cd4833e0 | ||
|
|
b57f7d72d4 | ||
|
|
8acd1707ae | ||
|
|
70df8cbfa9 | ||
|
|
eb480815e8 | ||
|
|
a4babea523 | ||
|
|
1c8cd221fd | ||
|
|
00ec365e30 | ||
|
|
41a275917d | ||
|
|
8059156500 | ||
|
|
24a6a88c46 | ||
|
|
6d00432dc7 | ||
|
|
86e982fa3f | ||
|
|
15cc2c886d | ||
|
|
77b89cec80 | ||
|
|
d62b06989b | ||
|
|
89beec2f78 | ||
|
|
6011d20b86 | ||
|
|
bafa8f61ee | ||
|
|
508414951e | ||
|
|
510d50ab58 | ||
|
|
a5befc4d52 | ||
|
|
262d246f31 | ||
|
|
88407d64af | ||
|
|
4587ac2c1d | ||
|
|
36eb58e893 | ||
|
|
8a3758959b | ||
|
|
3642771ba0 | ||
|
|
79fed76487 | ||
|
|
2b41b3f29e | ||
|
|
28232ab96a | ||
|
|
4bafe8c57e | ||
|
|
672c54a8cc | ||
|
|
648bb3c295 | ||
|
|
9f1f449d96 | ||
|
|
9ea67f22dd | ||
|
|
e61892ced7 | ||
|
|
32e7639231 | ||
|
|
04ad4ce15c | ||
|
|
1631c87989 | ||
|
|
7c212dfaab | ||
|
|
1e8fec699c | ||
|
|
3a62d41476 | ||
|
|
f6d2fd3c65 | ||
|
|
485f66bad8 | ||
|
|
f926a7277d | ||
|
|
740a11745d | ||
|
|
ad871734f3 | ||
|
|
a4967cb732 | ||
|
|
e8fe42b283 | ||
|
|
4c7f8ebf68 | ||
|
|
d514463edd | ||
|
|
f9e6919908 | ||
|
|
8c7c46e0e2 | ||
|
|
c2317eeeed | ||
|
|
45f31c3b75 | ||
|
|
95e98fd5f1 | ||
|
|
a69d3321d5 | ||
|
|
6b87ca01d3 | ||
|
|
2f21fcf8f7 | ||
|
|
7a3053be7c | ||
|
|
8d13fbaedc | ||
|
|
8f13e8b15c | ||
|
|
d8fe748376 | ||
|
|
c73ddac889 | ||
|
|
d49b5560f6 | ||
|
|
6ffe8e7af9 | ||
|
|
8d7a9fc3f0 | ||
|
|
7be0deebd4 | ||
|
|
c59f75b06b | ||
|
|
3f9573f621 | ||
|
|
2304fdc713 | ||
|
|
7b8e6298a7 | ||
|
|
e05b6448bb | ||
|
|
5efcdd071e | ||
|
|
b1ddba5462 | ||
|
|
4a2c42a901 | ||
|
|
da094790d1 | ||
|
|
734569ace3 | ||
|
|
31328ee895 | ||
|
|
b32672b47c | ||
|
|
26770629cb | ||
|
|
853a801f2f | ||
|
|
c2d47af025 | ||
|
|
3d44d9423d | ||
|
|
286ccd2f53 | ||
|
|
fd494a9fa7 | ||
|
|
847ad8c44f | ||
|
|
09e1750887 | ||
|
|
bd8174f9e9 | ||
|
|
554bc6e08c | ||
|
|
2b3b014c2c | ||
|
|
a3f4af82ea | ||
|
|
658b70b8fe | ||
|
|
3a3c1e6481 | ||
|
|
cb86a4ac6e | ||
|
|
2f54e79818 | ||
|
|
a8fad4b5c2 | ||
|
|
e18a2f732c | ||
|
|
ca348c78eb | ||
|
|
22913c11e0 | ||
|
|
41e519046b | ||
|
|
346c5b3338 | ||
|
|
b8bd224804 | ||
|
|
c4794638ff | ||
|
|
24591d0836 | ||
|
|
92edcff4a2 | ||
|
|
17fb3be9b6 | ||
|
|
e6b34e2b2d | ||
|
|
8361f4774f | ||
|
|
d4fb412f9e | ||
|
|
9183d5e2d0 | ||
|
|
9755776353 | ||
|
|
9840ff97d5 | ||
|
|
7a09f91cb7 | ||
|
|
dad8bbf372 | ||
|
|
e5ab6fbb21 | ||
|
|
0730f24d95 | ||
|
|
8cfdedfa7a | ||
|
|
f9d0fec4ff | ||
|
|
7326524050 | ||
|
|
a42f5c0479 | ||
|
|
ffff87d5a0 | ||
|
|
88749bec9f | ||
|
|
25f2ab773d | ||
|
|
8ac14c2917 | ||
|
|
99a153304d | ||
|
|
7d6df5d918 |
2
.github/renovate.json
vendored
@ -5,7 +5,9 @@
|
|||||||
"schedule": ["on sunday"],
|
"schedule": ["on sunday"],
|
||||||
"includePaths": [
|
"includePaths": [
|
||||||
"buildSrc/gradle/**",
|
"buildSrc/gradle/**",
|
||||||
|
"buildSrc/*.gradle.kts",
|
||||||
"gradle/**",
|
"gradle/**",
|
||||||
|
"*.gradle.kts",
|
||||||
".github/**"
|
".github/**"
|
||||||
],
|
],
|
||||||
"ignoreDeps": ["keiyoushi/issue-moderator-action"],
|
"ignoreDeps": ["keiyoushi/issue-moderator-action"],
|
||||||
|
|||||||
1
.github/scripts/create-repo.py
vendored
@ -79,6 +79,7 @@ for apk in REPO_APK_DIR.iterdir():
|
|||||||
"lang": source["lang"],
|
"lang": source["lang"],
|
||||||
"id": source["id"],
|
"id": source["id"],
|
||||||
"baseUrl": source["baseUrl"],
|
"baseUrl": source["baseUrl"],
|
||||||
|
"versionId": source["versionId"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
2
.github/scripts/generate-build-matrices.py
vendored
@ -12,7 +12,7 @@ MULTISRC_LIB_REGEX = re.compile(r"^lib-multisrc/(?P<multisrc>\w+)")
|
|||||||
LIB_REGEX = re.compile(r"^lib/(?P<lib>\w+)")
|
LIB_REGEX = re.compile(r"^lib/(?P<lib>\w+)")
|
||||||
MODULE_REGEX = re.compile(r"^:src:(?P<lang>\w+):(?P<extension>\w+)$")
|
MODULE_REGEX = re.compile(r"^:src:(?P<lang>\w+):(?P<extension>\w+)$")
|
||||||
CORE_FILES_REGEX = re.compile(
|
CORE_FILES_REGEX = re.compile(
|
||||||
r"^(buildSrc/|core/|gradle/|build\.gradle\.kts|common\.gradle|gradle\.properties|settings\.gradle\.kts)"
|
r"^(buildSrc/|core/|gradle/|build\.gradle\.kts|common\.gradle|gradle\.properties|settings\.gradle\.kts|.github/scripts)"
|
||||||
)
|
)
|
||||||
|
|
||||||
def run_command(command: str) -> str:
|
def run_command(command: str) -> str:
|
||||||
|
|||||||
6
.github/scripts/merge-repo.py
vendored
@ -22,7 +22,7 @@ for module in to_delete:
|
|||||||
shutil.copytree(src=LOCAL_REPO.joinpath("apk"), dst=REMOTE_REPO.joinpath("apk"), dirs_exist_ok = True)
|
shutil.copytree(src=LOCAL_REPO.joinpath("apk"), dst=REMOTE_REPO.joinpath("apk"), dirs_exist_ok = True)
|
||||||
shutil.copytree(src=LOCAL_REPO.joinpath("icon"), dst=REMOTE_REPO.joinpath("icon"), dirs_exist_ok = True)
|
shutil.copytree(src=LOCAL_REPO.joinpath("icon"), dst=REMOTE_REPO.joinpath("icon"), dirs_exist_ok = True)
|
||||||
|
|
||||||
with REMOTE_REPO.joinpath("index.min.json").open() as remote_index_file:
|
with REMOTE_REPO.joinpath("index.json").open() as remote_index_file:
|
||||||
remote_index = json.load(remote_index_file)
|
remote_index = json.load(remote_index_file)
|
||||||
|
|
||||||
with LOCAL_REPO.joinpath("index.min.json").open() as local_index_file:
|
with LOCAL_REPO.joinpath("index.min.json").open() as local_index_file:
|
||||||
@ -38,6 +38,10 @@ index.sort(key=lambda x: x["pkg"])
|
|||||||
with REMOTE_REPO.joinpath("index.json").open("w", encoding="utf-8") as index_file:
|
with REMOTE_REPO.joinpath("index.json").open("w", encoding="utf-8") as index_file:
|
||||||
json.dump(index, index_file, ensure_ascii=False, indent=2)
|
json.dump(index, index_file, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
for item in index:
|
||||||
|
for source in item["sources"]:
|
||||||
|
source.pop("versionId", None)
|
||||||
|
|
||||||
with REMOTE_REPO.joinpath("index.min.json").open("w", encoding="utf-8") as index_min_file:
|
with REMOTE_REPO.joinpath("index.min.json").open("w", encoding="utf-8") as index_min_file:
|
||||||
json.dump(index, index_min_file, ensure_ascii=False, separators=(",", ":"))
|
json.dump(index, index_min_file, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,7 @@ that existing contributors will not actively teach them to you.
|
|||||||
- [Android Studio](https://developer.android.com/studio)
|
- [Android Studio](https://developer.android.com/studio)
|
||||||
- Emulator or phone with developer options enabled and a recent version of Tachiyomi installed
|
- Emulator or phone with developer options enabled and a recent version of Tachiyomi installed
|
||||||
- [Icon Generator](https://as280093.github.io/AndroidAssetStudio/icons-launcher.html)
|
- [Icon Generator](https://as280093.github.io/AndroidAssetStudio/icons-launcher.html)
|
||||||
|
- [Try jsoup](https://try.jsoup.org/)
|
||||||
|
|
||||||
### Cloning the repository
|
### Cloning the repository
|
||||||
|
|
||||||
|
|||||||
@ -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.14.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@ -13,9 +13,7 @@ android {
|
|||||||
|
|
||||||
namespace = "eu.kanade.tachiyomi.lib.${project.name}"
|
namespace = "eu.kanade.tachiyomi.lib.${project.name}"
|
||||||
|
|
||||||
buildFeatures {
|
androidResources.enable = false
|
||||||
androidResources = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ android {
|
|||||||
namespace "eu.kanade.tachiyomi.extension"
|
namespace "eu.kanade.tachiyomi.extension"
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
manifest.srcFile "AndroidManifest.xml"
|
manifest.srcFile layout.buildDirectory.file('tempAndroidManifest.xml')
|
||||||
java.srcDirs = ['src']
|
java.srcDirs = ['src']
|
||||||
res.srcDirs = ['res']
|
res.srcDirs = ['res']
|
||||||
assets.srcDirs = ['assets']
|
assets.srcDirs = ['assets']
|
||||||
@ -105,21 +105,31 @@ dependencies {
|
|||||||
compileOnly(libs.bundles.common)
|
compileOnly(libs.bundles.common)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register("copyManifestFile", Copy) {
|
||||||
|
from 'AndroidManifest.xml'
|
||||||
|
rename { 'tempAndroidManifest.xml' }
|
||||||
|
into layout.buildDirectory
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register("writeManifestFile") {
|
tasks.register("writeManifestFile") {
|
||||||
|
dependsOn(copyManifestFile)
|
||||||
doLast {
|
doLast {
|
||||||
def manifest = android.sourceSets.getByName("main").manifest
|
File tempFile = android.sourceSets.getByName('main').manifest.srcFile
|
||||||
if (!manifest.srcFile.exists()) {
|
if (!tempFile.exists()) {
|
||||||
File tempFile = layout.buildDirectory.get().file("tempAndroidManifest.xml").getAsFile()
|
tempFile.write('<?xml version="1.0" encoding="utf-8"?>\n<manifest />\n')
|
||||||
if (!tempFile.exists()) {
|
|
||||||
tempFile.withWriter {
|
|
||||||
it.write('<?xml version="1.0" encoding="utf-8"?>\n<manifest />\n')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manifest.srcFile(tempFile.path)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
tasks.withType(com.android.build.gradle.tasks.PackageAndroidArtifact).configureEach {
|
||||||
|
// need to be in afterEvaluate to overwrite default value
|
||||||
|
createdBy = ""
|
||||||
|
// https://stackoverflow.com/a/77745844
|
||||||
|
doFirst { appMetadata.asFile.getOrNull()?.write('') }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
preBuild.dependsOn(writeManifestFile, lintKotlin)
|
preBuild.dependsOn(writeManifestFile, lintKotlin)
|
||||||
if (System.getenv("CI") != "true") {
|
if (System.getenv("CI") != "true") {
|
||||||
lintKotlin.dependsOn(formatKotlin)
|
lintKotlin.dependsOn(formatKotlin)
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin_version = "1.7.21"
|
kotlin = "1.7.21"
|
||||||
coroutines_version = "1.6.4"
|
coroutines = "1.6.4"
|
||||||
serialization_version = "1.4.0"
|
serialization = "1.4.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.6.1" }
|
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.13.0" }
|
||||||
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
|
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
|
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
|
||||||
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
|
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
|
||||||
|
|
||||||
tachiyomi-lib = { module = "com.github.keiyoushi:extensions-lib", version = "v1.4.2.1" }
|
tachiyomi-lib = { module = "com.github.keiyoushi:extensions-lib", version = "v1.4.2.1" }
|
||||||
|
|
||||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin_version" }
|
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
|
||||||
kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" }
|
kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization" }
|
||||||
kotlin-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" }
|
kotlin-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
||||||
|
|
||||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines_version" }
|
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines_version" }
|
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
|
|
||||||
injekt-core = { module = "com.github.null2264.injekt:injekt-core", version = "4135455a2a" }
|
injekt-core = { module = "com.github.null2264.injekt:injekt-core", version = "4135455a2a" }
|
||||||
rxjava = { module = "io.reactivex:rxjava", version = "1.3.8" }
|
rxjava = { module = "io.reactivex:rxjava", version = "1.3.8" }
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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.14.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 6
|
baseVersionCode = 8
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -34,7 +36,6 @@ import rx.Observable
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
@ -44,6 +45,7 @@ abstract class GigaViewer(
|
|||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
private val cdnUrl: String = "",
|
private val cdnUrl: String = "",
|
||||||
|
private val isPaginated: Boolean = false,
|
||||||
) : ParsedHttpSource() {
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
@ -134,7 +136,7 @@ abstract class GigaViewer(
|
|||||||
.attr("data-src")
|
.attr("data-src")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
protected fun chapterListParseSinglePage(response: Response): List<SChapter> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val aggregateId = document.selectFirst("script.js-valve")!!.attr("data-giga_series")
|
val aggregateId = document.selectFirst("script.js-valve")!!.attr("data-giga_series")
|
||||||
|
|
||||||
@ -180,6 +182,61 @@ abstract class GigaViewer(
|
|||||||
return chapters
|
return chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun paginatedChaptersRequest(referer: String, aggregateId: String, offset: Int): Response {
|
||||||
|
val headers = headers.newBuilder()
|
||||||
|
.set("Referer", referer)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val apiUrl = baseUrl.toHttpUrl().newBuilder()
|
||||||
|
.addPathSegment("api")
|
||||||
|
.addPathSegment("viewer")
|
||||||
|
.addPathSegment("pagination_readable_products")
|
||||||
|
.addQueryParameter("type", "episode")
|
||||||
|
.addQueryParameter("aggregate_id", aggregateId)
|
||||||
|
.addQueryParameter("sort_order", "desc")
|
||||||
|
.addQueryParameter("offset", offset.toString())
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
val request = GET(apiUrl, headers)
|
||||||
|
return client.newCall(request).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun chapterListParsePaginated(response: Response): List<SChapter> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val referer = response.request.url.toString()
|
||||||
|
val aggregateId = document.selectFirst("script.js-valve")!!.attr("data-giga_series")
|
||||||
|
|
||||||
|
val chapters = mutableListOf<SChapter>()
|
||||||
|
|
||||||
|
var offset = 0
|
||||||
|
|
||||||
|
// repeat until the offset is too large to return any chapters, resulting in an empty list
|
||||||
|
while (true) {
|
||||||
|
// make request
|
||||||
|
val result = paginatedChaptersRequest(referer, aggregateId, offset)
|
||||||
|
val resultData = result.parseAs<List<GigaViewerPaginationReadableProduct>>()
|
||||||
|
if (resultData.isEmpty()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
resultData.mapTo(chapters) { element ->
|
||||||
|
element.toSChapter(chapterListMode, publisher)
|
||||||
|
}
|
||||||
|
// increase offset
|
||||||
|
offset += resultData.size
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapters
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
return if (isPaginated) {
|
||||||
|
chapterListParsePaginated(response)
|
||||||
|
} else {
|
||||||
|
chapterListParseSinglePage(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun chapterListSelector() = "li.episode"
|
override fun chapterListSelector() = "li.episode"
|
||||||
|
|
||||||
protected open val chapterListMode = CHAPTER_LIST_PAID
|
protected open val chapterListMode = CHAPTER_LIST_PAID
|
||||||
@ -195,9 +252,7 @@ abstract class GigaViewer(
|
|||||||
} else if (chapterListMode == CHAPTER_LIST_LOCKED && element.hasClass("private")) {
|
} else if (chapterListMode == CHAPTER_LIST_LOCKED && element.hasClass("private")) {
|
||||||
name = LOCK + name
|
name = LOCK + name
|
||||||
}
|
}
|
||||||
date_upload = info.selectFirst("span.series-episode-list-date")
|
date_upload = DATE_PARSER_SIMPLE.tryParse(info.selectFirst("span.series-episode-list-date")?.text().orEmpty())
|
||||||
?.text().orEmpty()
|
|
||||||
.toDate()
|
|
||||||
scanlator = publisher
|
scanlator = publisher
|
||||||
setUrlWithoutDomain(if (info.tagName() == "a") info.attr("href") else mangaUrl)
|
setUrlWithoutDomain(if (info.tagName() == "a") info.attr("href") else mangaUrl)
|
||||||
}
|
}
|
||||||
@ -214,13 +269,18 @@ abstract class GigaViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isScrambled = episode.readableProduct.pageStructure.choJuGiga == "baku"
|
||||||
|
|
||||||
return episode.readableProduct.pageStructure.pages
|
return episode.readableProduct.pageStructure.pages
|
||||||
.filter { it.type == "main" }
|
.filter { it.type == "main" }
|
||||||
.mapIndexed { i, page ->
|
.mapIndexed { i, page ->
|
||||||
val imageUrl = page.src.toHttpUrl().newBuilder()
|
val imageUrl = page.src.toHttpUrl().newBuilder().apply {
|
||||||
.addQueryParameter("width", page.width.toString())
|
addQueryParameter("width", page.width.toString())
|
||||||
.addQueryParameter("height", page.height.toString())
|
addQueryParameter("height", page.height.toString())
|
||||||
.toString()
|
if (isScrambled) {
|
||||||
|
addQueryParameter("baku", "true")
|
||||||
|
}
|
||||||
|
}.toString()
|
||||||
Page(i, document.location(), imageUrl)
|
Page(i, document.location(), imageUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,7 +314,7 @@ abstract class GigaViewer(
|
|||||||
protected open fun imageIntercept(chain: Interceptor.Chain): Response {
|
protected open fun imageIntercept(chain: Interceptor.Chain): Response {
|
||||||
var request = chain.request()
|
var request = chain.request()
|
||||||
|
|
||||||
if (!request.url.toString().startsWith(cdnUrl)) {
|
if (!request.url.toString().startsWith(cdnUrl) || request.url.queryParameter("baku") != "true") {
|
||||||
return chain.proceed(request)
|
return chain.proceed(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,6 +324,7 @@ abstract class GigaViewer(
|
|||||||
val newUrl = request.url.newBuilder()
|
val newUrl = request.url.newBuilder()
|
||||||
.removeAllQueryParameters("width")
|
.removeAllQueryParameters("width")
|
||||||
.removeAllQueryParameters("height")
|
.removeAllQueryParameters("height")
|
||||||
|
.removeAllQueryParameters("baku")
|
||||||
.build()
|
.build()
|
||||||
request = request.newBuilder().url(newUrl).build()
|
request = request.newBuilder().url(newUrl).build()
|
||||||
|
|
||||||
@ -314,14 +375,7 @@ abstract class GigaViewer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toDate(): Long {
|
|
||||||
return runCatching { DATE_PARSER.parse(this)?.time }
|
|
||||||
.getOrNull() ?: 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val DATE_PARSER by lazy { SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH) }
|
|
||||||
|
|
||||||
private const val DIVIDE_NUM = 4
|
private const val DIVIDE_NUM = 4
|
||||||
private const val MULTIPLE = 8
|
private const val MULTIPLE = 8
|
||||||
private val jpegMediaType = "image/jpeg".toMediaType()
|
private val jpegMediaType = "image/jpeg".toMediaType()
|
||||||
@ -329,7 +383,7 @@ abstract class GigaViewer(
|
|||||||
const val CHAPTER_LIST_PAID = 0
|
const val CHAPTER_LIST_PAID = 0
|
||||||
const val CHAPTER_LIST_LOCKED = 1
|
const val CHAPTER_LIST_LOCKED = 1
|
||||||
|
|
||||||
private const val YEN_BANKNOTE = "💴 "
|
const val YEN_BANKNOTE = "💴 "
|
||||||
private const val LOCK = "🔒 "
|
const val LOCK = "🔒 "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.gigaviewer
|
package eu.kanade.tachiyomi.multisrc.gigaviewer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.CHAPTER_LIST_LOCKED
|
||||||
|
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.CHAPTER_LIST_PAID
|
||||||
|
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.LOCK
|
||||||
|
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer.Companion.YEN_BANKNOTE
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GigaViewerEpisodeDto(
|
data class GigaViewerEpisodeDto(
|
||||||
@ -15,6 +23,7 @@ data class GigaViewerReadableProduct(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class GigaViewerPageStructure(
|
data class GigaViewerPageStructure(
|
||||||
val pages: List<GigaViewerPage> = emptyList(),
|
val pages: List<GigaViewerPage> = emptyList(),
|
||||||
|
val choJuGiga: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -24,3 +33,31 @@ data class GigaViewerPage(
|
|||||||
val type: String = "",
|
val type: String = "",
|
||||||
val width: Int = 0,
|
val width: Int = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class GigaViewerPaginationReadableProduct(
|
||||||
|
private val display_open_at: String?,
|
||||||
|
private val readable_product_id: String = "",
|
||||||
|
private val status: GigaViewerPaginationReadableProductStatus?,
|
||||||
|
private val title: String = "",
|
||||||
|
) {
|
||||||
|
fun toSChapter(chapterListMode: Int, publisher: String) = SChapter.create().apply {
|
||||||
|
name = title
|
||||||
|
if (chapterListMode == CHAPTER_LIST_PAID && status?.label != "is_free") {
|
||||||
|
name = YEN_BANKNOTE + name
|
||||||
|
} else if (chapterListMode == CHAPTER_LIST_LOCKED && status?.label == "unpublished") {
|
||||||
|
name = LOCK + name
|
||||||
|
}
|
||||||
|
date_upload = DATE_PARSER_COMPLEX.tryParse(display_open_at)
|
||||||
|
scanlator = publisher
|
||||||
|
url = "/episode/$readable_product_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class GigaViewerPaginationReadableProductStatus(
|
||||||
|
val label: String?, // is_free, is_rentable, is_purchasable, unpublished
|
||||||
|
)
|
||||||
|
|
||||||
|
val DATE_PARSER_SIMPLE = SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH)
|
||||||
|
val DATE_PARSER_COMPLEX = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
|
||||||
|
|||||||
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 33
|
baseVersionCode = 34
|
||||||
|
|||||||
@ -373,8 +373,8 @@ abstract class GroupLe(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val readerMark = when {
|
val readerMark = when {
|
||||||
html.contains("rm_h.readerDoInit([") -> "rm_h.readerDoInit(["
|
html.contains("rm_h.readerInit(") -> "rm_h.readerInit("
|
||||||
html.contains("rm_h.readerInit([") -> "rm_h.readerInit(["
|
html.contains("rm_h.readerDoInit(") -> "rm_h.readerDoInit("
|
||||||
!response.request.url.toString().contains(baseUrl) -> {
|
!response.request.url.toString().contains(baseUrl) -> {
|
||||||
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
|
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,4 +2,8 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 22
|
baseVersionCode = 23
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly("com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.11")
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.kemono
|
package eu.kanade.tachiyomi.multisrc.kemono
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
@ -15,14 +16,19 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import keiyoushi.utils.getPreferences
|
import keiyoushi.utils.getPreferences
|
||||||
import kotlinx.serialization.json.Json
|
import keiyoushi.utils.parseAs
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import okhttp3.Cache
|
||||||
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.brotli.BrotliInterceptor
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.File
|
||||||
import java.lang.Thread.sleep
|
import java.lang.Thread.sleep
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
open class Kemono(
|
open class Kemono(
|
||||||
@ -32,13 +38,35 @@ open class Kemono(
|
|||||||
) : HttpSource(), ConfigurableSource {
|
) : HttpSource(), ConfigurableSource {
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder().rateLimit(1).build()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.rateLimit(1)
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
val request = chain.request()
|
||||||
|
if (request.url.pathSegments.first() == "api") {
|
||||||
|
chain.proceed(request.newBuilder().header("Accept", "text/css").build())
|
||||||
|
} else {
|
||||||
|
chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.apply {
|
||||||
|
val index = networkInterceptors().indexOfFirst { it is BrotliInterceptor }
|
||||||
|
if (index >= 0) interceptors().add(networkInterceptors().removeAt(index))
|
||||||
|
}
|
||||||
|
.cache(
|
||||||
|
Cache(
|
||||||
|
directory = File(Injekt.get<Application>().externalCacheDir, "network_cache_${name.lowercase()}"),
|
||||||
|
maxSize = 50L * 1024 * 1024, // 50 MiB
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val creatorsClient = client.newBuilder()
|
||||||
|
.readTimeout(5, TimeUnit.MINUTES)
|
||||||
|
.build()
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.add("Referer", "$baseUrl/")
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private val preferences = getPreferences()
|
private val preferences = getPreferences()
|
||||||
|
|
||||||
private val apiPath = "api/v1"
|
private val apiPath = "api/v1"
|
||||||
@ -47,8 +75,6 @@ open class Kemono(
|
|||||||
|
|
||||||
private val imgCdnUrl = baseUrl.replace("//", "//img.")
|
private val imgCdnUrl = baseUrl.replace("//", "//img.")
|
||||||
|
|
||||||
private var mangasCache: List<KemonoCreatorDto> = emptyList()
|
|
||||||
|
|
||||||
private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', imgCdnUrl)
|
private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', imgCdnUrl)
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
@ -85,6 +111,7 @@ open class Kemono(
|
|||||||
is SortFilter -> {
|
is SortFilter -> {
|
||||||
sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc"
|
sort = filter.getValue() to if (filter.state!!.ascending) "asc" else "desc"
|
||||||
}
|
}
|
||||||
|
|
||||||
is TypeFilter -> {
|
is TypeFilter -> {
|
||||||
filter.state.filter { state -> state.isIncluded() }.forEach { tri ->
|
filter.state.filter { state -> state.isIncluded() }.forEach { tri ->
|
||||||
typeIncluded.add(tri.value)
|
typeIncluded.add(tri.value)
|
||||||
@ -94,44 +121,60 @@ open class Kemono(
|
|||||||
typeExcluded.add(tri.value)
|
typeExcluded.add(tri.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is FavouritesFilter -> {
|
|
||||||
|
is FavoritesFilter -> {
|
||||||
fav = when (filter.state[0].state) {
|
fav = when (filter.state[0].state) {
|
||||||
0 -> null
|
0 -> null
|
||||||
1 -> true
|
1 -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var mangas = mangasCache
|
val mangas = run {
|
||||||
if (page == 1 || mangasCache.isEmpty()) {
|
val favorites = if (fav != null) {
|
||||||
var favourites: List<KemonoFavouritesDto> = emptyList()
|
val response = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
|
||||||
if (fav != null) {
|
|
||||||
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
|
|
||||||
|
|
||||||
if (favores.code == 401) throw Exception("You are not Logged In")
|
if (response.isSuccessful) {
|
||||||
favourites = favores.parseAs<List<KemonoFavouritesDto>>().filterNot { it.service.lowercase() == "discord" }
|
response.parseAs<List<KemonoFavoritesDto>>().filterNot { it.service.lowercase() == "discord" }
|
||||||
|
} else {
|
||||||
|
response.close()
|
||||||
|
val message = if (response.code == 401) "You are not logged in" else "HTTP error ${response.code}"
|
||||||
|
throw Exception("Failed to fetch favorites: $message")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = client.newCall(GET("$baseUrl/$apiPath/creators", headers)).execute()
|
val request = GET(
|
||||||
|
"$baseUrl/$apiPath/creators",
|
||||||
|
headers,
|
||||||
|
CacheControl.Builder().maxStale(30, TimeUnit.MINUTES).build(),
|
||||||
|
)
|
||||||
|
val response = creatorsClient.newCall(request).execute()
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
response.close()
|
||||||
|
throw Exception("HTTP error ${response.code}")
|
||||||
|
}
|
||||||
val allCreators = response.parseAs<List<KemonoCreatorDto>>().filterNot { it.service.lowercase() == "discord" }
|
val allCreators = response.parseAs<List<KemonoCreatorDto>>().filterNot { it.service.lowercase() == "discord" }
|
||||||
mangas = allCreators.filter {
|
allCreators.filter {
|
||||||
val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase())
|
val includeType = typeIncluded.isEmpty() || typeIncluded.contains(it.service.serviceName().lowercase())
|
||||||
val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase())
|
val excludeType = typeExcluded.isNotEmpty() && typeExcluded.contains(it.service.serviceName().lowercase())
|
||||||
|
|
||||||
val regularSearch = it.name.contains(title, true)
|
val regularSearch = it.name.contains(title, true)
|
||||||
|
|
||||||
val isFavourited = when (fav) {
|
val isFavorited = when (fav) {
|
||||||
true -> favourites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } }
|
true -> favorites.any { f -> f.id == it.id.also { _ -> it.fav = f.faved_seq } }
|
||||||
false -> favourites.none { f -> f.id == it.id }
|
false -> favorites.none { f -> f.id == it.id }
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
includeType && !excludeType && isFavourited &&
|
includeType && !excludeType && isFavorited &&
|
||||||
regularSearch
|
regularSearch
|
||||||
}.also { mangasCache = it }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val sorted = when (sort.first) {
|
val sorted = when (sort.first) {
|
||||||
@ -142,6 +185,7 @@ open class Kemono(
|
|||||||
mangas.sortedBy { it.favorited }
|
mangas.sortedBy { it.favorited }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"tit" -> {
|
"tit" -> {
|
||||||
if (sort.second == "desc") {
|
if (sort.second == "desc") {
|
||||||
mangas.sortedByDescending { it.name }
|
mangas.sortedByDescending { it.name }
|
||||||
@ -149,6 +193,7 @@ open class Kemono(
|
|||||||
mangas.sortedBy { it.name }
|
mangas.sortedBy { it.name }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"new" -> {
|
"new" -> {
|
||||||
if (sort.second == "desc") {
|
if (sort.second == "desc") {
|
||||||
mangas.sortedByDescending { it.id }
|
mangas.sortedByDescending { it.id }
|
||||||
@ -156,14 +201,16 @@ open class Kemono(
|
|||||||
mangas.sortedBy { it.id }
|
mangas.sortedBy { it.id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"fav" -> {
|
"fav" -> {
|
||||||
if (fav != true) throw Exception("Please check 'Favourites Only' Filter")
|
if (fav != true) throw Exception("Please check 'Favorites Only' Filter")
|
||||||
if (sort.second == "desc") {
|
if (sort.second == "desc") {
|
||||||
mangas.sortedByDescending { it.fav }
|
mangas.sortedByDescending { it.fav }
|
||||||
} else {
|
} else {
|
||||||
mangas.sortedBy { it.fav }
|
mangas.sortedBy { it.fav }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (sort.second == "desc") {
|
if (sort.second == "desc") {
|
||||||
mangas.sortedByDescending { it.updatedDate }
|
mangas.sortedByDescending { it.updatedDate }
|
||||||
@ -203,7 +250,7 @@ open class Kemono(
|
|||||||
var hasNextPage = true
|
var hasNextPage = true
|
||||||
val result = ArrayList<SChapter>()
|
val result = ArrayList<SChapter>()
|
||||||
while (offset < prefMaxPost && hasNextPage) {
|
while (offset < prefMaxPost && hasNextPage) {
|
||||||
val request = GET("$baseUrl/$apiPath${manga.url}?o=$offset", headers)
|
val request = GET("$baseUrl/$apiPath${manga.url}/posts?o=$offset", headers)
|
||||||
val page: List<KemonoPostDto> = retry(request).parseAs()
|
val page: List<KemonoPostDto> = retry(request).parseAs()
|
||||||
page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) }
|
page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) }
|
||||||
offset += PAGE_POST_LIMIT
|
offset += PAGE_POST_LIMIT
|
||||||
@ -252,10 +299,6 @@ open class Kemono(
|
|||||||
|
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T = use {
|
|
||||||
json.decodeFromStream(it.body.byteStream())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
ListPreference(screen.context).apply {
|
ListPreference(screen.context).apply {
|
||||||
key = POST_PAGES_PREF
|
key = POST_PAGES_PREF
|
||||||
@ -284,7 +327,7 @@ open class Kemono(
|
|||||||
getSortsList,
|
getSortsList,
|
||||||
),
|
),
|
||||||
TypeFilter("Types", getTypes),
|
TypeFilter("Types", getTypes),
|
||||||
FavouritesFilter(),
|
FavoritesFilter(),
|
||||||
)
|
)
|
||||||
|
|
||||||
open val getTypes: List<String> = emptyList()
|
open val getTypes: List<String> = emptyList()
|
||||||
@ -295,7 +338,7 @@ open class Kemono(
|
|||||||
Pair("Date Updated", "lat"),
|
Pair("Date Updated", "lat"),
|
||||||
Pair("Alphabetical Order", "tit"),
|
Pair("Alphabetical Order", "tit"),
|
||||||
Pair("Service", "serv"),
|
Pair("Service", "serv"),
|
||||||
Pair("Date Favourited", "fav"),
|
Pair("Date Favorited", "fav"),
|
||||||
)
|
)
|
||||||
|
|
||||||
internal open class TypeFilter(name: String, vals: List<String>) :
|
internal open class TypeFilter(name: String, vals: List<String>) :
|
||||||
@ -304,17 +347,19 @@ open class Kemono(
|
|||||||
vals.map { TriFilter(it, it.lowercase()) },
|
vals.map { TriFilter(it, it.lowercase()) },
|
||||||
)
|
)
|
||||||
|
|
||||||
internal class FavouritesFilter() :
|
internal class FavoritesFilter() :
|
||||||
Filter.Group<TriFilter>(
|
Filter.Group<TriFilter>(
|
||||||
"Favourites",
|
"Favorites",
|
||||||
listOf(TriFilter("Favourites Only", "fav")),
|
listOf(TriFilter("Favorites Only", "fav")),
|
||||||
)
|
)
|
||||||
|
|
||||||
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
|
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
|
||||||
|
|
||||||
internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) :
|
internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) :
|
||||||
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
|
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
|
||||||
fun getValue() = vals[state!!.index].second
|
fun getValue() = vals[state!!.index].second
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PAGE_POST_LIMIT = 50
|
private const val PAGE_POST_LIMIT = 50
|
||||||
private const val PAGE_CREATORS_LIMIT = 50
|
private const val PAGE_CREATORS_LIMIT = 50
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class KemonoFavouritesDto(
|
class KemonoFavoritesDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val service: String,
|
val service: String,
|
||||||
|
|||||||
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 36
|
baseVersionCode = 37
|
||||||
|
|||||||
@ -589,7 +589,7 @@ abstract class LibGroup(
|
|||||||
|
|
||||||
private const val API_DOMAIN_PREF = "MangaLibApiDomain"
|
private const val API_DOMAIN_PREF = "MangaLibApiDomain"
|
||||||
private const val API_DOMAIN_TITLE = "Выбор домена API"
|
private const val API_DOMAIN_TITLE = "Выбор домена API"
|
||||||
private const val API_DOMAIN_DEFAULT = "https://api.imglib.info"
|
private const val API_DOMAIN_DEFAULT = "https://api.cdnlibs.org"
|
||||||
|
|
||||||
private const val TOKEN_STORE = "TokenStore"
|
private const val TOKEN_STORE = "TokenStore"
|
||||||
|
|
||||||
@ -652,8 +652,8 @@ abstract class LibGroup(
|
|||||||
val domainApiPref = ListPreference(screen.context).apply {
|
val domainApiPref = ListPreference(screen.context).apply {
|
||||||
key = API_DOMAIN_PREF
|
key = API_DOMAIN_PREF
|
||||||
title = API_DOMAIN_TITLE
|
title = API_DOMAIN_TITLE
|
||||||
entries = arrayOf("Официальное приложение (api.imglib.info)", "Основной (api.lib.social)", "Резервный (api.mangalib.me)", "Резервный 2 (api2.mangalib.me)")
|
entries = arrayOf("Основной (api.cdnlibs.org)", "Резервный (api2.mangalib.me)", "Резервный (hapi.hentaicdn.org)", "Резервный (api.imglib.info)")
|
||||||
entryValues = arrayOf(API_DOMAIN_DEFAULT, "https://api.lib.social", "https://api.mangalib.me", "https://api2.mangalib.me")
|
entryValues = arrayOf(API_DOMAIN_DEFAULT, "https://api2.mangalib.me", "https://hapi.hentaicdn.org", "https://api.imglib.info")
|
||||||
summary = "%s" +
|
summary = "%s" +
|
||||||
"\n\nВыбор домена API, используемого для работы приложения." +
|
"\n\nВыбор домена API, используемого для работы приложения." +
|
||||||
"\n\nПо умолчанию «Официальное приложение»" +
|
"\n\nПо умолчанию «Официальное приложение»" +
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
font_size_title=Font size
|
|
||||||
font_size_summary=Font changes will not be applied to downloaded or cached chapters. The font size will be adjusted according to the size of the dialog box.
|
|
||||||
font_size_message=Font size changed to %s
|
|
||||||
default_font_size=Default
|
|
||||||
disable_website_setting_title=Disable source settings
|
|
||||||
disable_website_setting_summary=Site fonts will be disabled and your device's fonts will be applied. This does not apply to downloaded or cached chapters.
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
font_size_title=Tamanho da fonte
|
|
||||||
font_size_summary=As alterações de fonte não serão aplicadas aos capítulos baixados ou armazenados em cache. O tamanho da fonte será ajustado de acordo com o tamanho da caixa de diálogo.
|
|
||||||
font_size_message=Tamanho da fonte foi alterada para %s
|
|
||||||
default_font_size=Padrão
|
|
||||||
disable_website_setting_title=Desativar configurações do site
|
|
||||||
disable_website_setting_summary=As fontes do site serão desativadas e as fontes de seu dispositivo serão aplicadas. Isso não se aplica a capítulos baixados ou armazenados em cache.
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("lib-multisrc")
|
|
||||||
}
|
|
||||||
|
|
||||||
baseVersionCode = 5
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api(project(":lib:i18n"))
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 45 KiB |
@ -1,345 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Build
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
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.online.ParsedHttpSource
|
|
||||||
import keiyoushi.utils.getPreferencesLazy
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
abstract class MachineTranslations(
|
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
private val language: Language,
|
|
||||||
) : ParsedHttpSource(), ConfigurableSource {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override val lang = language.lang
|
|
||||||
|
|
||||||
protected val preferences: SharedPreferences by getPreferencesLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A flag that tracks whether the settings have been changed. It is used to indicate if
|
|
||||||
* any configuration change has occurred. Once the value is accessed, it resets to `false`.
|
|
||||||
* This is useful for tracking whether a preference has been modified, and ensures that
|
|
||||||
* the change status is cleared after it has been accessed, to prevent multiple triggers.
|
|
||||||
*/
|
|
||||||
private var isSettingsChanged: Boolean = false
|
|
||||||
get() {
|
|
||||||
val current = field
|
|
||||||
field = false
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
protected var fontSize: Int
|
|
||||||
get() = preferences.getString(FONT_SIZE_PREF, DEFAULT_FONT_SIZE)!!.toInt()
|
|
||||||
set(value) = preferences.edit().putString(FONT_SIZE_PREF, value.toString()).apply()
|
|
||||||
|
|
||||||
protected var disableSourceSettings: Boolean
|
|
||||||
get() = preferences.getBoolean(DISABLE_SOURCE_SETTINGS_PREF, language.disableSourceSettings)
|
|
||||||
set(value) = preferences.edit().putBoolean(DISABLE_SOURCE_SETTINGS_PREF, value).apply()
|
|
||||||
|
|
||||||
private val intl = Intl(
|
|
||||||
language = language.lang,
|
|
||||||
baseLanguage = "en",
|
|
||||||
availableLanguages = setOf("en", "es", "fr", "id", "it", "pt-BR"),
|
|
||||||
classLoader = this::class.java.classLoader!!,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val settings get() = language.apply {
|
|
||||||
fontSize = this@MachineTranslations.fontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
open val useDefaultComposedImageInterceptor: Boolean = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient get() = clientInstance!!
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This ensures that the `OkHttpClient` instance is only created when required, and it is rebuilt
|
|
||||||
* when there are configuration changes to ensure that the client uses the most up-to-date settings.
|
|
||||||
*/
|
|
||||||
private var clientInstance: OkHttpClient? = null
|
|
||||||
get() {
|
|
||||||
if (field == null || isSettingsChanged) {
|
|
||||||
field = clientBuilder().build()
|
|
||||||
}
|
|
||||||
return field
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun clientBuilder() = network.cloudflareClient.newBuilder()
|
|
||||||
.connectTimeout(1, TimeUnit.MINUTES)
|
|
||||||
.readTimeout(2, TimeUnit.MINUTES)
|
|
||||||
.addInterceptorIf(useDefaultComposedImageInterceptor, ComposedImageInterceptor(baseUrl, settings))
|
|
||||||
|
|
||||||
private fun OkHttpClient.Builder.addInterceptorIf(condition: Boolean, interceptor: Interceptor): OkHttpClient.Builder {
|
|
||||||
return this.takeIf { condition.not() } ?: this.addInterceptor(interceptor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
|
||||||
|
|
||||||
private val popularFilter = FilterList(SelectionList("", listOf(Option(value = "views", query = "sort_by"))))
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = searchMangaSelector()
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
|
||||||
|
|
||||||
private val latestFilter = FilterList(SelectionList("", listOf(Option(value = "recent", query = "sort_by"))))
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = searchMangaSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
// =========================== Search ============================
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$baseUrl/search".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
url.addQueryParameter("query", query)
|
|
||||||
}
|
|
||||||
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is SelectionList -> {
|
|
||||||
val selected = filter.selected()
|
|
||||||
if (selected.value.isBlank()) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
url.addQueryParameter(selected.query, selected.value)
|
|
||||||
}
|
|
||||||
is GenreList -> {
|
|
||||||
filter.state.filter(GenreCheckBox::state).forEach { genre ->
|
|
||||||
url.addQueryParameter("genres", genre.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
if (query.startsWith(PREFIX_SEARCH)) {
|
|
||||||
val slug = query.removePrefix(PREFIX_SEARCH)
|
|
||||||
return fetchMangaDetails(SManga.create().apply { url = "/comics/$slug" }).map { manga ->
|
|
||||||
MangasPage(listOf(manga), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "section h2 + div > div"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
|
||||||
title = element.selectFirst("h3")!!.text()
|
|
||||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "a[href*=search]:contains(Next)"
|
|
||||||
|
|
||||||
// =========================== Manga Details ============================
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
title = document.selectFirst("h1")!!.text()
|
|
||||||
description = document.selectFirst("p:has(span:contains(Synopsis))")?.ownText()
|
|
||||||
author = document.selectFirst("p:has(span:contains(Author))")?.ownText()
|
|
||||||
genre = document.select("h2:contains(Genres) + div span").joinToString { it.text() }
|
|
||||||
thumbnail_url = document.selectFirst("img.object-cover")?.absUrl("src")
|
|
||||||
document.selectFirst("p:has(span:contains(Status))")?.ownText()?.let {
|
|
||||||
status = when (it.lowercase()) {
|
|
||||||
"ongoing" -> SManga.ONGOING
|
|
||||||
"complete" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setUrlWithoutDomain(document.location())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Chapters ==============================
|
|
||||||
override fun chapterListSelector() = "section li"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
|
||||||
element.selectFirst("a")!!.let {
|
|
||||||
name = it.ownText()
|
|
||||||
setUrlWithoutDomain(it.absUrl("href"))
|
|
||||||
}
|
|
||||||
date_upload = parseChapterDate(element.selectFirst("span")?.text())
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Pages ================================
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
val pages = document.selectFirst("div#json-data")
|
|
||||||
?.ownText()?.parseAs<List<PageDto>>()
|
|
||||||
?: throw Exception("Pages not found")
|
|
||||||
|
|
||||||
return pages.mapIndexed { index, dto ->
|
|
||||||
val imageUrl = when {
|
|
||||||
dto.imageUrl.startsWith("http") -> dto.imageUrl
|
|
||||||
else -> "https://${dto.imageUrl}"
|
|
||||||
}
|
|
||||||
val fragment = json.encodeToString<List<Dialog>>(
|
|
||||||
dto.dialogues.filter { it.getTextBy(language).isNotBlank() },
|
|
||||||
)
|
|
||||||
Page(index, imageUrl = "$imageUrl#$fragment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = ""
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
|
||||||
|
|
||||||
private fun parseChapterDate(date: String?): Long {
|
|
||||||
date ?: return 0
|
|
||||||
return try { dateFormat.parse(date)!!.time } catch (_: Exception) { parseRelativeDate(date) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseRelativeDate(date: String): Long {
|
|
||||||
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
|
|
||||||
val cal = Calendar.getInstance()
|
|
||||||
|
|
||||||
return when {
|
|
||||||
date.contains("day", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
|
||||||
date.contains("hour", true) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
|
||||||
date.contains("minute", true) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
|
||||||
date.contains("second", true) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
|
||||||
date.contains("week", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> String.parseAs(): T {
|
|
||||||
return json.decodeFromString(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Filters ================================
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
val filters = mutableListOf<Filter<*>>(
|
|
||||||
SelectionList("Sort", sortByList),
|
|
||||||
Filter.Separator(),
|
|
||||||
GenreList(title = "Genres", genres = genreList),
|
|
||||||
)
|
|
||||||
|
|
||||||
return FilterList(filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
// Some libreoffice font sizes
|
|
||||||
val sizes = arrayOf(
|
|
||||||
"24", "26", "28",
|
|
||||||
"32", "36", "40",
|
|
||||||
"42", "44", "48",
|
|
||||||
"54", "60", "72",
|
|
||||||
"80", "88", "96",
|
|
||||||
)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = FONT_SIZE_PREF
|
|
||||||
title = intl["font_size_title"]
|
|
||||||
entries = sizes.map {
|
|
||||||
"${it}pt" + if (it == DEFAULT_FONT_SIZE) " - ${intl["default_font_size"]}" else ""
|
|
||||||
}.toTypedArray()
|
|
||||||
entryValues = sizes
|
|
||||||
summary = intl["font_size_summary"]
|
|
||||||
|
|
||||||
setOnPreferenceChange { _, newValue ->
|
|
||||||
val selected = newValue as String
|
|
||||||
val index = this.findIndexOfValue(selected)
|
|
||||||
val entry = entries[index] as String
|
|
||||||
|
|
||||||
fontSize = selected.toInt()
|
|
||||||
|
|
||||||
Toast.makeText(
|
|
||||||
screen.context,
|
|
||||||
intl["font_size_message"].format(entry),
|
|
||||||
Toast.LENGTH_LONG,
|
|
||||||
).show()
|
|
||||||
|
|
||||||
true // It's necessary to update the user interface
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
if (language.disableSourceSettings.not()) {
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = DISABLE_SOURCE_SETTINGS_PREF
|
|
||||||
title = "⚠ ${intl["disable_website_setting_title"]}"
|
|
||||||
summary = intl["disable_website_setting_summary"]
|
|
||||||
setDefaultValue(false)
|
|
||||||
setOnPreferenceChange { _, newValue ->
|
|
||||||
disableSourceSettings = newValue as Boolean
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets an `OnPreferenceChangeListener` for the preference, and before triggering the original listener,
|
|
||||||
* marks that the configuration has changed by setting `isSettingsChanged` to `true`.
|
|
||||||
* This behavior is useful for applying runtime configurations in the HTTP client,
|
|
||||||
* ensuring that the preference change is registered before invoking the original listener.
|
|
||||||
*/
|
|
||||||
protected fun Preference.setOnPreferenceChange(onPreferenceChangeListener: Preference.OnPreferenceChangeListener) {
|
|
||||||
setOnPreferenceChangeListener { preference, newValue ->
|
|
||||||
isSettingsChanged = true
|
|
||||||
onPreferenceChangeListener.onPreferenceChange(preference, newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
|
|
||||||
const val PREFIX_SEARCH = "id:"
|
|
||||||
private const val FONT_SIZE_PREF = "fontSizePref"
|
|
||||||
private const val DISABLE_SOURCE_SETTINGS_PREF = "disableSourceSettingsPref"
|
|
||||||
private const val DEFAULT_FONT_SIZE = "24"
|
|
||||||
|
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
|
||||||
|
|
||||||
class MachineTranslationsFactoryUtils
|
|
||||||
|
|
||||||
interface Language {
|
|
||||||
val lang: String
|
|
||||||
val target: String
|
|
||||||
val origin: String
|
|
||||||
var fontSize: Int
|
|
||||||
var disableSourceSettings: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
data class LanguageImpl(
|
|
||||||
override val lang: String,
|
|
||||||
override val target: String = lang,
|
|
||||||
override val origin: String = "en",
|
|
||||||
override var fontSize: Int = 24,
|
|
||||||
override var disableSourceSettings: Boolean = false,
|
|
||||||
) : Language
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
|
|
||||||
class SelectionList(displayName: String, private val vals: List<Option>, state: Int = 0) :
|
|
||||||
Filter.Select<String>(displayName, vals.map { it.name }.toTypedArray(), state) {
|
|
||||||
fun selected() = vals[state]
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Option(val name: String = "", val value: String = "", val query: String = "")
|
|
||||||
|
|
||||||
class GenreList(title: String, genres: List<Genre>) :
|
|
||||||
Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it.name, it.id) })
|
|
||||||
|
|
||||||
class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
|
|
||||||
|
|
||||||
class Genre(val name: String, val id: String = name)
|
|
||||||
|
|
||||||
val genreList: List<Genre> = listOf(
|
|
||||||
Genre("Action"),
|
|
||||||
Genre("Adult"),
|
|
||||||
Genre("Adventure"),
|
|
||||||
Genre("Comedy"),
|
|
||||||
Genre("Drama"),
|
|
||||||
Genre("Ecchi"),
|
|
||||||
Genre("Fantasy"),
|
|
||||||
Genre("Gender Bender"),
|
|
||||||
Genre("Harem"),
|
|
||||||
Genre("Historical"),
|
|
||||||
Genre("Horror"),
|
|
||||||
Genre("Josei"),
|
|
||||||
Genre("Lolicon"),
|
|
||||||
Genre("Martial Arts"),
|
|
||||||
Genre("Mature"),
|
|
||||||
Genre("Mecha"),
|
|
||||||
Genre("Mystery"),
|
|
||||||
Genre("Psychological"),
|
|
||||||
Genre("Romance"),
|
|
||||||
Genre("School Life"),
|
|
||||||
Genre("Sci-fi"),
|
|
||||||
Genre("Seinen"),
|
|
||||||
Genre("Shoujo"),
|
|
||||||
Genre("Shoujo Ai"),
|
|
||||||
Genre("Shounen"),
|
|
||||||
Genre("Shounen Ai"),
|
|
||||||
Genre("Slice of Life"),
|
|
||||||
Genre("Smut"),
|
|
||||||
Genre("Sports"),
|
|
||||||
Genre("Supernatural"),
|
|
||||||
Genre("Tragedy"),
|
|
||||||
Genre("Yaoi"),
|
|
||||||
Genre("Yuri"),
|
|
||||||
)
|
|
||||||
|
|
||||||
val sortByList = listOf(
|
|
||||||
Option("All"),
|
|
||||||
Option("Most Views", "views"),
|
|
||||||
Option("Most Recent", "recent"),
|
|
||||||
).map { it.copy(query = "sort_by") }
|
|
||||||
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 42
|
baseVersionCode = 44
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:cryptoaes"))
|
api(project(":lib:cryptoaes"))
|
||||||
|
|||||||
@ -163,6 +163,7 @@ abstract class Madara(
|
|||||||
override fun popularMangaSelector() = "div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector , .manga__item"
|
override fun popularMangaSelector() = "div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector , .manga__item"
|
||||||
|
|
||||||
open val popularMangaUrlSelector = "div.post-title a"
|
open val popularMangaUrlSelector = "div.post-title a"
|
||||||
|
open val popularMangaUrlSelectorImg = "img"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
@ -173,7 +174,7 @@ abstract class Madara(
|
|||||||
manga.title = it.ownText()
|
manga.title = it.ownText()
|
||||||
}
|
}
|
||||||
|
|
||||||
selectFirst("img")?.let {
|
selectFirst(popularMangaUrlSelectorImg)?.let {
|
||||||
manga.thumbnail_url = imageFromElement(it)
|
manga.thumbnail_url = imageFromElement(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -701,12 +702,11 @@ abstract class Madara(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val genres = select(mangaDetailsSelectorGenre)
|
val genres = select(mangaDetailsSelectorGenre)
|
||||||
.map { element -> element.text().lowercase(Locale.ROOT) }
|
.mapTo(ArrayList()) { element -> element.text() }
|
||||||
.toMutableSet()
|
|
||||||
|
|
||||||
if (mangaDetailsSelectorTag.isNotEmpty()) {
|
if (mangaDetailsSelectorTag.isNotEmpty()) {
|
||||||
select(mangaDetailsSelectorTag).forEach { element ->
|
select(mangaDetailsSelectorTag).forEach { element ->
|
||||||
if (genres.contains(element.text()).not() &&
|
if (
|
||||||
element.text().length <= 25 &&
|
element.text().length <= 25 &&
|
||||||
element.text().contains("read", true).not() &&
|
element.text().contains("read", true).not() &&
|
||||||
element.text().contains(name, true).not() &&
|
element.text().contains(name, true).not() &&
|
||||||
@ -714,29 +714,19 @@ abstract class Madara(
|
|||||||
element.text().contains(manga.title, true).not() &&
|
element.text().contains(manga.title, true).not() &&
|
||||||
element.text().contains(altName, true).not()
|
element.text().contains(altName, true).not()
|
||||||
) {
|
) {
|
||||||
genres.add(element.text().lowercase(Locale.ROOT))
|
genres.add(element.text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add manga/manhwa/manhua thinggy to genre
|
// add manga/manhwa/manhua thinggy to genre
|
||||||
document.selectFirst(seriesTypeSelector)?.ownText()?.let {
|
document.selectFirst(seriesTypeSelector)?.ownText()?.let {
|
||||||
if (it.isEmpty().not() && it.notUpdating() && it != "-" && genres.contains(it).not()) {
|
if (it.isEmpty().not() && it.notUpdating() && it != "-") {
|
||||||
genres.add(it.lowercase(Locale.ROOT))
|
genres.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manga.genre = genres.toList().joinToString { genre ->
|
manga.genre = genres.distinctBy(String::lowercase).joinToString()
|
||||||
genre.replaceFirstChar {
|
|
||||||
if (it.isLowerCase()) {
|
|
||||||
it.titlecase(
|
|
||||||
Locale.ROOT,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
it.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add alternative name to manga description
|
// add alternative name to manga description
|
||||||
document.selectFirst(altNameSelector)?.ownText()?.let {
|
document.selectFirst(altNameSelector)?.ownText()?.let {
|
||||||
@ -788,7 +778,7 @@ abstract class Madara(
|
|||||||
/**
|
/**
|
||||||
* Get the best image quality available from srcset
|
* Get the best image quality available from srcset
|
||||||
*/
|
*/
|
||||||
protected fun String.getSrcSetImage(): String? {
|
protected open fun String.getSrcSetImage(): String? {
|
||||||
return this.split(" ")
|
return this.split(" ")
|
||||||
.filter(URL_REGEX::matches)
|
.filter(URL_REGEX::matches)
|
||||||
.maxOfOrNull(String::toString)
|
.maxOfOrNull(String::toString)
|
||||||
@ -960,11 +950,11 @@ abstract class Madara(
|
|||||||
return when {
|
return when {
|
||||||
WordSet("hari", "gün", "jour", "día", "dia", "day", "วัน", "ngày", "giorni", "أيام", "天").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
WordSet("hari", "gün", "jour", "día", "dia", "day", "วัน", "ngày", "giorni", "أيام", "天").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||||
WordSet("jam", "saat", "heure", "hora", "hour", "ชั่วโมง", "giờ", "ore", "ساعة", "小时").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
WordSet("jam", "saat", "heure", "hora", "hour", "ชั่วโมง", "giờ", "ore", "ساعة", "小时").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||||
WordSet("menit", "dakika", "min", "minute", "minuto", "นาที", "دقائق").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
WordSet("menit", "dakika", "min", "minute", "minuto", "นาที", "دقائق", "phút").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||||
WordSet("detik", "segundo", "second", "วินาที").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
WordSet("detik", "segundo", "second", "วินาที", "giây").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||||
WordSet("week", "semana").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
|
WordSet("week", "semana", "tuần").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
|
||||||
WordSet("month", "mes").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
WordSet("month", "mes", "tháng").anyWordIn(date) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||||
WordSet("year", "año").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
WordSet("year", "año", "năm").anyWordIn(date) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 7
|
baseVersionCode = 8
|
||||||
|
|||||||
@ -155,7 +155,7 @@ abstract class MangaBox(
|
|||||||
|
|
||||||
open val simpleQueryPath = "search/story/"
|
open val simpleQueryPath = "search/story/"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap"
|
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap, div.comic-list > .list-comic-item-wrap"
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
return GET("$baseUrl/$popularUrlPath$page", headers)
|
return GET("$baseUrl/$popularUrlPath$page", headers)
|
||||||
@ -211,7 +211,7 @@ abstract class MangaBox(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".panel_story_list .story_item, div.list-truyen-item-wrap"
|
override fun searchMangaSelector() = ".panel_story_list .story_item, div.list-truyen-item-wrap, .list-comic-item-wrap .list-story-item"
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
||||||
|
|
||||||
|
|||||||
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 5
|
||||||
|
|||||||
@ -96,7 +96,6 @@ abstract class MangaCatalog(
|
|||||||
name = "$name1 - $name2"
|
name = "$name1 - $name2"
|
||||||
}
|
}
|
||||||
url = element.select(".col-span-4 > a").attr("abs:href")
|
url = element.select(".col-span-4 > a").attr("abs:href")
|
||||||
date_upload = System.currentTimeMillis()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
|
|||||||
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 1
|
baseVersionCode = 3
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
|||||||
@ -113,12 +113,11 @@ abstract class ManhwaZ(
|
|||||||
override fun searchMangaNextPageSelector(): String? = latestUpdatesNextPageSelector()
|
override fun searchMangaNextPageSelector(): String? = latestUpdatesNextPageSelector()
|
||||||
|
|
||||||
private val ongoingStatusList = listOf("ongoing", "đang ra")
|
private val ongoingStatusList = listOf("ongoing", "đang ra")
|
||||||
private val completedStatusList = listOf("completed", "hoàn thành")
|
private val completedStatusList = listOf("completed", "hoàn thành", "Truyện Full")
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
val statusText = document.selectFirst("div.summary-heading:contains($mangaDetailsStatusHeading) + div.summary-content")
|
val statusText = document.selectFirst("div.summary-heading:contains($mangaDetailsStatusHeading) + div.summary-content")
|
||||||
?.text()
|
?.text()
|
||||||
?.lowercase()
|
|
||||||
?: ""
|
?: ""
|
||||||
|
|
||||||
title = document.selectFirst("div.post-title h1")!!.text()
|
title = document.selectFirst("div.post-title h1")!!.text()
|
||||||
@ -126,8 +125,8 @@ abstract class ManhwaZ(
|
|||||||
description = document.selectFirst("div.summary__content")?.text()
|
description = document.selectFirst("div.summary__content")?.text()
|
||||||
genre = document.select("div.genres-content a[rel=tag]").joinToString { it.text() }
|
genre = document.select("div.genres-content a[rel=tag]").joinToString { it.text() }
|
||||||
status = when {
|
status = when {
|
||||||
ongoingStatusList.contains(statusText) -> SManga.ONGOING
|
ongoingStatusList.any { it.contains(statusText, ignoreCase = true) } -> SManga.ONGOING
|
||||||
completedStatusList.contains(statusText) -> SManga.COMPLETED
|
completedStatusList.any { it.contains(statusText, ignoreCase = true) } -> SManga.COMPLETED
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
thumbnail_url = document.selectFirst("div.summary_image img")?.imgAttr()
|
thumbnail_url = document.selectFirst("div.summary_image img")?.imgAttr()
|
||||||
|
|||||||
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 1
|
baseVersionCode = 2
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":lib:unpacker"))
|
implementation(project(":lib:unpacker"))
|
||||||
|
|||||||
@ -95,10 +95,10 @@ open class MMLook(
|
|||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
if (response.request.method == "GET") return popularMangaParse(response)
|
if (response.request.method == "GET") return popularMangaParse(response)
|
||||||
|
|
||||||
val entries = response.asJsoup().select(".col-auto").map { element ->
|
val entries = response.asJsoup().select(".item-data > div").map { element ->
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
url = element.selectFirst("a")!!.attr("href").mustRemoveSurrounding("/", "/")
|
url = element.selectFirst("a")!!.attr("href").mustRemoveSurrounding("/", "/")
|
||||||
title = element.selectFirst(".e-title")!!.text()
|
title = element.selectFirst(".e-title, .title")!!.text()
|
||||||
author = element.selectFirst(".tip")!!.text()
|
author = element.selectFirst(".tip")!!.text()
|
||||||
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
|
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
|
||||||
}.formatUrl()
|
}.formatUrl()
|
||||||
|
|||||||
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 10
|
baseVersionCode = 11
|
||||||
|
|||||||
@ -154,6 +154,7 @@ abstract class ZeistManga(
|
|||||||
protected open val mangaDetailsSelectorAuthor = "span#author"
|
protected open val mangaDetailsSelectorAuthor = "span#author"
|
||||||
protected open val mangaDetailsSelectorArtist = "span#artist"
|
protected open val mangaDetailsSelectorArtist = "span#artist"
|
||||||
protected open val mangaDetailsSelectorAltName = "header > p"
|
protected open val mangaDetailsSelectorAltName = "header > p"
|
||||||
|
protected open val mangaDetailsSelectorStatus = "span[data-status]"
|
||||||
protected open val mangaDetailsSelectorInfo = ".y6x11p"
|
protected open val mangaDetailsSelectorInfo = ".y6x11p"
|
||||||
protected open val mangaDetailsSelectorInfoTitle = "strong"
|
protected open val mangaDetailsSelectorInfoTitle = "strong"
|
||||||
protected open val mangaDetailsSelectorInfoDescription = "span.dt"
|
protected open val mangaDetailsSelectorInfoDescription = "span.dt"
|
||||||
@ -175,6 +176,7 @@ abstract class ZeistManga(
|
|||||||
.joinToString { it.text() }
|
.joinToString { it.text() }
|
||||||
author = profileManga.selectFirst(mangaDetailsSelectorAuthor)?.text()
|
author = profileManga.selectFirst(mangaDetailsSelectorAuthor)?.text()
|
||||||
artist = profileManga.selectFirst(mangaDetailsSelectorArtist)?.text()
|
artist = profileManga.selectFirst(mangaDetailsSelectorArtist)?.text()
|
||||||
|
status = parseStatus(profileManga.selectFirst(mangaDetailsSelectorStatus)?.text() ?: "")
|
||||||
|
|
||||||
val infoElement = profileManga.select(mangaDetailsSelectorInfo)
|
val infoElement = profileManga.select(mangaDetailsSelectorInfo)
|
||||||
infoElement.forEach { element ->
|
infoElement.forEach { element ->
|
||||||
|
|||||||
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 1
|
baseVersionCode = 3
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.zerotheme
|
package eu.kanade.tachiyomi.multisrc.zerotheme
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
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
|
||||||
|
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.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import keiyoushi.utils.parseAs
|
import keiyoushi.utils.parseAs
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
abstract class ZeroTheme(
|
abstract class ZeroTheme(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
@ -20,15 +23,26 @@ abstract class ZeroTheme(
|
|||||||
|
|
||||||
override val supportsLatest: Boolean = true
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
override val client = network.cloudflareClient
|
||||||
.rateLimit(2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
open val cdnUrl: String = "https://cdn.${baseUrl.substringAfterLast("/")}"
|
open val cdnUrl: String = "https://cdn.${baseUrl.substringAfterLast("/")}"
|
||||||
|
|
||||||
open val imageLocation: String = "images"
|
open val imageLocation: String = "/images"
|
||||||
|
|
||||||
private val sourceLocation: String get() = "$cdnUrl/$imageLocation"
|
open val mangaSubString: String by lazy {
|
||||||
|
val response = client.newCall(GET(baseUrl, headers)).execute()
|
||||||
|
val script = response.asJsoup().select("script")
|
||||||
|
.map(Element::data)
|
||||||
|
.firstOrNull(MANGA_SUBSTRING_REGEX::containsMatchIn)
|
||||||
|
?: throw IOException("manga substring não foi localizado")
|
||||||
|
|
||||||
|
MANGA_SUBSTRING_REGEX.find(script)?.groups?.get(1)?.value
|
||||||
|
?: throw IOException("Não foi extrair a substring do manga")
|
||||||
|
}
|
||||||
|
|
||||||
|
open val chapterSubString: String = "chapter"
|
||||||
|
|
||||||
|
open val sourceLocation: String get() = "$cdnUrl$imageLocation"
|
||||||
|
|
||||||
// =========================== Popular ================================
|
// =========================== Popular ================================
|
||||||
|
|
||||||
@ -61,14 +75,30 @@ abstract class ZeroTheme(
|
|||||||
|
|
||||||
// =========================== Details =================================
|
// =========================== Details =================================
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga) = "$baseUrl/$mangaSubString/${manga.url.substringAfterLast("/")}"
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
checkEntry(manga.url)
|
||||||
|
return GET(getMangaUrl(manga), headers)
|
||||||
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response) = response.toDto<MangaDetailsDto>().toSManga(sourceLocation)
|
override fun mangaDetailsParse(response: Response) = response.toDto<MangaDetailsDto>().toSManga(sourceLocation)
|
||||||
|
|
||||||
// =========================== Chapter =================================
|
// =========================== Chapter =================================
|
||||||
|
|
||||||
|
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/$chapterSubString/${chapter.url.substringAfterLast("/")}"
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
||||||
|
|
||||||
override fun chapterListParse(response: Response) = response.toDto<MangaDetailsDto>().toSChapterList()
|
override fun chapterListParse(response: Response) = response.toDto<MangaDetailsDto>().toSChapterList()
|
||||||
|
|
||||||
// =========================== Pages ===================================
|
// =========================== Pages ===================================
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
checkEntry(chapter.url)
|
||||||
|
return GET(getChapterUrl(chapter), headers)
|
||||||
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> =
|
override fun pageListParse(response: Response): List<Page> =
|
||||||
response.toDto<PageDto>().toPageList(sourceLocation)
|
response.toDto<PageDto>().toPageList(sourceLocation)
|
||||||
|
|
||||||
@ -76,8 +106,18 @@ abstract class ZeroTheme(
|
|||||||
|
|
||||||
// =========================== Utilities ===============================
|
// =========================== Utilities ===============================
|
||||||
|
|
||||||
|
private fun checkEntry(url: String) {
|
||||||
|
if (listOf(mangaSubString, chapterSubString).any(url::contains)) {
|
||||||
|
throw IOException("Migre a obra para extensão $name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline fun <reified T> Response.toDto(): T {
|
inline fun <reified T> Response.toDto(): T {
|
||||||
val jsonString = asJsoup().selectFirst("[data-page]")!!.attr("data-page")
|
val jsonString = asJsoup().selectFirst("[data-page]")!!.attr("data-page")
|
||||||
return jsonString.parseAs<T>()
|
return jsonString.parseAs<T>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val MANGA_SUBSTRING_REGEX = """"(\w+)\\/\{slug\}""".toRegex()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,7 +108,7 @@ class MangaDto(
|
|||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
genre = genres?.joinToString { it.name }
|
genre = genres?.joinToString { it.name }
|
||||||
url = "/comic/$slug"
|
url = slug
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -130,7 +130,7 @@ class ChapterDto(
|
|||||||
name = number.toString()
|
name = number.toString()
|
||||||
chapter_number = number
|
chapter_number = number
|
||||||
date_upload = dateFormat.tryParse(createdAt)
|
date_upload = dateFormat.tryParse(createdAt)
|
||||||
url = "/chapter/$path"
|
url = path
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Bato.to'
|
extName = 'Bato.to'
|
||||||
extClass = '.BatoToFactory'
|
extClass = '.BatoToFactory'
|
||||||
extVersionCode = 50
|
extVersionCode = 53
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ 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 kotlin.random.Random
|
||||||
|
|
||||||
open class BatoTo(
|
open class BatoTo(
|
||||||
final override val lang: String,
|
final override val lang: String,
|
||||||
@ -52,7 +53,17 @@ open class BatoTo(
|
|||||||
private val preferences by getPreferencesLazy { migrateMirrorPref() }
|
private val preferences by getPreferencesLazy { migrateMirrorPref() }
|
||||||
|
|
||||||
override val name: String = "Bato.to"
|
override val name: String = "Bato.to"
|
||||||
override val baseUrl: String get() = mirror
|
|
||||||
|
override var baseUrl: String = ""
|
||||||
|
get() {
|
||||||
|
val current = field
|
||||||
|
if (current.isNotEmpty()) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
field = getMirrorPref()
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
override val id: Long = when (lang) {
|
override val id: Long = when (lang) {
|
||||||
"zh-Hans" -> 2818874445640189582
|
"zh-Hans" -> 2818874445640189582
|
||||||
"zh-Hant" -> 38886079663327225
|
"zh-Hant" -> 38886079663327225
|
||||||
@ -69,7 +80,7 @@ open class BatoTo(
|
|||||||
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
|
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
mirror = newValue as String
|
baseUrl = newValue as String
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,20 +104,19 @@ open class BatoTo(
|
|||||||
screen.addPreference(removeOfficialPref)
|
screen.addPreference(removeOfficialPref)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mirror = ""
|
private fun getMirrorPref(): String {
|
||||||
get() {
|
if (System.getenv("CI") == "true") {
|
||||||
val current = field
|
return (MIRROR_PREF_ENTRY_VALUES.drop(1) + DEPRECATED_MIRRORS).joinToString("#, ")
|
||||||
if (current.isNotEmpty()) {
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
field = getMirrorPref()
|
|
||||||
return field
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMirrorPref(): String {
|
|
||||||
return preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
|
return preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
|
||||||
?.takeUnless { it == MIRROR_PREF_DEFAULT_VALUE }
|
?.takeUnless { it == MIRROR_PREF_DEFAULT_VALUE }
|
||||||
?: let {
|
?: let {
|
||||||
|
/* Semi-sticky mirror:
|
||||||
|
* - Don't randomize on boot
|
||||||
|
* - Don't randomize per language
|
||||||
|
* - Fallback for non-Android platform
|
||||||
|
*/
|
||||||
val seed = runCatching {
|
val seed = runCatching {
|
||||||
val pm = Injekt.get<Application>().packageManager
|
val pm = Injekt.get<Application>().packageManager
|
||||||
pm.getPackageInfo(BuildConfig.APPLICATION_ID, 0).lastUpdateTime
|
pm.getPackageInfo(BuildConfig.APPLICATION_ID, 0).lastUpdateTime
|
||||||
@ -114,7 +124,7 @@ open class BatoTo(
|
|||||||
BuildConfig.VERSION_NAME.hashCode().toLong()
|
BuildConfig.VERSION_NAME.hashCode().toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
MIRROR_PREF_ENTRY_VALUES[1 + (seed % (MIRROR_PREF_ENTRIES.size - 1)).toInt()]
|
MIRROR_PREF_ENTRY_VALUES.drop(1).random(Random(seed))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Buon Dua'
|
extName = 'Buon Dua'
|
||||||
extClass = '.BuonDua'
|
extClass = '.BuonDua'
|
||||||
extVersionCode = 4
|
extVersionCode = 6
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,12 @@ class BuonDua() : ParsedHttpSource() {
|
|||||||
val doc = response.asJsoup()
|
val doc = response.asJsoup()
|
||||||
val dateUploadStr = doc.selectFirst(".article-info > small")?.text()
|
val dateUploadStr = doc.selectFirst(".article-info > small")?.text()
|
||||||
val dateUpload = DATE_FORMAT.tryParse(dateUploadStr)
|
val dateUpload = DATE_FORMAT.tryParse(dateUploadStr)
|
||||||
val maxPage = doc.select("nav.pagination:first-of-type a.pagination-link").last()?.text()?.toInt() ?: 1
|
// /xiuren-no-10051---10065-1127-photos-467c89d5b3e204eebe33ddbc54d905b1-47452?page=57
|
||||||
|
val maxPage = doc.select("nav.pagination:first-of-type a.pagination-next").last()
|
||||||
|
?.absUrl("href")
|
||||||
|
?.takeIf { it.startsWith("http") }
|
||||||
|
?.toHttpUrl()
|
||||||
|
?.queryParameter("page")?.toInt() ?: 1
|
||||||
val basePageUrl = response.request.url
|
val basePageUrl = response.request.url
|
||||||
return (maxPage downTo 1).map { page ->
|
return (maxPage downTo 1).map { page ->
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".all.comickfun.ComickUrlActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="https" />
|
|
||||||
<data android:host="comick.io" />
|
|
||||||
<data android:host="comick.cc" />
|
|
||||||
<data android:host="comick.ink" />
|
|
||||||
<data android:host="comick.app" />
|
|
||||||
<data android:host="comick.fun" />
|
|
||||||
<data android:pathPattern="/comic/.*/..*" />
|
|
||||||
<data android:pathPattern="/comic/..*" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
ignored_groups_title=Ignored Groups
|
|
||||||
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
|
|
||||||
ignored_tags_title=Ignored Tags
|
|
||||||
ignored_tags_summary=Manga with these tags won't show up when browsing.\nOne tag per line (case-insensitive)
|
|
||||||
show_alternative_titles_title=Show Alternative Titles
|
|
||||||
show_alternative_titles_on=Adds alternative titles to the description
|
|
||||||
show_alternative_titles_off=Does not show alternative titles to the description
|
|
||||||
include_tags_title=Include Tags
|
|
||||||
include_tags_on=More specific, but might contain spoilers!
|
|
||||||
include_tags_off=Only the broader genres
|
|
||||||
group_tags_title=Group Tags (fork must support grouping)
|
|
||||||
group_tags_on=Will prefix tags with their type
|
|
||||||
group_tags_off=List all tags together
|
|
||||||
update_cover_title=Update Covers
|
|
||||||
update_cover_on=Keep cover updated
|
|
||||||
update_cover_off=Prefer first cover
|
|
||||||
local_title_title=Translated Title
|
|
||||||
local_title_on=if available
|
|
||||||
local_title_off=Use the default title from the site
|
|
||||||
score_position_title=Score Position in the Description
|
|
||||||
score_position_top=Top
|
|
||||||
score_position_middle=Middle
|
|
||||||
score_position_bottom=Bottom
|
|
||||||
score_position_none=Hide Score
|
|
||||||
chapter_score_filtering_title=Automatically de-duplicate chapters
|
|
||||||
chapter_score_filtering_on=For each chapter, only displays the scanlator with the highest score
|
|
||||||
chapter_score_filtering_off=Does not filterout any chapters based on score (any other scanlator filtering will still apply)
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
ignored_groups_title=Grupos Ignorados
|
|
||||||
ignored_groups_summary=Capítulos desses grupos não aparecerão.\nUm grupo por linha
|
|
||||||
show_alternative_titles_title=Mostrar Títulos Alternativos
|
|
||||||
show_alternative_titles_on=Adiciona títulos alternativos à descrição
|
|
||||||
show_alternative_titles_off=Não mostra títulos alternativos na descrição
|
|
||||||
include_tags_title=Incluir Tags
|
|
||||||
include_tags_on=Mais detalhadas, mas podem conter spoilers
|
|
||||||
include_tags_off=Apenas os gêneros básicos
|
|
||||||
group_tags_title=Agrupar Tags (necessário fork compatível)
|
|
||||||
group_tags_on=Prefixar tags com o respectivo tipo
|
|
||||||
group_tags_off=Listar todas as tags juntas
|
|
||||||
update_cover_title=Atualizar Capas
|
|
||||||
update_cover_on=Manter capas atualizadas
|
|
||||||
update_cover_off=Usar apenas a primeira capa
|
|
||||||
local_title_title=Título Traduzido
|
|
||||||
local_title_on=se disponível
|
|
||||||
local_title_off=Usar o título padrão do site
|
|
||||||
score_position_title=Posição da Nota na Descrição
|
|
||||||
score_position_top=Topo
|
|
||||||
score_position_middle=Meio
|
|
||||||
score_position_bottom=Final
|
|
||||||
score_position_none=Sem Nota
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'Comick'
|
|
||||||
extClass = '.ComickFactory'
|
|
||||||
extVersionCode = 57
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":lib:i18n"))
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@ -1,661 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.EditTextPreference
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import keiyoushi.utils.getPreferencesLazy
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Builder
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TimeZone
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
abstract class Comick(
|
|
||||||
override val lang: String,
|
|
||||||
private val comickLang: String,
|
|
||||||
) : ConfigurableSource, HttpSource() {
|
|
||||||
|
|
||||||
override val name = "Comick"
|
|
||||||
|
|
||||||
override val baseUrl = "https://comick.io"
|
|
||||||
|
|
||||||
private val apiUrl = "https://api.comick.fun"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json = Json {
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
isLenient = true
|
|
||||||
coerceInputValues = true
|
|
||||||
explicitNulls = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var searchResponse: List<SearchManga>
|
|
||||||
|
|
||||||
private val intl by lazy {
|
|
||||||
Intl(
|
|
||||||
language = lang,
|
|
||||||
baseLanguage = "en",
|
|
||||||
availableLanguages = setOf("en", "pt-BR"),
|
|
||||||
classLoader = this::class.java.classLoader!!,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val preferences by getPreferencesLazy { newLineIgnoredGroups() }
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
EditTextPreference(screen.context).apply {
|
|
||||||
key = IGNORED_GROUPS_PREF
|
|
||||||
title = intl["ignored_groups_title"]
|
|
||||||
summary = intl["ignored_groups_summary"]
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
preferences.edit()
|
|
||||||
.putString(IGNORED_GROUPS_PREF, newValue.toString())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
EditTextPreference(screen.context).apply {
|
|
||||||
key = IGNORED_TAGS_PREF
|
|
||||||
title = intl["ignored_tags_title"]
|
|
||||||
summary = intl["ignored_tags_summary"]
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = SHOW_ALTERNATIVE_TITLES_PREF
|
|
||||||
title = intl["show_alternative_titles_title"]
|
|
||||||
summaryOn = intl["show_alternative_titles_on"]
|
|
||||||
summaryOff = intl["show_alternative_titles_off"]
|
|
||||||
setDefaultValue(SHOW_ALTERNATIVE_TITLES_DEFAULT)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(SHOW_ALTERNATIVE_TITLES_PREF, newValue as Boolean)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = INCLUDE_MU_TAGS_PREF
|
|
||||||
title = intl["include_tags_title"]
|
|
||||||
summaryOn = intl["include_tags_on"]
|
|
||||||
summaryOff = intl["include_tags_off"]
|
|
||||||
setDefaultValue(INCLUDE_MU_TAGS_DEFAULT)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(INCLUDE_MU_TAGS_PREF, newValue as Boolean)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = GROUP_TAGS_PREF
|
|
||||||
title = intl["group_tags_title"]
|
|
||||||
summaryOn = intl["group_tags_on"]
|
|
||||||
summaryOff = intl["group_tags_off"]
|
|
||||||
setDefaultValue(GROUP_TAGS_DEFAULT)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(GROUP_TAGS_PREF, newValue as Boolean)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = FIRST_COVER_PREF
|
|
||||||
title = intl["update_cover_title"]
|
|
||||||
summaryOff = intl["update_cover_off"]
|
|
||||||
summaryOn = intl["update_cover_on"]
|
|
||||||
setDefaultValue(FIRST_COVER_DEFAULT)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(FIRST_COVER_PREF, newValue as Boolean)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = LOCAL_TITLE_PREF
|
|
||||||
title = intl["local_title_title"]
|
|
||||||
summaryOff = intl["local_title_off"]
|
|
||||||
summaryOn = intl["local_title_on"]
|
|
||||||
setDefaultValue(LOCAL_TITLE_DEFAULT)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(LOCAL_TITLE_PREF, newValue as Boolean)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = SCORE_POSITION_PREF
|
|
||||||
title = intl["score_position_title"]
|
|
||||||
summary = "%s"
|
|
||||||
entries = arrayOf(
|
|
||||||
intl["score_position_top"],
|
|
||||||
intl["score_position_middle"],
|
|
||||||
intl["score_position_bottom"],
|
|
||||||
intl["score_position_none"],
|
|
||||||
)
|
|
||||||
entryValues = arrayOf(SCORE_POSITION_DEFAULT, "middle", "bottom", "none")
|
|
||||||
setDefaultValue(SCORE_POSITION_DEFAULT)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val selected = newValue as String
|
|
||||||
val index = findIndexOfValue(selected)
|
|
||||||
val entry = entryValues[index] as String
|
|
||||||
|
|
||||||
preferences.edit()
|
|
||||||
.putString(SCORE_POSITION_PREF, entry)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = CHAPTER_SCORE_FILTERING_PREF
|
|
||||||
title = intl["chapter_score_filtering_title"]
|
|
||||||
summaryOff = intl["chapter_score_filtering_off"]
|
|
||||||
summaryOn = intl["chapter_score_filtering_on"]
|
|
||||||
setDefaultValue(CHAPTER_SCORE_FILTERING_DEFAULT)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(CHAPTER_SCORE_FILTERING_PREF, newValue as Boolean)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val SharedPreferences.ignoredGroups: Set<String>
|
|
||||||
get() = getString(IGNORED_GROUPS_PREF, "")
|
|
||||||
?.lowercase()
|
|
||||||
?.split("\n")
|
|
||||||
?.map(String::trim)
|
|
||||||
?.filter(String::isNotEmpty)
|
|
||||||
?.sorted()
|
|
||||||
.orEmpty()
|
|
||||||
.toSet()
|
|
||||||
|
|
||||||
private val SharedPreferences.ignoredTags: String
|
|
||||||
get() = getString(IGNORED_TAGS_PREF, "")
|
|
||||||
?.split("\n")
|
|
||||||
?.map(String::trim)
|
|
||||||
?.filter(String::isNotEmpty)
|
|
||||||
.orEmpty()
|
|
||||||
.joinToString(",")
|
|
||||||
|
|
||||||
private val SharedPreferences.showAlternativeTitles: Boolean
|
|
||||||
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
|
|
||||||
|
|
||||||
private val SharedPreferences.includeMuTags: Boolean
|
|
||||||
get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT)
|
|
||||||
|
|
||||||
private val SharedPreferences.groupTags: Boolean
|
|
||||||
get() = getBoolean(GROUP_TAGS_PREF, GROUP_TAGS_DEFAULT)
|
|
||||||
|
|
||||||
private val SharedPreferences.updateCover: Boolean
|
|
||||||
get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT)
|
|
||||||
|
|
||||||
private val SharedPreferences.localTitle: String
|
|
||||||
get() = if (getBoolean(
|
|
||||||
LOCAL_TITLE_PREF,
|
|
||||||
LOCAL_TITLE_DEFAULT,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
comickLang.lowercase()
|
|
||||||
} else {
|
|
||||||
"all"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val SharedPreferences.scorePosition: String
|
|
||||||
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
|
|
||||||
|
|
||||||
private val SharedPreferences.chapterScoreFiltering: Boolean
|
|
||||||
get() = getBoolean(CHAPTER_SCORE_FILTERING_PREF, CHAPTER_SCORE_FILTERING_DEFAULT)
|
|
||||||
|
|
||||||
override fun headersBuilder() = Headers.Builder().apply {
|
|
||||||
add("Referer", "$baseUrl/")
|
|
||||||
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.addNetworkInterceptor(::errorInterceptor)
|
|
||||||
.rateLimit(3, 1, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private fun errorInterceptor(chain: Interceptor.Chain): Response {
|
|
||||||
val response = chain.proceed(chain.request())
|
|
||||||
|
|
||||||
if (
|
|
||||||
response.isSuccessful ||
|
|
||||||
"application/json" !in response.header("Content-Type").orEmpty()
|
|
||||||
) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
val error = try {
|
|
||||||
response.parseAs<Error>()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
error?.run {
|
|
||||||
throw Exception("$name error $statusCode: $message")
|
|
||||||
} ?: throw Exception("HTTP error ${response.code}")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Popular Manga **/
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return searchMangaRequest(
|
|
||||||
page = page,
|
|
||||||
query = "",
|
|
||||||
filters = FilterList(
|
|
||||||
SortFilter("follow"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val result = response.parseAs<List<SearchManga>>()
|
|
||||||
return MangasPage(
|
|
||||||
result.map(SearchManga::toSManga),
|
|
||||||
hasNextPage = result.size >= LIMIT,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Latest Manga **/
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
return searchMangaRequest(
|
|
||||||
page = page,
|
|
||||||
query = "",
|
|
||||||
filters = FilterList(
|
|
||||||
SortFilter("uploaded"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
|
||||||
|
|
||||||
/** Manga Search **/
|
|
||||||
override fun fetchSearchManga(
|
|
||||||
page: Int,
|
|
||||||
query: String,
|
|
||||||
filters: FilterList,
|
|
||||||
): Observable<MangasPage> {
|
|
||||||
return if (query.startsWith(SLUG_SEARCH_PREFIX)) {
|
|
||||||
// url deep link
|
|
||||||
val slugOrHid = query.substringAfter(SLUG_SEARCH_PREFIX)
|
|
||||||
val manga = SManga.create().apply { this.url = "/comic/$slugOrHid#" }
|
|
||||||
fetchMangaDetails(manga).map {
|
|
||||||
MangasPage(listOf(it), false)
|
|
||||||
}
|
|
||||||
} else if (query.isEmpty()) {
|
|
||||||
// regular filtering without text search
|
|
||||||
client.newCall(searchMangaRequest(page, query, filters))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map(::searchMangaParse)
|
|
||||||
} else {
|
|
||||||
// text search, no pagination in api
|
|
||||||
if (page == 1) {
|
|
||||||
client.newCall(querySearchRequest(query))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map(::querySearchParse)
|
|
||||||
} else {
|
|
||||||
Observable.just(paginatedSearchPage(page))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun querySearchRequest(query: String): Request {
|
|
||||||
val url = "$apiUrl/v1.0/search?limit=300&page=1&tachiyomi=true"
|
|
||||||
.toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("q", query.trim())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun querySearchParse(response: Response): MangasPage {
|
|
||||||
searchResponse = response.parseAs()
|
|
||||||
|
|
||||||
return paginatedSearchPage(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun paginatedSearchPage(page: Int): MangasPage {
|
|
||||||
val end = min(page * LIMIT, searchResponse.size)
|
|
||||||
val entries = searchResponse.subList((page - 1) * LIMIT, end)
|
|
||||||
.map(SearchManga::toSManga)
|
|
||||||
return MangasPage(entries, end < searchResponse.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) {
|
|
||||||
tags.split(",").filter(String::isNotEmpty).forEach {
|
|
||||||
builder.addQueryParameter(
|
|
||||||
parameterName,
|
|
||||||
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
|
|
||||||
.replace("'-", "-and-039-").replace("'", "-and-039-"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply {
|
|
||||||
filters.forEach { it ->
|
|
||||||
when (it) {
|
|
||||||
is CompletedFilter -> {
|
|
||||||
if (it.state) {
|
|
||||||
addQueryParameter("completed", "true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is GenreFilter -> {
|
|
||||||
it.state.filter { it.isIncluded() }.forEach {
|
|
||||||
addQueryParameter("genres", it.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
it.state.filter { it.isExcluded() }.forEach {
|
|
||||||
addQueryParameter("excludes", it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is DemographicFilter -> {
|
|
||||||
it.state.filter { it.state }.forEach {
|
|
||||||
addQueryParameter("demographic", it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is TypeFilter -> {
|
|
||||||
it.state.filter { it.state }.forEach {
|
|
||||||
addQueryParameter("country", it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is SortFilter -> {
|
|
||||||
addQueryParameter("sort", it.getValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
is StatusFilter -> {
|
|
||||||
if (it.state > 0) {
|
|
||||||
addQueryParameter("status", it.getValue())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is ContentRatingFilter -> {
|
|
||||||
if (it.state > 0) {
|
|
||||||
addQueryParameter("content_rating", it.getValue())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is CreatedAtFilter -> {
|
|
||||||
if (it.state > 0) {
|
|
||||||
addQueryParameter("time", it.getValue())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is MinimumFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
addQueryParameter("minimum", it.state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is FromYearFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
addQueryParameter("from", it.state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is ToYearFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
addQueryParameter("to", it.state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is TagFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
addTagQueryParameters(this, it.state, "tags")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is ExcludedTagFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
addTagQueryParameters(this, it.state, "excluded-tags")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addTagQueryParameters(this, preferences.ignoredTags, "excluded-tags")
|
|
||||||
addQueryParameter("tachiyomi", "true")
|
|
||||||
addQueryParameter("limit", "$LIMIT")
|
|
||||||
addQueryParameter("page", "$page")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
|
||||||
|
|
||||||
/** Manga Details **/
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
// Migration from slug based urls to hid based ones
|
|
||||||
if (!manga.url.endsWith("#")) {
|
|
||||||
throw Exception("Migrate from Comick to Comick")
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangaUrl = manga.url.removeSuffix("#")
|
|
||||||
return GET("$apiUrl$mangaUrl?tachiyomi=true", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
mangaDetailsParse(response, manga).apply { initialized = true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga =
|
|
||||||
mangaDetailsParse(response, SManga.create())
|
|
||||||
|
|
||||||
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
|
|
||||||
val mangaData = response.parseAs<Manga>()
|
|
||||||
if (!preferences.updateCover && manga.thumbnail_url != mangaData.comic.cover) {
|
|
||||||
val coversUrl =
|
|
||||||
"$apiUrl/comic/${mangaData.comic.slug ?: mangaData.comic.hid}/covers?tachiyomi=true"
|
|
||||||
val covers = client.newCall(GET(coversUrl)).execute()
|
|
||||||
.parseAs<Covers>().mdCovers.reversed()
|
|
||||||
val firstVol = covers.filter { it.vol == "1" }.ifEmpty { covers }
|
|
||||||
val originalCovers = firstVol
|
|
||||||
.filter { mangaData.comic.isoLang.orEmpty().startsWith(it.locale.orEmpty()) }
|
|
||||||
val localCovers = firstVol
|
|
||||||
.filter { comickLang.startsWith(it.locale.orEmpty()) }
|
|
||||||
return mangaData.toSManga(
|
|
||||||
includeMuTags = preferences.includeMuTags,
|
|
||||||
scorePosition = preferences.scorePosition,
|
|
||||||
showAlternativeTitles = preferences.showAlternativeTitles,
|
|
||||||
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol },
|
|
||||||
groupTags = preferences.groupTags,
|
|
||||||
titleLang = preferences.localTitle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return mangaData.toSManga(
|
|
||||||
includeMuTags = preferences.includeMuTags,
|
|
||||||
scorePosition = preferences.scorePosition,
|
|
||||||
showAlternativeTitles = preferences.showAlternativeTitles,
|
|
||||||
groupTags = preferences.groupTags,
|
|
||||||
titleLang = preferences.localTitle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String {
|
|
||||||
return "$baseUrl${manga.url.removeSuffix("#")}"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Manga Chapter List **/
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
|
||||||
// Migration from slug based urls to hid based ones
|
|
||||||
if (!manga.url.endsWith("#")) {
|
|
||||||
throw Exception("Migrate from Comick to Comick")
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangaUrl = manga.url.removeSuffix("#")
|
|
||||||
val url = "$apiUrl$mangaUrl".toHttpUrl().newBuilder().apply {
|
|
||||||
addPathSegment("chapters")
|
|
||||||
if (comickLang != "all") addQueryParameter("lang", comickLang)
|
|
||||||
addQueryParameter("tachiyomi", "true")
|
|
||||||
addQueryParameter("limit", "$CHAPTERS_LIMIT")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val chapterListResponse = response.parseAs<ChapterList>()
|
|
||||||
|
|
||||||
val mangaUrl = response.request.url.toString()
|
|
||||||
.substringBefore("/chapters")
|
|
||||||
.substringAfter(apiUrl)
|
|
||||||
|
|
||||||
val currentTimestamp = System.currentTimeMillis()
|
|
||||||
|
|
||||||
return chapterListResponse.chapters
|
|
||||||
.filter {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
.filterOnScore(preferences.chapterScoreFiltering)
|
|
||||||
.map { it.toSChapter(mangaUrl) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<Chapter>.filterOnScore(shouldFilter: Boolean): Collection<Chapter> {
|
|
||||||
if (shouldFilter) {
|
|
||||||
return groupBy { it.chap }
|
|
||||||
.map { (_, chapters) -> chapters.maxBy { it.score } }
|
|
||||||
} else {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val publishedDateFormat =
|
|
||||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).apply {
|
|
||||||
timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String {
|
|
||||||
return "$baseUrl${chapter.url}"
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Chapter Pages **/
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val chapterHid = chapter.url.substringAfterLast("/").substringBefore("-")
|
|
||||||
return GET("$apiUrl/chapter/$chapterHid?tachiyomi=true", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val result = response.parseAs<PageList>()
|
|
||||||
val images = result.chapter.images.ifEmpty {
|
|
||||||
// cache busting
|
|
||||||
val url = response.request.url.newBuilder()
|
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
client.newCall(GET(url, headers)).execute()
|
|
||||||
.parseAs<PageList>().chapter.images
|
|
||||||
}
|
|
||||||
return images.mapIndexedNotNull { index, data ->
|
|
||||||
if (data.url == null) null else Page(index = index, imageUrl = data.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
|
||||||
return json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList() = getFilters()
|
|
||||||
|
|
||||||
private fun SharedPreferences.newLineIgnoredGroups() {
|
|
||||||
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return
|
|
||||||
|
|
||||||
val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty()
|
|
||||||
|
|
||||||
edit()
|
|
||||||
.putString(
|
|
||||||
IGNORED_GROUPS_PREF,
|
|
||||||
ignoredGroups
|
|
||||||
.split(",")
|
|
||||||
.map(String::trim)
|
|
||||||
.filter(String::isNotEmpty)
|
|
||||||
.joinToString("\n"),
|
|
||||||
)
|
|
||||||
.putBoolean(MIGRATED_IGNORED_GROUPS, true)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val SLUG_SEARCH_PREFIX = "id:"
|
|
||||||
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
|
|
||||||
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
|
|
||||||
private const val IGNORED_TAGS_PREF = "IgnoredTags"
|
|
||||||
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
|
|
||||||
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
|
|
||||||
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
|
|
||||||
const val INCLUDE_MU_TAGS_DEFAULT = false
|
|
||||||
private const val GROUP_TAGS_PREF = "GroupTags"
|
|
||||||
const val GROUP_TAGS_DEFAULT = false
|
|
||||||
private const val MIGRATED_IGNORED_GROUPS = "MigratedIgnoredGroups"
|
|
||||||
private const val FIRST_COVER_PREF = "DefaultCover"
|
|
||||||
private const val FIRST_COVER_DEFAULT = true
|
|
||||||
private const val SCORE_POSITION_PREF = "ScorePosition"
|
|
||||||
const val SCORE_POSITION_DEFAULT = "top"
|
|
||||||
private const val LOCAL_TITLE_PREF = "LocalTitle"
|
|
||||||
private const val LOCAL_TITLE_DEFAULT = false
|
|
||||||
private const val CHAPTER_SCORE_FILTERING_PREF = "ScoreAutoFiltering"
|
|
||||||
private const val CHAPTER_SCORE_FILTERING_DEFAULT = false
|
|
||||||
private const val LIMIT = 20
|
|
||||||
private const val CHAPTERS_LIMIT = 99999
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
// A legacy mapping of language codes to ensure that source IDs don't change
|
|
||||||
val legacyLanguageMappings = mapOf(
|
|
||||||
"pt-br" to "pt-BR", // Brazilian Portuguese
|
|
||||||
"zh-hk" to "zh-Hant", // Traditional Chinese,
|
|
||||||
"zh" to "zh-Hans", // Simplified Chinese
|
|
||||||
).withDefault { it } // country code matches language code
|
|
||||||
|
|
||||||
class ComickFactory : SourceFactory {
|
|
||||||
private val idMap = listOf(
|
|
||||||
"all" to 982606170401027267,
|
|
||||||
"en" to 2971557565147974499,
|
|
||||||
"pt-br" to 8729626158695297897,
|
|
||||||
"ru" to 5846182885417171581,
|
|
||||||
"fr" to 9126078936214680667,
|
|
||||||
"es-419" to 3182432228546767958,
|
|
||||||
"pl" to 7005108854993254607,
|
|
||||||
"tr" to 7186425300860782365,
|
|
||||||
"it" to 8807318985460553537,
|
|
||||||
"es" to 9052019484488287695,
|
|
||||||
"id" to 5506707690027487154,
|
|
||||||
"hu" to 7838940669485160901,
|
|
||||||
"vi" to 9191587139933034493,
|
|
||||||
"zh-hk" to 3140511316190656180,
|
|
||||||
"ar" to 8266599095155001097,
|
|
||||||
"de" to 7552236568334706863,
|
|
||||||
"zh" to 1071494508319622063,
|
|
||||||
"ca" to 2159382907508433047,
|
|
||||||
"bg" to 8981320463367739957,
|
|
||||||
"th" to 4246541831082737053,
|
|
||||||
"fa" to 3146252372540608964,
|
|
||||||
"uk" to 3505068018066717349,
|
|
||||||
"mn" to 2147260678391898600,
|
|
||||||
"ro" to 6676949771764486043,
|
|
||||||
"he" to 5354540502202034685,
|
|
||||||
"ms" to 4731643595200952045,
|
|
||||||
"tl" to 8549617092958820123,
|
|
||||||
"ja" to 8288710818308434509,
|
|
||||||
"hi" to 5176570178081213805,
|
|
||||||
"my" to 9199495862098963317,
|
|
||||||
"ko" to 3493720175703105662,
|
|
||||||
"cs" to 2651978322082769022,
|
|
||||||
"pt" to 4153491877797434408,
|
|
||||||
"nl" to 6104206360977276112,
|
|
||||||
"sv" to 979314012722687145,
|
|
||||||
"bn" to 3598159956413889411,
|
|
||||||
"no" to 5932005504194733317,
|
|
||||||
"lt" to 1792260331167396074,
|
|
||||||
"el" to 6190162673651111756,
|
|
||||||
"sr" to 571668187470919545,
|
|
||||||
"da" to 7137437402245830147,
|
|
||||||
).toMap()
|
|
||||||
override fun createSources(): List<Source> = idMap.keys.map {
|
|
||||||
object : Comick(legacyLanguageMappings.getValue(it), it) {
|
|
||||||
override val id: Long = idMap[it]!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,237 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.GROUP_TAGS_DEFAULT
|
|
||||||
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.INCLUDE_MU_TAGS_DEFAULT
|
|
||||||
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SCORE_POSITION_DEFAULT
|
|
||||||
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SHOW_ALTERNATIVE_TITLES_DEFAULT
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.math.RoundingMode
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class SearchManga(
|
|
||||||
private val hid: String,
|
|
||||||
private val title: String,
|
|
||||||
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
|
|
||||||
@SerialName("cover_url") val cover: String? = null,
|
|
||||||
) {
|
|
||||||
fun toSManga() = SManga.create().apply {
|
|
||||||
// appending # at end as part of migration from slug to hid
|
|
||||||
url = "/comic/$hid#"
|
|
||||||
title = this@SearchManga.title
|
|
||||||
thumbnail_url = parseCover(cover, mdCovers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Manga(
|
|
||||||
val comic: Comic,
|
|
||||||
private val artists: List<Name> = emptyList(),
|
|
||||||
private val authors: List<Name> = emptyList(),
|
|
||||||
private val genres: List<Genre> = emptyList(),
|
|
||||||
private val demographic: String? = null,
|
|
||||||
) {
|
|
||||||
fun toSManga(
|
|
||||||
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
|
|
||||||
scorePosition: String = SCORE_POSITION_DEFAULT,
|
|
||||||
showAlternativeTitles: Boolean = SHOW_ALTERNATIVE_TITLES_DEFAULT,
|
|
||||||
covers: List<MDcovers>? = null,
|
|
||||||
groupTags: Boolean = GROUP_TAGS_DEFAULT,
|
|
||||||
titleLang: String,
|
|
||||||
): SManga {
|
|
||||||
val entryTitle = comic.altTitles.firstOrNull {
|
|
||||||
titleLang != "all" && !it.lang.isNullOrBlank() && titleLang.startsWith(it.lang)
|
|
||||||
}?.title ?: comic.title
|
|
||||||
val titles = listOf(Title(title = comic.title)) + comic.altTitles
|
|
||||||
|
|
||||||
return SManga.create().apply {
|
|
||||||
// appennding # at end as part of migration from slug to hid
|
|
||||||
url = "/comic/${comic.hid}#"
|
|
||||||
title = entryTitle
|
|
||||||
description = buildString {
|
|
||||||
if (scorePosition == "top") append(comic.fancyScore)
|
|
||||||
val desc = comic.desc?.beautifyDescription()
|
|
||||||
if (!desc.isNullOrEmpty()) {
|
|
||||||
if (this.isNotEmpty()) append("\n\n")
|
|
||||||
append(desc)
|
|
||||||
}
|
|
||||||
if (scorePosition == "middle") {
|
|
||||||
if (this.isNotEmpty()) append("\n\n")
|
|
||||||
append(comic.fancyScore)
|
|
||||||
}
|
|
||||||
if (showAlternativeTitles && comic.altTitles.isNotEmpty()) {
|
|
||||||
if (this.isNotEmpty()) append("\n\n")
|
|
||||||
append("Alternative Titles:\n")
|
|
||||||
append(
|
|
||||||
titles.distinctBy { it.title }.filter { it.title != entryTitle }
|
|
||||||
.mapNotNull { title ->
|
|
||||||
title.title?.let { "• $it" }
|
|
||||||
}.joinToString("\n"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (scorePosition == "bottom") {
|
|
||||||
if (this.isNotEmpty()) append("\n\n")
|
|
||||||
append(comic.fancyScore)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status = comic.status.parseStatus(comic.translationComplete)
|
|
||||||
thumbnail_url = parseCover(
|
|
||||||
comic.cover,
|
|
||||||
covers ?: comic.mdCovers,
|
|
||||||
)
|
|
||||||
artist = artists.joinToString { it.name.trim() }
|
|
||||||
author = authors.joinToString { it.name.trim() }
|
|
||||||
genre = buildList {
|
|
||||||
comic.origination?.let { add(Genre("Origination", it.name)) }
|
|
||||||
demographic?.let { add(Genre("Demographic", it)) }
|
|
||||||
addAll(
|
|
||||||
comic.mdGenres.mapNotNull { it.genre }.sortedBy { it.group }
|
|
||||||
.sortedBy { it.name },
|
|
||||||
)
|
|
||||||
addAll(genres.sortedBy { it.group }.sortedBy { it.name })
|
|
||||||
if (includeMuTags) {
|
|
||||||
addAll(
|
|
||||||
comic.muGenres.categories.mapNotNull { it?.category?.title }.sorted()
|
|
||||||
.map { Genre("Category", it) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.distinctBy { it.name }
|
|
||||||
.filterNot { it.name.isNullOrBlank() || it.group.isNullOrBlank() }
|
|
||||||
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Comic(
|
|
||||||
val hid: String,
|
|
||||||
val title: String,
|
|
||||||
private val country: String? = null,
|
|
||||||
val slug: String? = null,
|
|
||||||
@SerialName("md_titles") val altTitles: List<Title> = emptyList(),
|
|
||||||
val desc: String? = null,
|
|
||||||
val status: Int? = 0,
|
|
||||||
@SerialName("translation_completed") val translationComplete: Boolean? = true,
|
|
||||||
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
|
|
||||||
@SerialName("cover_url") val cover: String? = null,
|
|
||||||
@SerialName("md_comic_md_genres") val mdGenres: List<MdGenres>,
|
|
||||||
@SerialName("mu_comics") val muGenres: MuComicCategories = MuComicCategories(emptyList()),
|
|
||||||
@SerialName("bayesian_rating") val score: String? = null,
|
|
||||||
@SerialName("iso639_1") val isoLang: String? = null,
|
|
||||||
) {
|
|
||||||
val origination = when (country) {
|
|
||||||
"jp" -> Name("Manga")
|
|
||||||
"kr" -> Name("Manhwa")
|
|
||||||
"cn" -> Name("Manhua")
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
val fancyScore: String = if (score.isNullOrEmpty()) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
val stars = score.toBigDecimal().div(BigDecimal(2))
|
|
||||||
.setScale(0, RoundingMode.HALF_UP).toInt()
|
|
||||||
buildString {
|
|
||||||
append("★".repeat(stars))
|
|
||||||
if (stars < 5) append("☆".repeat(5 - stars))
|
|
||||||
append(" $score")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MdGenres(
|
|
||||||
@SerialName("md_genres") val genre: Genre? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Genre(
|
|
||||||
val group: String? = null,
|
|
||||||
val name: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MuComicCategories(
|
|
||||||
@SerialName("mu_comic_categories") val categories: List<MuCategories?> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MuCategories(
|
|
||||||
@SerialName("mu_categories") val category: Title? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Covers(
|
|
||||||
@SerialName("md_covers") val mdCovers: List<MDcovers> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MDcovers(
|
|
||||||
val b2key: String?,
|
|
||||||
val vol: String? = null,
|
|
||||||
val locale: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Title(
|
|
||||||
val title: String?,
|
|
||||||
val lang: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Name(
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ChapterList(
|
|
||||||
val chapters: List<Chapter>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Chapter(
|
|
||||||
private val hid: String,
|
|
||||||
private val lang: String = "",
|
|
||||||
private val title: String = "",
|
|
||||||
@SerialName("created_at") private val createdAt: String = "",
|
|
||||||
@SerialName("publish_at") val publishedAt: String = "",
|
|
||||||
val chap: String = "",
|
|
||||||
private val vol: String = "",
|
|
||||||
@SerialName("group_name") val groups: List<String> = emptyList(),
|
|
||||||
@SerialName("up_count") private val upCount: Int,
|
|
||||||
@SerialName("down_count") private val downCount: Int,
|
|
||||||
) {
|
|
||||||
val score get() = upCount - downCount
|
|
||||||
|
|
||||||
fun toSChapter(mangaUrl: String) = SChapter.create().apply {
|
|
||||||
url = "$mangaUrl/$hid-chapter-$chap-$lang"
|
|
||||||
name = beautifyChapterName(vol, chap, title)
|
|
||||||
date_upload = createdAt.parseDate()
|
|
||||||
scanlator = groups.joinToString().takeUnless { it.isBlank() } ?: "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class PageList(
|
|
||||||
val chapter: ChapterPageData,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ChapterPageData(
|
|
||||||
val images: List<Page>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Page(
|
|
||||||
val url: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Error(
|
|
||||||
val statusCode: Int,
|
|
||||||
val message: String,
|
|
||||||
)
|
|
||||||
@ -1,208 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
|
||||||
|
|
||||||
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),
|
|
||||||
DemographicFilter("Demographic", getDemographicList),
|
|
||||||
TypeFilter("Type", getTypeList),
|
|
||||||
SortFilter(),
|
|
||||||
StatusFilter("Status", getStatusList),
|
|
||||||
ContentRatingFilter("Content Rating", getContentRatingList),
|
|
||||||
CompletedFilter("Completely Scanlated?"),
|
|
||||||
CreatedAtFilter("Created at", getCreatedAtList),
|
|
||||||
MinimumFilter("Minimum Chapters"),
|
|
||||||
Filter.Header("From Year, ex: 2010"),
|
|
||||||
FromYearFilter("From"),
|
|
||||||
Filter.Header("To Year, ex: 2021"),
|
|
||||||
ToYearFilter("To"),
|
|
||||||
Filter.Header("Separate tags with commas"),
|
|
||||||
TagFilter("Tags"),
|
|
||||||
ExcludedTagFilter("Excluded Tags"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Filters **/
|
|
||||||
internal class GenreFilter(name: String, genreList: List<Pair<String, String>>) :
|
|
||||||
Filter.Group<TriFilter>(name, genreList.map { TriFilter(it.first, it.second) })
|
|
||||||
|
|
||||||
internal class TagFilter(name: String) : TextFilter(name)
|
|
||||||
|
|
||||||
internal class ExcludedTagFilter(name: String) : TextFilter(name)
|
|
||||||
|
|
||||||
internal class DemographicFilter(name: String, demographicList: List<Pair<String, String>>) :
|
|
||||||
Filter.Group<CheckBoxFilter>(name, demographicList.map { CheckBoxFilter(it.first, it.second) })
|
|
||||||
|
|
||||||
internal class TypeFilter(name: String, typeList: List<Pair<String, String>>) :
|
|
||||||
Filter.Group<CheckBoxFilter>(name, typeList.map { CheckBoxFilter(it.first, it.second) })
|
|
||||||
|
|
||||||
internal class CompletedFilter(name: String) : CheckBoxFilter(name)
|
|
||||||
|
|
||||||
internal class CreatedAtFilter(name: String, createdAtList: List<Pair<String, String>>) :
|
|
||||||
SelectFilter(name, createdAtList)
|
|
||||||
|
|
||||||
internal class MinimumFilter(name: String) : TextFilter(name)
|
|
||||||
|
|
||||||
internal class FromYearFilter(name: String) : TextFilter(name)
|
|
||||||
|
|
||||||
internal class ToYearFilter(name: String) : TextFilter(name)
|
|
||||||
|
|
||||||
internal class SortFilter(defaultValue: String? = null, state: Int = 0) :
|
|
||||||
SelectFilter("Sort", getSortsList, state, defaultValue)
|
|
||||||
|
|
||||||
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
|
|
||||||
SelectFilter(name, statusList, state)
|
|
||||||
|
|
||||||
internal class ContentRatingFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
|
|
||||||
SelectFilter(name, statusList, state)
|
|
||||||
|
|
||||||
/** Generics **/
|
|
||||||
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
|
|
||||||
|
|
||||||
internal open class TextFilter(name: String) : Filter.Text(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, defaultValue: String? = null) :
|
|
||||||
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: state) {
|
|
||||||
fun getValue() = vals[state].second
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Filters Data **/
|
|
||||||
private val getGenresList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("4-Koma", "4-koma"),
|
|
||||||
Pair("Action", "action"),
|
|
||||||
Pair("Adaptation", "adaptation"),
|
|
||||||
Pair("Adult", "adult"),
|
|
||||||
Pair("Adventure", "adventure"),
|
|
||||||
Pair("Aliens", "aliens"),
|
|
||||||
Pair("Animals", "animals"),
|
|
||||||
Pair("Anthology", "anthology"),
|
|
||||||
Pair("Award Winning", "award-winning"),
|
|
||||||
Pair("Comedy", "comedy"),
|
|
||||||
Pair("Cooking", "cooking"),
|
|
||||||
Pair("Crime", "crime"),
|
|
||||||
Pair("Crossdressing", "crossdressing"),
|
|
||||||
Pair("Delinquents", "delinquents"),
|
|
||||||
Pair("Demons", "demons"),
|
|
||||||
Pair("Doujinshi", "doujinshi"),
|
|
||||||
Pair("Drama", "drama"),
|
|
||||||
Pair("Ecchi", "ecchi"),
|
|
||||||
Pair("Fan Colored", "fan-colored"),
|
|
||||||
Pair("Fantasy", "fantasy"),
|
|
||||||
Pair("Full Color", "full-color"),
|
|
||||||
Pair("Gender Bender", "gender-bender"),
|
|
||||||
Pair("Genderswap", "genderswap"),
|
|
||||||
Pair("Ghosts", "ghosts"),
|
|
||||||
Pair("Gore", "gore"),
|
|
||||||
Pair("Gyaru", "gyaru"),
|
|
||||||
Pair("Harem", "harem"),
|
|
||||||
Pair("Historical", "historical"),
|
|
||||||
Pair("Horror", "horror"),
|
|
||||||
Pair("Incest", "incest"),
|
|
||||||
Pair("Isekai", "isekai"),
|
|
||||||
Pair("Loli", "loli"),
|
|
||||||
Pair("Long Strip", "long-strip"),
|
|
||||||
Pair("Mafia", "mafia"),
|
|
||||||
Pair("Magic", "magic"),
|
|
||||||
Pair("Magical Girls", "magical-girls"),
|
|
||||||
Pair("Martial Arts", "martial-arts"),
|
|
||||||
Pair("Mature", "mature"),
|
|
||||||
Pair("Mecha", "mecha"),
|
|
||||||
Pair("Medical", "medical"),
|
|
||||||
Pair("Military", "military"),
|
|
||||||
Pair("Monster Girls", "monster-girls"),
|
|
||||||
Pair("Monsters", "monsters"),
|
|
||||||
Pair("Music", "music"),
|
|
||||||
Pair("Mystery", "mystery"),
|
|
||||||
Pair("Ninja", "ninja"),
|
|
||||||
Pair("Office Workers", "office-workers"),
|
|
||||||
Pair("Official Colored", "official-colored"),
|
|
||||||
Pair("Oneshot", "oneshot"),
|
|
||||||
Pair("Philosophical", "philosophical"),
|
|
||||||
Pair("Police", "police"),
|
|
||||||
Pair("Post-Apocalyptic", "post-apocalyptic"),
|
|
||||||
Pair("Psychological", "psychological"),
|
|
||||||
Pair("Reincarnation", "reincarnation"),
|
|
||||||
Pair("Reverse Harem", "reverse-harem"),
|
|
||||||
Pair("Romance", "romance"),
|
|
||||||
Pair("Samurai", "samurai"),
|
|
||||||
Pair("School Life", "school-life"),
|
|
||||||
Pair("Sci-Fi", "sci-fi"),
|
|
||||||
Pair("Sexual Violence", "sexual-violence"),
|
|
||||||
Pair("Shota", "shota"),
|
|
||||||
Pair("Shoujo Ai", "shoujo-ai"),
|
|
||||||
Pair("Shounen Ai", "shounen-ai"),
|
|
||||||
Pair("Slice of Life", "slice-of-life"),
|
|
||||||
Pair("Smut", "smut"),
|
|
||||||
Pair("Sports", "sports"),
|
|
||||||
Pair("Superhero", "superhero"),
|
|
||||||
Pair("Supernatural", "supernatural"),
|
|
||||||
Pair("Survival", "survival"),
|
|
||||||
Pair("Thriller", "thriller"),
|
|
||||||
Pair("Time Travel", "time-travel"),
|
|
||||||
Pair("Traditional Games", "traditional-games"),
|
|
||||||
Pair("Tragedy", "tragedy"),
|
|
||||||
Pair("User Created", "user-created"),
|
|
||||||
Pair("Vampires", "vampires"),
|
|
||||||
Pair("Video Games", "video-games"),
|
|
||||||
Pair("Villainess", "villainess"),
|
|
||||||
Pair("Virtual Reality", "virtual-reality"),
|
|
||||||
Pair("Web Comic", "web-comic"),
|
|
||||||
Pair("Wuxia", "wuxia"),
|
|
||||||
Pair("Yaoi", "yaoi"),
|
|
||||||
Pair("Yuri", "yuri"),
|
|
||||||
Pair("Zombies", "zombies"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val getDemographicList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("Shounen", "1"),
|
|
||||||
Pair("Shoujo", "2"),
|
|
||||||
Pair("Seinen", "3"),
|
|
||||||
Pair("Josei", "4"),
|
|
||||||
Pair("None", "5"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val getTypeList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("Manga", "jp"),
|
|
||||||
Pair("Manhwa", "kr"),
|
|
||||||
Pair("Manhua", "cn"),
|
|
||||||
Pair("Others", "others"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val getCreatedAtList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("", ""),
|
|
||||||
Pair("3 days", "3"),
|
|
||||||
Pair("7 days", "7"),
|
|
||||||
Pair("30 days", "30"),
|
|
||||||
Pair("3 months", "90"),
|
|
||||||
Pair("6 months", "180"),
|
|
||||||
Pair("1 year", "365"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val getSortsList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("Most popular", "follow"),
|
|
||||||
Pair("Most follows", "user_follow_count"),
|
|
||||||
Pair("Most views", "view"),
|
|
||||||
Pair("High rating", "rating"),
|
|
||||||
Pair("Last updated", "uploaded"),
|
|
||||||
Pair("Newest", "created_at"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val getStatusList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("All", "0"),
|
|
||||||
Pair("Ongoing", "1"),
|
|
||||||
Pair("Completed", "2"),
|
|
||||||
Pair("Cancelled", "3"),
|
|
||||||
Pair("Hiatus", "4"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val getContentRatingList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("All", ""),
|
|
||||||
Pair("Safe", "safe"),
|
|
||||||
Pair("Suggestive", "suggestive"),
|
|
||||||
Pair("Erotica", "erotica"),
|
|
||||||
)
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import org.jsoup.parser.Parser
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
private val dateFormat by lazy {
|
|
||||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.ENGLISH).apply {
|
|
||||||
timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val markdownLinksRegex = "\\[([^]]+)]\\(([^)]+)\\)".toRegex()
|
|
||||||
private val markdownItalicBoldRegex = "\\*+\\s*([^*]*)\\s*\\*+".toRegex()
|
|
||||||
private val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
|
|
||||||
|
|
||||||
internal fun String.beautifyDescription(): String {
|
|
||||||
return Parser.unescapeEntities(this, false)
|
|
||||||
.substringBefore("---")
|
|
||||||
.replace(markdownLinksRegex, "")
|
|
||||||
.replace(markdownItalicBoldRegex, "")
|
|
||||||
.replace(markdownItalicRegex, "")
|
|
||||||
.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun Int?.parseStatus(translationComplete: Boolean?): Int {
|
|
||||||
return when (this) {
|
|
||||||
1 -> SManga.ONGOING
|
|
||||||
2 -> {
|
|
||||||
if (translationComplete == true) {
|
|
||||||
SManga.COMPLETED
|
|
||||||
} else {
|
|
||||||
SManga.PUBLISHING_FINISHED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3 -> SManga.CANCELLED
|
|
||||||
4 -> SManga.ON_HIATUS
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun parseCover(thumbnailUrl: String?, mdCovers: List<MDcovers>): String? {
|
|
||||||
val b2key = mdCovers.firstOrNull()?.b2key
|
|
||||||
?: return thumbnailUrl
|
|
||||||
val vol = mdCovers.firstOrNull()?.vol.orEmpty()
|
|
||||||
|
|
||||||
return thumbnailUrl?.replaceAfterLast("/", "$b2key#$vol")
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun beautifyChapterName(vol: String, chap: String, title: String): String {
|
|
||||||
return buildString {
|
|
||||||
if (vol.isNotEmpty()) {
|
|
||||||
if (chap.isEmpty()) append("Volume $vol") else append("Vol. $vol")
|
|
||||||
}
|
|
||||||
if (chap.isNotEmpty()) {
|
|
||||||
if (vol.isEmpty()) append("Chapter $chap") else append(", Ch. $chap")
|
|
||||||
}
|
|
||||||
if (title.isNotEmpty()) {
|
|
||||||
if (chap.isEmpty()) append(title) else append(": $title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun String.parseDate(): Long {
|
|
||||||
return runCatching { dateFormat.parse(this)?.time }
|
|
||||||
.getOrNull() ?: 0L
|
|
||||||
}
|
|
||||||
@ -2,7 +2,7 @@ ext {
|
|||||||
extName = 'Coomer'
|
extName = 'Coomer'
|
||||||
extClass = '.Coomer'
|
extClass = '.Coomer'
|
||||||
themePkg = 'kemono'
|
themePkg = 'kemono'
|
||||||
baseUrl = 'https://coomer.su'
|
baseUrl = 'https://coomer.st'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 0
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.extension.all.coomer
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.kemono.Kemono
|
import eu.kanade.tachiyomi.multisrc.kemono.Kemono
|
||||||
|
|
||||||
class Coomer : Kemono("Coomer", "https://coomer.su", "all") {
|
class Coomer : Kemono("Coomer", "https://coomer.st", "all") {
|
||||||
override val getTypes = listOf(
|
override val getTypes = listOf(
|
||||||
"OnlyFans",
|
"OnlyFans",
|
||||||
"Fansly",
|
"Fansly",
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'Galaxy'
|
|
||||||
extClass = '.GalaxyFactory'
|
|
||||||
extVersionCode = 5
|
|
||||||
isNsfw = false
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
||||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@ -1,327 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.galaxy
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
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 kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
abstract class Galaxy(
|
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
override val lang: String,
|
|
||||||
) : HttpSource() {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.rateLimit(2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return if (page == 1) {
|
|
||||||
GET("$baseUrl/webtoons/romance/home", headers)
|
|
||||||
} else {
|
|
||||||
GET("$baseUrl/webtoons/action/home", headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val entries = document.select(
|
|
||||||
"""div.tabs div[wire:snapshot*=App\\Models\\Serie], main div:has(h2:matches(Today\'s Hot|الرائج اليوم)) a[wire:snapshot*=App\\Models\\Serie]""",
|
|
||||||
).map { element ->
|
|
||||||
SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(
|
|
||||||
if (element.tagName().equals("a")) {
|
|
||||||
element.absUrl("href")
|
|
||||||
} else {
|
|
||||||
element.selectFirst("a")!!.absUrl("href")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
|
||||||
title = element.selectFirst("div.text-sm")!!.text()
|
|
||||||
}
|
|
||||||
}.distinctBy { it.url }
|
|
||||||
|
|
||||||
return MangasPage(entries, response.request.url.pathSegments.getOrNull(1) == "romance")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
val url = "$baseUrl/latest?serie_type=webtoon&main_genres=romance" +
|
|
||||||
if (page > 1) {
|
|
||||||
"&page=$page"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val entries = document.select("div[wire:snapshot*=App\\\\Models\\\\Serie]").map { element ->
|
|
||||||
SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
|
||||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
|
||||||
title = element.select("div.flex a[href*=/series/]").last()!!.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val hasNextPage = document.selectFirst("[role=navigation] button[wire:click*=nextPage]") != null
|
|
||||||
|
|
||||||
return MangasPage(entries, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var filters: List<FilterData> = emptyList()
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
|
||||||
protected fun launchIO(block: () -> Unit) = scope.launch {
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
launchIO {
|
|
||||||
if (filters.isEmpty()) {
|
|
||||||
val document = client.newCall(GET("$baseUrl/search", headers)).execute().asJsoup()
|
|
||||||
|
|
||||||
val mainGenre = FilterData(
|
|
||||||
displayName = document.select("label[for$=main_genres]").text(),
|
|
||||||
options = document.select("select[wire:model.live=main_genres] option").map {
|
|
||||||
it.text() to it.attr("value")
|
|
||||||
},
|
|
||||||
queryParameter = "main_genres",
|
|
||||||
)
|
|
||||||
val typeFilter = FilterData(
|
|
||||||
displayName = document.select("label[for$=type]").text(),
|
|
||||||
options = document.select("select[wire:model.live=type] option").map {
|
|
||||||
it.text() to it.attr("value")
|
|
||||||
},
|
|
||||||
queryParameter = "type",
|
|
||||||
)
|
|
||||||
val statusFilter = FilterData(
|
|
||||||
displayName = document.select("label[for$=status]").text(),
|
|
||||||
options = document.select("select[wire:model.live=status] option").map {
|
|
||||||
it.text() to it.attr("value")
|
|
||||||
},
|
|
||||||
queryParameter = "status",
|
|
||||||
)
|
|
||||||
val genreFilter = FilterData(
|
|
||||||
displayName = if (lang == "ar") {
|
|
||||||
"التصنيفات"
|
|
||||||
} else {
|
|
||||||
"Genre"
|
|
||||||
},
|
|
||||||
options = document.select("div[x-data*=genre] > div").map {
|
|
||||||
it.text() to it.attr("wire:key")
|
|
||||||
},
|
|
||||||
queryParameter = "genre",
|
|
||||||
)
|
|
||||||
|
|
||||||
filters = listOf(mainGenre, typeFilter, statusFilter, genreFilter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val filters: List<Filter<*>> = filters.map {
|
|
||||||
SelectFilter(
|
|
||||||
it.displayName,
|
|
||||||
it.options,
|
|
||||||
it.queryParameter,
|
|
||||||
)
|
|
||||||
}.ifEmpty {
|
|
||||||
listOf(
|
|
||||||
Filter.Header("Press 'reset' to load filters"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return FilterList(filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("serie_type", "webtoon")
|
|
||||||
addQueryParameter("title", query.trim())
|
|
||||||
filters.filterIsInstance<SelectFilter>().forEach {
|
|
||||||
it.addFilterParameter(this)
|
|
||||||
}
|
|
||||||
if (page > 1) {
|
|
||||||
addQueryParameter("page", page.toString())
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = document.select("#full_model h3").text()
|
|
||||||
thumbnail_url = document.selectFirst("main img[src*=series/webtoon]")?.absUrl("src")
|
|
||||||
status = when (document.getQueryParam("status")) {
|
|
||||||
"ongoing", "soon" -> SManga.ONGOING
|
|
||||||
"completed", "droped" -> SManga.COMPLETED
|
|
||||||
"onhold" -> SManga.ON_HIATUS
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
genre = buildList {
|
|
||||||
document.getQueryParam("type")
|
|
||||||
?.capitalize()?.let(::add)
|
|
||||||
document.select("#full_model a[href*=search?genre]")
|
|
||||||
.eachText().let(::addAll)
|
|
||||||
}.joinToString()
|
|
||||||
author = document.select("#full_model [wire:key^=a-]").eachText().joinToString()
|
|
||||||
artist = document.select("#full_model [wire:key^=r-]").eachText().joinToString()
|
|
||||||
description = buildString {
|
|
||||||
append(document.select("#full_model p").text().trim())
|
|
||||||
append("\n\nAlternative Names:\n")
|
|
||||||
document.select("#full_model [wire:key^=n-]")
|
|
||||||
.joinToString("\n") { "• ${it.text().trim().removeMdEscaped()}" }
|
|
||||||
.let(::append)
|
|
||||||
}.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Document.getQueryParam(queryParam: String): String? {
|
|
||||||
return selectFirst("#full_model a[href*=search?$queryParam]")
|
|
||||||
?.absUrl("href")?.toHttpUrlOrNull()?.queryParameter(queryParam)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.capitalize(): String {
|
|
||||||
val result = StringBuilder(length)
|
|
||||||
var capitalize = true
|
|
||||||
for (char in this) {
|
|
||||||
result.append(
|
|
||||||
if (capitalize) {
|
|
||||||
char.uppercase()
|
|
||||||
} else {
|
|
||||||
char.lowercase()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
capitalize = char.isWhitespace()
|
|
||||||
}
|
|
||||||
return result.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mdRegex = Regex("""&#(\d+);""")
|
|
||||||
|
|
||||||
private fun String.removeMdEscaped(): String {
|
|
||||||
val char = mdRegex.find(this)?.groupValues?.get(1)?.toIntOrNull()
|
|
||||||
?: return this
|
|
||||||
|
|
||||||
return replaceFirst(mdRegex, Char(char).toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
return document.select("a[href*=/read/]:not([type=button])").map { element ->
|
|
||||||
SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain(element.absUrl("href"))
|
|
||||||
name = element.select("span.font-normal").text()
|
|
||||||
date_upload = element.selectFirst("div:not(:has(> svg)) > span.text-xs")
|
|
||||||
?.text().parseRelativeDate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun String?.parseRelativeDate(): Long {
|
|
||||||
this ?: return 0L
|
|
||||||
|
|
||||||
val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: 0
|
|
||||||
val cal = Calendar.getInstance()
|
|
||||||
|
|
||||||
return when {
|
|
||||||
listOf("second", "ثانية").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("دقيقتين", true) -> {
|
|
||||||
cal.apply { add(Calendar.MINUTE, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("minute", "دقائق").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("ساعتان", true) -> {
|
|
||||||
cal.apply { add(Calendar.HOUR, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("hour", "ساعات").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("يوم", true) -> {
|
|
||||||
cal.apply { add(Calendar.DAY_OF_YEAR, -1) }.timeInMillis
|
|
||||||
}
|
|
||||||
contains("يومين", true) -> {
|
|
||||||
cal.apply { add(Calendar.DAY_OF_YEAR, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("day", "أيام").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.DAY_OF_YEAR, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("أسبوع", true) -> {
|
|
||||||
cal.apply { add(Calendar.WEEK_OF_YEAR, -1) }.timeInMillis
|
|
||||||
}
|
|
||||||
contains("أسبوعين", true) -> {
|
|
||||||
cal.apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("week", "أسابيع").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("شهر", true) -> {
|
|
||||||
cal.apply { add(Calendar.MONTH, -1) }.timeInMillis
|
|
||||||
}
|
|
||||||
contains("شهرين", true) -> {
|
|
||||||
cal.apply { add(Calendar.MONTH, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("month", "أشهر").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
contains("سنة", true) -> {
|
|
||||||
cal.apply { add(Calendar.YEAR, -1) }.timeInMillis
|
|
||||||
}
|
|
||||||
contains("سنتان", true) -> {
|
|
||||||
cal.apply { add(Calendar.YEAR, -2) }.timeInMillis
|
|
||||||
}
|
|
||||||
listOf("year", "سنوات").any { contains(it, true) } -> {
|
|
||||||
cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> 0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
return document.select("[wire:key^=image] img").mapIndexed { idx, img ->
|
|
||||||
Page(idx, imageUrl = img.absUrl("src"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.galaxy
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
import keiyoushi.utils.getPreferencesLazy
|
|
||||||
|
|
||||||
class GalaxyFactory : SourceFactory {
|
|
||||||
|
|
||||||
class GalaxyWebtoon : Galaxy("Galaxy Webtoon", "https://galaxyaction.net", "en") {
|
|
||||||
override val id = 2602904659965278831
|
|
||||||
}
|
|
||||||
|
|
||||||
class GalaxyManga :
|
|
||||||
Galaxy("Galaxy Manga", "https://galaxymanga.net", "ar"),
|
|
||||||
ConfigurableSource {
|
|
||||||
override val id = 2729515745226258240
|
|
||||||
|
|
||||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences by getPreferencesLazy()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"
|
|
||||||
private const val BASE_URL_PREF_TITLE = "تعديل الرابط"
|
|
||||||
private const val BASE_URL_PREF = "overrideBaseUrl"
|
|
||||||
private const val BASE_URL_PREF_SUMMARY = ".للاستخدام المؤقت. تحديث التطبيق سيؤدي الى حذف الإعدادات"
|
|
||||||
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
val baseUrlPref = androidx.preference.EditTextPreference(screen.context).apply {
|
|
||||||
key = BASE_URL_PREF
|
|
||||||
title = BASE_URL_PREF_TITLE
|
|
||||||
summary = BASE_URL_PREF_SUMMARY
|
|
||||||
this.setDefaultValue(super.baseUrl)
|
|
||||||
dialogTitle = BASE_URL_PREF_TITLE
|
|
||||||
dialogMessage = "Default: ${super.baseUrl}"
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, _ ->
|
|
||||||
Toast.makeText(screen.context, RESTART_APP, Toast.LENGTH_LONG).show()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
screen.addPreference(baseUrlPref)
|
|
||||||
}
|
|
||||||
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, super.baseUrl)!!
|
|
||||||
|
|
||||||
init {
|
|
||||||
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { prefDefaultBaseUrl ->
|
|
||||||
if (prefDefaultBaseUrl != super.baseUrl) {
|
|
||||||
preferences.edit()
|
|
||||||
.putString(BASE_URL_PREF, super.baseUrl)
|
|
||||||
.putString(DEFAULT_BASE_URL_PREF, super.baseUrl)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createSources() = listOf(
|
|
||||||
GalaxyWebtoon(),
|
|
||||||
GalaxyManga(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.galaxy
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
class SelectFilter(
|
|
||||||
name: String,
|
|
||||||
private val options: List<Pair<String, String>>,
|
|
||||||
private val queryParam: String,
|
|
||||||
) : Filter.Select<String>(
|
|
||||||
name,
|
|
||||||
buildList {
|
|
||||||
add("")
|
|
||||||
addAll(options.map { it.first })
|
|
||||||
}.toTypedArray(),
|
|
||||||
) {
|
|
||||||
fun addFilterParameter(url: HttpUrl.Builder) {
|
|
||||||
if (state == 0) return
|
|
||||||
|
|
||||||
url.addQueryParameter(queryParam, options[state - 1].second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FilterData(
|
|
||||||
val displayName: String,
|
|
||||||
val options: List<Pair<String, String>>,
|
|
||||||
val queryParameter: String,
|
|
||||||
)
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Hentai Cosplay'
|
extName = 'Hentai Cosplay'
|
||||||
extClass = '.HentaiCosplay'
|
extClass = '.HentaiCosplay'
|
||||||
extVersionCode = 5
|
extVersionCode = 6
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -209,7 +209,7 @@ class HentaiCosplay : HttpSource() {
|
|||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
return document.select("amp-img[src*=upload]")
|
return document.select("amp-img[src*=upload]:not(.related-thumbnail)")
|
||||||
.mapIndexed { index, element ->
|
.mapIndexed { index, element ->
|
||||||
Page(
|
Page(
|
||||||
index = index,
|
index = index,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'izneo (webtoons)'
|
extName = 'izneo (webtoons)'
|
||||||
extClass = '.IzneoFactory'
|
extClass = '.IzneoFactory'
|
||||||
extVersionCode = 7
|
extVersionCode = 8
|
||||||
isNsfw = false
|
isNsfw = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,14 @@ import android.util.Base64
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.asResponseBody
|
||||||
|
import okio.buffer
|
||||||
|
import okio.cipherSource
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
object ImageInterceptor : Interceptor {
|
object ImageInterceptor : Interceptor {
|
||||||
private val mediaType = "image/jpeg".toMediaType()
|
|
||||||
|
|
||||||
private inline val AES: Cipher
|
private inline val AES: Cipher
|
||||||
get() = Cipher.getInstance("AES/CBC/PKCS7Padding")
|
get() = Cipher.getInstance("AES/CBC/PKCS7Padding")
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ object ImageInterceptor : Interceptor {
|
|||||||
|
|
||||||
private fun Response.decode(key: ByteArray, iv: ByteArray) = AES.let {
|
private fun Response.decode(key: ByteArray, iv: ByteArray) = AES.let {
|
||||||
it.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
it.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||||
newBuilder().body(it.doFinal(body.bytes()).toResponseBody(mediaType)).build()
|
newBuilder().body(body.source().cipherSource(it).buffer().asResponseBody("image/jpeg".toMediaType())).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.atob() = Base64.decode(this, Base64.URL_SAFE)
|
private fun String.atob() = Base64.decode(this, Base64.URL_SAFE)
|
||||||
|
|||||||
@ -2,8 +2,8 @@ ext {
|
|||||||
extName = 'Kemono'
|
extName = 'Kemono'
|
||||||
extClass = '.Kemono'
|
extClass = '.Kemono'
|
||||||
themePkg = 'kemono'
|
themePkg = 'kemono'
|
||||||
baseUrl = 'https://kemono.su'
|
baseUrl = 'https://kemono.cr'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 1
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.extension.all.kemono
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.kemono.Kemono
|
import eu.kanade.tachiyomi.multisrc.kemono.Kemono
|
||||||
|
|
||||||
class Kemono : Kemono("Kemono", "https://kemono.su", "all") {
|
class Kemono : Kemono("Kemono", "https://kemono.cr", "all") {
|
||||||
override val getTypes = listOf(
|
override val getTypes = listOf(
|
||||||
"Patreon",
|
"Patreon",
|
||||||
"Pixiv Fanbox",
|
"Pixiv Fanbox",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Komga'
|
extName = 'Komga'
|
||||||
extClass = '.KomgaFactory'
|
extClass = '.KomgaFactory'
|
||||||
extVersionCode = 63
|
extVersionCode = 64
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|||||||
@ -76,6 +76,8 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
|
|
||||||
private val password by lazy { preferences.getString(PREF_PASSWORD, "")!! }
|
private val password by lazy { preferences.getString(PREF_PASSWORD, "")!! }
|
||||||
|
|
||||||
|
private val apiKey by lazy { preferences.getString(PREF_API_KEY, "")!! }
|
||||||
|
|
||||||
private val defaultLibraries
|
private val defaultLibraries
|
||||||
get() = preferences.getStringSet(PREF_DEFAULT_LIBRARIES, emptySet())!!
|
get() = preferences.getStringSet(PREF_DEFAULT_LIBRARIES, emptySet())!!
|
||||||
|
|
||||||
@ -83,12 +85,17 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.set("User-Agent", "TachiyomiKomga/${AppInfo.getVersionName()}")
|
.set("User-Agent", "TachiyomiKomga/${AppInfo.getVersionName()}")
|
||||||
|
.also { builder ->
|
||||||
|
if (apiKey.isNotBlank()) {
|
||||||
|
builder.set("X-API-Key", apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override val client: OkHttpClient =
|
override val client: OkHttpClient =
|
||||||
network.cloudflareClient.newBuilder()
|
network.cloudflareClient.newBuilder()
|
||||||
.authenticator { _, response ->
|
.authenticator { _, response ->
|
||||||
if (response.request.header("Authorization") != null) {
|
if (apiKey.isNotBlank() || response.request.header("Authorization") != null) {
|
||||||
null // Give up, we've already failed to authenticate.
|
null // Give up if API key is set or we've already failed to authenticate.
|
||||||
} else {
|
} else {
|
||||||
response.request.newBuilder()
|
response.request.newBuilder()
|
||||||
.addHeader("Authorization", Credentials.basic(username, password))
|
.addHeader("Authorization", Credentials.basic(username, password))
|
||||||
@ -377,21 +384,33 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
|||||||
key = PREF_ADDRESS,
|
key = PREF_ADDRESS,
|
||||||
restartRequired = true,
|
restartRequired = true,
|
||||||
)
|
)
|
||||||
|
// API key preference (takes precedence over username/password)
|
||||||
screen.addEditTextPreference(
|
screen.addEditTextPreference(
|
||||||
title = "Username",
|
title = "API key",
|
||||||
default = "",
|
default = "",
|
||||||
summary = username.ifBlank { "The user account email" },
|
summary = if (apiKey.isBlank()) "Optional: Use an API key for authentication" else "*".repeat(apiKey.length),
|
||||||
key = PREF_USERNAME,
|
|
||||||
restartRequired = true,
|
|
||||||
)
|
|
||||||
screen.addEditTextPreference(
|
|
||||||
title = "Password",
|
|
||||||
default = "",
|
|
||||||
summary = if (password.isBlank()) "The user account password" else "*".repeat(password.length),
|
|
||||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||||
key = PREF_PASSWORD,
|
key = PREF_API_KEY,
|
||||||
restartRequired = true,
|
restartRequired = true,
|
||||||
)
|
)
|
||||||
|
// Only show username/password if API key is not set
|
||||||
|
if (apiKey.isBlank()) {
|
||||||
|
screen.addEditTextPreference(
|
||||||
|
title = "Username",
|
||||||
|
default = "",
|
||||||
|
summary = username.ifBlank { "The user account email" },
|
||||||
|
key = PREF_USERNAME,
|
||||||
|
restartRequired = true,
|
||||||
|
)
|
||||||
|
screen.addEditTextPreference(
|
||||||
|
title = "Password",
|
||||||
|
default = "",
|
||||||
|
summary = if (password.isBlank()) "The user account password" else "*".repeat(password.length),
|
||||||
|
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||||
|
key = PREF_PASSWORD,
|
||||||
|
restartRequired = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
MultiSelectListPreference(screen.context).apply {
|
MultiSelectListPreference(screen.context).apply {
|
||||||
key = PREF_DEFAULT_LIBRARIES
|
key = PREF_DEFAULT_LIBRARIES
|
||||||
@ -529,6 +548,7 @@ private const val PREF_DISPLAY_NAME = "Source display name"
|
|||||||
private const val PREF_ADDRESS = "Address"
|
private const val PREF_ADDRESS = "Address"
|
||||||
private const val PREF_USERNAME = "Username"
|
private const val PREF_USERNAME = "Username"
|
||||||
private const val PREF_PASSWORD = "Password"
|
private const val PREF_PASSWORD = "Password"
|
||||||
|
private const val PREF_API_KEY = "API key"
|
||||||
private const val PREF_DEFAULT_LIBRARIES = "Default libraries"
|
private const val PREF_DEFAULT_LIBRARIES = "Default libraries"
|
||||||
private const val PREF_CHAPTER_NAME_TEMPLATE = "Chapter name template"
|
private const val PREF_CHAPTER_NAME_TEMPLATE = "Chapter name template"
|
||||||
private const val PREF_CHAPTER_NAME_TEMPLATE_DEFAULT = "{number} - {title} ({size})"
|
private const val PREF_CHAPTER_NAME_TEMPLATE_DEFAULT = "{number} - {title} ({size})"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Luscious'
|
extName = 'Luscious'
|
||||||
extClass = '.LusciousFactory'
|
extClass = '.LusciousFactory'
|
||||||
extVersionCode = 20
|
extVersionCode = 22
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,11 +31,11 @@ import kotlinx.serialization.json.putJsonArray
|
|||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.asResponseBody
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
@ -63,8 +63,8 @@ abstract class Luscious(
|
|||||||
private val rewriteOctetStream: Interceptor = Interceptor { chain ->
|
private val rewriteOctetStream: Interceptor = Interceptor { chain ->
|
||||||
val originalResponse: Response = chain.proceed(chain.request())
|
val originalResponse: Response = chain.proceed(chain.request())
|
||||||
if (originalResponse.headers("Content-Type").contains("application/octet-stream") && originalResponse.request.url.toString().contains(".webp")) {
|
if (originalResponse.headers("Content-Type").contains("application/octet-stream") && originalResponse.request.url.toString().contains(".webp")) {
|
||||||
val orgBody = originalResponse.body.bytes()
|
val orgBody = originalResponse.body.source()
|
||||||
val newBody = orgBody.toResponseBody("image/webp".toMediaTypeOrNull())
|
val newBody = orgBody.asResponseBody("image/webp".toMediaType())
|
||||||
originalResponse.newBuilder()
|
originalResponse.newBuilder()
|
||||||
.body(newBody)
|
.body(newBody)
|
||||||
.build()
|
.build()
|
||||||
@ -845,7 +845,7 @@ abstract class Luscious(
|
|||||||
private const val MIRROR_PREF_KEY = "MIRROR"
|
private const val MIRROR_PREF_KEY = "MIRROR"
|
||||||
private const val MIRROR_PREF_TITLE = "Mirror"
|
private const val MIRROR_PREF_TITLE = "Mirror"
|
||||||
private val MIRROR_PREF_ENTRIES = arrayOf("Guest", "API", "Members")
|
private val MIRROR_PREF_ENTRIES = arrayOf("Guest", "API", "Members")
|
||||||
private val MIRROR_PREF_ENTRY_VALUES = arrayOf("https://www.luscious.net", "https://api.luscious.net", "https://members.luscious.net")
|
private val MIRROR_PREF_ENTRY_VALUES = arrayOf("https://www.luscious.net", "https://apicdn.luscious.net", "https://members.luscious.net")
|
||||||
private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
|
private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'MangaDex'
|
extName = 'MangaDex'
|
||||||
extClass = '.MangaDexFactory'
|
extClass = '.MangaDexFactory'
|
||||||
extVersionCode = 203
|
extVersionCode = 204
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,6 @@ object MDConstants {
|
|||||||
const val apiMangaUrl = "$apiUrl/manga"
|
const val apiMangaUrl = "$apiUrl/manga"
|
||||||
const val apiChapterUrl = "$apiUrl/chapter"
|
const val apiChapterUrl = "$apiUrl/chapter"
|
||||||
const val apiListUrl = "$apiUrl/list"
|
const val apiListUrl = "$apiUrl/list"
|
||||||
const val atHomePostUrl = "https://api.mangadex.network/report"
|
|
||||||
val whitespaceRegex = "\\s".toRegex()
|
val whitespaceRegex = "\\s".toRegex()
|
||||||
|
|
||||||
val mdAtHomeTokenLifespan = 5.minutes.inWholeMilliseconds
|
val mdAtHomeTokenLifespan = 5.minutes.inWholeMilliseconds
|
||||||
|
|||||||
@ -73,7 +73,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
|||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.rateLimit(3)
|
.rateLimit(3)
|
||||||
.addInterceptor(MdAtHomeReportInterceptor(network.cloudflareClient, headers))
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Popular manga section
|
// Popular manga section
|
||||||
|
|||||||
@ -1,109 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangadex
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ImageReportDto
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.Callback
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interceptor to post to MD@Home for MangaDex Stats
|
|
||||||
*/
|
|
||||||
class MdAtHomeReportInterceptor(
|
|
||||||
private val client: OkHttpClient,
|
|
||||||
private val headers: Headers,
|
|
||||||
) : Interceptor {
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val originalRequest = chain.request()
|
|
||||||
val response = chain.proceed(chain.request())
|
|
||||||
val url = originalRequest.url.toString()
|
|
||||||
|
|
||||||
if (!url.contains(MD_AT_HOME_URL_REGEX)) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.e("MangaDex", "Connecting to MD@Home node at $url")
|
|
||||||
|
|
||||||
val reportRequest = mdAtHomeReportRequest(response)
|
|
||||||
|
|
||||||
// Execute the report endpoint network call asynchronously to avoid blocking
|
|
||||||
// the reader from showing the image once it's fully loaded if the report call
|
|
||||||
// gets stuck, as it tend to happens sometimes.
|
|
||||||
client.newCall(reportRequest).enqueue(REPORT_CALLBACK)
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
response.close()
|
|
||||||
|
|
||||||
Log.e("MangaDex", "Error connecting to MD@Home node, fallback to uploads server")
|
|
||||||
|
|
||||||
val imagePath = originalRequest.url.pathSegments
|
|
||||||
.dropWhile { it != "data" && it != "data-saver" }
|
|
||||||
.joinToString("/")
|
|
||||||
|
|
||||||
val fallbackUrl = MDConstants.cdnUrl.toHttpUrl().newBuilder()
|
|
||||||
.addPathSegments(imagePath)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val fallbackRequest = originalRequest.newBuilder()
|
|
||||||
.url(fallbackUrl)
|
|
||||||
.headers(headers)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return chain.proceed(fallbackRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mdAtHomeReportRequest(response: Response): Request {
|
|
||||||
val result = ImageReportDto(
|
|
||||||
url = response.request.url.toString(),
|
|
||||||
success = response.isSuccessful,
|
|
||||||
bytes = response.peekBody(Long.MAX_VALUE).bytes().size,
|
|
||||||
cached = response.headers["X-Cache"] == "HIT",
|
|
||||||
duration = response.receivedResponseAtMillis - response.sentRequestAtMillis,
|
|
||||||
)
|
|
||||||
|
|
||||||
val payload = json.encodeToString(result)
|
|
||||||
|
|
||||||
return POST(
|
|
||||||
url = MDConstants.atHomePostUrl,
|
|
||||||
headers = headers,
|
|
||||||
body = payload.toRequestBody(JSON_MEDIA_TYPE),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val JSON_MEDIA_TYPE = "application/json".toMediaType()
|
|
||||||
private val MD_AT_HOME_URL_REGEX =
|
|
||||||
"""^https://[\w\d]+\.[\w\d]+\.mangadex(\b-test\b)?\.network.*${'$'}""".toRegex()
|
|
||||||
|
|
||||||
private val REPORT_CALLBACK = object : Callback {
|
|
||||||
override fun onFailure(call: Call, e: okio.IOException) {
|
|
||||||
Log.e("MangaDex", "Error trying to POST report to MD@Home: ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
Log.e("MangaDex", "Error trying to POST report to MD@Home: HTTP error ${response.code}")
|
|
||||||
}
|
|
||||||
|
|
||||||
response.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'MANGA Plus by SHUEISHA'
|
extName = 'MANGA Plus by SHUEISHA'
|
||||||
extClass = '.MangaPlusFactory'
|
extClass = '.MangaPlusFactory'
|
||||||
extVersionCode = 54
|
extVersionCode = 55
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|||||||
@ -211,6 +211,7 @@ class Label(val label: LabelCode? = LabelCode.WEEKLY_SHOUNEN_JUMP) {
|
|||||||
LabelCode.MANGA_PLUS_CREATORS -> "MANGA Plus Creators"
|
LabelCode.MANGA_PLUS_CREATORS -> "MANGA Plus Creators"
|
||||||
LabelCode.SAIKYOU_JUMP -> "Saikyou Jump"
|
LabelCode.SAIKYOU_JUMP -> "Saikyou Jump"
|
||||||
LabelCode.ULTRA_JUMP -> "Ultra Jump"
|
LabelCode.ULTRA_JUMP -> "Ultra Jump"
|
||||||
|
LabelCode.DX -> "Dash X Comic"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,6 +251,9 @@ enum class LabelCode {
|
|||||||
|
|
||||||
@SerialName("UJ")
|
@SerialName("UJ")
|
||||||
ULTRA_JUMP,
|
ULTRA_JUMP,
|
||||||
|
|
||||||
|
@SerialName("DX")
|
||||||
|
DX,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".zh.dmzj.DmzjUrlActivity"
|
android:name=".all.mangapluscreators.MPCUrlActivity"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
@ -14,28 +14,29 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="m.dmzj.com"
|
android:host="mangaplus-creators.jp"
|
||||||
android:pathPattern="/info/..*"
|
android:pathAdvancedPattern="/episodes/.+"
|
||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
<data
|
<data
|
||||||
android:host="www.dmzj.com"
|
android:host="mangaplus-creators.jp"
|
||||||
android:pathPattern="/info/..*"
|
android:pathPattern="/titles/..*"
|
||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
<data
|
<data
|
||||||
android:host="manhua.dmzj.com"
|
android:host="mangaplus-creators.jp"
|
||||||
android:pathPattern="/..*"
|
android:pathAdvancedPattern="/authors/.+"
|
||||||
|
android:scheme="https" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="medibang.com"
|
||||||
|
android:pathAdvancedPattern="/mpc/episodes/.+"
|
||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
<data
|
<data
|
||||||
android:host="m.muwai.com"
|
android:host="medibang.com"
|
||||||
android:pathPattern="/info/..*"
|
android:pathAdvancedPattern="/mpc/titles/..+"
|
||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
<data
|
<data
|
||||||
android:host="www.muwai.com"
|
android:host="medibang.com"
|
||||||
android:pathPattern="/info/..*"
|
android:pathAdvancedPattern="/mpc/authors/[0-9]+"
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:host="manhua.muwai.com"
|
|
||||||
android:pathPattern="/..*"
|
|
||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'MANGA Plus Creators by SHUEISHA'
|
extName = 'MANGA Plus Creators by SHUEISHA'
|
||||||
extClass = '.MangaPlusCreatorsFactory'
|
extClass = '.MangaPlusCreatorsFactory'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.mangapluscreators
|
||||||
|
|
||||||
|
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 MPCUrlActivity : Activity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
if (pathSegments != null && pathSegments.size > 1) {
|
||||||
|
// {medibang.com/mpc,mangaplus-creators.jp}/{episodes,titles,authors}
|
||||||
|
// TODO: val pathIndex = if (intent?.data?.host?.startsWith("medibang") == true) 1 else 0
|
||||||
|
val host = intent?.data?.host ?: ""
|
||||||
|
val pathIndex = with(host) {
|
||||||
|
when {
|
||||||
|
equals("medibang.com") -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val idIndex = pathIndex + 1
|
||||||
|
val query = when {
|
||||||
|
pathSegments[pathIndex].equals("episodes") -> {
|
||||||
|
MangaPlusCreators.PREFIX_EPISODE_ID_SEARCH + pathSegments[idIndex]
|
||||||
|
}
|
||||||
|
pathSegments[pathIndex].equals("authors") -> {
|
||||||
|
MangaPlusCreators.PREFIX_AUTHOR_ID_SEARCH + pathSegments[idIndex]
|
||||||
|
}
|
||||||
|
pathSegments[pathIndex].equals("titles") -> {
|
||||||
|
MangaPlusCreators.PREFIX_TITLE_ID_SEARCH + pathSegments[idIndex]
|
||||||
|
}
|
||||||
|
else -> null // TODO: is this required?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query != null) {
|
||||||
|
// TODO: val mainIntent = Intent().setAction("eu.kanade.tachiyomi.SEARCH").apply {
|
||||||
|
val mainIntent = Intent().apply {
|
||||||
|
setAction("eu.kanade.tachiyomi.SEARCH")
|
||||||
|
putExtra("query", query)
|
||||||
|
putExtra("filter", packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(mainIntent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Log.e("MPCUrlActivity", e.toString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("MPCUrlActivity", "Missing alphanumeric ID from the URL")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("MPCUrlActivity", "Could not parse URI from intent $intent")
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangapluscreators
|
package eu.kanade.tachiyomi.extension.all.mangapluscreators
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.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
|
||||||
@ -8,102 +10,199 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.decodeFromString
|
import keiyoushi.utils.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import keiyoushi.utils.tryParse
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class MangaPlusCreators(override val lang: String) : HttpSource() {
|
class MangaPlusCreators(override val lang: String) : HttpSource() {
|
||||||
|
|
||||||
override val name = "MANGA Plus Creators by SHUEISHA"
|
override val name = "MANGA Plus Creators by SHUEISHA"
|
||||||
|
|
||||||
override val baseUrl = "https://medibang.com/mpc"
|
override val baseUrl = "https://mangaplus-creators.jp"
|
||||||
|
|
||||||
|
private val apiUrl = "$baseUrl/api"
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||||
.add("Origin", baseUrl.substringBeforeLast("/"))
|
|
||||||
.add("Referer", baseUrl)
|
.add("Referer", baseUrl)
|
||||||
.add("User-Agent", USER_AGENT)
|
.add("User-Agent", USER_AGENT)
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
// POPULAR Section
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
val newHeaders = headersBuilder()
|
val popularUrl = "$baseUrl/titles/popular/?p=m&l=$lang".toHttpUrl()
|
||||||
.set("Referer", "$baseUrl/titles/popular/?p=m")
|
return GET(popularUrl, headers)
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$API_URL/titles/popular/list".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.addQueryParameter("pageSize", POPULAR_PAGE_SIZE)
|
|
||||||
.addQueryParameter("l", lang)
|
|
||||||
.addQueryParameter("p", "m")
|
|
||||||
.addQueryParameter("isWebview", "false")
|
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return GET(apiUrl, newHeaders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage = parseMangasPageFromElement(
|
||||||
val result = response.asMpcResponse()
|
response,
|
||||||
|
"div.item-recent",
|
||||||
|
)
|
||||||
|
|
||||||
checkNotNull(result.titles) { EMPTY_RESPONSE_ERROR }
|
private fun parseMangasPageFromElement(response: Response, selector: String): MangasPage {
|
||||||
|
val result = response.asJsoup()
|
||||||
|
|
||||||
val titles = result.titles.titleList.orEmpty().map(MpcTitle::toSManga)
|
val mangas = result.select(selector).map { element ->
|
||||||
|
popularElementToSManga(element)
|
||||||
|
}
|
||||||
|
|
||||||
return MangasPage(titles, result.titles.pagination?.hasNextPage ?: false)
|
return MangasPage(mangas, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun popularElementToSManga(element: Element): SManga {
|
||||||
|
val titleThumbnailUrl = element.selectFirst(".image-area img")!!.attr("src")
|
||||||
|
val titleContentId = titleThumbnailUrl.toHttpUrl().pathSegments[2]
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = element.selectFirst(".title-area .title")!!.text()
|
||||||
|
thumbnail_url = titleThumbnailUrl
|
||||||
|
setUrlWithoutDomain("/titles/$titleContentId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LATEST Section
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
val newHeaders = headersBuilder()
|
val apiUrl = "$apiUrl/titles/recent/".toHttpUrl().newBuilder()
|
||||||
.set("Referer", "$baseUrl/titles/recent/?t=episode")
|
.addQueryParameter("page", page.toString())
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
.addQueryParameter("l", lang)
|
||||||
|
.addQueryParameter("t", "episode")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val apiUrl = "$API_URL/titles/recent/list".toHttpUrl().newBuilder()
|
return GET(apiUrl, headers)
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.addQueryParameter("pageSize", POPULAR_PAGE_SIZE)
|
|
||||||
.addQueryParameter("l", lang)
|
|
||||||
.addQueryParameter("c", "episode")
|
|
||||||
.addQueryParameter("isWebview", "false")
|
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return GET(apiUrl, newHeaders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val result = response.parseAs<MpcResponse>()
|
||||||
|
|
||||||
|
val titles = result.titles.orEmpty().map { title -> title.toSManga() }
|
||||||
|
|
||||||
|
// TODO: handle last page of latest
|
||||||
|
return MangasPage(titles, result.status != "error")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MpcTitle.toSManga(): SManga {
|
||||||
|
val mTitle = this.title
|
||||||
|
val mAuthor = this.author.name // TODO: maybe not required
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = mTitle
|
||||||
|
thumbnail_url = thumbnail
|
||||||
|
setUrlWithoutDomain("/titles/${latestEpisode.titleConnectId}")
|
||||||
|
author = mAuthor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEARCH Section
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
// TODO: HTTPSource::fetchSearchManga is deprecated? super.getSearchManga
|
||||||
|
if (query.startsWith(PREFIX_TITLE_ID_SEARCH)) {
|
||||||
|
val titleContentId = query.removePrefix(PREFIX_TITLE_ID_SEARCH)
|
||||||
|
val titleUrl = "$baseUrl/titles/$titleContentId"
|
||||||
|
return client.newCall(GET(titleUrl, headers))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
val result = response.asJsoup()
|
||||||
|
val bookBox = result.selectFirst(".book-box")!!
|
||||||
|
val title = SManga.create().apply {
|
||||||
|
title = bookBox.selectFirst("div.title")!!.text()
|
||||||
|
thumbnail_url = bookBox.selectFirst("div.cover img")!!.attr("data-src")
|
||||||
|
setUrlWithoutDomain(titleUrl)
|
||||||
|
}
|
||||||
|
MangasPage(listOf(title), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (query.startsWith(PREFIX_EPISODE_ID_SEARCH)) {
|
||||||
|
val episodeId = query.removePrefix(PREFIX_EPISODE_ID_SEARCH)
|
||||||
|
return client.newCall(GET("$baseUrl/episodes/$episodeId", headers))
|
||||||
|
.asObservableSuccess().map { response ->
|
||||||
|
val result = response.asJsoup()
|
||||||
|
val readerElement = result.selectFirst("div[react=viewer]")!!
|
||||||
|
val dataTitle = readerElement.attr("data-title")
|
||||||
|
val dataTitleResult = dataTitle.parseAs<MpcReaderDataTitle>()
|
||||||
|
val episodeAsSManga = dataTitleResult.toSManga()
|
||||||
|
MangasPage(listOf(episodeAsSManga), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (query.startsWith(PREFIX_AUTHOR_ID_SEARCH)) {
|
||||||
|
val authorId = query.removePrefix(PREFIX_AUTHOR_ID_SEARCH)
|
||||||
|
return client.newCall(GET("$baseUrl/authors/$authorId", headers))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
val result = response.asJsoup()
|
||||||
|
val elements = result.select("#works .manga-list li .md\\:block")
|
||||||
|
val smangas = elements.map { element ->
|
||||||
|
val titleThumbnailUrl = element.selectFirst(".image-area img")!!.attr("src")
|
||||||
|
val titleContentId = titleThumbnailUrl.toHttpUrl().pathSegments[2]
|
||||||
|
SManga.create().apply {
|
||||||
|
title = element.selectFirst("p.text-white")!!.text().toString()
|
||||||
|
thumbnail_url = titleThumbnailUrl
|
||||||
|
setUrlWithoutDomain("/titles/$titleContentId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MangasPage(smangas, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
return super.fetchSearchManga(page, query, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing to search, filters active -> browsing /genres instead
|
||||||
|
// TODO: check if there's a better way (filters is independent of search but part of it)
|
||||||
|
val genreUrl = baseUrl.toHttpUrl().newBuilder()
|
||||||
|
.apply {
|
||||||
|
addPathSegment("genres")
|
||||||
|
addQueryParameter("l", lang)
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is SortFilter -> {
|
||||||
|
if (filter.selected.isNotEmpty()) {
|
||||||
|
addQueryParameter("s", filter.selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is GenreFilter -> addPathSegment(filter.selected)
|
||||||
|
else -> { /* Nothing else is supported for now */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return client.newCall(GET(genreUrl, headers))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
popularMangaParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MpcReaderDataTitle.toSManga(): SManga {
|
||||||
|
val mTitle = title
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = mTitle
|
||||||
|
thumbnail_url = thumbnail
|
||||||
|
setUrlWithoutDomain("/titles/$contentsId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val refererUrl = "$baseUrl/keywords".toHttpUrl().newBuilder()
|
// TODO: maybe this needn't be a new builder and just similar to `popularUrl` above?
|
||||||
|
val searchUrl = "$baseUrl/keywords".toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("q", query)
|
.addQueryParameter("q", query)
|
||||||
.toString()
|
.addQueryParameter("s", "date")
|
||||||
|
.addQueryParameter("lang", lang)
|
||||||
val newHeaders = headersBuilder()
|
|
||||||
.set("Referer", refererUrl)
|
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val apiUrl = "$API_URL/search/titles".toHttpUrl().newBuilder()
|
return GET(searchUrl, headers)
|
||||||
.addQueryParameter("keyword", query)
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
.addQueryParameter("pageSize", POPULAR_PAGE_SIZE)
|
|
||||||
.addQueryParameter("sort", "newly")
|
|
||||||
.addQueryParameter("lang", lang)
|
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return GET(apiUrl, newHeaders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
override fun searchMangaParse(response: Response): MangasPage = parseMangasPageFromElement(
|
||||||
|
response,
|
||||||
|
"div.item-search",
|
||||||
|
)
|
||||||
|
|
||||||
|
// MANGA Section
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
val result = response.asJsoup()
|
val result = response.asJsoup()
|
||||||
val bookBox = result.selectFirst(".book-box")!!
|
val bookBox = result.selectFirst(".book-box")!!
|
||||||
@ -119,62 +218,82 @@ class MangaPlusCreators(override val lang: String) : HttpSource() {
|
|||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
genre = bookBox.select("div.genre-area div.tag-genre")
|
genre = bookBox.select("div.genre-area div.tag-genre")
|
||||||
.joinToString { it.text() }
|
.joinToString(", ") { it.text() }
|
||||||
thumbnail_url = bookBox.selectFirst("div.cover img")!!.attr("data-src")
|
thumbnail_url = bookBox.selectFirst("div.cover img")!!.attr("data-src")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CHAPTER Section
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
val titleId = manga.url.substringAfterLast("/")
|
val titleContentId = (baseUrl + manga.url).toHttpUrl().pathSegments[1]
|
||||||
|
return chapterListPageRequest(1, titleContentId)
|
||||||
|
}
|
||||||
|
|
||||||
val newHeaders = headersBuilder()
|
private fun chapterListPageRequest(page: Int, titleContentId: String): Request {
|
||||||
.set("Referer", baseUrl + manga.url)
|
return GET("$baseUrl/titles/$titleContentId/?page=$page", headers)
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val apiUrl = "$API_URL/titles/$titleId/episodes/".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", "1")
|
|
||||||
.addQueryParameter("pageSize", CHAPTER_PAGE_SIZE)
|
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
return GET(apiUrl, newHeaders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val result = response.asMpcResponse()
|
val chapterListResponse = chapterListPageParse(response)
|
||||||
|
val chapterListResult = chapterListResponse.chapters.toMutableList()
|
||||||
|
|
||||||
checkNotNull(result.episodes) { EMPTY_RESPONSE_ERROR }
|
var hasNextPage = chapterListResponse.hasNextPage
|
||||||
|
val titleContentId = response.request.url.pathSegments[1]
|
||||||
|
var page = 1
|
||||||
|
while (hasNextPage) {
|
||||||
|
page += 1
|
||||||
|
val nextPageRequest = chapterListPageRequest(page, titleContentId)
|
||||||
|
val nextPageResponse = client.newCall(nextPageRequest).execute()
|
||||||
|
val nextPageResult = chapterListPageParse(nextPageResponse)
|
||||||
|
if (nextPageResult.chapters.isEmpty()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
chapterListResult.addAll(nextPageResult.chapters)
|
||||||
|
hasNextPage = nextPageResult.hasNextPage
|
||||||
|
}
|
||||||
|
|
||||||
return result.episodes.episodeList.orEmpty()
|
return chapterListResult.asReversed()
|
||||||
.sortedByDescending(MpcEpisode::numbering)
|
|
||||||
.map(MpcEpisode::toSChapter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
private fun chapterListPageParse(response: Response): ChaptersPage {
|
||||||
val chapterId = chapter.url.substringAfterLast("/")
|
val result = response.asJsoup()
|
||||||
|
val chapters = result.select(".mod-item-series").map {
|
||||||
val newHeaders = headersBuilder()
|
element ->
|
||||||
.set("Referer", baseUrl + chapter.url)
|
chapterElementToSChapter(element)
|
||||||
.add("X-Requested-With", "XMLHttpRequest")
|
}
|
||||||
.build()
|
val hasResult = result.select(".mod-pagination .next").isNotEmpty()
|
||||||
|
return ChaptersPage(
|
||||||
val apiUrl = "$API_URL/episodes/pageList/$chapterId/".toHttpUrl().newBuilder()
|
chapters,
|
||||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
hasResult,
|
||||||
.toString()
|
)
|
||||||
|
|
||||||
return GET(apiUrl, newHeaders)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun chapterElementToSChapter(element: Element): SChapter {
|
||||||
|
val episode = element.attr("href").substringAfterLast("/")
|
||||||
|
val latestUpdatedDate = element.selectFirst(".first-update")!!.text()
|
||||||
|
val chapterNumberElement = element.selectFirst(".number")!!.text()
|
||||||
|
val chapterNumber = chapterNumberElement.substringAfter("#").toFloatOrNull()
|
||||||
|
return SChapter.create().apply {
|
||||||
|
setUrlWithoutDomain("/episodes/$episode")
|
||||||
|
date_upload = CHAPTER_DATE_FORMAT.tryParse(latestUpdatedDate)
|
||||||
|
name = chapterNumberElement
|
||||||
|
chapter_number = if (chapterNumberElement == "One-shot") {
|
||||||
|
0F
|
||||||
|
} else {
|
||||||
|
chapterNumber ?: -1F
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PAGES & IMAGES Section
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val result = response.asMpcResponse()
|
val result = response.asJsoup()
|
||||||
|
val readerElement = result.selectFirst("div[react=viewer]")!!
|
||||||
checkNotNull(result.pageList) { EMPTY_RESPONSE_ERROR }
|
val dataPages = readerElement.attr("data-pages")
|
||||||
|
val refererUrl = response.request.url.toString()
|
||||||
val referer = response.request.header("Referer")!!
|
return dataPages.parseAs<MpcReaderDataPages>().pc.map {
|
||||||
|
page ->
|
||||||
return result.pageList.mapIndexed { i, page ->
|
Page(page.pageNo, refererUrl, page.imageUrl)
|
||||||
Page(i, referer, page.publicBgImage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,18 +310,66 @@ class MangaPlusCreators(override val lang: String) : HttpSource() {
|
|||||||
return GET(page.imageUrl!!, newHeaders)
|
return GET(page.imageUrl!!, newHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Response.asMpcResponse(): MpcResponse = use {
|
|
||||||
json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val API_URL = "https://medibang.com/api/mpc"
|
private val CHAPTER_DATE_FORMAT by lazy {
|
||||||
|
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||||
|
}
|
||||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
|
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"
|
||||||
|
const val PREFIX_TITLE_ID_SEARCH = "title:"
|
||||||
private const val POPULAR_PAGE_SIZE = "30"
|
const val PREFIX_EPISODE_ID_SEARCH = "episode:"
|
||||||
private const val CHAPTER_PAGE_SIZE = "200"
|
const val PREFIX_AUTHOR_ID_SEARCH = "author:"
|
||||||
|
|
||||||
private const val EMPTY_RESPONSE_ERROR = "Empty response from the API. Try again later."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FILTERS Section
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
Filter.Separator(),
|
||||||
|
Filter.Header("NOTE: Ignored if using text search!"),
|
||||||
|
Filter.Separator(),
|
||||||
|
SortFilter(),
|
||||||
|
GenreFilter(),
|
||||||
|
Filter.Separator(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class SortFilter() : SelectFilter(
|
||||||
|
"Sort",
|
||||||
|
listOf(
|
||||||
|
SelectFilterOption("Popularity", ""),
|
||||||
|
SelectFilterOption("Date", "latest_desc"),
|
||||||
|
SelectFilterOption("Likes", "like_desc"),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
private class GenreFilter() : SelectFilter(
|
||||||
|
"Genres",
|
||||||
|
listOf(
|
||||||
|
SelectFilterOption("Fantasy", "fantasy"),
|
||||||
|
SelectFilterOption("Action", "action"),
|
||||||
|
SelectFilterOption("Romance", "romance"),
|
||||||
|
SelectFilterOption("Horror", "horror"),
|
||||||
|
SelectFilterOption("Slice of Life", "slice_of_life"),
|
||||||
|
SelectFilterOption("Comedy", "comedy"),
|
||||||
|
SelectFilterOption("Sports", "sports"),
|
||||||
|
SelectFilterOption("Sci-Fi", "sf"),
|
||||||
|
SelectFilterOption("Mystery", "mystery"),
|
||||||
|
SelectFilterOption("Others", "others"),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
private abstract class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
private val options: List<SelectFilterOption>,
|
||||||
|
default: Int = 0,
|
||||||
|
) : Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
options.map { it.name }.toTypedArray(),
|
||||||
|
default,
|
||||||
|
) {
|
||||||
|
val selected: String
|
||||||
|
get() = options[state].value
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SelectFilterOption(val name: String, val value: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,68 +1,51 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangapluscreators
|
package eu.kanade.tachiyomi.extension.all.mangapluscreators
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MpcResponse(
|
class MpcResponse(
|
||||||
@SerialName("mpcEpisodesDto") val episodes: MpcEpisodesDto? = null,
|
val status: String,
|
||||||
@SerialName("mpcTitlesDto") val titles: MpcTitlesDto? = null,
|
val titles: List<MpcTitle>? = null,
|
||||||
val pageList: List<MpcPage>? = emptyList(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MpcEpisodesDto(
|
class MpcTitle(
|
||||||
val pagination: MpcPagination? = null,
|
|
||||||
val episodeList: List<MpcEpisode>? = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MpcTitlesDto(
|
|
||||||
val pagination: MpcPagination? = null,
|
|
||||||
val titleList: List<MpcTitle>? = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MpcPagination(
|
|
||||||
val page: Int,
|
|
||||||
val maxPage: Int,
|
|
||||||
) {
|
|
||||||
|
|
||||||
val hasNextPage: Boolean
|
|
||||||
get() = page < maxPage
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class MpcTitle(
|
|
||||||
@SerialName("titleId") val id: String,
|
|
||||||
val title: String,
|
val title: String,
|
||||||
val thumbnailUrl: String,
|
val thumbnail: String,
|
||||||
) {
|
@SerialName("is_one_shot") val isOneShot: Boolean,
|
||||||
|
val author: MpcAuthorDto,
|
||||||
fun toSManga(): SManga = SManga.create().apply {
|
@SerialName("latest_episode") val latestEpisode: MpcLatestEpisode,
|
||||||
title = this@MpcTitle.title
|
)
|
||||||
thumbnail_url = thumbnailUrl
|
|
||||||
url = "/titles/$id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MpcEpisode(
|
class MpcAuthorDto(
|
||||||
@SerialName("episodeId") val id: String,
|
val name: String,
|
||||||
@SerialName("episodeTitle") val title: String,
|
)
|
||||||
val numbering: Int,
|
|
||||||
val oneshot: Boolean = false,
|
|
||||||
val publishDate: Long,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun toSChapter(): SChapter = SChapter.create().apply {
|
|
||||||
name = if (oneshot) "One-shot" else title
|
|
||||||
date_upload = publishDate
|
|
||||||
url = "/episodes/$id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MpcPage(val publicBgImage: String)
|
class MpcLatestEpisode(
|
||||||
|
@SerialName("title_connect_id") val titleConnectId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MpcReaderDataPages(
|
||||||
|
val pc: List<MpcReaderPage>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MpcReaderPage(
|
||||||
|
@SerialName("page_no") val pageNo: Int,
|
||||||
|
@SerialName("image_url") val imageUrl: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MpcReaderDataTitle(
|
||||||
|
val title: String,
|
||||||
|
val thumbnail: String,
|
||||||
|
@SerialName("is_oneshot") val isOneShot: Boolean,
|
||||||
|
@SerialName("contents_id") val contentsId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChaptersPage(val chapters: List<SChapter>, val hasNextPage: Boolean)
|
||||||
|
|||||||
25
src/all/manhuarm/assets/fonts/LICENSE_ANIMEACE.txt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
This font is © 2006 Nate Piekos. All Rights Reserved.
|
||||||
|
Created for Blambot Fonts
|
||||||
|
|
||||||
|
This font is freeware for independent comic book creation and
|
||||||
|
non-profit use ONLY. ( This excludes use by "mainstream" publishers,
|
||||||
|
(Marvel, DC, Dark Horse, Oni, Image, SLG, Top Cow, Crossgen and their
|
||||||
|
subsidiaries) without a license fee. Use by a "mainstream" publisher
|
||||||
|
(or it's employee), and use for commercial non-comic book production
|
||||||
|
(eg. magazine ads, merchandise lables etc.) incurs a license fee
|
||||||
|
be paid to the designer, Nate Piekos.
|
||||||
|
This font may not be redistributed without the author's permission and
|
||||||
|
never with this text file missing from the .zip, .sit or .hqx.
|
||||||
|
|
||||||
|
Blambot/Nate Piekos makes no guarantees about these font files,
|
||||||
|
the completeness of character sets, or safety of these files on your
|
||||||
|
computer. By installing these fonts on your system, you prove that
|
||||||
|
you have read and understand the above.
|
||||||
|
|
||||||
|
If you have any questions, visit http://www.blambot.com/license.shtml
|
||||||
|
|
||||||
|
For more free and original fonts visit Blambot.
|
||||||
|
www.blambot.com
|
||||||
|
|
||||||
|
Nate Piekos
|
||||||
|
studio@blambot.com
|
||||||
93
src/all/manhuarm/assets/fonts/LICENSE_COMIC_NEUE.txt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2014 The Comic Neue Project Authors (https://github.com/crozynski/comicneue)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@ -1,202 +1,202 @@
|
|||||||
|
|
||||||
Apache License
|
Apache License
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
http://www.apache.org/licenses/
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
1. Definitions.
|
1. Definitions.
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
the copyright owner that is granting the License.
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
other entities that control, are controlled by, or are under common
|
other entities that control, are controlled by, or are under common
|
||||||
control with that entity. For the purposes of this definition,
|
control with that entity. For the purposes of this definition,
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
direction or management of such entity, whether by contract or
|
direction or management of such entity, whether by contract or
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
exercising permissions granted by this License.
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
including but not limited to software source code, documentation
|
including but not limited to software source code, documentation
|
||||||
source, and configuration files.
|
source, and configuration files.
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
"Object" form shall mean any form resulting from mechanical
|
||||||
transformation or translation of a Source form, including but
|
transformation or translation of a Source form, including but
|
||||||
not limited to compiled object code, generated documentation,
|
not limited to compiled object code, generated documentation,
|
||||||
and conversions to other media types.
|
and conversions to other media types.
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
Object form, made available under the License, as indicated by a
|
Object form, made available under the License, as indicated by a
|
||||||
copyright notice that is included in or attached to the work
|
copyright notice that is included in or attached to the work
|
||||||
(an example is provided in the Appendix below).
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
form, that is based on (or derived from) the Work and for which the
|
form, that is based on (or derived from) the Work and for which the
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
of this License, Derivative Works shall not include works that remain
|
of this License, Derivative Works shall not include works that remain
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
the Work and Derivative Works thereof.
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
"Contribution" shall mean any work of authorship, including
|
||||||
the original version of the Work and any modifications or additions
|
the original version of the Work and any modifications or additions
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
means any form of electronic, verbal, or written communication sent
|
means any form of electronic, verbal, or written communication sent
|
||||||
to the Licensor or its representatives, including but not limited to
|
to the Licensor or its representatives, including but not limited to
|
||||||
communication on electronic mailing lists, source code control systems,
|
communication on electronic mailing lists, source code control systems,
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
excluding communication that is conspicuously marked or otherwise
|
excluding communication that is conspicuously marked or otherwise
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
subsequently incorporated within the Work.
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
Work and such Derivative Works in Source or Object form.
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
(except as stated in this section) patent license to make, have made,
|
(except as stated in this section) patent license to make, have made,
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
where such license applies only to those patent claims licensable
|
where such license applies only to those patent claims licensable
|
||||||
by such Contributor that are necessarily infringed by their
|
by such Contributor that are necessarily infringed by their
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
institute patent litigation against any entity (including a
|
institute patent litigation against any entity (including a
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
or contributory patent infringement, then any patent licenses
|
or contributory patent infringement, then any patent licenses
|
||||||
granted to You under this License for that Work shall terminate
|
granted to You under this License for that Work shall terminate
|
||||||
as of the date such litigation is filed.
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
modifications, and in Source or Object form, provided that You
|
modifications, and in Source or Object form, provided that You
|
||||||
meet the following conditions:
|
meet the following conditions:
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
(a) You must give any other recipients of the Work or
|
||||||
Derivative Works a copy of this License; and
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
(b) You must cause any modified files to carry prominent notices
|
||||||
stating that You changed the files; and
|
stating that You changed the files; and
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
that You distribute, all copyright, patent, trademark, and
|
that You distribute, all copyright, patent, trademark, and
|
||||||
attribution notices from the Source form of the Work,
|
attribution notices from the Source form of the Work,
|
||||||
excluding those notices that do not pertain to any part of
|
excluding those notices that do not pertain to any part of
|
||||||
the Derivative Works; and
|
the Derivative Works; and
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
distribution, then any Derivative Works that You distribute must
|
distribution, then any Derivative Works that You distribute must
|
||||||
include a readable copy of the attribution notices contained
|
include a readable copy of the attribution notices contained
|
||||||
within such NOTICE file, excluding those notices that do not
|
within such NOTICE file, excluding those notices that do not
|
||||||
pertain to any part of the Derivative Works, in at least one
|
pertain to any part of the Derivative Works, in at least one
|
||||||
of the following places: within a NOTICE text file distributed
|
of the following places: within a NOTICE text file distributed
|
||||||
as part of the Derivative Works; within the Source form or
|
as part of the Derivative Works; within the Source form or
|
||||||
documentation, if provided along with the Derivative Works; or,
|
documentation, if provided along with the Derivative Works; or,
|
||||||
within a display generated by the Derivative Works, if and
|
within a display generated by the Derivative Works, if and
|
||||||
wherever such third-party notices normally appear. The contents
|
wherever such third-party notices normally appear. The contents
|
||||||
of the NOTICE file are for informational purposes only and
|
of the NOTICE file are for informational purposes only and
|
||||||
do not modify the License. You may add Your own attribution
|
do not modify the License. You may add Your own attribution
|
||||||
notices within Derivative Works that You distribute, alongside
|
notices within Derivative Works that You distribute, alongside
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
that such additional attribution notices cannot be construed
|
that such additional attribution notices cannot be construed
|
||||||
as modifying the License.
|
as modifying the License.
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
You may add Your own copyright statement to Your modifications and
|
||||||
may provide additional or different license terms and conditions
|
may provide additional or different license terms and conditions
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
the conditions stated in this License.
|
the conditions stated in this License.
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
this License, without any additional terms or conditions.
|
this License, without any additional terms or conditions.
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
the terms of any separate license agreement you may have executed
|
the terms of any separate license agreement you may have executed
|
||||||
with Licensor regarding such Contributions.
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
except as required for reasonable and customary use in describing the
|
except as required for reasonable and customary use in describing the
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
implied, including, without limitation, any warranties or conditions
|
implied, including, without limitation, any warranties or conditions
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
appropriateness of using or redistributing the Work and assume any
|
appropriateness of using or redistributing the Work and assume any
|
||||||
risks associated with Your exercise of permissions under this License.
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
unless required by applicable law (such as deliberate and grossly
|
unless required by applicable law (such as deliberate and grossly
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
liable to You for damages, including any direct, indirect, special,
|
liable to You for damages, including any direct, indirect, special,
|
||||||
incidental, or consequential damages of any character arising as a
|
incidental, or consequential damages of any character arising as a
|
||||||
result of this License or out of the use or inability to use the
|
result of this License or out of the use or inability to use the
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
other commercial damages or losses), even if such Contributor
|
other commercial damages or losses), even if such Contributor
|
||||||
has been advised of the possibility of such damages.
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
or other liability obligations and/or rights consistent with this
|
or other liability obligations and/or rights consistent with this
|
||||||
License. However, in accepting such obligations, You may act only
|
License. However, in accepting such obligations, You may act only
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
defend, and hold each Contributor harmless for any liability
|
defend, and hold each Contributor harmless for any liability
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
of your accepting any such warranty or additional liability.
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
To apply the Apache License to your work, attach the following
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
replaced with your own identifying information. (Don't include
|
replaced with your own identifying information. (Don't include
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
comment syntax for the file format. We also recommend that a
|
comment syntax for the file format. We also recommend that a
|
||||||
file or class name and description of purpose be included on the
|
file or class name and description of purpose be included on the
|
||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||