Compare commits
321 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0413d502c1 | |||
| d3416a4df5 | |||
| 1a12caa487 | |||
| 72b7dc805c | |||
| 8029ff8ecc | |||
| cd8543d40b | |||
| 7d1fd9f0bb | |||
| fd65aa8fe4 | |||
| 0f349da3bc | |||
| 33d0507f98 | |||
| 16f9ca381e | |||
| 2ff186eaec | |||
| f3678e3fd3 | |||
| c29690282c | |||
| f93d21774f | |||
| efe0d9de05 | |||
| 04da26195e | |||
| e8e7f96be5 | |||
| 37ce2140f3 | |||
| c5ef58ac72 | |||
| cb1ea6f571 | |||
| a207b4b729 | |||
| e9ec281159 | |||
| ede6a54b30 | |||
| 79770a9deb | |||
| 197083e437 | |||
| f8da1fba7d | |||
| 56ead63798 | |||
| 7d81d812bc | |||
| e25991544e | |||
| f91837a019 | |||
| 14db2351c6 | |||
| a616193cdf | |||
| 72b0fcca9d | |||
| d4de925ec7 | |||
| b6c7e96ddc | |||
| c5b5c8c21d | |||
| 0ab28fd6d6 | |||
| 7b5dd4fed4 | |||
| 4896f3d16c | |||
| eb5469d362 | |||
| 227de5d838 | |||
| 86c228243d | |||
| 88690008f7 | |||
| 4388e7bc3a | |||
| ded58541f5 | |||
| aadfa2aa8c | |||
| e63b15a133 | |||
| 84590688be | |||
| 2270d3d8e3 | |||
| bc9602a0f2 | |||
| bd8084c565 | |||
| 6be485ea79 | |||
| e80a4bea18 | |||
| 73a1c27dd9 | |||
| eb2ceebeeb | |||
| d7eac12eee | |||
| e633da5567 | |||
| e26ab22e41 | |||
| 9a3faad499 | |||
| c635d72b30 | |||
| 15b826074f | |||
| b235521dd1 | |||
| fc6d9aaf51 | |||
| e629703afd | |||
| 24d5d5737e | |||
| 6168dadba3 | |||
| a0c9418174 | |||
| 08bf113dcb | |||
| 369e075ed9 | |||
| 7bb4191d41 | |||
| 8539d5b4db | |||
| f8763c71ff | |||
| 925ecb282c | |||
| 675d6e95d3 | |||
| 0ccfca51e7 | |||
| 284a456184 | |||
| 4cbb2ae082 | |||
| 466cfd82c9 | |||
| 9cb600e9d6 | |||
| 4f50fcadeb | |||
| 1eb8ee502e | |||
| 2b845ec01f | |||
| dbadec2c67 | |||
| dc6aa11bc7 | |||
| 628eedf15a | |||
| fcc095ffa3 | |||
| 4a86f39a40 | |||
| 9603186927 | |||
| 5d600166ea | |||
| 2e580cfb55 | |||
| ec1fe205ad | |||
| 9a70f25552 | |||
| 78fbef637c | |||
| 26385c9225 | |||
| deaefe8fa6 | |||
| fbc041846b | |||
| a2aad23eae | |||
| 1619282e19 | |||
| 9320221a4e | |||
| 6fdff1b03b | |||
| bf7af0c099 | |||
| ffc628fc97 | |||
| 87db322ec6 | |||
| 55f3ade9e7 | |||
| 788ed6dcc9 | |||
| dd3b8c7967 | |||
| c376699c37 | |||
| 8bd5b75fd9 | |||
| e3ee3159fc | |||
| 355170b8ff | |||
| 9fe039ba3f | |||
| cdd5f3b345 | |||
| c270a8c51d | |||
| 80c11a32c2 | |||
| fc481e4fd4 | |||
| 41607ab259 | |||
| 3927c62a32 | |||
| b0981a00bd | |||
| 295af5306b | |||
| cf1ce9e069 | |||
| 18f02a85ac | |||
| 64eeab7c5e | |||
| 1e2f4fc35e | |||
| a088e1ffc2 | |||
| 0b7f8da84e | |||
| 9f2e582281 | |||
| d63eae4444 | |||
| 4552b9f849 | |||
| 02e3b49dc7 | |||
| 5c21f7ec30 | |||
| 9235f0e5ed | |||
| 2a211c68a9 | |||
| 653ae10caf | |||
| 9ec67db8cb | |||
| 4d6bd382e8 | |||
| 48b713aad5 | |||
| 677909cd6e | |||
| e0afe65096 | |||
| 003e916ab0 | |||
| 94f0dd8362 | |||
| 2b8a0f2215 | |||
| 34f8407983 | |||
| a17c4c151f | |||
| 0a4fcb480d | |||
| e6d62dd1dc | |||
| 3b364c91f1 | |||
| 8db57aef6c | |||
| 25caba6905 | |||
| c92c9fada5 | |||
| 84a28ddb87 | |||
| 3d400981b8 | |||
| 0248e2b5d0 | |||
| 3844615a98 | |||
| a47e88a953 | |||
| 54071507c1 | |||
| 7a893e3009 | |||
| 70d5907cc8 | |||
| d5a912bda2 | |||
| 3340ca83c6 | |||
| b6acb3d7f6 | |||
| 64f6904ddb | |||
| e713340ced | |||
| 5fefefcb23 | |||
| 4a268de6dc | |||
| 80c7abe098 | |||
| 37787f040c | |||
| ae15a178e5 | |||
| 9465803e5b | |||
| f29ad69534 | |||
| 05793d8a60 | |||
| 6ea90d982d | |||
| fe5058c94b | |||
| 85425a66a2 | |||
| 7b4311c7dc | |||
| d4b0e2869d | |||
| a0ac2daad1 | |||
| 0edff11353 | |||
| c7bedb96a0 | |||
| 954573fc33 | |||
| cda886ecb3 | |||
| 57c4ead5fb | |||
| 3a5182103a | |||
| 85853b159d | |||
| a04f848ad1 | |||
| 73e861ec9e | |||
| 6bb7b676bd | |||
| 33fac3e96b | |||
| 35936e3c9a | |||
| 7006341fab | |||
| 4afe227e02 | |||
| fca7dad7b0 | |||
| 3683665e8a | |||
| a68f18d180 | |||
| d09406dc29 | |||
| d41c619c8a | |||
| 7330be555d | |||
| fd3e0bc449 | |||
| c8dd2190ba | |||
| 1b41546bc9 | |||
| 6da1654825 | |||
| 25e56f3c77 | |||
| fd050b8178 | |||
| 5387e24bb4 | |||
| e363e2fbb2 | |||
| b7986a6773 | |||
| d6d8cbd346 | |||
| 27496ae77b | |||
| bd73b1b068 | |||
| 208d4574db | |||
| e92e942fcd | |||
| a6d4644713 | |||
| c9062cc089 | |||
| ac2301e4be | |||
| c073f71ec1 | |||
| f1a65edd3a | |||
| 4b91013750 | |||
| 556fdb2e8d | |||
| 68c12d79ee | |||
| 142aa0f02a | |||
| a1102d790f | |||
| f4549c5910 | |||
| 4e026c1cf1 | |||
| 73fc1ac80f | |||
| 838953a739 | |||
| 66d2e8090a | |||
| 7bd5157bca | |||
| 298c1e92db | |||
| 5ba087f2ee | |||
| 68113f8c7d | |||
| b824f09966 | |||
| 0f000caa9d | |||
| ae3a36cc5a | |||
| ee0625e9a6 | |||
| 6028fdfc4d | |||
| 68f209b91b | |||
| 74dc7a645d | |||
| a0981c4944 | |||
| aab3ba5b48 | |||
| 2bd67860a7 | |||
| b57dc1a6c8 | |||
| cbd25332e1 | |||
| d8bb15cdcd | |||
| ce0726d863 | |||
| 28fca8c839 | |||
| 23ac4b271c | |||
| 423983d41a | |||
| 651f4659a5 | |||
| b8751f6d15 | |||
| 8a8d8f7189 | |||
| 7a6a33d5fe | |||
| 3e6c3ac151 | |||
| 8dd435b5b5 | |||
| 6d4136248c | |||
| 67b919423a | |||
| d5af2ed80c | |||
| 8ee8cae1f7 | |||
| 171a610d0d | |||
| 1d134a94a6 | |||
| f143ac1572 | |||
| 1f3c805a0f | |||
| 426af9c93c | |||
| 50cc6e5e83 | |||
| b6d0594d10 | |||
| f5f71fa4a7 | |||
| 9085c142d5 | |||
| 73092a2832 | |||
| d2e594be0c | |||
| aef21dabd5 | |||
| cd9c26f278 | |||
| dbda419d29 | |||
| 92cc022fb4 | |||
| 1c5ffcca24 | |||
| 59fb4a71e6 | |||
| 157f7802b2 | |||
| 406f77c645 | |||
| 959559a89f | |||
| df1daa6b7f | |||
| 56a4ef33f5 | |||
| 4be204e0b1 | |||
| 8e659f3355 | |||
| e78197ab48 | |||
| a25d7b39a7 | |||
| 08e52cca97 | |||
| 4b3d92a050 | |||
| a907c93147 | |||
| 294bb286e9 | |||
| 54736ea410 | |||
| 897f5f1732 | |||
| 7fc7ea9c9b | |||
| 7cdda6241f | |||
| 45ef778e6d | |||
| 528c2dbae7 | |||
| e078e34ab0 | |||
| 1a609e557b | |||
| d3b7f639b5 | |||
| f99aa721d0 | |||
| 0519d8ea17 | |||
| 0c84d51e1b | |||
| bcd7a2d21b | |||
| d332be88ba | |||
| f10a499a36 | |||
| db06c6614e | |||
| 51032fa65b | |||
| 98a20b2756 | |||
| c00ba701b3 | |||
| 52c5c35e1a | |||
| de03174131 | |||
| f33a4e2ecc | |||
| 118e1d302b | |||
| 1dcead9d79 | |||
| b001768b96 | |||
| 78f48d28c3 | |||
| a7b05f372b | |||
| d392b58ced | |||
| 1dcf161131 | |||
| dfabe1d2fa | |||
| bbc70801db | |||
| d68341aaba | |||
| e1e64c79d2 | |||
| 7d53c7af4a |
@@ -1,33 +0,0 @@
|
||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
||||
3. What is your type of issue?
|
||||
* [Catalogue request](#catalogue-requests)
|
||||
* [Bugs](#bugs)
|
||||
* [Feature requests](#feature-requests)
|
||||
* [Translations](https://tachiyomi.org/help/contribution/#translation)
|
||||
4. After following 1. and 3. you can [open your issue](https://github.com/tachiyomiorg/tachiyomi/issues/new)
|
||||
|
||||
***
|
||||
|
||||
# Catalogue requests
|
||||
|
||||
* Catalogue requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions#readme, not here
|
||||
|
||||
# Bugs
|
||||
* Include version (More > About > Version)
|
||||
* If not latest, try updating, it may have already been solved
|
||||
* Preview version is equal to the number of commits as seen in the main page
|
||||
* Include steps to reproduce (if not obvious from description)
|
||||
* Include screenshot (if needed)
|
||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||
* For large logs use http://pastebin.com/ (or similar)
|
||||
* Don't group unrelated requests into one issue
|
||||
|
||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
||||
|
||||
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
|
||||
|
||||
# Feature requests
|
||||
|
||||
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
||||
* Include screenshot (if needed)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.4.1)
|
||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ labels: "bug"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.4.1)
|
||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ labels: "feature"
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app (stable is v1.4.1)
|
||||
- I have updated to the latest version of the app (stable is v1.5.0)
|
||||
- I have updated all extensions
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/tachiyomiorg/tachiyomi#issues-feature-requests-and-contributing).
|
||||
|
||||
---
|
||||
|
||||
Thanks for your interest in contributing to Tachiyomi!
|
||||
|
||||
|
||||
# Code contributions
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/help/contribution/#translation) for more details.
|
||||
|
||||
|
||||
# Forks
|
||||
|
||||
Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE).
|
||||
|
||||
When creating a fork, remember to:
|
||||
|
||||
- To avoid confusion with the main app:
|
||||
- Change the app name
|
||||
- Change the app icon
|
||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt)
|
||||
- To avoid installation conflicts:
|
||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own
|
||||
- If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own
|
||||
@@ -174,29 +174,3 @@
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
@@ -107,6 +107,11 @@ DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
|
||||
Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
|
||||
</details>
|
||||
|
||||
<details><summary>Contributing</summary>
|
||||
|
||||
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
|
||||
</details>
|
||||
|
||||
## FAQ
|
||||
|
||||
[See our website.](https://tachiyomi.org/)
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'com.github.zellius.shortcut-helper'
|
||||
// Realm (EH)
|
||||
apply plugin: 'realm-android'
|
||||
|
||||
shortcutHelper.filePath = './shortcuts.xml'
|
||||
|
||||
ext {
|
||||
// Git is needed in your system PATH for these commands to work.
|
||||
// If it's not installed, you can return a random value as a workaround
|
||||
getCommitCount = {
|
||||
return 'git rev-list --count HEAD'.execute().text.trim()
|
||||
// return "1"
|
||||
}
|
||||
|
||||
getGitSha = {
|
||||
return 'git rev-parse --short HEAD'.execute().text.trim()
|
||||
// return "1"
|
||||
}
|
||||
|
||||
getBuildTime = {
|
||||
def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"))
|
||||
return df.format(new Date())
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion AndroidConfig.compileSdk
|
||||
buildToolsVersion AndroidConfig.buildTools
|
||||
ndkVersion AndroidConfig.ndk
|
||||
|
||||
defaultConfig {
|
||||
applicationId "eu.kanade.tachiyomi.sy"
|
||||
minSdkVersion AndroidConfig.minSdk
|
||||
targetSdkVersion AndroidConfig.targetSdk
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode 11
|
||||
versionName "1.4.1"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
versionNameSuffix "-${getCommitCount()}"
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
releaseTest {
|
||||
applicationIdSuffix ".rt"
|
||||
// minifyEnabled true
|
||||
// shrinkResources true
|
||||
zipAlignEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
zipAlignEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "default"
|
||||
|
||||
productFlavors {
|
||||
standard {
|
||||
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
|
||||
dimension "default"
|
||||
}
|
||||
fdroid {
|
||||
dimension "default"
|
||||
}
|
||||
dev {
|
||||
resConfigs "en", "xxhdpi"
|
||||
dimension "default"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/DEPENDENCIES'
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'META-INF/LICENSE'
|
||||
exclude 'META-INF/LICENSE.txt'
|
||||
exclude 'META-INF/NOTICE'
|
||||
exclude 'META-INF/*.kotlin_module'
|
||||
|
||||
// Compatibility for two RxJava versions (EXH)
|
||||
exclude 'META-INF/rxjava.properties'
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
disable 'ExtraTranslation'
|
||||
|
||||
abortOnError false
|
||||
checkReleaseBuilds false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
implementation 'tachiyomi.sourceapi:source-api:1.1'
|
||||
|
||||
// AndroidX libraries
|
||||
implementation 'androidx.annotation:annotation:1.2.0-alpha01'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
|
||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha01'
|
||||
implementation 'androidx.browser:browser:1.3.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.0-alpha1'
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.5.0-alpha05'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
|
||||
final lifecycle_version = '2.3.0-beta01'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||
|
||||
// Job scheduling
|
||||
implementation "androidx.work:work-runtime-ktx:2.5.0-beta02"
|
||||
|
||||
// UI library
|
||||
implementation 'com.google.android.material:material:1.3.0-alpha04'
|
||||
|
||||
standardImplementation 'com.google.firebase:firebase-core:18.0.0'
|
||||
|
||||
// ReactiveX
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
implementation 'io.reactivex:rxjava:1.3.8'
|
||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
|
||||
|
||||
// Network client
|
||||
final okhttp_version = '4.10.0-RC1'
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
|
||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
|
||||
implementation 'com.squareup.okio:okio:2.9.0'
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.1'
|
||||
|
||||
// REST
|
||||
final retrofit_version = '2.9.0'
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||
|
||||
// JSON
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
|
||||
// JavaScript engine
|
||||
implementation 'com.squareup.duktape:duktape-android:1.3.0'
|
||||
|
||||
// Disk
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||
implementation 'com.github.inorichi:unifile:e9ee588'
|
||||
implementation 'com.github.junrar:junrar:7.4.0'
|
||||
|
||||
// HTML parser
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
||||
// [EXH] Android 7 SSL Workaround
|
||||
implementation 'com.google.android.gms:play-services-safetynet:17.0.0'
|
||||
|
||||
// Changelog
|
||||
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||
|
||||
// Database
|
||||
implementation 'androidx.sqlite:sqlite-ktx:2.1.0'
|
||||
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
|
||||
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
|
||||
implementation 'io.requery:sqlite-android:3.33.0'
|
||||
|
||||
// Preferences
|
||||
implementation 'com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3'
|
||||
|
||||
// Model View Presenter
|
||||
final nucleus_version = '3.0.0'
|
||||
implementation "info.android15.nucleus:nucleus:$nucleus_version"
|
||||
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||
|
||||
// Dependency injection
|
||||
implementation "com.github.inorichi.injekt:injekt-core:65b0440"
|
||||
|
||||
// Image library
|
||||
final glide_version = '4.11.0'
|
||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||
|
||||
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:6caf219'
|
||||
|
||||
// Logging
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
||||
// Crash reports
|
||||
//implementation 'ch.acra:acra-http:5.7.0'
|
||||
|
||||
// Sort
|
||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
|
||||
// UI
|
||||
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||
implementation 'eu.davidea:flexible-adapter:5.1.0'
|
||||
implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
|
||||
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
|
||||
|
||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||
final material_dialogs_version = '3.1.1'
|
||||
implementation "com.afollestad.material-dialogs:core:$material_dialogs_version"
|
||||
implementation "com.afollestad.material-dialogs:input:$material_dialogs_version"
|
||||
implementation "com.afollestad.material-dialogs:datetime:$material_dialogs_version"
|
||||
|
||||
// Conductor
|
||||
implementation 'com.bluelinelabs:conductor:2.1.5'
|
||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||
exclude group: "com.android.support"
|
||||
}
|
||||
implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
|
||||
|
||||
// FlowBinding
|
||||
final flowbinding_version = '0.12.0'
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
|
||||
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
|
||||
|
||||
// Licenses
|
||||
implementation "com.mikepenz:aboutlibraries:$BuildPluginsVersion.ABOUTLIB_PLUGIN"
|
||||
|
||||
// Tests
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'org.assertj:assertj-core:3.16.1'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
|
||||
final robolectric_version = '3.1.4'
|
||||
testImplementation "org.robolectric:robolectric:$robolectric_version"
|
||||
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
|
||||
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
|
||||
|
||||
final coroutines_version = '1.4.2'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
||||
|
||||
// SY for mangadex utils
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.1"
|
||||
|
||||
// Text distance (EH)
|
||||
implementation 'info.debatty:java-string-similarity:2.0.0'
|
||||
|
||||
// Firebase (EH)
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx:18.0.0'
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx:17.3.0'
|
||||
|
||||
// Better logging (EH)
|
||||
implementation 'com.elvishew:xlog:1.7.1'
|
||||
|
||||
// Debug utils (EH)
|
||||
final def debug_overlay_version = '1.1.3'
|
||||
debugImplementation "com.ms-square:debugoverlay:$debug_overlay_version"
|
||||
releaseTestImplementation "com.ms-square:debugoverlay:$debug_overlay_version"
|
||||
releaseImplementation "com.ms-square:debugoverlay-no-op:$debug_overlay_version"
|
||||
testImplementation "com.ms-square:debugoverlay-no-op:$debug_overlay_version"
|
||||
|
||||
// RatingBar (SY)
|
||||
implementation 'me.zhanghai.android.materialratingbar:library:1.4.0'
|
||||
|
||||
// JsonReader for similar manga
|
||||
implementation 'com.squareup.moshi:moshi:1.11.0'
|
||||
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$BuildPluginsVersion.KOTLIN"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
|
||||
tasks.withType(AbstractKotlinCompile).all {
|
||||
kotlinOptions.freeCompilerArgs += [
|
||||
"-Xopt-in=kotlin.Experimental",
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
|
||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
]
|
||||
}
|
||||
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
task copyHebrewStrings(type: Copy) {
|
||||
from './src/main/res/values-he'
|
||||
into './src/main/res/values-iw'
|
||||
include '**/*'
|
||||
}
|
||||
|
||||
preBuild.dependsOn(formatKotlin, copyHebrewStrings)
|
||||
|
||||
if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
// Firebase Crashlytics
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("com.mikepenz.aboutlibraries.plugin")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
kotlin("plugin.parcelize")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.github.zellius.shortcut-helper")
|
||||
// Realm (EH)
|
||||
id("realm-android")
|
||||
}
|
||||
|
||||
if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
// Firebase Crashlytics
|
||||
apply(plugin = "com.google.firebase.crashlytics")
|
||||
}
|
||||
|
||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
|
||||
android {
|
||||
compileSdkVersion(AndroidConfig.compileSdk)
|
||||
buildToolsVersion(AndroidConfig.buildTools)
|
||||
ndkVersion = AndroidConfig.ndk
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi.sy"
|
||||
minSdkVersion(AndroidConfig.minSdk)
|
||||
targetSdkVersion(AndroidConfig.targetSdk)
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode = 13
|
||||
versionName = "1.5.0"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
ndk {
|
||||
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("debug") {
|
||||
versionNameSuffix = "-${getCommitCount()}"
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
create("releaseTest") {
|
||||
applicationIdSuffix = ".rt"
|
||||
//isMinifyEnabled = true
|
||||
//isShrinkResources = true
|
||||
isZipAlignEnabled = true
|
||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
||||
}
|
||||
named("release") {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
isZipAlignEnabled = true
|
||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions("default")
|
||||
|
||||
productFlavors {
|
||||
create("standard") {
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
|
||||
dimension = "default"
|
||||
}
|
||||
create("fdroid") {
|
||||
dimension = "default"
|
||||
}
|
||||
create("dev") {
|
||||
resConfigs("en", "xxhdpi")
|
||||
dimension = "default"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude("META-INF/DEPENDENCIES")
|
||||
exclude("LICENSE.txt")
|
||||
exclude("META-INF/LICENSE")
|
||||
exclude("META-INF/LICENSE.txt")
|
||||
exclude("META-INF/NOTICE")
|
||||
|
||||
// Compatibility for two RxJava versions (EXH)
|
||||
exclude("META-INF/rxjava.properties")
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable("MissingTranslation", "ExtraTranslation")
|
||||
isAbortOnError = false
|
||||
isCheckReleaseBuilds = false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||
|
||||
// AndroidX libraries
|
||||
implementation("androidx.annotation:annotation:1.2.0-beta01")
|
||||
implementation("androidx.appcompat:appcompat:1.3.0-beta01")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
|
||||
implementation("androidx.browser:browser:1.3.0")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||
implementation("androidx.core:core-ktx:1.5.0-beta01")
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.0-beta01")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
|
||||
val lifecycleVersion = "2.3.0-rc01"
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||
|
||||
// Job scheduling
|
||||
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
||||
|
||||
// UI library
|
||||
implementation("com.google.android.material:material:1.3.0")
|
||||
|
||||
"standardImplementation"("com.google.firebase:firebase-core:18.0.2")
|
||||
|
||||
// ReactiveX
|
||||
implementation("io.reactivex:rxandroid:1.2.1")
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||
|
||||
// Network client
|
||||
val okhttpVersion = "4.10.0-RC1"
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
implementation("com.squareup.okio:okio:2.10.0")
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
||||
|
||||
// JSON
|
||||
val kotlinSerializationVersion = "1.0.1"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||
|
||||
// JavaScript engine
|
||||
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
||||
|
||||
// Disk
|
||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||
implementation("com.github.inorichi:unifile:e9ee588")
|
||||
implementation("com.github.junrar:junrar:7.4.0")
|
||||
|
||||
// HTML parser
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
|
||||
// Database
|
||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||
implementation("io.requery:sqlite-android:3.33.0")
|
||||
|
||||
// Preferences
|
||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
|
||||
|
||||
// Model View Presenter
|
||||
val nucleusVersion = "3.0.0"
|
||||
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
|
||||
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
|
||||
|
||||
// Dependency injection
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
|
||||
// Image library
|
||||
val glideVersion = "4.11.0"
|
||||
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
||||
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
||||
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
|
||||
// TODO: switch to new decoder for stable releases
|
||||
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
|
||||
|
||||
// Logging
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
|
||||
// Crash reports
|
||||
//implementation("ch.acra:acra-http:5.7.0")
|
||||
|
||||
// Sort
|
||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||
|
||||
// UI
|
||||
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
|
||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:7d0617d")
|
||||
|
||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
||||
val materialDialogsVersion = "3.1.1"
|
||||
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
|
||||
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
|
||||
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
|
||||
|
||||
// Conductor
|
||||
implementation("com.bluelinelabs:conductor:2.1.5")
|
||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
||||
exclude(group = "com.android.support")
|
||||
}
|
||||
implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1")
|
||||
|
||||
// FlowBinding
|
||||
val flowbindingVersion = "0.12.0"
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
||||
|
||||
// Licenses
|
||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||
|
||||
// Tests
|
||||
testImplementation("junit:junit:4.13.1")
|
||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||
|
||||
val robolectricVersion = "3.1.4"
|
||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
|
||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||
|
||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||
|
||||
val coroutinesVersion = "1.4.2"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
|
||||
|
||||
// SY -->
|
||||
// [EXH] Android 7 SSL Workaround
|
||||
implementation("com.google.android.gms:play-services-safetynet:17.0.0")
|
||||
|
||||
// Changelog
|
||||
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
|
||||
|
||||
// Text distance (EH)
|
||||
implementation ("info.debatty:java-string-similarity:2.0.0")
|
||||
|
||||
// Firebase (EH)
|
||||
implementation("com.google.firebase:firebase-analytics-ktx:18.0.0")
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx:17.3.0")
|
||||
|
||||
// Better logging (EH)
|
||||
implementation("com.elvishew:xlog:1.7.1")
|
||||
|
||||
// Debug utils (EH)
|
||||
val debugOverlayVersion = "1.1.3"
|
||||
debugImplementation("com.ms-square:debugoverlay:$debugOverlayVersion")
|
||||
"releaseTestImplementation"("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
||||
releaseImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
||||
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
|
||||
|
||||
// RatingBar (SY)
|
||||
implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
|
||||
|
||||
// JsonReader for similar manga
|
||||
implementation("com.squareup.moshi:moshi:1.11.0")
|
||||
|
||||
implementation("androidx.gridlayout:gridlayout:1.0.0")
|
||||
|
||||
implementation("com.mikepenz:fastadapter:5.3.4")
|
||||
// SY -->
|
||||
}
|
||||
|
||||
tasks {
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-Xopt-in=kotlin.Experimental",
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
|
||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
)
|
||||
}
|
||||
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) {
|
||||
from("./src/main/res/values-he")
|
||||
into("./src/main/res/values-iw")
|
||||
include("**/*")
|
||||
}
|
||||
|
||||
preBuild {
|
||||
dependsOn(formatKotlin, copyHebrewStrings)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Git is needed in your system PATH for these commands to work.
|
||||
// If it's not installed, you can return a random value as a workaround
|
||||
fun getCommitCount(): String {
|
||||
return runCommand("git rev-list --count HEAD")
|
||||
// return "1"
|
||||
}
|
||||
|
||||
fun getGitSha(): String {
|
||||
return runCommand("git rev-parse --short HEAD")
|
||||
// return "1"
|
||||
}
|
||||
|
||||
fun getBuildTime(): String {
|
||||
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||
df.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return df.format(Date())
|
||||
}
|
||||
|
||||
fun runCommand(command: String): String {
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
project.exec {
|
||||
commandLine = command.split(" ")
|
||||
standardOutput = byteOut
|
||||
}
|
||||
return String(byteOut.toByteArray()).trim()
|
||||
}
|
||||
@@ -2,16 +2,24 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi">
|
||||
|
||||
<!-- Internet -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<!-- To view extension packages in API 30+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
@@ -43,7 +51,7 @@
|
||||
android:name=".ui.main.DeepLinkActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:label="@string/process_text_action_name">
|
||||
android:label="@string/action_global_search">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||
@@ -55,7 +63,7 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
@@ -96,7 +104,18 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||
android:configChanges="uiMode|orientation|screenSize" />
|
||||
android:label="MyAnimeList">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="myanimelist-auth"
|
||||
android:scheme="tachiyomi" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||
android:label="Shikimori">
|
||||
@@ -186,39 +205,39 @@
|
||||
<!-- EH -->
|
||||
<data
|
||||
android:host="g.e-hentai.org"
|
||||
android:pathPattern="/g/.*"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:host="g.e-hentai.org"
|
||||
android:pathPattern="/g/.*"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="e-hentai.org"
|
||||
android:pathPattern="/g/.*"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:host="e-hentai.org"
|
||||
android:pathPattern="/g/.*"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="https" />
|
||||
|
||||
<!-- EXH -->
|
||||
<data
|
||||
android:host="exhentai.org"
|
||||
android:pathPattern="/g/.*"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:host="exhentai.org"
|
||||
android:pathPattern="/g/.*"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="https" />
|
||||
|
||||
<!-- nhentai -->
|
||||
<data
|
||||
android:host="nhentai.net"
|
||||
android:pathPattern="/g/.*"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:host="nhentai.net"
|
||||
android:pathPattern="/g/.*"
|
||||
android:pathPrefix="/g/"
|
||||
android:scheme="https" />
|
||||
|
||||
<!-- Perv Eden -->
|
||||
@@ -234,57 +253,57 @@
|
||||
<!-- Hentai Cafe -->
|
||||
<data
|
||||
android:host="hentai.cafe"
|
||||
android:pathPattern="/.*/.*"
|
||||
android:pathPrefix="/hc.fyi/"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:host="hentai.cafe"
|
||||
android:pathPattern="/.*/.*"
|
||||
android:pathPrefix="/hc.fyi/"
|
||||
android:scheme="https" />
|
||||
|
||||
<!-- Tsumino -->
|
||||
<data
|
||||
android:host="www.tsumino.com"
|
||||
android:pathPattern="/Book/Info/.*"
|
||||
android:pathPrefix="/Book/Info/"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:host="www.tsumino.com"
|
||||
android:pathPattern="/Book/Info/.*"
|
||||
android:pathPrefix="/Book/Info/"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="www.tsumino.com"
|
||||
android:pathPattern="/Read/View/.*"
|
||||
android:pathPrefix="/Read/View/"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:host="www.tsumino.com"
|
||||
android:pathPattern="/Read/View/.*"
|
||||
android:pathPrefix="/Read/View/"
|
||||
android:scheme="https" />
|
||||
|
||||
<!-- Hitomi.la -->
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPattern="/galleries/.*"
|
||||
android:pathPrefix="/galleries/"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPattern="/reader/.*"
|
||||
android:pathPrefix="/reader/"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPattern="/galleries/.*"
|
||||
android:pathPrefix="/galleries/"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPattern="/reader/.*"
|
||||
android:pathPrefix="/reader/"
|
||||
android:scheme="https" />
|
||||
|
||||
<!-- Pururin.io -->
|
||||
<data
|
||||
android:host="pururin.io"
|
||||
android:pathPattern="/gallery/.*"
|
||||
android:pathPrefix="/gallery/"
|
||||
android:scheme="http" />
|
||||
<data
|
||||
android:host="pururin.io"
|
||||
android:pathPattern="/gallery/.*"
|
||||
android:pathPrefix="/gallery/"
|
||||
android:scheme="https" />
|
||||
|
||||
<!-- HBrowse -->
|
||||
@@ -299,36 +318,53 @@
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.org"
|
||||
android:pathPattern="/manga/..*" />
|
||||
android:pathPrefix="/manga/" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="mangadex.org"
|
||||
android:pathPattern="/manga/..*" />
|
||||
android:pathPrefix="/manga/" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.cc"
|
||||
android:pathPattern="/manga/..*" />
|
||||
android:pathPrefix="/manga/" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.cc"
|
||||
android:pathPattern="/manga/..*" />
|
||||
android:pathPrefix="/manga/" />
|
||||
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.org"
|
||||
android:pathPattern="/title/..*" />
|
||||
android:pathPrefix="/title/" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="mangadex.org"
|
||||
android:pathPattern="/title/..*" />
|
||||
android:pathPrefix="/title/" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.cc"
|
||||
android:pathPattern="/title/..*" />
|
||||
android:pathPrefix="/title/" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.cc"
|
||||
android:pathPattern="/title/..*" />
|
||||
android:pathPrefix="/title/" />
|
||||
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.org"
|
||||
android:pathPrefix="/chapter/" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="mangadex.org"
|
||||
android:pathPrefix="/chapter/" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.cc"
|
||||
android:pathPrefix="/chapter/" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.mangadex.cc"
|
||||
android:pathPrefix="/chapter/" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
|
||||
@@ -44,9 +44,7 @@ import kotlinx.coroutines.launch
|
||||
import org.conscrypt.Conscrypt
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.InjektScope
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||
import java.io.File
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.Security
|
||||
@@ -59,6 +57,8 @@ import kotlin.time.days
|
||||
|
||||
open class App : Application(), LifecycleObserver {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -69,21 +69,11 @@ open class App : Application(), LifecycleObserver {
|
||||
|
||||
workaroundAndroid7BrokenSSL()
|
||||
|
||||
// Debug tool; see https://fbflipper.com/
|
||||
// SoLoader.init(this, false)
|
||||
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
|
||||
// val client = AndroidFlipperClient.getInstance(this)
|
||||
// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
||||
// client.addPlugin(DatabasesFlipperPlugin(this))
|
||||
// client.start()
|
||||
// }
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
}
|
||||
|
||||
Injekt = InjektScope(DefaultRegistrar())
|
||||
Injekt.importModule(AppModule(this))
|
||||
|
||||
setupNotificationChannels()
|
||||
@@ -138,7 +128,6 @@ open class App : Application(), LifecycleObserver {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
@Suppress("unused")
|
||||
fun onAppBackgrounded() {
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
if (preferences.lockAppAfter().get() >= 0) {
|
||||
SecureActivityDelegate.locked = true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
@@ -13,8 +14,6 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import exh.eh.EHentaiUpdateHelper
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
@@ -56,19 +55,20 @@ class AppModule(val app: Application) : InjektModule {
|
||||
// SY <--
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
Handler().post {
|
||||
get<PreferencesHelper>()
|
||||
|
||||
GlobalScope.launch { get<PreferencesHelper>() }
|
||||
get<NetworkHelper>()
|
||||
|
||||
GlobalScope.launch { get<NetworkHelper>() }
|
||||
get<SourceManager>()
|
||||
|
||||
GlobalScope.launch { get<SourceManager>() }
|
||||
get<DatabaseHelper>()
|
||||
|
||||
GlobalScope.launch { get<DatabaseHelper>() }
|
||||
get<DownloadManager>()
|
||||
|
||||
GlobalScope.launch { get<DownloadManager>() }
|
||||
|
||||
// SY -->
|
||||
GlobalScope.launch { get<CustomMangaManager>() }
|
||||
// SY <--
|
||||
// SY -->
|
||||
get<CustomMangaManager>()
|
||||
// SY <--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +118,11 @@ object Migrations {
|
||||
putInt(PreferenceKeys.filterCompleted, convertBooleanPrefToTriState("pref_filter_completed_key"))
|
||||
remove("pref_filter_completed_key")
|
||||
}
|
||||
|
||||
}
|
||||
if (oldVersion < 53) {
|
||||
// Force MAL log out due to login flow change
|
||||
// v52: switched from scraping to WebView
|
||||
// v53: switched from WebView to OAuth
|
||||
val trackManager = Injekt.get<TrackManager>()
|
||||
if (trackManager.myAnimeList.isLogged) {
|
||||
trackManager.myAnimeList.logout()
|
||||
|
||||
@@ -5,14 +5,15 @@ import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import exh.eh.EHentaiThrottleManager
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class AbstractBackupManager(protected val context: Context) {
|
||||
@@ -33,29 +34,29 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
||||
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
|
||||
|
||||
/**
|
||||
* [Observable] that fetches chapter information
|
||||
* Fetches chapter information.
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @param chapters list of chapters in the backup
|
||||
* @return [Observable] that contains manga
|
||||
* @return Updated manga chapters.
|
||||
*/
|
||||
internal open fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
return (
|
||||
if (source is EHentai) {
|
||||
source.fetchChapterList(manga, throttleManager::throttle)
|
||||
} else {
|
||||
source.fetchChapterList(manga)
|
||||
}
|
||||
).map {
|
||||
syncChaptersWithSource(databaseHelper, it, manga, source)
|
||||
internal open suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter> /* SY --> */, throttleManager: EHentaiThrottleManager /* SY <-- */): Pair<List<Chapter>, List<Chapter>> {
|
||||
// SY -->
|
||||
val fetchedChapters = if (source is EHentai) {
|
||||
source.getChapterList(manga.toMangaInfo(), throttleManager::throttle)
|
||||
.map { it.toSChapter() }
|
||||
} else {
|
||||
source.getChapterList(manga.toMangaInfo())
|
||||
.map { it.toSChapter() }
|
||||
}
|
||||
.doOnNext { (first) ->
|
||||
if (first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
updateChapters(chapters)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
val syncedChapters = syncChaptersWithSource(databaseHelper, fetchedChapters, manga, source)
|
||||
if (syncedChapters.first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
updateChapters(chapters)
|
||||
}
|
||||
return syncedChapters
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,6 +68,9 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
||||
databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||
|
||||
// SY -->
|
||||
protected fun getReadManga(): List<Manga> =
|
||||
databaseHelper.getReadNotInLibraryMangas().executeAsBlocking()
|
||||
|
||||
/**
|
||||
* Returns list containing merged manga that are possibly not in the library
|
||||
*
|
||||
@@ -98,6 +102,13 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
||||
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a list of chapters with known database ids
|
||||
*/
|
||||
protected fun updateKnownChapters(chapters: List<Chapter>) {
|
||||
databaseHelper.updateKnownChaptersBackup(chapters).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return number of backups.
|
||||
*
|
||||
|
||||
@@ -10,9 +10,9 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import exh.eh.EHentaiThrottleManager
|
||||
import kotlinx.coroutines.Job
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -42,9 +42,9 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
|
||||
|
||||
protected val errors = mutableListOf<Pair<Date, String>>()
|
||||
|
||||
abstract fun performRestore(uri: Uri): Boolean
|
||||
abstract suspend fun performRestore(uri: Uri): Boolean
|
||||
|
||||
fun restoreBackup(uri: Uri): Boolean {
|
||||
suspend fun restoreBackup(uri: Uri): Boolean {
|
||||
val startTime = System.currentTimeMillis()
|
||||
restoreProgress = 0
|
||||
errors.clear()
|
||||
@@ -63,48 +63,48 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that fetches chapter information
|
||||
* Fetches chapter information.
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
* @return Updated manga chapters.
|
||||
*/
|
||||
internal fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
return backupManager.restoreChapterFetchObservable(source, manga, chapters /* SY --> */, throttleManager /* SY <-- */)
|
||||
internal suspend fun updateChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
|
||||
return try {
|
||||
backupManager.restoreChapters(source, manga, chapters /* SY --> */, throttleManager /* SY <-- */)
|
||||
} catch (e: Exception) {
|
||||
// If there's any error, return empty update and continue.
|
||||
.onErrorReturn {
|
||||
val errorMessage = if (it is NoChaptersException) {
|
||||
context.getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
it.message
|
||||
}
|
||||
errors.add(Date() to "${manga.title} - $errorMessage")
|
||||
Pair(emptyList(), emptyList())
|
||||
val errorMessage = if (e is NoChaptersException) {
|
||||
context.getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
e.message
|
||||
}
|
||||
errors.add(Date() to "${manga.title} - $errorMessage")
|
||||
Pair(emptyList(), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that refreshes tracking information
|
||||
* Refreshes tracking information.
|
||||
*
|
||||
* @param manga manga that needs updating.
|
||||
* @param tracks list containing tracks from restore file.
|
||||
* @return [Observable] that contains updated track item
|
||||
*/
|
||||
internal fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
||||
return Observable.from(tracks)
|
||||
.flatMap { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
service.refresh(track)
|
||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||
.onErrorReturn {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
track
|
||||
}
|
||||
} else {
|
||||
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, service?.name)}")
|
||||
Observable.empty()
|
||||
internal suspend fun updateTracking(manga: Manga, tracks: List<Track>) {
|
||||
tracks.forEach { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
try {
|
||||
val updatedTrack = service.refresh(track)
|
||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
}
|
||||
} else {
|
||||
val serviceName = service?.nameRes()?.let { context.getString(it) }
|
||||
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, serviceName)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,15 +125,15 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
|
||||
internal fun writeErrorLog(): File {
|
||||
try {
|
||||
if (errors.isNotEmpty()) {
|
||||
val destFile = File(context.externalCacheDir, "tachiyomi_restore.txt")
|
||||
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||
|
||||
destFile.bufferedWriter().use { out ->
|
||||
file.bufferedWriter().use { out ->
|
||||
errors.forEach { (date, message) ->
|
||||
out.write("[${sdf.format(date)}] $message\n")
|
||||
}
|
||||
}
|
||||
return destFile
|
||||
return file
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Empty
|
||||
|
||||
@@ -111,7 +111,7 @@ class BackupCreateService : Service() {
|
||||
|
||||
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||
notifier.showBackupComplete(unifile)
|
||||
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
|
||||
} catch (e: Exception) {
|
||||
notifier.showBackupError(e.message)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class BackupNotifier(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun showBackupComplete(unifile: UniFile) {
|
||||
fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
|
||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||
|
||||
with(completeNotificationBuilder) {
|
||||
@@ -68,14 +68,12 @@ class BackupNotifier(private val context: Context) {
|
||||
setContentText(unifile.filePath ?: unifile.name)
|
||||
|
||||
// Clear old actions if they exist
|
||||
if (mActions.isNotEmpty()) {
|
||||
mActions.clear()
|
||||
}
|
||||
clearActions()
|
||||
|
||||
addAction(
|
||||
R.drawable.ic_share_24dp,
|
||||
context.getString(R.string.action_share),
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
|
||||
)
|
||||
|
||||
show(Notifications.ID_BACKUP_COMPLETE)
|
||||
@@ -94,9 +92,7 @@ class BackupNotifier(private val context: Context) {
|
||||
setOnlyAlertOnce(true)
|
||||
|
||||
// Clear old actions if they exist
|
||||
if (mActions.isNotEmpty()) {
|
||||
mActions.clear()
|
||||
}
|
||||
clearActions()
|
||||
|
||||
addAction(
|
||||
R.drawable.ic_close_24dp,
|
||||
@@ -137,9 +133,7 @@ class BackupNotifier(private val context: Context) {
|
||||
setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
|
||||
|
||||
// Clear old actions if they exist
|
||||
if (mActions.isNotEmpty()) {
|
||||
mActions.clear()
|
||||
}
|
||||
clearActions()
|
||||
|
||||
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
||||
val destFile = File(path, file)
|
||||
|
||||
@@ -14,7 +14,10 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -68,12 +71,14 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
private lateinit var ioScope: CoroutineScope
|
||||
private var backupRestore: AbstractBackupRestore<*>? = null
|
||||
private lateinit var notifier: BackupNotifier
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
notifier = BackupNotifier(this)
|
||||
wakeLock = acquireWakeLock(javaClass.name)
|
||||
|
||||
@@ -92,6 +97,7 @@ class BackupRestoreService : Service() {
|
||||
|
||||
private fun destroyJob() {
|
||||
backupRestore?.job?.cancel()
|
||||
ioScope?.cancel()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
@@ -122,6 +128,7 @@ class BackupRestoreService : Service() {
|
||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
|
||||
else -> LegacyBackupRestore(this, notifier)
|
||||
}
|
||||
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Timber.e(exception)
|
||||
backupRestore?.writeErrorLog()
|
||||
@@ -129,14 +136,15 @@ class BackupRestoreService : Service() {
|
||||
notifier.showRestoreError(exception.message)
|
||||
stopSelf(startId)
|
||||
}
|
||||
backupRestore?.job = GlobalScope.launch(handler) {
|
||||
val job = ioScope.launch(handler) {
|
||||
if (backupRestore?.restoreBackup(uri) == false) {
|
||||
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
|
||||
}
|
||||
}
|
||||
backupRestore?.job?.invokeOnCompletion {
|
||||
job.invokeOnCompletion {
|
||||
stopSelf(startId)
|
||||
}
|
||||
backupRestore?.job = job
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
@@ -29,15 +29,17 @@ import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||
import exh.savedsearches.JsonSavedSearch
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.source.getMainSource
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import exh.util.executeOnIO
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -45,11 +47,9 @@ import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.sink
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import kotlin.math.max
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
|
||||
val parser = ProtoBuf
|
||||
@@ -65,7 +65,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
var backup: Backup? = null
|
||||
|
||||
databaseHelper.inTransaction {
|
||||
val databaseManga = getFavoriteManga() /* SY --> */ + getMergedManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY <-- */
|
||||
val databaseManga = getFavoriteManga() /* SY --> */ + getReadManga() + getMergedManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY <-- */
|
||||
|
||||
backup = Backup(
|
||||
backupManga(databaseManga, flags),
|
||||
@@ -235,29 +235,26 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that fetches manga information
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
* @return Updated manga info.
|
||||
*/
|
||||
fun restoreMangaFetchObservable(source: Source?, manga: Manga, online: Boolean): Observable<Manga> {
|
||||
suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga {
|
||||
return if (online && source != null /* SY --> */ && source !is MergedSource /* SY <-- */) {
|
||||
source.fetchMangaDetails(manga)
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.favorite = manga.favorite
|
||||
manga.initialized = true
|
||||
manga.id = insertManga(manga)
|
||||
manga
|
||||
}
|
||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
manga.also {
|
||||
it.copyFrom(networkManga.toSManga())
|
||||
it.favorite = manga.favorite
|
||||
it.initialized = true
|
||||
it.id = insertManga(manga)
|
||||
}
|
||||
} else {
|
||||
Observable.just(manga)
|
||||
.map {
|
||||
it.initialized = it.description != null
|
||||
it.id = insertManga(it)
|
||||
it
|
||||
}
|
||||
manga.also {
|
||||
it.initialized = it.description != null
|
||||
it.id = insertManga(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +299,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
*/
|
||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
|
||||
categories.forEach { backupCategoryOrder ->
|
||||
backupCategories.firstOrNull {
|
||||
it.order == backupCategoryOrder
|
||||
@@ -329,7 +326,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
*/
|
||||
internal fun restoreHistoryForManga(history: List<BackupHistory>) {
|
||||
// List containing history to be updated
|
||||
val historyToBeUpdated = mutableListOf<History>()
|
||||
val historyToBeUpdated = ArrayList<History>(history.size)
|
||||
for ((url, lastRead) in history) {
|
||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||
// Check if history already in database and update
|
||||
@@ -413,9 +410,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
|
||||
chapters.forEach { chapter ->
|
||||
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
|
||||
if (pos != -1) {
|
||||
val dbChapter = dbChapters[pos]
|
||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
||||
if (dbChapter != null) {
|
||||
chapter.id = dbChapter.id
|
||||
chapter.copyFrom(dbChapter)
|
||||
if (dbChapter.read && !chapter.read) {
|
||||
@@ -428,12 +424,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
chapter.bookmark = dbChapter.bookmark
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter the chapters that couldn't be found.
|
||||
chapters.filter { it.id != null }
|
||||
chapters.map { it.manga_id = manga.id }
|
||||
|
||||
updateChapters(chapters)
|
||||
chapter.manga_id = manga.id
|
||||
}
|
||||
|
||||
// Filter the chapters that couldn't be found.
|
||||
updateChapters(chapters.filter { it.id != null })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -441,9 +438,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
|
||||
chapters.forEach { chapter ->
|
||||
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
|
||||
if (pos != -1) {
|
||||
val dbChapter = dbChapters[pos]
|
||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
||||
if (dbChapter != null) {
|
||||
chapter.id = dbChapter.id
|
||||
chapter.copyFrom(dbChapter)
|
||||
if (dbChapter.read && !chapter.read) {
|
||||
@@ -456,11 +452,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
chapter.bookmark = dbChapter.bookmark
|
||||
}
|
||||
}
|
||||
}
|
||||
chapters.map { it.manga_id = manga.id }
|
||||
|
||||
updateChapters(chapters.filter { it.id != null })
|
||||
insertChapters(chapters.filter { it.id == null })
|
||||
chapter.manga_id = manga.id
|
||||
}
|
||||
|
||||
val newChapters = chapters.groupBy { it.id != null }
|
||||
newChapters[true]?.let { updateKnownChapters(it) }
|
||||
newChapters[false]?.let { insertChapters(it) }
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@@ -529,12 +527,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
|
||||
internal suspend fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
|
||||
manga.id?.let { mangaId ->
|
||||
databaseHelper.getFlatMetadataForManga(mangaId).executeAsBlocking().let {
|
||||
databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let {
|
||||
if (it == null) {
|
||||
val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId)
|
||||
databaseHelper.insertFlatMetadata(flatMetadata).await()
|
||||
databaseHelper.insertFlatMetadataAsync(flatMetadata).await()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,18 +18,15 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import exh.EXHMigrations
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.source
|
||||
import rx.Observable
|
||||
import java.util.Date
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
||||
|
||||
override fun performRestore(uri: Uri): Boolean {
|
||||
override suspend fun performRestore(uri: Uri): Boolean {
|
||||
// SY -->
|
||||
throttleManager.resetThrottle()
|
||||
// SY <--
|
||||
@@ -84,7 +81,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
|
||||
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
|
||||
var manga = backupManga.getMangaImpl()
|
||||
val chapters = backupManga.getChaptersImpl()
|
||||
val categories = backupManga.categories
|
||||
@@ -99,16 +96,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
manga = EXHMigrations.migrateBackupEntry(manga)
|
||||
// SY <--
|
||||
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
|
||||
try {
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
if (source != null || !online) {
|
||||
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
|
||||
} else {
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} - ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
@@ -125,7 +123,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
* @param history history data from json
|
||||
* @param tracks tracking data from json
|
||||
*/
|
||||
private fun restoreMangaData(
|
||||
private suspend fun restoreMangaData(
|
||||
manga: Manga,
|
||||
source: Source?,
|
||||
chapters: List<Chapter>,
|
||||
@@ -153,13 +151,13 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that fetches manga information
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param manga manga that needs updating
|
||||
* @param chapters chapters of manga that needs updating
|
||||
* @param categories categories that need updating
|
||||
*/
|
||||
private fun restoreMangaFetch(
|
||||
private suspend fun restoreMangaFetch(
|
||||
source: Source?,
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
@@ -171,37 +169,29 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
online: Boolean
|
||||
) {
|
||||
backupManager.restoreMangaFetchObservable(source, manga, online)
|
||||
.doOnError {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
}
|
||||
.filter { it.id != null }
|
||||
.flatMap {
|
||||
if (online && source != null) {
|
||||
// SY -->
|
||||
if (source !is MergedSource) {
|
||||
chapterFetchObservable(source, it, chapters)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
} else {
|
||||
Observable.just(manga)
|
||||
}
|
||||
// SY <--
|
||||
} else {
|
||||
backupManager.restoreChaptersForMangaOffline(it, chapters)
|
||||
Observable.just(manga)
|
||||
try {
|
||||
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
|
||||
fetchedManga.id ?: return
|
||||
|
||||
if (online && source != null) {
|
||||
// SY -->
|
||||
if (source !is MergedSource) {
|
||||
updateChapters(source, fetchedManga, chapters)
|
||||
}
|
||||
// SY <--
|
||||
} else {
|
||||
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
|
||||
}
|
||||
.doOnNext {
|
||||
restoreExtraForManga(it, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
|
||||
}
|
||||
.flatMap {
|
||||
trackingFetchObservable(it, tracks)
|
||||
}
|
||||
.subscribe()
|
||||
|
||||
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
|
||||
|
||||
updateTracking(fetchedManga, tracks)
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreMangaNoFetch(
|
||||
private suspend fun restoreMangaNoFetch(
|
||||
source: Source?,
|
||||
backupManga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
@@ -213,30 +203,20 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
||||
flatMetadata: BackupFlatMetadata?,
|
||||
online: Boolean
|
||||
) {
|
||||
Observable.just(backupManga)
|
||||
.flatMap { manga ->
|
||||
if (online && source != null) {
|
||||
if (/* SY --> */ source !is MergedSource && /* SY <-- */ !backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||
chapterFetchObservable(source, manga, chapters)
|
||||
.map { manga }
|
||||
} else {
|
||||
Observable.just(manga)
|
||||
}
|
||||
} else {
|
||||
backupManager.restoreChaptersForMangaOffline(manga, chapters)
|
||||
Observable.just(manga)
|
||||
}
|
||||
if (online && source != null) {
|
||||
if (/* SY --> */ source !is MergedSource && /* SY <-- */ !backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||
updateChapters(source, backupManga, chapters)
|
||||
}
|
||||
.doOnNext {
|
||||
restoreExtraForManga(it, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
|
||||
}
|
||||
.flatMap { manga ->
|
||||
trackingFetchObservable(manga, tracks)
|
||||
}
|
||||
.subscribe()
|
||||
} else {
|
||||
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
|
||||
}
|
||||
|
||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
|
||||
|
||||
updateTracking(backupManga, tracks)
|
||||
}
|
||||
|
||||
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>, mergedMangaReferences: List<BackupMergedMangaReference>, flatMetadata: BackupFlatMetadata?) {
|
||||
private suspend fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>, mergedMangaReferences: List<BackupMergedMangaReference>, flatMetadata: BackupFlatMetadata?) {
|
||||
// Restore categories
|
||||
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
|
||||
|
||||
|
||||
+1
-3
@@ -5,12 +5,10 @@ import android.net.Uri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
import okio.source
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
/**
|
||||
* Checks for critical backup file data.
|
||||
@@ -41,7 +39,7 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
val missingTrackers = trackers
|
||||
.mapNotNull { trackManager.getService(it) }
|
||||
.filter { !it.isLogged }
|
||||
.map { it.name }
|
||||
.map { context.getString(it.nameRes()) }
|
||||
.sorted()
|
||||
|
||||
return Results(missingSources, missingTrackers)
|
||||
|
||||
@@ -25,7 +25,6 @@ data class BackupTracking(
|
||||
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
||||
// finishedReadingDate is called endReadTime in 1.x
|
||||
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
||||
|
||||
) {
|
||||
fun getTrackingImpl(): TrackImpl {
|
||||
return TrackImpl().apply {
|
||||
|
||||
@@ -48,21 +48,19 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.util.lang.asObservable
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.eh.EHentaiThrottleManager
|
||||
import exh.merged.sql.models.MergedMangaReference
|
||||
import exh.savedsearches.JsonSavedSearch
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -71,33 +69,17 @@ import kotlin.math.max
|
||||
|
||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||
|
||||
var parserVersion: Int = version
|
||||
private set
|
||||
|
||||
var parser: Gson = initParser()
|
||||
|
||||
/**
|
||||
* Set version of parser
|
||||
*
|
||||
* @param version version of parser
|
||||
*/
|
||||
internal fun setVersion(version: Int) {
|
||||
this.parserVersion = version
|
||||
parser = initParser()
|
||||
}
|
||||
|
||||
private fun initParser(): Gson = when (parserVersion) {
|
||||
2 ->
|
||||
GsonBuilder()
|
||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||
// SY -->
|
||||
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
|
||||
// SY <--
|
||||
.create()
|
||||
val parser: Gson = when (version) {
|
||||
2 -> GsonBuilder()
|
||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||
// SY -->
|
||||
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
|
||||
// SY <--
|
||||
.create()
|
||||
else -> throw Exception("Unknown backup version")
|
||||
}
|
||||
|
||||
@@ -290,21 +272,20 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that fetches manga information
|
||||
* Fetches manga information
|
||||
*
|
||||
* @param source source of manga
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
* @return Updated manga.
|
||||
*/
|
||||
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
||||
return source.fetchMangaDetails(manga)
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.favorite = true
|
||||
manga.initialized = true
|
||||
manga.id = insertManga(manga)
|
||||
manga
|
||||
}
|
||||
suspend fun fetchManga(source: Source, manga: Manga): Manga {
|
||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
return manga.also {
|
||||
it.copyFrom(networkManga.toSManga())
|
||||
it.favorite = true
|
||||
it.initialized = true
|
||||
it.id = insertManga(manga)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,18 +295,17 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
*/
|
||||
override fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
override suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Pair<List<Chapter>, List<Chapter>> {
|
||||
// SY -->
|
||||
return if (source is MergedSource) {
|
||||
val syncedChapters = runBlocking { source.fetchChaptersAndSync(manga, false) }
|
||||
syncedChapters.onEach { pair ->
|
||||
if (pair.first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
updateChapters(chapters)
|
||||
}
|
||||
}.asObservable()
|
||||
val syncedChapters = source.fetchChaptersAndSync(manga, false)
|
||||
syncedChapters.first.onEach {
|
||||
it.manga_id = manga.id
|
||||
}
|
||||
updateChapters(syncedChapters.first)
|
||||
syncedChapters
|
||||
} else {
|
||||
super.restoreChapterFetchObservable(source, manga, chapters, throttleManager)
|
||||
super.restoreChapters(source, manga, chapters, throttleManager)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +351,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
*/
|
||||
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
|
||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
|
||||
for (backupCategoryStr in categories) {
|
||||
for (dbCategory in dbCategories) {
|
||||
if (backupCategoryStr == dbCategory.name) {
|
||||
@@ -395,7 +375,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
*/
|
||||
internal fun restoreHistoryForManga(history: List<DHistory>) {
|
||||
// List containing history to be updated
|
||||
val historyToBeUpdated = mutableListOf<History>()
|
||||
val historyToBeUpdated = ArrayList<History>(history.size)
|
||||
for ((url, lastRead) in history) {
|
||||
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
|
||||
// Check if history already in database and update
|
||||
@@ -424,14 +404,14 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
* @param tracks the track list to restore.
|
||||
*/
|
||||
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
|
||||
// Fix foreign keys with the current manga id
|
||||
tracks.map { it.manga_id = manga.id!! }
|
||||
|
||||
// Get tracks from database
|
||||
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||
val trackToUpdate = mutableListOf<Track>()
|
||||
val trackToUpdate = ArrayList<Track>(tracks.size)
|
||||
|
||||
tracks.forEach { track ->
|
||||
// Fix foreign keys with the current manga id
|
||||
track.manga_id = manga.id!!
|
||||
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
var isInDatabase = false
|
||||
@@ -486,12 +466,13 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
chapter.copyFrom(dbChapter)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Filter the chapters that couldn't be found.
|
||||
chapters.filter { it.id != null }
|
||||
chapters.map { it.manga_id = manga.id }
|
||||
|
||||
updateChapters(chapters)
|
||||
chapter.manga_id = manga.id
|
||||
}
|
||||
|
||||
// Filter the chapters that couldn't be found.
|
||||
updateChapters(chapters.filter { it.id != null })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,11 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import exh.EXHMigrations
|
||||
import rx.Observable
|
||||
import java.util.Date
|
||||
|
||||
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||
|
||||
override fun performRestore(uri: Uri): Boolean {
|
||||
override suspend fun performRestore(uri: Uri): Boolean {
|
||||
// SY -->
|
||||
throttleManager.resetThrottle()
|
||||
// SY <--
|
||||
@@ -91,7 +90,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private fun restoreManga(mangaJson: JsonObject) {
|
||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||
/* SY --> */ var /* SY <-- */ manga = backupManager.parser.fromJson<MangaImpl>(
|
||||
mangaJson.get(
|
||||
Backup.MANGA
|
||||
@@ -118,16 +117,17 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
manga = EXHMigrations.migrateBackupEntry(manga)
|
||||
// <-- EXH
|
||||
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
|
||||
try {
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
if (source != null) {
|
||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||
} else {
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} - ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
@@ -144,7 +144,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
* @param history history data from json
|
||||
* @param tracks tracking data from json
|
||||
*/
|
||||
private fun restoreMangaData(
|
||||
private suspend fun restoreMangaData(
|
||||
manga: Manga,
|
||||
source: Source,
|
||||
chapters: List<Chapter>,
|
||||
@@ -168,13 +168,13 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that fetches manga information
|
||||
* Fetches manga information.
|
||||
*
|
||||
* @param manga manga that needs updating
|
||||
* @param chapters chapters of manga that needs updating
|
||||
* @param categories categories that need updating
|
||||
*/
|
||||
private fun restoreMangaFetch(
|
||||
private suspend fun restoreMangaFetch(
|
||||
source: Source,
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
@@ -182,27 +182,21 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
) {
|
||||
backupManager.restoreMangaFetchObservable(source, manga)
|
||||
.onErrorReturn {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
manga
|
||||
}
|
||||
.filter { it.id != null }
|
||||
.flatMap {
|
||||
chapterFetchObservable(source, it, chapters)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
.doOnNext {
|
||||
restoreExtraForManga(it, categories, history, tracks)
|
||||
}
|
||||
.flatMap {
|
||||
trackingFetchObservable(it, tracks)
|
||||
}
|
||||
.subscribe()
|
||||
try {
|
||||
val fetchedManga = backupManager.fetchManga(source, manga)
|
||||
fetchedManga.id ?: return
|
||||
|
||||
updateChapters(source, fetchedManga, chapters)
|
||||
|
||||
restoreExtraForManga(fetchedManga, categories, history, tracks)
|
||||
|
||||
updateTracking(fetchedManga, tracks)
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreMangaNoFetch(
|
||||
private suspend fun restoreMangaNoFetch(
|
||||
source: Source,
|
||||
backupManga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
@@ -210,22 +204,13 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
history: List<DHistory>,
|
||||
tracks: List<Track>
|
||||
) {
|
||||
Observable.just(backupManga)
|
||||
.flatMap { manga ->
|
||||
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||
chapterFetchObservable(source, manga, chapters)
|
||||
.map { manga }
|
||||
} else {
|
||||
Observable.just(manga)
|
||||
}
|
||||
}
|
||||
.doOnNext {
|
||||
restoreExtraForManga(it, categories, history, tracks)
|
||||
}
|
||||
.flatMap { manga ->
|
||||
trackingFetchObservable(manga, tracks)
|
||||
}
|
||||
.subscribe()
|
||||
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
||||
updateChapters(source, backupManga, chapters)
|
||||
}
|
||||
|
||||
restoreExtraForManga(backupManga, categories, history, tracks)
|
||||
|
||||
updateTracking(backupManga, tracks)
|
||||
}
|
||||
|
||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
val missingTrackers = trackers
|
||||
.mapNotNull { trackManager.getService(it) }
|
||||
.filter { !it.isLogged }
|
||||
.map { it.name }
|
||||
.map { context.getString(it.nameRes()) }
|
||||
.sorted()
|
||||
|
||||
return Results(missingSources, missingTrackers)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@@ -36,16 +36,14 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_NAME, obj.name)
|
||||
put(COL_ORDER, obj.order)
|
||||
put(COL_FLAGS, obj.flags)
|
||||
// SY -->
|
||||
val orderString = obj.mangaOrder.joinToString("/")
|
||||
put(COL_MANGA_ORDER, orderString)
|
||||
// SY <--
|
||||
}
|
||||
override fun mapToContentValues(obj: Category) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_NAME to obj.name,
|
||||
COL_ORDER to obj.order,
|
||||
COL_FLAGS to obj.flags,
|
||||
COL_MANGA_ORDER to obj.mangaOrder.joinToString("/")
|
||||
)
|
||||
}
|
||||
|
||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@@ -43,20 +43,21 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_MANGA_ID, obj.manga_id)
|
||||
put(COL_URL, obj.url)
|
||||
put(COL_NAME, obj.name)
|
||||
put(COL_READ, obj.read)
|
||||
put(COL_SCANLATOR, obj.scanlator)
|
||||
put(COL_BOOKMARK, obj.bookmark)
|
||||
put(COL_DATE_FETCH, obj.date_fetch)
|
||||
put(COL_DATE_UPLOAD, obj.date_upload)
|
||||
put(COL_LAST_PAGE_READ, obj.last_page_read)
|
||||
put(COL_CHAPTER_NUMBER, obj.chapter_number)
|
||||
put(COL_SOURCE_ORDER, obj.source_order)
|
||||
}
|
||||
override fun mapToContentValues(obj: Chapter) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_MANGA_ID to obj.manga_id,
|
||||
COL_URL to obj.url,
|
||||
COL_NAME to obj.name,
|
||||
COL_READ to obj.read,
|
||||
COL_SCANLATOR to obj.scanlator,
|
||||
COL_BOOKMARK to obj.bookmark,
|
||||
COL_DATE_FETCH to obj.date_fetch,
|
||||
COL_DATE_UPLOAD to obj.date_upload,
|
||||
COL_LAST_PAGE_READ to obj.last_page_read,
|
||||
COL_CHAPTER_NUMBER to obj.chapter_number,
|
||||
COL_SOURCE_ORDER to obj.source_order
|
||||
)
|
||||
}
|
||||
|
||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@@ -35,12 +35,13 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_CHAPTER_ID, obj.chapter_id)
|
||||
put(COL_LAST_READ, obj.last_read)
|
||||
put(COL_TIME_READ, obj.time_read)
|
||||
}
|
||||
override fun mapToContentValues(obj: History) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_CHAPTER_ID to obj.chapter_id,
|
||||
COL_LAST_READ to obj.last_read,
|
||||
COL_TIME_READ to obj.time_read
|
||||
)
|
||||
}
|
||||
|
||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||
|
||||
+7
-6
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@@ -33,11 +33,12 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_MANGA_ID, obj.manga_id)
|
||||
put(COL_CATEGORY_ID, obj.category_id)
|
||||
}
|
||||
override fun mapToContentValues(obj: MangaCategory) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_MANGA_ID to obj.manga_id,
|
||||
COL_CATEGORY_ID to obj.category_id
|
||||
)
|
||||
}
|
||||
|
||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@@ -48,27 +48,28 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_SOURCE, obj.source)
|
||||
put(COL_URL, obj.url)
|
||||
// SY -->
|
||||
put(COL_ARTIST, obj.originalArtist)
|
||||
put(COL_AUTHOR, obj.originalAuthor)
|
||||
put(COL_DESCRIPTION, obj.originalDescription)
|
||||
put(COL_GENRE, obj.originalGenre)
|
||||
put(COL_TITLE, obj.originalTitle)
|
||||
// SY <--
|
||||
put(COL_STATUS, obj.status)
|
||||
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
|
||||
put(COL_FAVORITE, obj.favorite)
|
||||
put(COL_LAST_UPDATE, obj.last_update)
|
||||
put(COL_INITIALIZED, obj.initialized)
|
||||
put(COL_VIEWER, obj.viewer)
|
||||
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
|
||||
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
|
||||
put(COL_DATE_ADDED, obj.date_added)
|
||||
}
|
||||
override fun mapToContentValues(obj: Manga) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_SOURCE to obj.source,
|
||||
COL_URL to obj.url,
|
||||
// SY -->
|
||||
COL_ARTIST to obj.originalArtist,
|
||||
COL_AUTHOR to obj.originalAuthor,
|
||||
COL_DESCRIPTION to obj.originalDescription,
|
||||
COL_GENRE to obj.originalGenre,
|
||||
COL_TITLE to obj.originalTitle,
|
||||
// SY <--
|
||||
COL_STATUS to obj.status,
|
||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||
COL_FAVORITE to obj.favorite,
|
||||
COL_LAST_UPDATE to obj.last_update,
|
||||
COL_INITIALIZED to obj.initialized,
|
||||
COL_VIEWER to obj.viewer,
|
||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||
COL_DATE_ADDED to obj.date_added
|
||||
)
|
||||
}
|
||||
|
||||
interface BaseMangaGetResolver {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.mappers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||
@@ -44,21 +44,22 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
||||
.whereArgs(obj.id)
|
||||
.build()
|
||||
|
||||
override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
|
||||
put(COL_ID, obj.id)
|
||||
put(COL_MANGA_ID, obj.manga_id)
|
||||
put(COL_SYNC_ID, obj.sync_id)
|
||||
put(COL_MEDIA_ID, obj.media_id)
|
||||
put(COL_LIBRARY_ID, obj.library_id)
|
||||
put(COL_TITLE, obj.title)
|
||||
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
|
||||
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
|
||||
put(COL_STATUS, obj.status)
|
||||
put(COL_TRACKING_URL, obj.tracking_url)
|
||||
put(COL_SCORE, obj.score)
|
||||
put(COL_START_DATE, obj.started_reading_date)
|
||||
put(COL_FINISH_DATE, obj.finished_reading_date)
|
||||
}
|
||||
override fun mapToContentValues(obj: Track) =
|
||||
contentValuesOf(
|
||||
COL_ID to obj.id,
|
||||
COL_MANGA_ID to obj.manga_id,
|
||||
COL_SYNC_ID to obj.sync_id,
|
||||
COL_MEDIA_ID to obj.media_id,
|
||||
COL_LIBRARY_ID to obj.library_id,
|
||||
COL_TITLE to obj.title,
|
||||
COL_LAST_CHAPTER_READ to obj.last_chapter_read,
|
||||
COL_TOTAL_CHAPTERS to obj.total_chapters,
|
||||
COL_STATUS to obj.status,
|
||||
COL_TRACKING_URL to obj.tracking_url,
|
||||
COL_SCORE to obj.score,
|
||||
COL_START_DATE to obj.started_reading_date,
|
||||
COL_FINISH_DATE to obj.finished_reading_date
|
||||
)
|
||||
}
|
||||
|
||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||
|
||||
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterKnownBackupPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||
@@ -111,6 +112,11 @@ interface ChapterQueries : DbProvider {
|
||||
.withPutResolver(ChapterBackupPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateKnownChaptersBackup(chapters: List<Chapter>) = db.put()
|
||||
.objects(chapters)
|
||||
.withPutResolver(ChapterKnownBackupPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
||||
.`object`(chapter)
|
||||
.withPutResolver(ChapterProgressPutResolver())
|
||||
|
||||
@@ -79,6 +79,15 @@ interface MangaQueries : DbProvider {
|
||||
.prepare()
|
||||
|
||||
// SY -->
|
||||
fun getReadNotInLibraryMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getReadMangaNotInLibraryQuery())
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun updateMangaInfo(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaInfoPutResolver())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.queries
|
||||
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
||||
@@ -61,6 +61,18 @@ fun getMergedChaptersQuery() =
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID}
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get manga that are not in library, but have read chapters
|
||||
*/
|
||||
fun getReadMangaNotInLibraryQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*
|
||||
FROM ${Manga.TABLE}
|
||||
WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN(
|
||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1
|
||||
)
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the manga from the library, with their categories and unread count.
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,15 @@ import eu.kanade.tachiyomi.data.track.TrackService
|
||||
|
||||
interface TrackQueries : DbProvider {
|
||||
|
||||
fun getTracks() = db.get()
|
||||
.listOfObjects(Track::class.java)
|
||||
.withQuery(
|
||||
Query.builder()
|
||||
.table(TrackTable.TABLE)
|
||||
.build()
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getTracks(manga: Manga) = db.get()
|
||||
.listOfObjects(Track::class.java)
|
||||
.withQuery(
|
||||
|
||||
+7
-6
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,9 +25,10 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
||||
.whereArgs(chapter.url)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||
put(ChapterTable.COL_READ, chapter.read)
|
||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||
}
|
||||
fun mapToContentValues(chapter: Chapter) =
|
||||
contentValuesOf(
|
||||
ChapterTable.COL_READ to chapter.read,
|
||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
||||
)
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
|
||||
class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(chapter)
|
||||
val contentValues = mapToContentValues(chapter)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||
.table(ChapterTable.TABLE)
|
||||
.where("${ChapterTable.COL_ID} = ?")
|
||||
.whereArgs(chapter.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) =
|
||||
contentValuesOf(
|
||||
ChapterTable.COL_READ to chapter.read,
|
||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
||||
)
|
||||
}
|
||||
+7
-6
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,9 +25,10 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
||||
.whereArgs(chapter.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||
put(ChapterTable.COL_READ, chapter.read)
|
||||
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||
}
|
||||
fun mapToContentValues(chapter: Chapter) =
|
||||
contentValuesOf(
|
||||
ChapterTable.COL_READ to chapter.read,
|
||||
ChapterTable.COL_BOOKMARK to chapter.bookmark,
|
||||
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
|
||||
)
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,7 +25,8 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
||||
.whereArgs(chapter.url, chapter.manga_id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
|
||||
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
|
||||
}
|
||||
fun mapToContentValues(chapter: Chapter) =
|
||||
contentValuesOf(
|
||||
ChapterTable.COL_SOURCE_ORDER to chapter.source_order
|
||||
)
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||
@@ -57,7 +57,8 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
* Create content query
|
||||
* @param history object
|
||||
*/
|
||||
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
|
||||
put(HistoryTable.COL_LAST_READ, history.last_read)
|
||||
}
|
||||
fun mapToUpdateContentValues(history: History) =
|
||||
contentValuesOf(
|
||||
HistoryTable.COL_LAST_READ to history.last_read
|
||||
)
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,7 +25,8 @@ class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_COVER_LAST_MODIFIED, manga.cover_last_modified)
|
||||
}
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified
|
||||
)
|
||||
}
|
||||
|
||||
+6
-5
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,8 +25,9 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(2).apply {
|
||||
put(MangaTable.COL_FAVORITE, manga.favorite)
|
||||
put(MangaTable.COL_DATE_ADDED, manga.date_added)
|
||||
}
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_FAVORITE to manga.favorite,
|
||||
MangaTable.COL_DATE_ADDED to manga.date_added
|
||||
)
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -35,7 +35,8 @@ class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolve
|
||||
}
|
||||
}
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
|
||||
}
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags
|
||||
)
|
||||
}
|
||||
|
||||
+8
-7
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,13 +26,13 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_TITLE, manga.originalTitle)
|
||||
put(MangaTable.COL_GENRE, manga.originalGenre)
|
||||
put(MangaTable.COL_AUTHOR, manga.originalAuthor)
|
||||
put(MangaTable.COL_ARTIST, manga.originalArtist)
|
||||
put(MangaTable.COL_DESCRIPTION, manga.originalDescription)
|
||||
}
|
||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||
MangaTable.COL_TITLE to manga.originalTitle,
|
||||
MangaTable.COL_GENRE to manga.originalGenre,
|
||||
MangaTable.COL_AUTHOR to manga.originalAuthor,
|
||||
MangaTable.COL_ARTIST to manga.originalArtist,
|
||||
MangaTable.COL_DESCRIPTION to manga.originalDescription
|
||||
)
|
||||
|
||||
fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
val splitter = "▒ ▒∩▒"
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,7 +25,8 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
|
||||
}
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_LAST_UPDATE to manga.last_update
|
||||
)
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,11 +25,11 @@ class MangaMigrationPutResolver : PutResolver<Manga>() {
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(5).apply {
|
||||
put(MangaTable.COL_FAVORITE, manga.favorite)
|
||||
put(MangaTable.COL_DATE_ADDED, manga.date_added)
|
||||
put(MangaTable.COL_TITLE, manga.title)
|
||||
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
|
||||
put(MangaTable.COL_VIEWER, manga.viewer)
|
||||
}
|
||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||
MangaTable.COL_FAVORITE to manga.favorite,
|
||||
MangaTable.COL_DATE_ADDED to manga.date_added,
|
||||
MangaTable.COL_TITLE to manga.title,
|
||||
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags,
|
||||
MangaTable.COL_VIEWER to manga.viewer
|
||||
)
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,7 +25,8 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_TITLE, manga.title)
|
||||
}
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_TITLE to manga.title
|
||||
)
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -26,7 +26,7 @@ class MangaUrlPutResolver : PutResolver<Manga>() {
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_URL, manga.url)
|
||||
}
|
||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||
MangaTable.COL_URL to manga.url
|
||||
)
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
@@ -25,7 +25,8 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_VIEWER, manga.viewer)
|
||||
}
|
||||
fun mapToContentValues(manga: Manga) =
|
||||
contentValuesOf(
|
||||
MangaTable.COL_VIEWER to manga.viewer
|
||||
)
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
||||
val filteredChapters = getChaptersToDelete(chapters)
|
||||
|
||||
queue.remove(filteredChapters)
|
||||
removeFromDownloadQueue(filteredChapters)
|
||||
|
||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||
chapterDirs.forEach { it.delete() }
|
||||
@@ -224,6 +224,24 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
return filteredChapters
|
||||
}
|
||||
|
||||
private fun removeFromDownloadQueue(chapters: List<Chapter>) {
|
||||
val wasRunning = downloader.isRunning
|
||||
if (wasRunning) {
|
||||
downloader.pause()
|
||||
}
|
||||
|
||||
downloader.queue.remove(chapters)
|
||||
|
||||
if (wasRunning) {
|
||||
if (downloader.queue.isEmpty()) {
|
||||
DownloadService.stop(context)
|
||||
downloader.stop()
|
||||
} else if (downloader.queue.isNotEmpty()) {
|
||||
downloader.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* return the list of all manga folders
|
||||
@@ -284,7 +302,7 @@ class DownloadManager(/* SY private */ val context: Context) {
|
||||
* @param source the source of the manga.
|
||||
*/
|
||||
fun deleteManga(manga: Manga, source: Source) {
|
||||
queue.remove(manga)
|
||||
downloader.queue.remove(manga)
|
||||
provider.findMangaDir(manga, source)?.delete()
|
||||
cache.removeManga(manga)
|
||||
}
|
||||
|
||||
@@ -66,15 +66,6 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
context.notificationManager.notify(id, build())
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old actions if they exist.
|
||||
*/
|
||||
private fun NotificationCompat.Builder.clearActions() {
|
||||
if (mActions.isNotEmpty()) {
|
||||
mActions.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
||||
* those can only be dismissed by the user.
|
||||
@@ -165,6 +156,8 @@ internal class DownloadNotifier(private val context: Context) {
|
||||
* This function shows a notification to inform download tasks are done.
|
||||
*/
|
||||
fun onComplete() {
|
||||
dismissProgress()
|
||||
|
||||
if (!errorThrown) {
|
||||
// Create notification
|
||||
with(completeNotificationBuilder) {
|
||||
|
||||
@@ -9,11 +9,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
@@ -26,7 +25,7 @@ class DownloadProvider(private val context: Context) {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
private val scope = MainScope()
|
||||
|
||||
/**
|
||||
* The root directory for downloads.
|
||||
@@ -55,6 +54,7 @@ class DownloadProvider(private val context: Context) {
|
||||
.createDirectory(getSourceDirName(source))
|
||||
.createDirectory(getMangaDirName(manga))
|
||||
} catch (e: NullPointerException) {
|
||||
Timber.w(e)
|
||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,12 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||
import eu.kanade.tachiyomi.util.lang.RetryWithDelay
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import exh.isEhBasedSource
|
||||
import kotlinx.coroutines.async
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
@@ -31,7 +30,6 @@ import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
@@ -123,8 +121,8 @@ class Downloader(
|
||||
initializeSubscriptions()
|
||||
}
|
||||
|
||||
val pending = queue.filter { it.status != Download.DOWNLOADED }
|
||||
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
|
||||
val pending = queue.filter { it.status != Download.State.DOWNLOADED }
|
||||
pending.forEach { if (it.status != Download.State.QUEUE) it.status = Download.State.QUEUE }
|
||||
|
||||
notifier.paused = false
|
||||
|
||||
@@ -138,20 +136,21 @@ class Downloader(
|
||||
fun stop(reason: String? = null) {
|
||||
destroySubscriptions()
|
||||
queue
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
.forEach { it.status = Download.ERROR }
|
||||
.filter { it.status == Download.State.DOWNLOADING }
|
||||
.forEach { it.status = Download.State.ERROR }
|
||||
|
||||
if (reason != null) {
|
||||
notifier.onWarning(reason)
|
||||
} else {
|
||||
if (notifier.paused) {
|
||||
notifier.paused = false
|
||||
notifier.onPaused()
|
||||
} else {
|
||||
notifier.dismissProgress()
|
||||
notifier.onComplete()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (notifier.paused && !queue.isEmpty()) {
|
||||
notifier.onPaused()
|
||||
} else {
|
||||
notifier.onComplete()
|
||||
}
|
||||
|
||||
notifier.paused = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,8 +159,8 @@ class Downloader(
|
||||
fun pause() {
|
||||
destroySubscriptions()
|
||||
queue
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
.forEach { it.status = Download.QUEUE }
|
||||
.filter { it.status == Download.State.DOWNLOADING }
|
||||
.forEach { it.status = Download.State.QUEUE }
|
||||
notifier.paused = true
|
||||
}
|
||||
|
||||
@@ -176,8 +175,8 @@ class Downloader(
|
||||
// Needed to update the chapter view
|
||||
if (isNotification) {
|
||||
queue
|
||||
.filter { it.status == Download.QUEUE }
|
||||
.forEach { it.status = Download.NOT_DOWNLOADED }
|
||||
.filter { it.status == Download.State.QUEUE }
|
||||
.forEach { it.status = Download.State.NOT_DOWNLOADED }
|
||||
}
|
||||
queue.clear()
|
||||
notifier.dismissProgress()
|
||||
@@ -236,8 +235,8 @@ class Downloader(
|
||||
* @param chapters the list of chapters to download.
|
||||
* @param autoStart whether to start the downloader after enqueing the chapters.
|
||||
*/
|
||||
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
|
||||
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchIO {
|
||||
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
|
||||
val wasEmpty = queue.isEmpty()
|
||||
// Called in background thread, the operation can be slow with SAF.
|
||||
val chaptersWithoutDir = async {
|
||||
@@ -280,7 +279,7 @@ class Downloader(
|
||||
|
||||
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
||||
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
||||
download.status = Download.ERROR
|
||||
download.status = Download.State.ERROR
|
||||
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
||||
return@defer Observable.just(download)
|
||||
}
|
||||
@@ -310,7 +309,7 @@ class Downloader(
|
||||
?.forEach { it.delete() }
|
||||
|
||||
download.downloadedImages = 0
|
||||
download.status = Download.DOWNLOADING
|
||||
download.status = Download.State.DOWNLOADING
|
||||
}
|
||||
// Get all the URLs to the source images, fetch pages if necessary
|
||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||
@@ -326,7 +325,7 @@ class Downloader(
|
||||
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
|
||||
// If the page list threw, it will resume here
|
||||
.onErrorReturn { error ->
|
||||
download.status = Download.ERROR
|
||||
download.status = Download.State.ERROR
|
||||
notifier.onError(error.message, download.chapter.name)
|
||||
download
|
||||
}
|
||||
@@ -390,19 +389,7 @@ class Downloader(
|
||||
private fun downloadImage(page: Page, source: HttpSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
|
||||
page.status = Page.DOWNLOAD_IMAGE
|
||||
page.progress = 0
|
||||
return /* SY --> If the source is E-Hentai request a new page if null */ Observable.just(Unit)
|
||||
.flatMap {
|
||||
if (page.imageUrl == null && source.isEhBasedSource()) {
|
||||
source.fetchImageUrl(page)
|
||||
} else Observable.just(null)
|
||||
}
|
||||
.doOnNext { imageUrl ->
|
||||
if (imageUrl != null) page.imageUrl = imageUrl
|
||||
}
|
||||
.flatMap {
|
||||
source.fetchImage(page)
|
||||
}
|
||||
// SY <--
|
||||
return source.fetchImage(page)
|
||||
.map { response ->
|
||||
val file = tmpDir.createFile("$filename.tmp")
|
||||
try {
|
||||
@@ -412,9 +399,6 @@ class Downloader(
|
||||
} catch (e: Exception) {
|
||||
response.close()
|
||||
file.delete()
|
||||
// SY --> E-Hentai sometimes has dead pages, so we request a new one if it fails
|
||||
if (source.isEhBasedSource()) page.imageUrl = null
|
||||
// SY <--
|
||||
throw e
|
||||
}
|
||||
file
|
||||
@@ -481,13 +465,13 @@ class Downloader(
|
||||
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
|
||||
|
||||
download.status = if (downloadedImages.size == download.pages!!.size) {
|
||||
Download.DOWNLOADED
|
||||
Download.State.DOWNLOADED
|
||||
} else {
|
||||
Download.ERROR
|
||||
Download.State.ERROR
|
||||
}
|
||||
|
||||
// Only rename the directory if it's downloaded.
|
||||
if (download.status == Download.DOWNLOADED) {
|
||||
if (download.status == Download.State.DOWNLOADED) {
|
||||
if (preferences.saveChaptersAsCBZ().get()) {
|
||||
val zip = mangaDir.createFile("$dirname.cbz.tmp")
|
||||
val zipOut = ZipOutputStream(BufferedOutputStream(zip.openOutputStream()))
|
||||
@@ -532,7 +516,7 @@ class Downloader(
|
||||
*/
|
||||
private fun completeDownload(download: Download) {
|
||||
// Delete successful downloads from queue
|
||||
if (download.status == Download.DOWNLOADED) {
|
||||
if (download.status == Download.State.DOWNLOADED) {
|
||||
// remove downloaded chapter from queue
|
||||
queue.remove(download)
|
||||
}
|
||||
@@ -545,7 +529,7 @@ class Downloader(
|
||||
* Returns true if all the queued downloads are in DOWNLOADED or ERROR state.
|
||||
*/
|
||||
private fun areAllDownloadsFinished(): Boolean {
|
||||
return queue.none { it.status <= Download.DOWNLOADING }
|
||||
return queue.none { it.status.value <= Download.State.DOWNLOADING.value }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -20,7 +20,7 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
||||
|
||||
@Volatile
|
||||
@Transient
|
||||
var status: Int = 0
|
||||
var status: State = State.NOT_DOWNLOADED
|
||||
set(status) {
|
||||
field = status
|
||||
statusSubject?.onNext(this)
|
||||
@@ -47,11 +47,11 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NOT_DOWNLOADED = 0
|
||||
const val QUEUE = 1
|
||||
const val DOWNLOADING = 2
|
||||
const val DOWNLOADED = 3
|
||||
const val ERROR = 4
|
||||
enum class State(val value: Int) {
|
||||
NOT_DOWNLOADED(0),
|
||||
QUEUE(1),
|
||||
DOWNLOADING(2),
|
||||
DOWNLOADED(3),
|
||||
ERROR(4),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class DownloadQueue(
|
||||
downloads.forEach { download ->
|
||||
download.setStatusSubject(statusSubject)
|
||||
download.setStatusCallback(::setPagesFor)
|
||||
download.status = Download.QUEUE
|
||||
download.status = Download.State.QUEUE
|
||||
}
|
||||
queue.addAll(downloads)
|
||||
store.addAll(downloads)
|
||||
@@ -34,8 +34,8 @@ class DownloadQueue(
|
||||
store.remove(download)
|
||||
download.setStatusSubject(null)
|
||||
download.setStatusCallback(null)
|
||||
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
|
||||
download.status = Download.NOT_DOWNLOADED
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
if (removed) {
|
||||
updatedRelay.call(Unit)
|
||||
@@ -60,8 +60,8 @@ class DownloadQueue(
|
||||
queue.forEach { download ->
|
||||
download.setStatusSubject(null)
|
||||
download.setStatusCallback(null)
|
||||
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
|
||||
download.status = Download.NOT_DOWNLOADED
|
||||
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
|
||||
download.status = Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
}
|
||||
queue.clear()
|
||||
@@ -70,7 +70,7 @@ class DownloadQueue(
|
||||
}
|
||||
|
||||
fun getActiveDownloads(): Observable<Download> =
|
||||
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
|
||||
Observable.from(this).filter { download -> download.status == Download.State.DOWNLOADING }
|
||||
|
||||
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
|
||||
|
||||
@@ -79,7 +79,7 @@ class DownloadQueue(
|
||||
.map { this }
|
||||
|
||||
private fun setPagesFor(download: Download) {
|
||||
if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
||||
if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
||||
setPagesSubject(download.pages, null)
|
||||
}
|
||||
}
|
||||
@@ -88,19 +88,19 @@ class DownloadQueue(
|
||||
return statusSubject.onBackpressureBuffer()
|
||||
.startWith(getActiveDownloads())
|
||||
.flatMap { download ->
|
||||
if (download.status == Download.DOWNLOADING) {
|
||||
if (download.status == Download.State.DOWNLOADING) {
|
||||
val pageStatusSubject = PublishSubject.create<Int>()
|
||||
setPagesSubject(download.pages, pageStatusSubject)
|
||||
return@flatMap pageStatusSubject
|
||||
.onBackpressureBuffer()
|
||||
.filter { it == Page.READY }
|
||||
.map { download }
|
||||
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
|
||||
} else if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
|
||||
setPagesSubject(download.pages, null)
|
||||
}
|
||||
Observable.just(download)
|
||||
}
|
||||
.filter { it.status == Download.DOWNLOADING }
|
||||
.filter { it.status == Download.State.DOWNLOADING }
|
||||
}
|
||||
|
||||
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
|
||||
|
||||
@@ -14,16 +14,12 @@ class CustomMangaManager(val context: Context) {
|
||||
|
||||
private val editJson = File(context.getExternalFilesDir(null), "edits.json")
|
||||
|
||||
private var customMangaMap = mutableMapOf<Long, Manga>()
|
||||
|
||||
init {
|
||||
fetchCustomData()
|
||||
}
|
||||
private val customMangaMap = fetchCustomData()
|
||||
|
||||
fun getManga(manga: Manga): Manga? = customMangaMap[manga.id]
|
||||
|
||||
private fun fetchCustomData() {
|
||||
if (!editJson.exists() || !editJson.isFile) return
|
||||
private fun fetchCustomData(): MutableMap<Long, Manga> {
|
||||
if (!editJson.exists() || !editJson.isFile) return mutableMapOf()
|
||||
|
||||
val json = try {
|
||||
Json.decodeFromString<MangaList>(
|
||||
@@ -31,10 +27,10 @@ class CustomMangaManager(val context: Context) {
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: return
|
||||
} ?: return mutableMapOf()
|
||||
|
||||
val mangasJson = json.mangas ?: return
|
||||
customMangaMap = mangasJson.mapNotNull { mangaJson ->
|
||||
val mangasJson = json.mangas ?: return mutableMapOf()
|
||||
return mangasJson.mapNotNull { mangaJson ->
|
||||
val id = mangaJson.id ?: return@mapNotNull null
|
||||
val manga = MangaImpl().apply {
|
||||
this.id = id
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.elvishew.xlog.XLog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
@@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
||||
@@ -24,36 +24,47 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryGroup
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||
import eu.kanade.tachiyomi.util.lang.asObservable
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.mangaDexSourceIds
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.source.getMainSource
|
||||
import exh.source.mangaDexSourceIds
|
||||
import exh.util.executeOnIO
|
||||
import exh.util.nullIfBlank
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.schedulers.Schedulers
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
@@ -73,17 +84,11 @@ class LibraryUpdateService(
|
||||
val coverCache: CoverCache = Injekt.get()
|
||||
) : Service() {
|
||||
|
||||
/**
|
||||
* Wake lock that will be held until the service is destroyed.
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
private lateinit var notifier: LibraryUpdateNotifier
|
||||
private lateinit var ioScope: CoroutineScope
|
||||
|
||||
/**
|
||||
* Subscription where the update is done.
|
||||
*/
|
||||
private var subscription: Subscription? = null
|
||||
private var updateJob: Job? = null
|
||||
|
||||
/**
|
||||
* Defines what should be updated within a service execution.
|
||||
@@ -176,6 +181,7 @@ class LibraryUpdateService(
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
notifier = LibraryUpdateNotifier(this)
|
||||
wakeLock = acquireWakeLock(javaClass.name)
|
||||
|
||||
@@ -187,7 +193,8 @@ class LibraryUpdateService(
|
||||
* lock.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
subscription?.unsubscribe()
|
||||
updateJob?.cancel()
|
||||
ioScope?.cancel()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
@@ -215,38 +222,29 @@ class LibraryUpdateService(
|
||||
?: return START_NOT_STICKY
|
||||
|
||||
// Unsubscribe from any previous subscription if needed.
|
||||
subscription?.unsubscribe()
|
||||
updateJob?.cancel()
|
||||
|
||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||
subscription = Observable
|
||||
.defer {
|
||||
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
||||
val mangaList = getMangaToUpdate(intent, target)
|
||||
.sortedWith(rankingScheme[selectedScheme])
|
||||
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
||||
val mangaList = getMangaToUpdate(intent, target)
|
||||
.sortedWith(rankingScheme[selectedScheme])
|
||||
|
||||
// Update either chapter list or manga details.
|
||||
when (target) {
|
||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||
Target.COVERS -> updateCovers(mangaList)
|
||||
Target.TRACKING -> updateTrackings(mangaList)
|
||||
// SY -->
|
||||
Target.SYNC_FOLLOWS -> syncFollows()
|
||||
Target.PUSH_FAVORITES -> pushFavorites()
|
||||
// SY <--
|
||||
}
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Timber.e(exception)
|
||||
stopSelf(startId)
|
||||
}
|
||||
updateJob = ioScope.launch(handler) {
|
||||
when (target) {
|
||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||
Target.COVERS -> updateCovers(mangaList)
|
||||
Target.TRACKING -> updateTrackings(mangaList)
|
||||
// SY -->
|
||||
Target.SYNC_FOLLOWS -> syncFollows()
|
||||
Target.PUSH_FAVORITES -> pushFavorites()
|
||||
// SY <--
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{
|
||||
},
|
||||
{
|
||||
Timber.e(it)
|
||||
stopSelf(startId)
|
||||
},
|
||||
{
|
||||
stopSelf(startId)
|
||||
}
|
||||
)
|
||||
}
|
||||
updateJob?.invokeOnCompletion { stopSelf(startId) }
|
||||
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
@@ -331,84 +329,72 @@ class LibraryUpdateService(
|
||||
* @param mangaToUpdate the list to update
|
||||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
fun updateChapterList(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||
// Initialize the variables holding the progress of the updates.
|
||||
val count = AtomicInteger(0)
|
||||
// List containing new updates
|
||||
suspend fun updateChapterList(mangaToUpdate: List<LibraryManga>) {
|
||||
val semaphore = Semaphore(5)
|
||||
val progressCount = AtomicInteger(0)
|
||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
||||
// List containing failed updates
|
||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
||||
// Boolean to determine if DownloadManager has downloads
|
||||
var hasDownloads = false
|
||||
|
||||
// Emit each manga and update it sequentially.
|
||||
return Observable.from(mangaToUpdate)
|
||||
// Notify manga that will update.
|
||||
.doOnNext { notifier.showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
|
||||
// Update the chapters of the manga
|
||||
.concatMap { manga ->
|
||||
// SY -->
|
||||
if (manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) {
|
||||
// Ignore EXH manga, updating chapters for every manga will get you banned
|
||||
Observable.empty()
|
||||
} else {
|
||||
// SY <--
|
||||
updateManga(manga)
|
||||
// If there's any error, return empty update and continue.
|
||||
.onErrorReturn {
|
||||
val errorMessage = if (it is NoChaptersException) {
|
||||
getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
it.message
|
||||
}
|
||||
failedUpdates.add(Pair(manga, errorMessage))
|
||||
Pair(emptyList(), emptyList())
|
||||
}
|
||||
// Filter out mangas without new chapters (or failed).
|
||||
.filter { (first) -> first.isNotEmpty() }
|
||||
.doOnNext {
|
||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(manga, it.first)
|
||||
hasDownloads = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map {
|
||||
Pair(
|
||||
manga,
|
||||
(
|
||||
it.first.sortedByDescending { ch -> ch.source_order }
|
||||
.toTypedArray()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Add manga with new chapters to the list.
|
||||
.doOnNext { manga ->
|
||||
// Add to the list
|
||||
newUpdates.add(manga)
|
||||
}
|
||||
// Notify result of the overall update.
|
||||
.doOnCompleted {
|
||||
notifier.cancelProgressNotification()
|
||||
withIOContext {
|
||||
mangaToUpdate.groupBy { it.source }
|
||||
.filterNot { it.key in LIBRARY_UPDATE_EXCLUDED_SOURCES }
|
||||
.values.map { mangaInSource ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
mangaInSource
|
||||
.onEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return@async
|
||||
}
|
||||
|
||||
if (newUpdates.isNotEmpty()) {
|
||||
notifier.showUpdateNotifications(newUpdates)
|
||||
if (hasDownloads) {
|
||||
DownloadService.start(this)
|
||||
}
|
||||
}
|
||||
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
|
||||
|
||||
if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) {
|
||||
val errorFile = writeErrorFile(failedUpdates)
|
||||
notifier.showUpdateErrorNotification(
|
||||
failedUpdates.map { it.first.title },
|
||||
errorFile.getUriCompat(this)
|
||||
)
|
||||
}
|
||||
try {
|
||||
val (newChapters, _) = updateManga(manga)
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||
downloadChapters(manga, newChapters)
|
||||
hasDownloads = true
|
||||
}
|
||||
|
||||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(
|
||||
manga to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||
.toTypedArray()
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = if (e is NoChaptersException) {
|
||||
getString(R.string.no_chapters_error)
|
||||
} else {
|
||||
e.message
|
||||
}
|
||||
failedUpdates.add(manga to errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
|
||||
if (newUpdates.isNotEmpty()) {
|
||||
notifier.showUpdateNotifications(newUpdates)
|
||||
if (hasDownloads) {
|
||||
DownloadService.start(this)
|
||||
}
|
||||
.map { (first) -> first }
|
||||
}
|
||||
|
||||
if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) {
|
||||
val errorFile = writeErrorFile(failedUpdates)
|
||||
notifier.showUpdateErrorNotification(
|
||||
failedUpdates.map { it.first.title },
|
||||
errorFile.getUriCompat(this)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
@@ -428,168 +414,182 @@ class LibraryUpdateService(
|
||||
* @param manga the manga to update.
|
||||
* @return a pair of the inserted and removed chapters.
|
||||
*/
|
||||
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
||||
val source = sourceManager.getOrStub(manga.source).getMainSource()
|
||||
|
||||
// Update manga details metadata in the background
|
||||
if (preferences.autoUpdateMetadata()) {
|
||||
source.fetchMangaDetails(manga)
|
||||
.map { updatedManga ->
|
||||
// Avoid "losing" existing cover
|
||||
if (!updatedManga.thumbnail_url.isNullOrEmpty()) {
|
||||
manga.prepUpdateCover(coverCache, updatedManga, false)
|
||||
} else {
|
||||
updatedManga.thumbnail_url = manga.thumbnail_url
|
||||
}
|
||||
|
||||
manga.copyFrom(updatedManga)
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
manga
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Timber.e(exception)
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.IO + handler) {
|
||||
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
val sManga = updatedManga.toSManga()
|
||||
// Avoid "losing" existing cover
|
||||
if (!sManga.thumbnail_url.isNullOrEmpty()) {
|
||||
manga.prepUpdateCover(coverCache, sManga, false)
|
||||
} else {
|
||||
sManga.thumbnail_url = manga.thumbnail_url
|
||||
}
|
||||
.onErrorResumeNext { Observable.just(manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
|
||||
manga.copyFrom(sManga)
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
/* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() }
|
||||
else /* SY <-- */ source.fetchChapterList(manga)
|
||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||
// SY -->
|
||||
)
|
||||
.doOnNext {
|
||||
if (source is MangaDex && trackManager.mdList.isLogged) {
|
||||
try {
|
||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||
if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) {
|
||||
var track = trackManager.mdList.createInitialTracker(manga)
|
||||
track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() }
|
||||
db.insertTrack(track).executeAsBlocking()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
XLog.tag("LibraryUpdateService").e(e)
|
||||
}
|
||||
// SY -->
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Timber.e(exception)
|
||||
}
|
||||
ioScope.launch(handler) {
|
||||
if (source is MangaDex && trackManager.mdList.isLogged) {
|
||||
val tracks = db.getTracks(manga).executeOnIO()
|
||||
if (tracks.isEmpty() || tracks.none { it.sync_id == TrackManager.MDLIST }) {
|
||||
var track = trackManager.mdList.createInitialTracker(manga)
|
||||
track = trackManager.mdList.refresh(track)
|
||||
db.insertTrack(track).executeOnIO()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source is MergedSource) {
|
||||
return source.fetchChaptersAndSync(manga, false)
|
||||
}
|
||||
// SY <--
|
||||
|
||||
val chapters = source.getChapterList(manga.toMangaInfo())
|
||||
.map { it.toSChapter() }
|
||||
|
||||
return syncChaptersWithSource(db, chapters, manga, source)
|
||||
}
|
||||
|
||||
private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||
var count = 0
|
||||
private suspend fun updateCovers(mangaToUpdate: List<LibraryManga>) {
|
||||
var progressCount = 0
|
||||
|
||||
return Observable.from(mangaToUpdate)
|
||||
.doOnNext {
|
||||
notifier.showProgressNotification(it, count++, mangaToUpdate.size)
|
||||
mangaToUpdate.forEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
}
|
||||
.flatMap { manga ->
|
||||
val source = sourceManager.get(manga.source)
|
||||
?: return@flatMap Observable.empty<LibraryManga>()
|
||||
|
||||
source.fetchMangaDetails(manga)
|
||||
.map { networkManga ->
|
||||
manga.prepUpdateCover(coverCache, networkManga, true)
|
||||
networkManga.thumbnail_url?.let {
|
||||
manga.thumbnail_url = it
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
manga
|
||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
||||
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
try {
|
||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
||||
val sManga = networkManga.toSManga()
|
||||
manga.prepUpdateCover(coverCache, sManga, true)
|
||||
sManga.thumbnail_url?.let {
|
||||
manga.thumbnail_url = it
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
.onErrorReturn { manga }
|
||||
}
|
||||
.doOnCompleted {
|
||||
notifier.cancelProgressNotification()
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||
* background thread, so it's safe to do heavy operations or network calls here.
|
||||
*/
|
||||
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
|
||||
// Initialize the variables holding the progress of the updates.
|
||||
var count = 0
|
||||
|
||||
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
|
||||
var progressCount = 0
|
||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||
|
||||
// Emit each manga and update it sequentially.
|
||||
return Observable.from(mangaToUpdate)
|
||||
// Notify manga that will update.
|
||||
.doOnNext { notifier.showProgressNotification(it, count++, mangaToUpdate.size) }
|
||||
// Update the tracking details.
|
||||
.concatMap { manga ->
|
||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||
mangaToUpdate.forEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
}
|
||||
|
||||
Observable.from(tracks)
|
||||
.concatMap { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service in loggedServices) {
|
||||
service.refresh(track)
|
||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||
.onErrorReturn { track }
|
||||
} else {
|
||||
Observable.empty()
|
||||
// Notify manga that will update.
|
||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
||||
|
||||
// Update the tracking details.
|
||||
db.getTracks(manga).executeAsBlocking()
|
||||
.map { track ->
|
||||
supervisorScope {
|
||||
async {
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service in loggedServices) {
|
||||
try {
|
||||
val updatedTrack = service.refresh(track)
|
||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||
} catch (e: Throwable) {
|
||||
// Ignore errors and continue
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.map { manga }
|
||||
}
|
||||
.doOnCompleted {
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* filter all follows from Mangadex and only add reading or rereading manga to library
|
||||
*/
|
||||
private fun syncFollows(): Observable<LibraryManga> {
|
||||
private suspend fun syncFollows() {
|
||||
val count = AtomicInteger(0)
|
||||
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return Observable.empty()
|
||||
return mangaDex.fetchAllFollows(true)
|
||||
.asObservable()
|
||||
.map { listManga ->
|
||||
listManga.filter { (_, metadata) ->
|
||||
metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int
|
||||
}
|
||||
}
|
||||
.doOnNext { listManga ->
|
||||
listManga.forEach { (networkManga, metadata) ->
|
||||
notifier.showProgressNotification(networkManga, count.andIncrement, listManga.size)
|
||||
var dbManga = db.getManga(networkManga.url, mangaDex.id)
|
||||
.executeAsBlocking()
|
||||
if (dbManga == null) {
|
||||
dbManga = Manga.create(
|
||||
networkManga.url,
|
||||
networkManga.title,
|
||||
mangaDex.id
|
||||
)
|
||||
dbManga.date_added = Date().time
|
||||
}
|
||||
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return
|
||||
|
||||
dbManga.copyFrom(networkManga)
|
||||
dbManga.favorite = true
|
||||
val id = db.insertManga(dbManga).executeAsBlocking().insertedId()
|
||||
if (id != null) {
|
||||
metadata.mangaId = id
|
||||
db.insertFlatMetadata(metadata.flatten()).await()
|
||||
}
|
||||
val size: Int
|
||||
mangaDex.fetchAllFollows(true)
|
||||
.filter { (_, metadata) ->
|
||||
metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int
|
||||
}
|
||||
.also { size = it.size }
|
||||
.forEach { (networkManga, metadata) ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
}
|
||||
|
||||
notifier.showProgressNotification(networkManga, count.andIncrement, size)
|
||||
|
||||
var dbManga = db.getManga(networkManga.url, mangaDex.id)
|
||||
.executeOnIO()
|
||||
if (dbManga == null) {
|
||||
dbManga = Manga.create(
|
||||
networkManga.url,
|
||||
networkManga.title,
|
||||
mangaDex.id
|
||||
)
|
||||
dbManga.date_added = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
dbManga.copyFrom(networkManga)
|
||||
dbManga.favorite = true
|
||||
val id = db.insertManga(dbManga).executeOnIO().insertedId()
|
||||
if (id != null) {
|
||||
metadata.mangaId = id
|
||||
db.insertFlatMetadata(metadata.flatten()).await()
|
||||
}
|
||||
}
|
||||
.doOnCompleted {
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
.flatMap { Observable.empty() }
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that updates the all mangas which are not tracked as "reading" on mangadex
|
||||
*/
|
||||
private fun pushFavorites(): Observable<LibraryManga> {
|
||||
private suspend fun pushFavorites() {
|
||||
val count = AtomicInteger(0)
|
||||
val listManga = db.getFavoriteMangas().executeAsBlocking().filter { it.source in mangaDexSourceIds }
|
||||
|
||||
// filter all follows from Mangadex and only add reading or rereading manga to library
|
||||
return Observable.from(if (trackManager.mdList.isLogged) listManga else emptyList())
|
||||
.flatMap { manga ->
|
||||
if (trackManager.mdList.isLogged) {
|
||||
listManga.forEach { manga ->
|
||||
if (updateJob?.isActive != true) {
|
||||
return
|
||||
}
|
||||
|
||||
notifier.showProgressNotification(manga, count.andIncrement, listManga.size)
|
||||
|
||||
// Get this manga's trackers from the database
|
||||
@@ -600,18 +600,13 @@ class LibraryUpdateService(
|
||||
|
||||
if (tracker.track?.status == FollowStatus.UNFOLLOWED.int) {
|
||||
tracker.track.status = FollowStatus.READING.int
|
||||
tracker.service.update(tracker.track)
|
||||
} else Observable.just(null)
|
||||
}
|
||||
.doOnNext { returnedTracker ->
|
||||
returnedTracker?.let {
|
||||
db.insertTrack(returnedTracker)
|
||||
val updatedTrack = tracker.service.update(tracker.track)
|
||||
db.insertTrack(updatedTrack).executeOnIO()
|
||||
}
|
||||
}
|
||||
.doOnCompleted {
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
.flatMap { Observable.empty() }
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
}
|
||||
// SY <--
|
||||
|
||||
@@ -621,15 +616,14 @@ class LibraryUpdateService(
|
||||
private fun writeErrorFile(errors: List<Pair<Manga, String?>>): File {
|
||||
try {
|
||||
if (errors.isNotEmpty()) {
|
||||
val destFile = File(externalCacheDir, "tachiyomi_update_errors.txt")
|
||||
|
||||
destFile.bufferedWriter().use { out ->
|
||||
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
||||
file.bufferedWriter().use { out ->
|
||||
errors.forEach { (manga, error) ->
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
out.write("${manga.title} ($source): $error\n")
|
||||
}
|
||||
}
|
||||
return destFile
|
||||
return file
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Empty
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -70,9 +71,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
)
|
||||
// Share backup file
|
||||
ACTION_SHARE_BACKUP ->
|
||||
shareBackup(
|
||||
shareFile(
|
||||
context,
|
||||
intent.getParcelableExtra(EXTRA_URI),
|
||||
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/octet-stream+gzip",
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||
@@ -101,6 +103,14 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
markAsRead(urls, mangaId)
|
||||
}
|
||||
}
|
||||
// Share crash dump file
|
||||
ACTION_SHARE_CRASH_LOG ->
|
||||
shareFile(
|
||||
context,
|
||||
intent.getParcelableExtra(EXTRA_URI),
|
||||
"text/plain",
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||
)
|
||||
// SY -->
|
||||
ACTION_CANCEL_SIMILAR_UPDATE -> cancelSimilarUpdate(context)
|
||||
// SY <--
|
||||
@@ -124,14 +134,13 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param notificationId id of notification
|
||||
*/
|
||||
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
||||
// Create intent
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
val uri = File(path).getUriCompat(context)
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
clipData = ClipData.newRawUri(null, uri)
|
||||
type = "image/*"
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
// Dismiss notification
|
||||
dismissNotification(context, notificationId)
|
||||
// Launch share activity
|
||||
context.startActivity(intent)
|
||||
@@ -144,10 +153,11 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param path path of file
|
||||
* @param notificationId id of notification
|
||||
*/
|
||||
private fun shareBackup(context: Context, uri: Uri, notificationId: Int) {
|
||||
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
type = "application/json"
|
||||
clipData = ClipData.newRawUri(null, uri)
|
||||
type = fileMimeType
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
// Dismiss notification
|
||||
@@ -260,59 +270,34 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
companion object {
|
||||
private const val NAME = "NotificationReceiver"
|
||||
|
||||
// Called to launch share intent.
|
||||
private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
|
||||
|
||||
// Called to delete image.
|
||||
private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
|
||||
|
||||
// Called to launch send intent.
|
||||
private const val ACTION_SHARE_BACKUP = "$ID.$NAME.SEND_BACKUP"
|
||||
|
||||
// Called to cancel backup restore job.
|
||||
private const val ACTION_SHARE_CRASH_LOG = "$ID.$NAME.SEND_CRASH_LOG"
|
||||
|
||||
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
|
||||
|
||||
// Called to cancel library update.
|
||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||
|
||||
// Called to mark manga chapters as read.
|
||||
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
||||
|
||||
// Called to open chapter.
|
||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||
|
||||
// Value containing file location.
|
||||
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
|
||||
|
||||
// Called to resume downloads.
|
||||
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
||||
|
||||
// Called to pause downloads.
|
||||
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
|
||||
|
||||
// Called to clear downloads.
|
||||
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
||||
|
||||
// Called to dismiss notification.
|
||||
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
||||
|
||||
// Value containing uri.
|
||||
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
|
||||
private const val EXTRA_URI = "$ID.$NAME.URI"
|
||||
|
||||
// Value containing notification id.
|
||||
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
|
||||
|
||||
// Value containing group id.
|
||||
private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
|
||||
|
||||
// Value containing manga id.
|
||||
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
||||
|
||||
// Value containing chapter id.
|
||||
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
||||
|
||||
// Value containing chapter url.
|
||||
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
||||
private const val EXTRA_IS_LEGACY_BACKUP = "$ID.$NAME.EXTRA_IS_LEGACY_BACKUP"
|
||||
|
||||
// Sy -->
|
||||
// Called to cancel similar manga update.
|
||||
@@ -530,10 +515,11 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param notificationId id of notification
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
|
||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, isLegacyFormat: Boolean, notificationId: Int): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_SHARE_BACKUP
|
||||
putExtra(EXTRA_URI, uri)
|
||||
putExtra(EXTRA_IS_LEGACY_BACKUP, isLegacyFormat)
|
||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
@@ -555,6 +541,23 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a share activity for a crash log dump file.
|
||||
*
|
||||
* @param context context of application
|
||||
* @param uri uri of file
|
||||
* @param notificationId id of notification
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun shareCrashLogPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_SHARE_CRASH_LOG
|
||||
putExtra(EXTRA_URI, uri)
|
||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that cancels a backup restore job.
|
||||
*
|
||||
|
||||
@@ -62,13 +62,19 @@ object Notifications {
|
||||
const val ID_BACKUP_COMPLETE = -502
|
||||
const val ID_RESTORE_COMPLETE = -504
|
||||
|
||||
/**
|
||||
* Notification channel used for crash log file sharing.
|
||||
*/
|
||||
const val CHANNEL_CRASH_LOGS = "crash_logs_channel"
|
||||
const val ID_CRASH_LOGS = -601
|
||||
|
||||
// SY -->
|
||||
/**
|
||||
* Notification channel and ids used for backup and restore.
|
||||
*/
|
||||
const val CHANNEL_SIMILAR = "similar_channel"
|
||||
const val ID_SIMILAR_PROGRESS = -601
|
||||
const val ID_SIMILAR_COMPLETE = -602
|
||||
const val ID_SIMILAR_PROGRESS = -901
|
||||
const val ID_SIMILAR_COMPLETE = -902
|
||||
// SY <--
|
||||
|
||||
private val deprecatedChannels = listOf(
|
||||
@@ -153,9 +159,14 @@ object Notifications {
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
},
|
||||
NotificationChannel(
|
||||
CHANNEL_CRASH_LOGS,
|
||||
context.getString(R.string.channel_crash_logs),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
),
|
||||
NotificationChannel(
|
||||
CHANNEL_SIMILAR,
|
||||
context.getString(R.string.similar),
|
||||
context.getString(R.string.similar_manga),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
setShowBadge(false)
|
||||
|
||||
@@ -23,6 +23,8 @@ object PreferenceKeys {
|
||||
|
||||
const val showPageNumber = "pref_show_page_number_key"
|
||||
|
||||
const val dualPageSplit = "pref_dual_page_split"
|
||||
|
||||
const val showReadingMode = "pref_show_reading_mode"
|
||||
|
||||
const val trueColor = "pref_true_color_key"
|
||||
@@ -57,7 +59,9 @@ object PreferenceKeys {
|
||||
|
||||
const val readWithTapping = "reader_tap"
|
||||
|
||||
const val readWithTappingInverted = "reader_tapping_inverted"
|
||||
const val pagerNavInverted = "reader_tapping_inverted"
|
||||
|
||||
const val webtoonNavInverted = "reader_tapping_inverted_webtoon"
|
||||
|
||||
const val readWithLongTap = "reader_long_tap"
|
||||
|
||||
@@ -65,6 +69,10 @@ object PreferenceKeys {
|
||||
|
||||
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
|
||||
|
||||
const val navigationModePager = "reader_navigation_mode_pager"
|
||||
|
||||
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
|
||||
|
||||
const val webtoonSidePadding = "webtoon_side_padding"
|
||||
|
||||
const val portraitColumns = "pref_library_columns_portrait_key"
|
||||
@@ -117,10 +125,10 @@ object PreferenceKeys {
|
||||
|
||||
const val filterCompleted = "pref_filter_library_completed"
|
||||
|
||||
const val filterStarted = "pref_filter_library_started"
|
||||
|
||||
const val filterTracked = "pref_filter_library_tracked"
|
||||
|
||||
const val filterStarted = "pref_filter_library_started"
|
||||
|
||||
const val filterLewd = "pref_filter_library_lewd"
|
||||
|
||||
const val librarySortingMode = "library_sorting_mode"
|
||||
@@ -169,7 +177,7 @@ object PreferenceKeys {
|
||||
|
||||
const val categoryTabs = "display_category_tabs"
|
||||
|
||||
const val skipPreMigration = "skip_pre_migration"
|
||||
const val categoryNumberOfItems = "display_number_of_items"
|
||||
|
||||
const val alwaysShowChapterTransition = "always_show_chapter_transition"
|
||||
|
||||
@@ -197,6 +205,8 @@ object PreferenceKeys {
|
||||
|
||||
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||
|
||||
const val skipPreMigration = "skip_pre_migration"
|
||||
|
||||
const val eh_showSyncIntro = "eh_show_sync_intro"
|
||||
|
||||
const val eh_readOnlySync = "eh_sync_read_only"
|
||||
|
||||
@@ -24,6 +24,7 @@ object PreferenceValues {
|
||||
blue,
|
||||
amoled,
|
||||
red,
|
||||
midnightdusk,
|
||||
}
|
||||
|
||||
enum class DisplayMode {
|
||||
@@ -37,11 +38,11 @@ object PreferenceValues {
|
||||
LIST,
|
||||
}
|
||||
|
||||
enum class TappingInvertMode {
|
||||
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
||||
NONE,
|
||||
HORIZONTAL,
|
||||
VERTICAL,
|
||||
BOTH
|
||||
HORIZONTAL(shouldInvertHorizontal = true),
|
||||
VERTICAL(shouldInvertVertical = true),
|
||||
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
|
||||
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.io.File
|
||||
@@ -23,7 +22,6 @@ import java.util.Locale
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
||||
block(get())
|
||||
return asFlow()
|
||||
@@ -38,7 +36,6 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
||||
set(get() - item)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PreferencesHelper(val context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
@@ -92,6 +89,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
||||
|
||||
fun dualPageSplit() = flowPrefs.getBoolean(Keys.dualPageSplit, false)
|
||||
|
||||
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
||||
|
||||
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
|
||||
@@ -130,7 +129,9 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
|
||||
|
||||
fun readWithTappingInverted() = flowPrefs.getEnum(Keys.readWithTappingInverted, Values.TappingInvertMode.NONE)
|
||||
fun pagerNavInverted() = flowPrefs.getEnum(Keys.pagerNavInverted, Values.TappingInvertMode.NONE)
|
||||
|
||||
fun webtoonNavInverted() = flowPrefs.getEnum(Keys.webtoonNavInverted, Values.TappingInvertMode.NONE)
|
||||
|
||||
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
|
||||
|
||||
@@ -138,6 +139,10 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
|
||||
|
||||
fun navigationModePager() = flowPrefs.getInt(Keys.navigationModePager, 0)
|
||||
|
||||
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
|
||||
|
||||
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
|
||||
|
||||
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
||||
@@ -212,15 +217,17 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
|
||||
|
||||
fun categoryNumberOfItems() = flowPrefs.getBoolean(Keys.categoryNumberOfItems, false)
|
||||
|
||||
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterStarted() = flowPrefs.getInt(Keys.filterStarted, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
fun filterTracking(name: Int) = flowPrefs.getInt("${Keys.filterTracked}_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterTracked() = flowPrefs.getInt(Keys.filterTracked, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
fun filterStarted() = flowPrefs.getInt(Keys.filterStarted, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class TrackManager(context: Context) {
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
fun hasLoggedServices(isMangaDexManga: Boolean = true) = services.any { it.isLogged && ((it.id == MDLIST && isMangaDexManga) || it.id != MDLIST) }
|
||||
fun hasLoggedServices() = services.any { it.isLogged }
|
||||
|
||||
// SY -->
|
||||
fun mapTrackingOrder(status: String, context: Context): Int {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import okhttp3.OkHttpClient
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class TrackService(val id: Int) {
|
||||
@@ -20,7 +20,8 @@ abstract class TrackService(val id: Int) {
|
||||
get() = networkService.client
|
||||
|
||||
// Name of the manga sync service to display
|
||||
abstract val name: String
|
||||
@StringRes
|
||||
abstract fun nameRes(): Int
|
||||
|
||||
// Application and remote support for reading dates
|
||||
open val supportsReadingDates: Boolean = false
|
||||
@@ -28,6 +29,7 @@ abstract class TrackService(val id: Int) {
|
||||
@DrawableRes
|
||||
abstract fun getLogo(): Int
|
||||
|
||||
@ColorInt
|
||||
abstract fun getLogoColor(): Int
|
||||
|
||||
abstract fun getStatusList(): List<Int>
|
||||
@@ -44,17 +46,17 @@ abstract class TrackService(val id: Int) {
|
||||
|
||||
abstract fun displayScore(track: Track): String
|
||||
|
||||
abstract fun add(track: Track): Observable<Track>
|
||||
abstract suspend fun add(track: Track): Track
|
||||
|
||||
abstract fun update(track: Track): Observable<Track>
|
||||
abstract suspend fun update(track: Track): Track
|
||||
|
||||
abstract fun bind(track: Track): Observable<Track>
|
||||
abstract suspend fun bind(track: Track): Track
|
||||
|
||||
abstract fun search(query: String): Observable<List<TrackSearch>>
|
||||
abstract suspend fun search(query: String): List<TrackSearch>
|
||||
|
||||
abstract fun refresh(track: Track): Observable<Track>
|
||||
abstract suspend fun refresh(track: Track): Track
|
||||
|
||||
abstract fun login(username: String, password: String): Completable
|
||||
abstract suspend fun login(username: String, password: String)
|
||||
|
||||
@CallSuper
|
||||
open fun logout() {
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
@@ -9,8 +10,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
@@ -23,9 +22,6 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
const val PLANNING = 5
|
||||
const val REPEATING = 6
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
|
||||
const val POINT_100 = "POINT_100"
|
||||
const val POINT_10 = "POINT_10"
|
||||
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
|
||||
@@ -33,8 +29,6 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
const val POINT_3 = "POINT_3"
|
||||
}
|
||||
|
||||
override val name = "AniList"
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
|
||||
@@ -53,6 +47,9 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
}
|
||||
|
||||
@StringRes
|
||||
override fun nameRes() = R.string.tracker_anilist
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_anilist
|
||||
|
||||
override fun getLogoColor() = Color.rgb(18, 25, 35)
|
||||
@@ -131,65 +128,58 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
override suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
override suspend fun update(track: Track): Track {
|
||||
// If user was using API v1 fetch library_id
|
||||
if (track.library_id == null || track.library_id!! == 0L) {
|
||||
return api.findLibManga(track, getUsername().toInt()).flatMap {
|
||||
if (it == null) {
|
||||
throw Exception("$track not found on user library")
|
||||
}
|
||||
track.library_id = it.library_id
|
||||
api.updateLibManga(track)
|
||||
}
|
||||
val libManga = api.findLibManga(track, getUsername().toInt())
|
||||
?: throw Exception("$track not found on user library")
|
||||
track.library_id = libManga.library_id
|
||||
}
|
||||
|
||||
return api.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername().toInt())
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
override suspend fun bind(track: Track): Track {
|
||||
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.status = READING
|
||||
track.score = 0F
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track, getUsername().toInt())
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track
|
||||
}
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteTrack = api.getLibManga(track, getUsername().toInt())
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
override suspend fun login(username: String, password: String) = login(password)
|
||||
|
||||
fun login(token: String): Completable {
|
||||
val oauth = api.createOAuth(token)
|
||||
interceptor.setAuth(oauth)
|
||||
return api.getCurrentUser().map { (username, scoreType) ->
|
||||
suspend fun login(token: String) {
|
||||
try {
|
||||
val oauth = api.createOAuth(token)
|
||||
interceptor.setAuth(oauth)
|
||||
val (username, scoreType) = api.getCurrentUser()
|
||||
scorePreference.set(scoreType)
|
||||
saveCredentials(username.toString(), oauth.access_token)
|
||||
}.doOnError {
|
||||
} catch (e: Throwable) {
|
||||
logout()
|
||||
}.toCompletable()
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
|
||||
@@ -5,9 +5,10 @@ import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
@@ -19,23 +20,18 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Calendar
|
||||
|
||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
val query =
|
||||
"""
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||
| id
|
||||
@@ -43,32 +39,34 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|}
|
||||
|}
|
||||
|""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("mangaId", track.media_id)
|
||||
put("progress", track.last_chapter_read)
|
||||
put("status", track.toAnilistStatus())
|
||||
}
|
||||
}
|
||||
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
track.library_id = response["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
|
||||
track
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("mangaId", track.media_id)
|
||||
put("progress", track.last_chapter_read)
|
||||
put("status", track.toAnilistStatus())
|
||||
}
|
||||
}
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
body = payload.toString().toRequestBody(jsonMime)
|
||||
)
|
||||
)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
track.library_id =
|
||||
it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
val query =
|
||||
"""
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||
|id
|
||||
@@ -77,25 +75,25 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|}
|
||||
|}
|
||||
|""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("listId", track.library_id)
|
||||
put("progress", track.last_chapter_read)
|
||||
put("status", track.toAnilistStatus())
|
||||
put("score", track.score.toInt())
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("listId", track.library_id)
|
||||
put("progress", track.last_chapter_read)
|
||||
put("status", track.toAnilistStatus())
|
||||
put("score", track.score.toInt())
|
||||
}
|
||||
}
|
||||
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.await()
|
||||
track
|
||||
}
|
||||
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val query =
|
||||
"""
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
|query Search(${'$'}query: String) {
|
||||
|Page (perPage: 50) {
|
||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||
@@ -119,33 +117,34 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|}
|
||||
|}
|
||||
|""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("query", search)
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("query", search)
|
||||
}
|
||||
}
|
||||
}
|
||||
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
body = payload.toString().toRequestBody(jsonMime)
|
||||
)
|
||||
)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let { response ->
|
||||
val data = response["data"]!!.jsonObject
|
||||
val page = data["Page"]!!.jsonObject
|
||||
val media = page["media"]!!.jsonArray
|
||||
val entries = media.map { jsonToALManga(it.jsonObject) }
|
||||
entries.map { it.toTrack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, userid: Int): Observable<Track?> {
|
||||
val query =
|
||||
"""
|
||||
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||
|Page {
|
||||
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
|
||||
@@ -175,43 +174,43 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|}
|
||||
|}
|
||||
|""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("id", userid)
|
||||
put("manga_id", track.media_id)
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
putJsonObject("variables") {
|
||||
put("id", userid)
|
||||
put("manga_id", track.media_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
body = payload.toString().toRequestBody(jsonMime)
|
||||
)
|
||||
)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let { response ->
|
||||
val data = response["data"]!!.jsonObject
|
||||
val page = data["Page"]!!.jsonObject
|
||||
val media = page["mediaList"]!!.jsonArray
|
||||
val entries = media.map { jsonToALUserManga(it.jsonObject) }
|
||||
entries.firstOrNull()?.toTrack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track, userid: Int): Observable<Track> {
|
||||
return findLibManga(track, userid)
|
||||
.map { it ?: throw Exception("Could not find manga") }
|
||||
suspend fun getLibManga(track: Track, userid: Int): Track {
|
||||
return findLibManga(track, userid) ?: throw Exception("Could not find manga")
|
||||
}
|
||||
|
||||
fun createOAuth(token: String): OAuth {
|
||||
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Observable<Pair<Int, String>> {
|
||||
val query =
|
||||
"""
|
||||
suspend fun getCurrentUser(): Pair<Int, String> {
|
||||
return withIOContext {
|
||||
val query =
|
||||
"""
|
||||
|query User {
|
||||
|Viewer {
|
||||
|id
|
||||
@@ -221,26 +220,26 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|}
|
||||
|}
|
||||
|""".trimMargin()
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
}
|
||||
return authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)
|
||||
val data = response["data"]!!.jsonObject
|
||||
val payload = buildJsonObject {
|
||||
put("query", query)
|
||||
}
|
||||
authClient.newCall(
|
||||
POST(
|
||||
apiUrl,
|
||||
body = payload.toString().toRequestBody(jsonMime)
|
||||
)
|
||||
)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
val data = it["data"]!!.jsonObject
|
||||
val viewer = data["Viewer"]!!.jsonObject
|
||||
Pair(
|
||||
viewer["id"]!!.jsonPrimitive.int,
|
||||
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToALManga(struct: JsonObject): ALManga {
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.bangumi
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
@@ -9,20 +10,19 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override val name = "Bangumi"
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val interceptor by lazy { BangumiInterceptor(this) }
|
||||
|
||||
private val api by lazy { BangumiApi(client, interceptor) }
|
||||
|
||||
@StringRes
|
||||
override fun nameRes() = R.string.tracker_bangumi
|
||||
|
||||
override fun getScoreList(): List<String> {
|
||||
return IntRange(0, 10).map(Int::toString)
|
||||
}
|
||||
@@ -31,52 +31,44 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
override suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track)
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
override suspend fun update(track: Track): Track {
|
||||
return api.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.statusLibManga(track)
|
||||
.flatMap {
|
||||
api.findLibManga(track).flatMap { remoteTrack ->
|
||||
if (remoteTrack != null && it != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
track.status = remoteTrack.status
|
||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||
refresh(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
add(track)
|
||||
update(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
override suspend fun bind(track: Track): Track {
|
||||
val statusTrack = api.statusLibManga(track)
|
||||
val remoteTrack = api.findLibManga(track)
|
||||
return if (remoteTrack != null && statusTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
track.status = remoteTrack.status
|
||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||
refresh(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.status = READING
|
||||
track.score = 0F
|
||||
add(track)
|
||||
update(track)
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.statusLibManga(track)
|
||||
.flatMap {
|
||||
track.copyPersonalFrom(it!!)
|
||||
api.findLibManga(track)
|
||||
.map { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track.status = remoteTrack.status
|
||||
}
|
||||
track
|
||||
}
|
||||
}
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteStatusTrack = api.statusLibManga(track)
|
||||
track.copyPersonalFrom(remoteStatusTrack!!)
|
||||
api.findLibManga(track)?.let { remoteTrack ->
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track.status = remoteTrack.status
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_bangumi
|
||||
@@ -100,17 +92,16 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override fun getCompletionStatus(): Int = COMPLETED
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
override suspend fun login(username: String, password: String) = login(password)
|
||||
|
||||
fun login(code: String): Completable {
|
||||
return api.accessToken(code).map { oauth: OAuth? ->
|
||||
suspend fun login(code: String) {
|
||||
try {
|
||||
val oauth = api.accessToken(code)
|
||||
interceptor.newAuth(oauth)
|
||||
if (oauth != null) {
|
||||
saveCredentials(oauth.user_id.toString(), oauth.access_token)
|
||||
}
|
||||
}.doOnError {
|
||||
saveCredentials(oauth.user_id.toString(), oauth.access_token)
|
||||
} catch (e: Throwable) {
|
||||
logout()
|
||||
}.toCompletable()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveToken(oauth: OAuth?) {
|
||||
@@ -137,8 +128,5 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
const val ON_HOLD = 4
|
||||
const val DROPPED = 5
|
||||
const val PLANNING = 1
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@@ -20,7 +21,6 @@ import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
|
||||
@@ -30,59 +30,64 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
val body = FormBody.Builder()
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
return authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = body))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val body = FormBody.Builder()
|
||||
.add("rating", track.score.toInt().toString())
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = body))
|
||||
.await()
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
// read status update
|
||||
val sbody = FormBody.Builder()
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
return authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}.flatMap {
|
||||
// chapter update
|
||||
val body = FormBody.Builder()
|
||||
.add("watched_eps", track.last_chapter_read.toString())
|
||||
.build()
|
||||
authClient.newCall(POST("$apiUrl/subject/${track.media_id}/update/watched_eps", body = body))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
// read status update
|
||||
val sbody = FormBody.Builder()
|
||||
.add("status", track.toBangumiStatus())
|
||||
.build()
|
||||
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
|
||||
.await()
|
||||
|
||||
// chapter update
|
||||
val body = FormBody.Builder()
|
||||
.add("watched_eps", track.last_chapter_read.toString())
|
||||
.build()
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"$apiUrl/subject/${track.media_id}/update/watched_eps",
|
||||
body = body
|
||||
)
|
||||
).await()
|
||||
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||
.toUri()
|
||||
.buildUpon()
|
||||
.appendQueryParameter("max_results", "20")
|
||||
.build()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.await()
|
||||
.use {
|
||||
var responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||
.toUri()
|
||||
.buildUpon()
|
||||
.appendQueryParameter("max_results", "20")
|
||||
.build()
|
||||
return authClient.newCall(GET(url.toString()))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
var responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
if (responseBody.contains("\"code\":404")) {
|
||||
responseBody = "{\"results\":0,\"list\":[]}"
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)["list"]?.jsonArray
|
||||
response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 }
|
||||
?.map { jsonToSearch(it.jsonObject) }.orEmpty()
|
||||
}
|
||||
if (responseBody.contains("\"code\":404")) {
|
||||
responseBody = "{\"results\":0,\"list\":[]}"
|
||||
}
|
||||
val response = json.decodeFromString<JsonObject>(responseBody)["list"]?.jsonArray
|
||||
response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 }?.map { jsonToSearch(it.jsonObject) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||
@@ -95,62 +100,42 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToTrack(mangas: JsonObject): Track {
|
||||
return Track.create(TrackManager.BANGUMI).apply {
|
||||
title = mangas["name"]!!.jsonPrimitive.content
|
||||
media_id = mangas["id"]!!.jsonPrimitive.int
|
||||
score = try {
|
||||
mangas["rating"]!!.jsonObject["score"]!!.jsonPrimitive.float
|
||||
} catch (_: Exception) {
|
||||
0f
|
||||
}
|
||||
status = Bangumi.DEFAULT_STATUS
|
||||
tracking_url = mangas["url"]!!.jsonPrimitive.content
|
||||
suspend fun findLibManga(track: Track): Track? {
|
||||
return withIOContext {
|
||||
authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let { jsonToSearch(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track): Observable<Track?> {
|
||||
return authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
// get comic info
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
jsonToTrack(json.decodeFromString(responseBody))
|
||||
}
|
||||
}
|
||||
suspend fun statusLibManga(track: Track): Track? {
|
||||
return withIOContext {
|
||||
val urlUserRead = "$apiUrl/collection/${track.media_id}"
|
||||
val requestUserRead = Request.Builder()
|
||||
.url(urlUserRead)
|
||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
fun statusLibManga(track: Track): Observable<Track?> {
|
||||
val urlUserRead = "$apiUrl/collection/${track.media_id}"
|
||||
val requestUserRead = Request.Builder()
|
||||
.url(urlUserRead)
|
||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
// todo get user readed chapter here
|
||||
return authClient.newCall(requestUserRead)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val resp = netResponse.body?.string()
|
||||
val coll = json.decodeFromString<Collection>(resp!!)
|
||||
track.status = coll.status?.id!!
|
||||
track.last_chapter_read = coll.ep_status!!
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun accessToken(code: String): Observable<OAuth> {
|
||||
return client.newCall(accessTokenRequest(code))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
json.decodeFromString<OAuth>(responseBody)
|
||||
// TODO: get user readed chapter here
|
||||
authClient.newCall(requestUserRead)
|
||||
.await()
|
||||
.parseAs<Collection>()
|
||||
.let {
|
||||
track.status = it.status?.id!!
|
||||
track.last_chapter_read = it.ep_status!!
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun accessToken(code: String): OAuth {
|
||||
return withIOContext {
|
||||
client.newCall(accessTokenRequest(code))
|
||||
.await()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(
|
||||
|
||||
@@ -2,13 +2,14 @@ package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import com.google.gson.Gson
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DecimalFormat
|
||||
|
||||
@@ -20,16 +21,14 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
const val ON_HOLD = 3
|
||||
const val DROPPED = 4
|
||||
const val PLAN_TO_READ = 5
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0f
|
||||
}
|
||||
|
||||
override val name = "Kitsu"
|
||||
@StringRes
|
||||
override fun nameRes() = R.string.tracker_kitsu
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val interceptor by lazy { KitsuInterceptor(this, gson) }
|
||||
private val interceptor by lazy { KitsuInterceptor(this) }
|
||||
|
||||
private val api by lazy { KitsuApi(client, interceptor) }
|
||||
|
||||
@@ -68,49 +67,43 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
return df.format(track.score)
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
override suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track, getUserId())
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
override suspend fun update(track: Track): Track {
|
||||
return api.updateLibManga(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUserId())
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.media_id = remoteTrack.media_id
|
||||
update(track)
|
||||
} else {
|
||||
track.score = DEFAULT_SCORE
|
||||
track.status = DEFAULT_STATUS
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
override suspend fun bind(track: Track): Track {
|
||||
val remoteTrack = api.findLibManga(track, getUserId())
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.media_id = remoteTrack.media_id
|
||||
update(track)
|
||||
} else {
|
||||
track.status = READING
|
||||
track.score = 0F
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track)
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track
|
||||
}
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteTrack = api.getLibManga(track)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String): Completable {
|
||||
return api.login(username, password)
|
||||
.doOnNext { interceptor.newAuth(it) }
|
||||
.flatMap { api.getCurrentUser() }
|
||||
.doOnNext { userId -> saveCredentials(username, userId) }
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
override suspend fun login(username: String, password: String) {
|
||||
val token = api.login(username, password)
|
||||
interceptor.newAuth(token)
|
||||
val userId = api.getCurrentUser()
|
||||
saveCredentials(username, userId)
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
@@ -123,13 +116,12 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
}
|
||||
|
||||
fun saveToken(oauth: OAuth?) {
|
||||
val json = gson.toJson(oauth)
|
||||
preferences.trackToken(this).set(json)
|
||||
preferences.trackToken(this).set(json.encodeToString(oauth))
|
||||
}
|
||||
|
||||
fun restoreToken(): OAuth? {
|
||||
return try {
|
||||
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
|
||||
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -1,254 +1,253 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.int
|
||||
import com.github.salomonbrys.kotson.jsonObject
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonObject
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers.Companion.headersOf
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.PATCH
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
import rx.Observable
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
||||
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
private val rest = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(authClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(Rest::class.java)
|
||||
suspend fun addLibManga(track: Track, userId: String): Track {
|
||||
return withIOContext {
|
||||
val data = buildJsonObject {
|
||||
putJsonObject("data") {
|
||||
put("type", "libraryEntries")
|
||||
putJsonObject("attributes") {
|
||||
put("status", track.toKitsuStatus())
|
||||
put("progress", track.last_chapter_read)
|
||||
}
|
||||
putJsonObject("relationships") {
|
||||
putJsonObject("user") {
|
||||
putJsonObject("data") {
|
||||
put("id", userId)
|
||||
put("type", "users")
|
||||
}
|
||||
}
|
||||
putJsonObject("media") {
|
||||
putJsonObject("data") {
|
||||
put("id", track.media_id)
|
||||
put("type", "manga")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val searchRest = Retrofit.Builder()
|
||||
.baseUrl(algoliaKeyUrl)
|
||||
.client(authClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(SearchKeyRest::class.java)
|
||||
|
||||
private val algoliaRest = Retrofit.Builder()
|
||||
.baseUrl(algoliaUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(AgoliaSearchRest::class.java)
|
||||
|
||||
fun addLibManga(track: Track, userId: String): Observable<Track> {
|
||||
return Observable.defer {
|
||||
// @formatter:off
|
||||
val data = jsonObject(
|
||||
"type" to "libraryEntries",
|
||||
"attributes" to jsonObject(
|
||||
"status" to track.toKitsuStatus(),
|
||||
"progress" to track.last_chapter_read
|
||||
),
|
||||
"relationships" to jsonObject(
|
||||
"user" to jsonObject(
|
||||
"data" to jsonObject(
|
||||
"id" to userId,
|
||||
"type" to "users"
|
||||
)
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"${baseUrl}library-entries",
|
||||
headers = headersOf(
|
||||
"Content-Type",
|
||||
"application/vnd.api+json"
|
||||
),
|
||||
"media" to jsonObject(
|
||||
"data" to jsonObject(
|
||||
"id" to track.media_id,
|
||||
"type" to "manga"
|
||||
)
|
||||
)
|
||||
body = data.toString().toRequestBody("application/vnd.api+json".toMediaType())
|
||||
)
|
||||
)
|
||||
|
||||
rest.addLibManga(jsonObject("data" to data))
|
||||
.map { json ->
|
||||
track.media_id = json["data"]["id"].int
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.int
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
// @formatter:off
|
||||
val data = jsonObject(
|
||||
"type" to "libraryEntries",
|
||||
"id" to track.media_id,
|
||||
"attributes" to jsonObject(
|
||||
"status" to track.toKitsuStatus(),
|
||||
"progress" to track.last_chapter_read,
|
||||
"ratingTwenty" to track.toKitsuScore()
|
||||
)
|
||||
)
|
||||
// @formatter:on
|
||||
suspend fun updateLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val data = buildJsonObject {
|
||||
putJsonObject("data") {
|
||||
put("type", "libraryEntries")
|
||||
put("id", track.media_id)
|
||||
putJsonObject("attributes") {
|
||||
put("status", track.toKitsuStatus())
|
||||
put("progress", track.last_chapter_read)
|
||||
put("ratingTwenty", track.toKitsuScore())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rest.updateLibManga(track.media_id, jsonObject("data" to data))
|
||||
.map { track }
|
||||
authClient.newCall(
|
||||
Request.Builder()
|
||||
.url("${baseUrl}library-entries/${track.media_id}")
|
||||
.headers(
|
||||
headersOf(
|
||||
"Content-Type",
|
||||
"application/vnd.api+json"
|
||||
)
|
||||
)
|
||||
.patch(data.toString().toRequestBody("application/vnd.api+json".toMediaType()))
|
||||
.build()
|
||||
)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String): Observable<List<TrackSearch>> {
|
||||
return searchRest
|
||||
.getKey().map { json ->
|
||||
json["media"].asJsonObject["key"].string
|
||||
}.flatMap { key ->
|
||||
algoliaSearch(key, query)
|
||||
}
|
||||
}
|
||||
|
||||
private fun algoliaSearch(key: String, query: String): Observable<List<TrackSearch>> {
|
||||
val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
|
||||
return algoliaRest
|
||||
.getSearchQuery(algoliaAppId, key, jsonObject)
|
||||
.map { json ->
|
||||
val data = json["hits"].array
|
||||
data.map { KitsuSearchManga(it.obj) }
|
||||
.filter { it.subType != "novel" }
|
||||
.map { it.toTrack() }
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, userId: String): Observable<Track?> {
|
||||
return rest.findLibManga(track.media_id, userId)
|
||||
.map { json ->
|
||||
val data = json["data"].array
|
||||
if (data.size() > 0) {
|
||||
val manga = json["included"].array[0].obj
|
||||
KitsuLibManga(data[0].obj, manga).toTrack()
|
||||
} else {
|
||||
null
|
||||
suspend fun search(query: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
authClient.newCall(GET(algoliaKeyUrl))
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content
|
||||
algoliaSearch(key, query)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track): Observable<Track> {
|
||||
return rest.getLibManga(track.media_id)
|
||||
.map { json ->
|
||||
val data = json["data"].array
|
||||
if (data.size() > 0) {
|
||||
val manga = json["included"].array[0].obj
|
||||
KitsuLibManga(data[0].obj, manga).toTrack()
|
||||
} else {
|
||||
throw Exception("Could not find manga")
|
||||
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val jsonObject = buildJsonObject {
|
||||
put("params", "query=$query$algoliaFilter")
|
||||
}
|
||||
|
||||
client.newCall(
|
||||
POST(
|
||||
algoliaUrl,
|
||||
headers = headersOf(
|
||||
"X-Algolia-Application-Id",
|
||||
algoliaAppId,
|
||||
"X-Algolia-API-Key",
|
||||
key,
|
||||
),
|
||||
body = jsonObject.toString().toRequestBody(jsonMime)
|
||||
)
|
||||
)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
it["hits"]!!.jsonArray
|
||||
.map { KitsuSearchManga(it.jsonObject) }
|
||||
.filter { it.subType != "novel" }
|
||||
.map { it.toTrack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(username: String, password: String): Observable<OAuth> {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(loginUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||
.build()
|
||||
.create(LoginRest::class.java)
|
||||
.requestAccessToken(username, password)
|
||||
suspend fun findLibManga(track: Track, userId: String): Track? {
|
||||
return withIOContext {
|
||||
val url = "${baseUrl}library-entries".toUri().buildUpon()
|
||||
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
|
||||
.appendQueryParameter("include", "manga")
|
||||
.build()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
val data = it["data"]!!.jsonArray
|
||||
if (data.size > 0) {
|
||||
val manga = it["included"]!!.jsonArray[0].jsonObject
|
||||
KitsuLibManga(data[0].jsonObject, manga).toTrack()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Observable<String> {
|
||||
return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
|
||||
suspend fun getLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val url = "${baseUrl}library-entries".toUri().buildUpon()
|
||||
.encodedQuery("filter[id]=${track.media_id}")
|
||||
.appendQueryParameter("include", "manga")
|
||||
.build()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
val data = it["data"]!!.jsonArray
|
||||
if (data.size > 0) {
|
||||
val manga = it["included"]!!.jsonArray[0].jsonObject
|
||||
KitsuLibManga(data[0].jsonObject, manga).toTrack()
|
||||
} else {
|
||||
throw Exception("Could not find manga")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface Rest {
|
||||
|
||||
@Headers("Content-Type: application/vnd.api+json")
|
||||
@POST("library-entries")
|
||||
fun addLibManga(
|
||||
@Body data: JsonObject
|
||||
): Observable<JsonObject>
|
||||
|
||||
@Headers("Content-Type: application/vnd.api+json")
|
||||
@PATCH("library-entries/{id}")
|
||||
fun updateLibManga(
|
||||
@Path("id") remoteId: Int,
|
||||
@Body data: JsonObject
|
||||
): Observable<JsonObject>
|
||||
|
||||
@GET("library-entries")
|
||||
fun findLibManga(
|
||||
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
||||
@Query("filter[user_id]", encoded = true) userId: String,
|
||||
@Query("include") includes: String = "manga"
|
||||
): Observable<JsonObject>
|
||||
|
||||
@GET("library-entries")
|
||||
fun getLibManga(
|
||||
@Query("filter[id]", encoded = true) remoteId: Int,
|
||||
@Query("include") includes: String = "manga"
|
||||
): Observable<JsonObject>
|
||||
|
||||
@GET("users")
|
||||
fun getCurrentUser(
|
||||
@Query("filter[self]", encoded = true) self: Boolean = true
|
||||
): Observable<JsonObject>
|
||||
suspend fun login(username: String, password: String): OAuth {
|
||||
return withIOContext {
|
||||
val formBody: RequestBody = FormBody.Builder()
|
||||
.add("username", username)
|
||||
.add("password", password)
|
||||
.add("grant_type", "password")
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.build()
|
||||
client.newCall(POST(loginUrl, body = formBody))
|
||||
.await()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
private interface SearchKeyRest {
|
||||
@GET("media/")
|
||||
fun getKey(): Observable<JsonObject>
|
||||
}
|
||||
|
||||
private interface AgoliaSearchRest {
|
||||
@POST("query/")
|
||||
fun getSearchQuery(@Header("X-Algolia-Application-Id") appid: String, @Header("X-Algolia-API-Key") key: String, @Body json: JsonObject): Observable<JsonObject>
|
||||
}
|
||||
|
||||
private interface LoginRest {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("oauth/token")
|
||||
fun requestAccessToken(
|
||||
@Field("username") username: String,
|
||||
@Field("password") password: String,
|
||||
@Field("grant_type") grantType: String = "password",
|
||||
@Field("client_id") client_id: String = clientId,
|
||||
@Field("client_secret") client_secret: String = clientSecret
|
||||
): Observable<OAuth>
|
||||
suspend fun getCurrentUser(): String {
|
||||
return withIOContext {
|
||||
val url = "${baseUrl}users".toUri().buildUpon()
|
||||
.encodedQuery("filter[self]=true")
|
||||
.build()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||
private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||
private const val clientId =
|
||||
"dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||
private const val clientSecret =
|
||||
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||
|
||||
private const val baseUrl = "https://kitsu.io/api/edge/"
|
||||
private const val loginUrl = "https://kitsu.io/api/"
|
||||
private const val loginUrl = "https://kitsu.io/api/oauth/token"
|
||||
private const val baseMangaUrl = "https://kitsu.io/manga/"
|
||||
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/"
|
||||
private const val algoliaUrl = "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/"
|
||||
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/media/"
|
||||
|
||||
private const val algoliaUrl =
|
||||
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/"
|
||||
private const val algoliaAppId = "AWQO5J657S"
|
||||
private const val algoliaFilter = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
private const val algoliaFilter =
|
||||
"&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||
|
||||
fun mangaUrl(remoteId: Int): String {
|
||||
return baseMangaUrl + remoteId
|
||||
}
|
||||
|
||||
fun refreshTokenRequest(token: String) = POST(
|
||||
"${loginUrl}oauth/token",
|
||||
loginUrl,
|
||||
body = FormBody.Builder()
|
||||
.add("grant_type", "refresh_token")
|
||||
.add("refresh_token", token)
|
||||
.add("client_id", clientId)
|
||||
.add("client_secret", clientSecret)
|
||||
.add("refresh_token", token)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
|
||||
class KitsuInterceptor(val kitsu: Kitsu) : Interceptor {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
@@ -22,7 +26,7 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
|
||||
if (currAuth.isExpired()) {
|
||||
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
||||
if (response.isSuccessful) {
|
||||
newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java))
|
||||
newAuth(json.decodeFromString(response.body!!.string()))
|
||||
} else {
|
||||
response.close()
|
||||
}
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import com.github.salomonbrys.kotson.byInt
|
||||
import com.github.salomonbrys.kotson.byString
|
||||
import com.github.salomonbrys.kotson.nullInt
|
||||
import com.github.salomonbrys.kotson.nullObj
|
||||
import com.github.salomonbrys.kotson.nullString
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class KitsuSearchManga(obj: JsonObject) {
|
||||
val id by obj.byInt
|
||||
private val canonicalTitle by obj.byString
|
||||
private val chapterCount = obj.get("chapterCount").nullInt
|
||||
val subType = obj.get("subtype").nullString
|
||||
val original = obj.get("posterImage").nullObj?.get("original")?.asString
|
||||
private val synopsis by obj.byString
|
||||
private var startDate = obj.get("startDate").nullString?.let {
|
||||
val id = obj["id"]!!.jsonPrimitive.int
|
||||
private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content
|
||||
private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.intOrNull
|
||||
val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull
|
||||
val original = try {
|
||||
obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// posterImage is sometimes a jsonNull object instead
|
||||
null
|
||||
}
|
||||
private val synopsis = obj["synopsis"]!!.jsonPrimitive.content
|
||||
private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let {
|
||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
outputDf.format(Date(it.toLong() * 1000))
|
||||
}
|
||||
private val endDate = obj.get("endDate").nullString
|
||||
private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull
|
||||
|
||||
@CallSuper
|
||||
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
||||
@@ -47,17 +51,17 @@ class KitsuSearchManga(obj: JsonObject) {
|
||||
}
|
||||
|
||||
class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
|
||||
val id by manga.byInt
|
||||
private val canonicalTitle by manga["attributes"].byString
|
||||
private val chapterCount = manga["attributes"].obj.get("chapterCount").nullInt
|
||||
val type = manga["attributes"].obj.get("mangaType").nullString.orEmpty()
|
||||
val original by manga["attributes"].obj["posterImage"].byString
|
||||
private val synopsis by manga["attributes"].byString
|
||||
private val startDate = manga["attributes"].obj.get("startDate").nullString.orEmpty()
|
||||
private val libraryId by obj.byInt("id")
|
||||
val status by obj["attributes"].byString
|
||||
private val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
|
||||
val progress by obj["attributes"].byInt
|
||||
val id = manga["id"]!!.jsonPrimitive.int
|
||||
private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content
|
||||
private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.intOrNull
|
||||
val type = manga["attributes"]!!.jsonObject["mangaType"]?.jsonPrimitive?.contentOrNull.orEmpty()
|
||||
val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content
|
||||
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
|
||||
private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty()
|
||||
private val libraryId = obj["id"]!!.jsonPrimitive.int
|
||||
val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
|
||||
private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
|
||||
val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
|
||||
|
||||
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
|
||||
media_id = libraryId
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.track.kitsu
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OAuth(
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
|
||||
@@ -2,33 +2,27 @@ package eu.kanade.tachiyomi.data.track.mdlist
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.asObservable
|
||||
import eu.kanade.tachiyomi.source.model.toMangaInfo
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.util.floor
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
|
||||
class MdList(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
private val mdex by lazy { MdUtil.getEnabledMangaDex() }
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
override val name = "MDList"
|
||||
@StringRes
|
||||
override fun nameRes(): Int = R.string.mdlist
|
||||
|
||||
override fun getLogo(): Int {
|
||||
return R.drawable.ic_tracker_mangadex_logo
|
||||
@@ -49,89 +43,93 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override fun displayScore(track: Track) = track.score.toInt().toString()
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return update(track)
|
||||
}
|
||||
override suspend fun add(track: Track): Track = update(track)
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
val mdex = mdex ?: throw Exception("Mangadex not enabled")
|
||||
return Observable.defer {
|
||||
db.getManga(track.tracking_url.substringAfter(".org"), mdex.id)
|
||||
.asRxObservable()
|
||||
.map { manga ->
|
||||
val mangaMetadata = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise(MangaDexSearchMetadata::class) ?: throw Exception("Invalid manga metadata")
|
||||
val followStatus = FollowStatus.fromInt(track.status)!!
|
||||
override suspend fun update(track: Track): Track {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
|
||||
// allow follow status to update
|
||||
if (mangaMetadata.follow_status != followStatus.int) {
|
||||
runBlocking { mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus).collect() }
|
||||
mangaMetadata.follow_status = followStatus.int
|
||||
db.insertFlatMetadata(mangaMetadata.flatten()).await()
|
||||
}
|
||||
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
|
||||
val followStatus = FollowStatus.fromInt(track.status)
|
||||
|
||||
if (track.score.toInt() > 0) {
|
||||
runBlocking { mdex.updateRating(track).collect() }
|
||||
}
|
||||
|
||||
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
|
||||
|
||||
if (followStatus != FollowStatus.UNFOLLOWED) {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = FollowStatus.COMPLETED.int
|
||||
}
|
||||
|
||||
runBlocking { mdex.updateReadingProgress(track).collect() }
|
||||
} else if (track.last_chapter_read != 0) {
|
||||
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
|
||||
track.last_chapter_read = 0
|
||||
}
|
||||
track
|
||||
}
|
||||
// this updates the follow status in the metadata
|
||||
// allow follow status to update
|
||||
if (remoteTrack.status != followStatus.int) {
|
||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
|
||||
remoteTrack.status = followStatus.int
|
||||
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
|
||||
}
|
||||
|
||||
if (track.score.toInt() > 0) {
|
||||
mdex.updateRating(track)
|
||||
}
|
||||
|
||||
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
|
||||
|
||||
if (followStatus != FollowStatus.UNFOLLOWED) {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = FollowStatus.COMPLETED.int
|
||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
|
||||
}
|
||||
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
|
||||
val newFollowStatus = FollowStatus.READING
|
||||
track.status = FollowStatus.READING.int
|
||||
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
|
||||
remoteTrack.status = newFollowStatus.int
|
||||
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
|
||||
}
|
||||
|
||||
mdex.updateReadingProgress(track)
|
||||
} else if (track.last_chapter_read != 0) {
|
||||
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
|
||||
track.last_chapter_read = 0
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
val mdex = mdex ?: throw Exception("Mangadex not enabled")
|
||||
return mdex.fetchTrackingInfo(track.tracking_url).asObservable()
|
||||
.doOnNext { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
val manga = db.getManga(track.manga_id).executeAsBlocking()
|
||||
track.total_chapters = if (manga != null && manga.status == SManga.COMPLETED && remoteTrack.total_chapters == 0) {
|
||||
db.getChapters(track.manga_id).executeAsBlocking().maxOfOrNull { it.chapter_number }?.floor() ?: remoteTrack.total_chapters
|
||||
} else {
|
||||
remoteTrack.total_chapters
|
||||
}
|
||||
update(track)
|
||||
}
|
||||
}
|
||||
override suspend fun bind(track: Track): Track = update(refresh(track))
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
val mdex = mdex ?: throw Exception("Mangadex not enabled")
|
||||
return mdex.fetchTrackingInfo(track.tracking_url).asObservable()
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
val manga = db.getManga(track.manga_id).executeAsBlocking()
|
||||
track.total_chapters = if (manga != null && manga.status == SManga.COMPLETED && remoteTrack.total_chapters == 0) {
|
||||
db.getChapters(track.manga_id).executeAsBlocking().maxOfOrNull { it.chapter_number }?.floor() ?: remoteTrack.total_chapters
|
||||
} else {
|
||||
remoteTrack.total_chapters
|
||||
}
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun createInitialTracker(manga: Manga): Track {
|
||||
val track = Track.create(TrackManager.MDLIST)
|
||||
track.manga_id = manga.id!!
|
||||
track.status = FollowStatus.UNFOLLOWED.int
|
||||
track.tracking_url = MdUtil.baseUrl + manga.url
|
||||
track.title = manga.title
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
|
||||
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> = throw Exception("not used")
|
||||
fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track {
|
||||
val track = Track.create(TrackManager.MDLIST)
|
||||
track.manga_id = dbManga.id!!
|
||||
track.status = FollowStatus.UNFOLLOWED.int
|
||||
track.tracking_url = MdUtil.baseUrl + mdManga.url
|
||||
track.title = mdManga.title
|
||||
return track
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String): Completable = throw Exception("not used")
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
return mdex.fetchSearchManga(0, query, mdex.getFilterList())
|
||||
.flatMap { page ->
|
||||
runAsObservable({
|
||||
page.mangas.map {
|
||||
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
|
||||
}
|
||||
})
|
||||
}
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply {
|
||||
tracking_url = MdUtil.baseUrl + mangaInfo.key
|
||||
title = mangaInfo.title
|
||||
cover_url = mangaInfo.cover
|
||||
summary = mangaInfo.description
|
||||
}
|
||||
|
||||
override suspend fun login(username: String, password: String): Unit = throw Exception("not used")
|
||||
|
||||
class MangaDexNotFoundException : Exception("Mangadex not enabled")
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
@@ -18,20 +20,19 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||
const val ON_HOLD = 3
|
||||
const val DROPPED = 4
|
||||
const val PLAN_TO_READ = 6
|
||||
const val REREADING = 7
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
|
||||
const val BASE_URL = "https://myanimelist.net"
|
||||
const val USER_SESSION_COOKIE = "MALSESSIONID"
|
||||
const val LOGGED_IN_COOKIE = "is_logged_in"
|
||||
private const val SEARCH_ID_PREFIX = "id:"
|
||||
private const val SEARCH_LIST_PREFIX = "my:"
|
||||
}
|
||||
|
||||
private val interceptor by lazy { MyAnimeListInterceptor(this) }
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) }
|
||||
private val api by lazy { MyAnimeListApi(client, interceptor) }
|
||||
|
||||
override val name: String
|
||||
get() = "MyAnimeList"
|
||||
@StringRes
|
||||
override fun nameRes() = R.string.tracker_myanimelist
|
||||
|
||||
override val supportsReadingDates: Boolean = true
|
||||
|
||||
@@ -40,7 +41,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
||||
|
||||
override fun getStatusList(): List<Int> {
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
|
||||
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||
}
|
||||
|
||||
override fun getStatus(status: Int): String = with(context) {
|
||||
@@ -50,6 +51,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||
ON_HOLD -> getString(R.string.on_hold)
|
||||
DROPPED -> getString(R.string.dropped)
|
||||
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||
REREADING -> getString(R.string.repeating)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
@@ -64,77 +66,75 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
return api.addLibManga(track)
|
||||
override suspend fun add(track: Track): Track {
|
||||
track.status = READING
|
||||
track.score = 0F
|
||||
return api.updateItem(track)
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
return api.updateLibManga(track)
|
||||
override suspend fun update(track: Track): Track {
|
||||
return api.updateItem(track)
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track)
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
add(track)
|
||||
}
|
||||
override suspend fun bind(track: Track): Track {
|
||||
val remoteTrack = api.findListItem(track)
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.media_id = remoteTrack.media_id
|
||||
update(track)
|
||||
} else {
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
if (query.startsWith(SEARCH_ID_PREFIX)) {
|
||||
query.substringAfter(SEARCH_ID_PREFIX).toIntOrNull()?.let { id ->
|
||||
return listOf(api.getMangaDetails(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (query.startsWith(SEARCH_LIST_PREFIX)) {
|
||||
query.substringAfter(SEARCH_LIST_PREFIX).let { title ->
|
||||
return api.findListItems(title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.getLibManga(track)
|
||||
.map { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
track
|
||||
}
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
return api.findListItem(track) ?: add(track)
|
||||
}
|
||||
|
||||
fun login(csrfToken: String): Completable = login("myanimelist", csrfToken)
|
||||
override suspend fun login(username: String, password: String) = login(password)
|
||||
|
||||
override fun login(username: String, password: String): Completable {
|
||||
return Observable.fromCallable { saveCSRF(password) }
|
||||
.doOnNext { saveCredentials(username, password) }
|
||||
.doOnError { logout() }
|
||||
.toCompletable()
|
||||
}
|
||||
|
||||
fun ensureLoggedIn() {
|
||||
if (isAuthorized) return
|
||||
if (!isLogged) throw Exception(context.getString(R.string.myanimelist_creds_missing))
|
||||
suspend fun login(authCode: String) {
|
||||
try {
|
||||
val oauth = api.getAccessToken(authCode)
|
||||
interceptor.setAuth(oauth)
|
||||
val username = api.getCurrentUser()
|
||||
saveCredentials(username, oauth.access_token)
|
||||
} catch (e: Throwable) {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).delete()
|
||||
networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!)
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
private val isAuthorized: Boolean
|
||||
get() = super.isLogged &&
|
||||
getCSRF().isNotEmpty() &&
|
||||
checkCookies()
|
||||
fun saveOAuth(oAuth: OAuth?) {
|
||||
preferences.trackToken(this).set(json.encodeToString(oAuth))
|
||||
}
|
||||
|
||||
fun getCSRF(): String = preferences.trackToken(this).get()
|
||||
|
||||
private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
|
||||
|
||||
private fun checkCookies(): Boolean {
|
||||
val url = BASE_URL.toHttpUrlOrNull()!!
|
||||
val ckCount = networkService.cookieManager.get(url).count {
|
||||
it.name == USER_SESSION_COOKIE || it.name == LOGGED_IN_COOKIE
|
||||
fun loadOAuth(): OAuth? {
|
||||
return try {
|
||||
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
return ckCount == 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,472 +1,271 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.lang.toCalendar
|
||||
import eu.kanade.tachiyomi.util.selectInt
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.PkceUtil
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.parser.Parser
|
||||
import rx.Observable
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.Locale
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
||||
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun search(query: String): Observable<List<TrackSearch>> {
|
||||
return if (query.startsWith(PREFIX_MY)) {
|
||||
val realQuery = query.removePrefix(PREFIX_MY)
|
||||
getList()
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { it.title.contains(realQuery, true) }
|
||||
.toList()
|
||||
} else {
|
||||
client.newCall(GET(searchUrl(query)))
|
||||
.asObservable()
|
||||
.flatMap { response ->
|
||||
Observable.from(
|
||||
Jsoup.parse(response.consumeBody())
|
||||
.select("div.js-categories-seasonal.js-block-list.list")
|
||||
.select("table").select("tbody")
|
||||
.select("tr").drop(1)
|
||||
)
|
||||
suspend fun getAccessToken(authCode: String): OAuth {
|
||||
return withIOContext {
|
||||
val formBody: RequestBody = FormBody.Builder()
|
||||
.add("client_id", clientId)
|
||||
.add("code", authCode)
|
||||
.add("code_verifier", codeVerifier)
|
||||
.add("grant_type", "authorization_code")
|
||||
.build()
|
||||
client.newCall(POST("$baseOAuthUrl/token", body = formBody))
|
||||
.await()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCurrentUser(): String {
|
||||
return withIOContext {
|
||||
val request = Request.Builder()
|
||||
.url("$baseApiUrl/users/@me")
|
||||
.get()
|
||||
.build()
|
||||
authClient.newCall(request)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let { it["name"]!!.jsonPrimitive.content }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun search(query: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val url = "$baseApiUrl/manga".toUri().buildUpon()
|
||||
.appendQueryParameter("q", query)
|
||||
.appendQueryParameter("nsfw", "true")
|
||||
.build()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
it["data"]!!.jsonArray
|
||||
.map { data -> data.jsonObject["node"]!!.jsonObject }
|
||||
.map { node ->
|
||||
val id = node["id"]!!.jsonPrimitive.int
|
||||
async { getMangaDetails(id) }
|
||||
}
|
||||
.awaitAll()
|
||||
.filter { trackSearch -> trackSearch.publishing_type != "novel" }
|
||||
}
|
||||
.filter { row ->
|
||||
row.select(TD)[2].text() != "Novel"
|
||||
}
|
||||
.map { row ->
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaDetails(id: Int): TrackSearch {
|
||||
return withIOContext {
|
||||
val url = "$baseApiUrl/manga".toUri().buildUpon()
|
||||
.appendPath(id.toString())
|
||||
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
|
||||
.build()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
val obj = it.jsonObject
|
||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||
title = row.searchTitle()
|
||||
media_id = row.searchMediaId()
|
||||
total_chapters = row.searchTotalChapters()
|
||||
summary = row.searchSummary()
|
||||
cover_url = row.searchCoverUrl()
|
||||
tracking_url = mangaUrl(media_id)
|
||||
publishing_status = row.searchPublishingStatus()
|
||||
publishing_type = row.searchPublishingType()
|
||||
start_date = row.searchStartDate()
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
|
||||
.asObservableSuccess()
|
||||
.map { track }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track): Observable<Track> {
|
||||
return Observable.defer {
|
||||
// Get track data
|
||||
val response = authClient.newCall(GET(url = editPageUrl(track.media_id))).execute()
|
||||
val editData = response.use {
|
||||
val page = Jsoup.parse(it.consumeBody())
|
||||
|
||||
// Extract track data from MAL page
|
||||
extractDataFromEditPage(page).apply {
|
||||
// Apply changes to the just fetched data
|
||||
copyPersonalFrom(track)
|
||||
}
|
||||
}
|
||||
|
||||
// Update remote
|
||||
authClient.newCall(POST(url = editPageUrl(track.media_id), body = mangaEditPostBody(editData)))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track): Observable<Track?> {
|
||||
return authClient.newCall(GET(url = editPageUrl(track.media_id)))
|
||||
.asObservable()
|
||||
.map { response ->
|
||||
var libTrack: Track? = null
|
||||
response.use {
|
||||
if (it.priorResponse?.isRedirect != true) {
|
||||
val trackForm = Jsoup.parse(it.consumeBody())
|
||||
|
||||
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
|
||||
last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
|
||||
total_chapters = trackForm.select("#totalChap").text().toInt()
|
||||
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
|
||||
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull()
|
||||
?: 0f
|
||||
started_reading_date = trackForm.searchDatePicker("#add_manga_start_date")
|
||||
finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date")
|
||||
media_id = obj["id"]!!.jsonPrimitive.int
|
||||
title = obj["title"]!!.jsonPrimitive.content
|
||||
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
|
||||
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
||||
cover_url = obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content ?: ""
|
||||
tracking_url = "https://myanimelist.net/manga/$media_id"
|
||||
publishing_status = obj["status"]!!.jsonPrimitive.content.replace("_", " ")
|
||||
publishing_type = obj["media_type"]!!.jsonPrimitive.content.replace("_", " ")
|
||||
start_date = try {
|
||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
outputDf.format(obj["start_date"]!!)
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
libTrack
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibManga(track: Track): Observable<Track> {
|
||||
return findLibManga(track)
|
||||
.map { it ?: throw Exception("Could not find manga") }
|
||||
}
|
||||
|
||||
private fun getList(): Observable<List<TrackSearch>> {
|
||||
return getListUrl()
|
||||
.flatMap { url ->
|
||||
getListXml(url)
|
||||
}
|
||||
.flatMap { doc ->
|
||||
Observable.from(doc.select("manga"))
|
||||
}
|
||||
.map {
|
||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||
title = it.selectText("manga_title")!!
|
||||
media_id = it.selectInt("manga_mangadb_id")
|
||||
last_chapter_read = it.selectInt("my_read_chapters")
|
||||
status = getStatus(it.selectText("my_status")!!)
|
||||
score = it.selectInt("my_score").toFloat()
|
||||
total_chapters = it.selectInt("manga_chapters")
|
||||
tracking_url = mangaUrl(media_id)
|
||||
started_reading_date = it.searchDateXml("my_start_date")
|
||||
finished_reading_date = it.searchDateXml("my_finish_date")
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun getListUrl(): Observable<String> {
|
||||
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
|
||||
.asObservable()
|
||||
.map { response ->
|
||||
baseUrl + Jsoup.parse(response.consumeBody())
|
||||
.select("div.goodresult")
|
||||
.select("a")
|
||||
.attr("href")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getListXml(url: String): Observable<Document> {
|
||||
return authClient.newCall(GET(url))
|
||||
.asObservable()
|
||||
.map { response ->
|
||||
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Response.consumeBody(): String? {
|
||||
use {
|
||||
if (it.code != 200) throw Exception("HTTP error ${it.code}")
|
||||
return it.body?.string()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Response.consumeXmlBody(): String? {
|
||||
use { res ->
|
||||
if (res.code != 200) throw Exception("Export list error")
|
||||
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
|
||||
val sb = StringBuilder()
|
||||
reader.forEachLine { line ->
|
||||
sb.append(line)
|
||||
suspend fun updateItem(track: Track): Track {
|
||||
return withIOContext {
|
||||
val formBodyBuilder = FormBody.Builder()
|
||||
.add("status", track.toMyAnimeListStatus() ?: "reading")
|
||||
.add("is_rereading", (track.status == MyAnimeList.REREADING).toString())
|
||||
.add("score", track.score.toString())
|
||||
.add("num_chapters_read", track.last_chapter_read.toString())
|
||||
convertToIsoDate(track.started_reading_date)?.let {
|
||||
formBodyBuilder.add("start_date", it)
|
||||
}
|
||||
convertToIsoDate(track.finished_reading_date)?.let {
|
||||
formBodyBuilder.add("finish_date", it)
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(mangaUrl(track.media_id).toString())
|
||||
.put(formBodyBuilder.build())
|
||||
.build()
|
||||
authClient.newCall(request)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let { parseMangaItem(it, track) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findListItem(track: Track): Track? {
|
||||
return withIOContext {
|
||||
val uri = "$baseApiUrl/manga".toUri().buildUpon()
|
||||
.appendPath(track.media_id.toString())
|
||||
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
|
||||
.build()
|
||||
authClient.newCall(GET(uri.toString()))
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let { obj ->
|
||||
track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
||||
obj.jsonObject["my_list_status"]?.jsonObject?.let {
|
||||
parseMangaItem(it, track)
|
||||
}
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findListItems(query: String, offset: Int = 0): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val json = getListPage(offset)
|
||||
val obj = json.jsonObject
|
||||
|
||||
val matches = obj["data"]!!.jsonArray
|
||||
.filter {
|
||||
it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains(
|
||||
query,
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
.map {
|
||||
val id = it.jsonObject["node"]!!.jsonObject["id"]!!.jsonPrimitive.int
|
||||
async { getMangaDetails(id) }
|
||||
}
|
||||
.awaitAll()
|
||||
|
||||
// Check next page if there's more
|
||||
if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) {
|
||||
matches + findListItems(query, offset + listPaginationAmount)
|
||||
} else {
|
||||
matches
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractDataFromEditPage(page: Document): MyAnimeListEditData {
|
||||
val tables = page.select("form#main-form table")
|
||||
private suspend fun getListPage(offset: Int): JsonObject {
|
||||
return withIOContext {
|
||||
val urlBuilder = "$baseApiUrl/users/@me/mangalist".toUri().buildUpon()
|
||||
.appendQueryParameter("fields", "list_status{start_date,finish_date}")
|
||||
.appendQueryParameter("limit", listPaginationAmount.toString())
|
||||
if (offset > 0) {
|
||||
urlBuilder.appendQueryParameter("offset", offset.toString())
|
||||
}
|
||||
|
||||
return MyAnimeListEditData(
|
||||
entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0
|
||||
manga_id = tables[0].select("#manga_id").`val`(),
|
||||
status = tables[0].select("#add_manga_status > option[selected]").`val`(),
|
||||
num_read_volumes = tables[0].select("#add_manga_num_read_volumes").`val`(),
|
||||
last_completed_vol = tables[0].select("input[name=last_completed_vol]").`val`(), // Always empty
|
||||
num_read_chapters = tables[0].select("#add_manga_num_read_chapters").`val`(),
|
||||
score = tables[0].select("#add_manga_score > option[selected]").`val`(),
|
||||
start_date_month = tables[0].select("#add_manga_start_date_month > option[selected]").`val`(),
|
||||
start_date_day = tables[0].select("#add_manga_start_date_day > option[selected]").`val`(),
|
||||
start_date_year = tables[0].select("#add_manga_start_date_year > option[selected]").`val`(),
|
||||
finish_date_month = tables[0].select("#add_manga_finish_date_month > option[selected]").`val`(),
|
||||
finish_date_day = tables[0].select("#add_manga_finish_date_day > option[selected]").`val`(),
|
||||
finish_date_year = tables[0].select("#add_manga_finish_date_year > option[selected]").`val`(),
|
||||
tags = tables[1].select("#add_manga_tags").`val`(),
|
||||
priority = tables[1].select("#add_manga_priority > option[selected]").`val`(),
|
||||
storage_type = tables[1].select("#add_manga_storage_type > option[selected]").`val`(),
|
||||
num_retail_volumes = tables[1].select("#add_manga_num_retail_volumes").`val`(),
|
||||
num_read_times = tables[1].select("#add_manga_num_read_times").`val`(),
|
||||
reread_value = tables[1].select("#add_manga_reread_value > option[selected]").`val`(),
|
||||
comments = tables[1].select("#add_manga_comments").`val`(),
|
||||
is_asked_to_discuss = tables[1].select("#add_manga_is_asked_to_discuss > option[selected]").`val`(),
|
||||
sns_post_type = tables[1].select("#add_manga_sns_post_type > option[selected]").`val`()
|
||||
)
|
||||
val request = Request.Builder()
|
||||
.url(urlBuilder.build().toString())
|
||||
.get()
|
||||
.build()
|
||||
authClient.newCall(request)
|
||||
.await()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMangaItem(response: JsonObject, track: Track): Track {
|
||||
val obj = response.jsonObject
|
||||
return track.apply {
|
||||
val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean
|
||||
status = if (isRereading) MyAnimeList.REREADING else getStatus(obj["status"]!!.jsonPrimitive.content)
|
||||
last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.int
|
||||
score = obj["score"]!!.jsonPrimitive.int.toFloat()
|
||||
obj["start_date"]?.let {
|
||||
started_reading_date = parseDate(it.jsonPrimitive.content)
|
||||
}
|
||||
obj["finish_date"]?.let {
|
||||
finished_reading_date = parseDate(it.jsonPrimitive.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(isoDate: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
||||
}
|
||||
|
||||
private fun convertToIsoDate(epochTime: Long): String? {
|
||||
if (epochTime == 0L) {
|
||||
return ""
|
||||
}
|
||||
return try {
|
||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
outputDf.format(epochTime)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CSRF = "csrf_token"
|
||||
// Registered under arkon's MAL account
|
||||
private const val clientId = "8fd3313bc138e8b890551aa1de1a2589"
|
||||
|
||||
private const val baseUrl = "https://myanimelist.net"
|
||||
private const val baseMangaUrl = "$baseUrl/manga/"
|
||||
private const val baseModifyListUrl = "$baseUrl/ownlist/manga"
|
||||
private const val PREFIX_MY = "my:"
|
||||
private const val TD = "td"
|
||||
private const val baseOAuthUrl = "https://myanimelist.net/v1/oauth2"
|
||||
private const val baseApiUrl = "https://api.myanimelist.net/v2"
|
||||
|
||||
fun loginUrl() = baseUrl.toUri().buildUpon()
|
||||
.appendPath("login.php")
|
||||
.toString()
|
||||
private const val listPaginationAmount = 250
|
||||
|
||||
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
||||
private var codeVerifier: String = ""
|
||||
|
||||
private fun searchUrl(query: String): String {
|
||||
val col = "c[]"
|
||||
return baseUrl.toUri().buildUpon()
|
||||
.appendPath("manga.php")
|
||||
.appendQueryParameter("q", query)
|
||||
.appendQueryParameter(col, "a")
|
||||
.appendQueryParameter(col, "b")
|
||||
.appendQueryParameter(col, "c")
|
||||
.appendQueryParameter(col, "d")
|
||||
.appendQueryParameter(col, "e")
|
||||
.appendQueryParameter(col, "g")
|
||||
.toString()
|
||||
}
|
||||
fun authUrl(): Uri = "$baseOAuthUrl/authorize".toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("code_challenge", getPkceChallengeCode())
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.build()
|
||||
|
||||
private fun exportListUrl() = baseUrl.toUri().buildUpon()
|
||||
.appendPath("panel.php")
|
||||
.appendQueryParameter("go", "export")
|
||||
.toString()
|
||||
fun mangaUrl(id: Int): Uri = "$baseApiUrl/manga".toUri().buildUpon()
|
||||
.appendPath(id.toString())
|
||||
.appendPath("my_list_status")
|
||||
.build()
|
||||
|
||||
private fun editPageUrl(mediaId: Int) = baseModifyListUrl.toUri().buildUpon()
|
||||
.appendPath(mediaId.toString())
|
||||
.appendPath("edit")
|
||||
.toString()
|
||||
|
||||
private fun addUrl() = baseModifyListUrl.toUri().buildUpon()
|
||||
.appendPath("add.json")
|
||||
.toString()
|
||||
|
||||
private fun exportPostBody(): RequestBody {
|
||||
return FormBody.Builder()
|
||||
.add("type", "2")
|
||||
.add("subexport", "Export My List")
|
||||
fun refreshTokenRequest(refreshToken: String): Request {
|
||||
val formBody: RequestBody = FormBody.Builder()
|
||||
.add("client_id", clientId)
|
||||
.add("refresh_token", refreshToken)
|
||||
.add("grant_type", "refresh_token")
|
||||
.build()
|
||||
return POST("$baseOAuthUrl/token", body = formBody)
|
||||
}
|
||||
|
||||
private fun mangaPostPayload(track: Track): RequestBody {
|
||||
val body = JSONObject()
|
||||
.put("manga_id", track.media_id)
|
||||
.put("status", track.status)
|
||||
.put("score", track.score)
|
||||
.put("num_read_chapters", track.last_chapter_read)
|
||||
|
||||
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
}
|
||||
|
||||
private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody {
|
||||
return FormBody.Builder()
|
||||
.add("entry_id", track.entry_id)
|
||||
.add("manga_id", track.manga_id)
|
||||
.add("add_manga[status]", track.status)
|
||||
.add("add_manga[num_read_volumes]", track.num_read_volumes)
|
||||
.add("last_completed_vol", track.last_completed_vol)
|
||||
.add("add_manga[num_read_chapters]", track.num_read_chapters)
|
||||
.add("add_manga[score]", track.score)
|
||||
.add("add_manga[start_date][month]", track.start_date_month)
|
||||
.add("add_manga[start_date][day]", track.start_date_day)
|
||||
.add("add_manga[start_date][year]", track.start_date_year)
|
||||
.add("add_manga[finish_date][month]", track.finish_date_month)
|
||||
.add("add_manga[finish_date][day]", track.finish_date_day)
|
||||
.add("add_manga[finish_date][year]", track.finish_date_year)
|
||||
.add("add_manga[tags]", track.tags)
|
||||
.add("add_manga[priority]", track.priority)
|
||||
.add("add_manga[storage_type]", track.storage_type)
|
||||
.add("add_manga[num_retail_volumes]", track.num_retail_volumes)
|
||||
.add("add_manga[num_read_times]", track.num_read_times)
|
||||
.add("add_manga[reread_value]", track.reread_value)
|
||||
.add("add_manga[comments]", track.comments)
|
||||
.add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss)
|
||||
.add("add_manga[sns_post_type]", track.sns_post_type)
|
||||
.add("submitIt", track.submitIt)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun Element.searchDateXml(field: String): Long {
|
||||
val text = selectText(field, "0000-00-00")!!
|
||||
// MAL sets the data to 0000-00-00 when date is invalid or missing
|
||||
if (text == "0000-00-00") {
|
||||
return 0L
|
||||
}
|
||||
|
||||
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(text)?.time ?: 0L
|
||||
}
|
||||
|
||||
private fun Element.searchDatePicker(id: String): Long {
|
||||
val month = select(id + "_month > option[selected]").`val`().toIntOrNull()
|
||||
val day = select(id + "_day > option[selected]").`val`().toIntOrNull()
|
||||
val year = select(id + "_year > option[selected]").`val`().toIntOrNull()
|
||||
if (year == null || month == null || day == null) {
|
||||
return 0L
|
||||
}
|
||||
|
||||
return GregorianCalendar(year, month - 1, day).timeInMillis
|
||||
}
|
||||
|
||||
private fun Element.searchTitle() = select("strong").text()!!
|
||||
|
||||
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
||||
|
||||
private fun Element.searchCoverUrl() = select("img")
|
||||
.attr("data-src")
|
||||
.split("\\?")[0]
|
||||
.replace("/r/50x70/", "/")
|
||||
|
||||
private fun Element.searchMediaId() = select("div.picSurround")
|
||||
.select("a").attr("id")
|
||||
.replace("sarea", "")
|
||||
.toInt()
|
||||
|
||||
private fun Element.searchSummary() = select("div.pt4")
|
||||
.first()
|
||||
.ownText()!!
|
||||
|
||||
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
||||
|
||||
private fun Element.searchPublishingType() = select(TD)[2].text()!!
|
||||
|
||||
private fun Element.searchStartDate() = select(TD)[6].text()!!
|
||||
|
||||
private fun getStatus(status: String) = when (status) {
|
||||
"Reading" -> 1
|
||||
"Completed" -> 2
|
||||
"On-Hold" -> 3
|
||||
"Dropped" -> 4
|
||||
"Plan to Read" -> 6
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
|
||||
private class MyAnimeListEditData(
|
||||
// entry_id
|
||||
var entry_id: String,
|
||||
|
||||
// manga_id
|
||||
var manga_id: String,
|
||||
|
||||
// add_manga[status]
|
||||
var status: String,
|
||||
|
||||
// add_manga[num_read_volumes]
|
||||
var num_read_volumes: String,
|
||||
|
||||
// last_completed_vol
|
||||
var last_completed_vol: String,
|
||||
|
||||
// add_manga[num_read_chapters]
|
||||
var num_read_chapters: String,
|
||||
|
||||
// add_manga[score]
|
||||
var score: String,
|
||||
|
||||
// add_manga[start_date][month]
|
||||
var start_date_month: String, // [1-12]
|
||||
|
||||
// add_manga[start_date][day]
|
||||
var start_date_day: String,
|
||||
|
||||
// add_manga[start_date][year]
|
||||
var start_date_year: String,
|
||||
|
||||
// add_manga[finish_date][month]
|
||||
var finish_date_month: String, // [1-12]
|
||||
|
||||
// add_manga[finish_date][day]
|
||||
var finish_date_day: String,
|
||||
|
||||
// add_manga[finish_date][year]
|
||||
var finish_date_year: String,
|
||||
|
||||
// add_manga[tags]
|
||||
var tags: String,
|
||||
|
||||
// add_manga[priority]
|
||||
var priority: String,
|
||||
|
||||
// add_manga[storage_type]
|
||||
var storage_type: String,
|
||||
|
||||
// add_manga[num_retail_volumes]
|
||||
var num_retail_volumes: String,
|
||||
|
||||
// add_manga[num_read_times]
|
||||
var num_read_times: String,
|
||||
|
||||
// add_manga[reread_value]
|
||||
var reread_value: String,
|
||||
|
||||
// add_manga[comments]
|
||||
var comments: String,
|
||||
|
||||
// add_manga[is_asked_to_discuss]
|
||||
var is_asked_to_discuss: String,
|
||||
|
||||
// add_manga[sns_post_type]
|
||||
var sns_post_type: String,
|
||||
|
||||
// submitIt
|
||||
val submitIt: String = "0"
|
||||
) {
|
||||
fun copyPersonalFrom(track: Track) {
|
||||
num_read_chapters = track.last_chapter_read.toString()
|
||||
val numScore = track.score.toInt()
|
||||
if (numScore == 0) {
|
||||
score = ""
|
||||
} else if (numScore in 1..10) {
|
||||
score = numScore.toString()
|
||||
}
|
||||
status = track.status.toString()
|
||||
if (track.started_reading_date == 0L) {
|
||||
start_date_month = ""
|
||||
start_date_day = ""
|
||||
start_date_year = ""
|
||||
}
|
||||
if (track.finished_reading_date == 0L) {
|
||||
finish_date_month = ""
|
||||
finish_date_day = ""
|
||||
finish_date_year = ""
|
||||
}
|
||||
track.started_reading_date.toCalendar()?.let { cal ->
|
||||
start_date_month = (cal[Calendar.MONTH] + 1).toString()
|
||||
start_date_day = cal[Calendar.DAY_OF_MONTH].toString()
|
||||
start_date_year = cal[Calendar.YEAR].toString()
|
||||
}
|
||||
track.finished_reading_date.toCalendar()?.let { cal ->
|
||||
finish_date_month = (cal[Calendar.MONTH] + 1).toString()
|
||||
finish_date_day = cal[Calendar.DAY_OF_MONTH].toString()
|
||||
finish_date_year = cal[Calendar.YEAR].toString()
|
||||
}
|
||||
private fun getPkceChallengeCode(): String {
|
||||
codeVerifier = PkceUtil.generateCodeVerifier()
|
||||
return codeVerifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-39
@@ -1,52 +1,58 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import okio.Buffer
|
||||
import org.json.JSONObject
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor {
|
||||
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private var oauth: OAuth? = null
|
||||
set(value) {
|
||||
field = value?.copy(expires_in = System.currentTimeMillis() + (value.expires_in * 1000))
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
myanimelist.ensureLoggedIn()
|
||||
val originalRequest = chain.request()
|
||||
|
||||
val request = chain.request()
|
||||
return chain.proceed(updateRequest(request))
|
||||
}
|
||||
|
||||
private fun updateRequest(request: Request): Request {
|
||||
return request.body?.let {
|
||||
val contentType = it.contentType().toString()
|
||||
val updatedBody = when {
|
||||
contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)
|
||||
contentType.contains("json") -> updateJsonBody(it)
|
||||
else -> it
|
||||
}
|
||||
request.newBuilder().post(updatedBody).build()
|
||||
} ?: request
|
||||
}
|
||||
|
||||
private fun bodyToString(requestBody: RequestBody): String {
|
||||
Buffer().use {
|
||||
requestBody.writeTo(it)
|
||||
return it.readUtf8()
|
||||
if (token.isNullOrEmpty()) {
|
||||
throw Exception("Not authenticated with MyAnimeList")
|
||||
}
|
||||
if (oauth == null) {
|
||||
oauth = myanimelist.loadOAuth()
|
||||
}
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth!!.isExpired()) {
|
||||
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
|
||||
if (it.isSuccessful) {
|
||||
setAuth(json.decodeFromString(it.body!!.string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Throw on null auth.
|
||||
if (oauth == null) {
|
||||
throw Exception("No authentication token")
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request.
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
}
|
||||
|
||||
private fun updateFormBody(requestBody: RequestBody): RequestBody {
|
||||
val formString = bodyToString(requestBody)
|
||||
|
||||
return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody(requestBody.contentType())
|
||||
}
|
||||
|
||||
private fun updateJsonBody(requestBody: RequestBody): RequestBody {
|
||||
val jsonString = bodyToString(requestBody)
|
||||
val newBody = JSONObject(jsonString)
|
||||
.put(MyAnimeListApi.CSRF, myanimelist.getCSRF())
|
||||
|
||||
return newBody.toString().toRequestBody(requestBody.contentType())
|
||||
/**
|
||||
* Called when the user authenticates with MyAnimeList for the first time. Sets the refresh token
|
||||
* and the oauth object.
|
||||
*/
|
||||
fun setAuth(oauth: OAuth?) {
|
||||
token = oauth?.access_token
|
||||
this.oauth = oauth
|
||||
myanimelist.saveOAuth(oauth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
|
||||
fun Track.toMyAnimeListStatus() = when (status) {
|
||||
MyAnimeList.READING -> "reading"
|
||||
MyAnimeList.COMPLETED -> "completed"
|
||||
MyAnimeList.ON_HOLD -> "on_hold"
|
||||
MyAnimeList.DROPPED -> "dropped"
|
||||
MyAnimeList.PLAN_TO_READ -> "plan_to_read"
|
||||
MyAnimeList.REREADING -> "reading"
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun getStatus(status: String) = when (status) {
|
||||
"reading" -> MyAnimeList.READING
|
||||
"completed" -> MyAnimeList.COMPLETED
|
||||
"on_hold" -> MyAnimeList.ON_HOLD
|
||||
"dropped" -> MyAnimeList.DROPPED
|
||||
"plan_to_read" -> MyAnimeList.PLAN_TO_READ
|
||||
else -> MyAnimeList.READING
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OAuth(
|
||||
val refresh_token: String,
|
||||
val access_token: String,
|
||||
val token_type: String,
|
||||
val expires_in: Long
|
||||
) {
|
||||
|
||||
fun isExpired() = System.currentTimeMillis() > expires_in
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.StringRes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
@@ -9,8 +10,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||
@@ -22,19 +21,17 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||
const val DROPPED = 4
|
||||
const val PLANNING = 5
|
||||
const val REPEATING = 6
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
const val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
override val name = "Shikimori"
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val interceptor by lazy { ShikimoriInterceptor(this) }
|
||||
|
||||
private val api by lazy { ShikimoriApi(client, interceptor) }
|
||||
|
||||
@StringRes
|
||||
override fun nameRes() = R.string.tracker_shikimori
|
||||
|
||||
override fun getScoreList(): List<String> {
|
||||
return IntRange(0, 10).map(Int::toString)
|
||||
}
|
||||
@@ -43,43 +40,38 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||
return track.score.toInt().toString()
|
||||
}
|
||||
|
||||
override fun add(track: Track): Observable<Track> {
|
||||
override suspend fun add(track: Track): Track {
|
||||
return api.addLibManga(track, getUsername())
|
||||
}
|
||||
|
||||
override fun update(track: Track): Observable<Track> {
|
||||
override suspend fun update(track: Track): Track {
|
||||
return api.updateLibManga(track, getUsername())
|
||||
}
|
||||
|
||||
override fun bind(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername())
|
||||
.flatMap { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
override suspend fun bind(track: Track): Track {
|
||||
val remoteTrack = api.findLibManga(track, getUsername())
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.status = READING
|
||||
track.score = 0F
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
||||
override suspend fun search(query: String): List<TrackSearch> {
|
||||
return api.search(query)
|
||||
}
|
||||
|
||||
override fun refresh(track: Track): Observable<Track> {
|
||||
return api.findLibManga(track, getUsername())
|
||||
.map { remoteTrack ->
|
||||
if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
}
|
||||
track
|
||||
}
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
api.findLibManga(track, getUsername())?.let { remoteTrack ->
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_shikimori
|
||||
@@ -104,18 +96,17 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override fun getCompletionStatus(): Int = COMPLETED
|
||||
|
||||
override fun login(username: String, password: String) = login(password)
|
||||
override suspend fun login(username: String, password: String) = login(password)
|
||||
|
||||
fun login(code: String): Completable {
|
||||
return api.accessToken(code).map { oauth: OAuth? ->
|
||||
suspend fun login(code: String) {
|
||||
try {
|
||||
val oauth = api.accessToken(code)
|
||||
interceptor.newAuth(oauth)
|
||||
if (oauth != null) {
|
||||
val user = api.getCurrentUser()
|
||||
saveCredentials(user.toString(), oauth.access_token)
|
||||
}
|
||||
}.doOnError {
|
||||
val user = api.getCurrentUser()
|
||||
saveCredentials(user.toString(), oauth.access_token)
|
||||
} catch (e: Throwable) {
|
||||
logout()
|
||||
}.toCompletable()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveToken(oauth: OAuth?) {
|
||||
|
||||
@@ -6,9 +6,11 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.jsonMime
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
@@ -19,55 +21,53 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
fun addLibManga(track: Track, user_id: String): Observable<Track> {
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("user_rate") {
|
||||
put("user_id", user_id)
|
||||
put("target_id", track.media_id)
|
||||
put("target_type", "Manga")
|
||||
put("chapters", track.last_chapter_read)
|
||||
put("score", track.score.toInt())
|
||||
put("status", track.toShikimoriStatus())
|
||||
suspend fun addLibManga(track: Track, user_id: String): Track {
|
||||
return withIOContext {
|
||||
val payload = buildJsonObject {
|
||||
putJsonObject("user_rate") {
|
||||
put("user_id", user_id)
|
||||
put("target_id", track.media_id)
|
||||
put("target_type", "Manga")
|
||||
put("chapters", track.last_chapter_read)
|
||||
put("score", track.score.toInt())
|
||||
put("status", track.toShikimoriStatus())
|
||||
}
|
||||
}
|
||||
authClient.newCall(
|
||||
POST(
|
||||
"$apiUrl/v2/user_rates",
|
||||
body = payload.toString().toRequestBody(jsonMime)
|
||||
)
|
||||
).await()
|
||||
track
|
||||
}
|
||||
return authClient.newCall(POST("$apiUrl/v2/user_rates", body = payload.toString().toRequestBody(jsonMime)))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
track
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
||||
suspend fun updateLibManga(track: Track, user_id: String): Track = addLibManga(track, user_id)
|
||||
|
||||
fun search(search: String): Observable<List<TrackSearch>> {
|
||||
val url = "$apiUrl/mangas".toUri().buildUpon()
|
||||
.appendQueryParameter("order", "popularity")
|
||||
.appendQueryParameter("search", search)
|
||||
.appendQueryParameter("limit", "20")
|
||||
.build()
|
||||
return authClient.newCall(GET(url.toString()))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
suspend fun search(search: String): List<TrackSearch> {
|
||||
return withIOContext {
|
||||
val url = "$apiUrl/mangas".toUri().buildUpon()
|
||||
.appendQueryParameter("order", "popularity")
|
||||
.appendQueryParameter("search", search)
|
||||
.appendQueryParameter("limit", "20")
|
||||
.build()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.await()
|
||||
.parseAs<JsonArray>()
|
||||
.let { response ->
|
||||
response.map {
|
||||
jsonToSearch(it.jsonObject)
|
||||
}
|
||||
}
|
||||
val response = json.decodeFromString<JsonArray>(responseBody)
|
||||
response.map { jsonToSearch(it.jsonObject) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||
@@ -96,57 +96,52 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
||||
}
|
||||
}
|
||||
|
||||
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
||||
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
|
||||
.appendPath(track.media_id.toString())
|
||||
.build()
|
||||
return authClient.newCall(GET(urlMangas.toString()))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
json.decodeFromString<JsonObject>(responseBody)
|
||||
}.flatMap { mangas ->
|
||||
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
|
||||
.appendQueryParameter("user_id", user_id)
|
||||
.appendQueryParameter("target_id", track.media_id.toString())
|
||||
.appendQueryParameter("target_type", "Manga")
|
||||
.build()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = json.decodeFromString<JsonArray>(responseBody)
|
||||
if (response.size > 1) {
|
||||
throw Exception("Too much mangas in response")
|
||||
}
|
||||
val entry = response.map {
|
||||
jsonToTrack(it.jsonObject, mangas)
|
||||
}
|
||||
entry.firstOrNull()
|
||||
suspend fun findLibManga(track: Track, user_id: String): Track? {
|
||||
return withIOContext {
|
||||
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
|
||||
.appendPath(track.media_id.toString())
|
||||
.build()
|
||||
val mangas = authClient.newCall(GET(urlMangas.toString()))
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
|
||||
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
|
||||
.appendQueryParameter("user_id", user_id)
|
||||
.appendQueryParameter("target_id", track.media_id.toString())
|
||||
.appendQueryParameter("target_type", "Manga")
|
||||
.build()
|
||||
authClient.newCall(GET(url.toString()))
|
||||
.await()
|
||||
.parseAs<JsonArray>()
|
||||
.let { response ->
|
||||
if (response.size > 1) {
|
||||
throw Exception("Too much mangas in response")
|
||||
}
|
||||
}
|
||||
val entry = response.map {
|
||||
jsonToTrack(it.jsonObject, mangas)
|
||||
}
|
||||
entry.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentUser(): Int {
|
||||
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string()!!
|
||||
return json.decodeFromString<JsonObject>(user)["id"]!!.jsonPrimitive.int
|
||||
return runBlocking {
|
||||
authClient.newCall(GET("$apiUrl/users/whoami"))
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let {
|
||||
it["id"]!!.jsonPrimitive.int
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun accessToken(code: String): Observable<OAuth> {
|
||||
return client.newCall(accessTokenRequest(code))
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
netResponse.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
json.decodeFromString<OAuth>(responseBody)
|
||||
}
|
||||
}
|
||||
suspend fun accessToken(code: String): OAuth {
|
||||
return withIOContext {
|
||||
client.newCall(accessTokenRequest(code))
|
||||
.await()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
private fun accessTokenRequest(code: String) = POST(
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.updater.github
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Used to connect with the GitHub API to get the latest release version from a repo.
|
||||
*/
|
||||
interface GithubService {
|
||||
|
||||
companion object {
|
||||
fun create(): GithubService {
|
||||
val restAdapter = Retrofit.Builder()
|
||||
.baseUrl("https://api.github.com")
|
||||
.addConverterFactory(Json { ignoreUnknownKeys = true }.asConverterFactory("application/json".toMediaType()))
|
||||
.client(Injekt.get<NetworkHelper>().client)
|
||||
.build()
|
||||
|
||||
return restAdapter.create(GithubService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@GET("/repos/{repo}/releases/latest")
|
||||
suspend fun getLatestVersion(@Path("repo", encoded = true) repo: String): GithubRelease
|
||||
}
|
||||
@@ -2,11 +2,17 @@ package eu.kanade.tachiyomi.data.updater.github
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateResult
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.syDebugVersion
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class GithubUpdateChecker {
|
||||
|
||||
private val service: GithubService = GithubService.create()
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
|
||||
private val repo: String by lazy {
|
||||
// Sy -->
|
||||
@@ -19,19 +25,26 @@ class GithubUpdateChecker {
|
||||
}
|
||||
|
||||
suspend fun checkForUpdate(): UpdateResult {
|
||||
val release = service.getLatestVersion(repo)
|
||||
|
||||
// Check if latest version is different from current version
|
||||
// SY -->
|
||||
val newVersion = release.version
|
||||
return if ((newVersion != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && newVersion != syDebugVersion)) {
|
||||
// SY <--
|
||||
GithubUpdateResult.NewUpdate(release)
|
||||
} else {
|
||||
GithubUpdateResult.NoNewUpdate()
|
||||
return withIOContext {
|
||||
networkService.client
|
||||
.newCall(GET("https://api.github.com/repos/$repo/releases/latest"))
|
||||
.await()
|
||||
.parseAs<GithubRelease>()
|
||||
.let {
|
||||
// Check if latest version is different from current version
|
||||
if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) {
|
||||
GithubUpdateResult.NewUpdate(it)
|
||||
} else {
|
||||
GithubUpdateResult.NoNewUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun isNewVersionSY(versionTag: String) = (versionTag != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && versionTag != syDebugVersion)
|
||||
// SY <--
|
||||
|
||||
private fun isNewVersion(versionTag: String): Boolean {
|
||||
// Removes prefixes like "r" or "v"
|
||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||
|
||||
@@ -19,10 +19,10 @@ import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.lang.launchNow
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.MERGED_SOURCE_ID
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.source.EH_SOURCE_ID
|
||||
import exh.source.EXH_SOURCE_ID
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import kotlinx.coroutines.async
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -154,7 +154,7 @@ class ExtensionManager(
|
||||
}
|
||||
|
||||
// EXH -->
|
||||
fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
|
||||
private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
|
||||
val blacklistEnabled = preferences.enableSourceBlacklist().get()
|
||||
return filter {
|
||||
if (it.isBlacklisted(blacklistEnabled)) {
|
||||
@@ -164,10 +164,7 @@ class ExtensionManager(
|
||||
}
|
||||
}
|
||||
|
||||
fun Extension.isBlacklisted(
|
||||
blacklistEnabled: Boolean =
|
||||
preferences.enableSourceBlacklist().get()
|
||||
): Boolean {
|
||||
fun Extension.isBlacklisted(blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()): Boolean {
|
||||
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
@@ -5,9 +5,12 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.source.BlacklistedSources
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@@ -17,18 +20,25 @@ import java.util.Date
|
||||
|
||||
internal class ExtensionGithubApi {
|
||||
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
suspend fun findExtensions(): List<Extension.Available> {
|
||||
val service: ExtensionGithubService = ExtensionGithubService.create()
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val response = service.getRepo()
|
||||
parseResponse(response)
|
||||
} /* SY --> */ + preferences.extensionRepos().get().flatMap {
|
||||
val url = "$BASE_URL$it/repo/"
|
||||
val response = service.getRepo("${url}index.min.json")
|
||||
parseResponse(response, url)
|
||||
return withIOContext {
|
||||
networkService.client
|
||||
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
|
||||
.await()
|
||||
.parseAs<JsonArray>()
|
||||
.let { parseResponse(it) }
|
||||
} /* SY --> */ + preferences.extensionRepos().get().flatMap { repoPath ->
|
||||
val url = "$BASE_URL$repoPath/repo/"
|
||||
networkService.client
|
||||
.newCall(GET("${url}index.min.json"))
|
||||
.await()
|
||||
.parseAs<JsonArray>()
|
||||
.let {
|
||||
parseResponse(it, url)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Url
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Used to get the extension repo listing from GitHub.
|
||||
*/
|
||||
interface ExtensionGithubService {
|
||||
|
||||
companion object {
|
||||
fun create(): ExtensionGithubService {
|
||||
val network: NetworkHelper by injectLazy()
|
||||
val adapter = Retrofit.Builder()
|
||||
.baseUrl(ExtensionGithubApi.BASE_URL)
|
||||
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
|
||||
.client(network.client)
|
||||
.build()
|
||||
|
||||
return adapter.create(ExtensionGithubService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@GET
|
||||
suspend fun getRepo(@Url url: String = "${ExtensionGithubApi.REPO_URL_PREFIX}index.min.json"): JsonArray
|
||||
// SY <--
|
||||
}
|
||||
@@ -18,6 +18,7 @@ sealed class Extension {
|
||||
override val versionCode: Int,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val pkgFactory: String?,
|
||||
val sources: List<Source>,
|
||||
val hasUpdate: Boolean = false,
|
||||
val isObsolete: Boolean = false,
|
||||
|
||||
@@ -31,6 +31,7 @@ internal object ExtensionLoader {
|
||||
|
||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||
const val LIB_VERSION_MIN = 1.2
|
||||
const val LIB_VERSION_MAX = 1.2
|
||||
@@ -184,7 +185,8 @@ internal object ExtensionLoader {
|
||||
versionCode,
|
||||
lang,
|
||||
isNsfw,
|
||||
sources,
|
||||
sources = sources,
|
||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||
isUnofficial = signatureHash != officialSignature
|
||||
)
|
||||
return LoadResult.Success(extension)
|
||||
|
||||
@@ -99,7 +99,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||
|
||||
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||
webview.settings.userAgentString = request.header("User-Agent")
|
||||
?: HttpSource.DEFAULT_USERAGENT
|
||||
?: HttpSource.DEFAULT_USER_AGENT
|
||||
|
||||
webview.webViewClient = object : WebViewClientCompat() {
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.fullType
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
fun Call.asObservable(): Observable<Response> {
|
||||
return Observable.unsafeCreate { subscriber ->
|
||||
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||
@@ -52,12 +60,12 @@ fun Call.asObservable(): Observable<Response> {
|
||||
}
|
||||
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
||||
suspend fun Call.await(): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (assertSuccess && !response.isSuccessful) {
|
||||
if (!response.isSuccessful) {
|
||||
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
||||
return
|
||||
}
|
||||
@@ -105,3 +113,12 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
|
||||
|
||||
return progressClient.newCall(request)
|
||||
}
|
||||
|
||||
inline fun <reified T> Response.parseAs(/* SY --> */ json: Json = Injekt.getInstance(fullType<Json>().type) /* SY <-- */): T {
|
||||
// Avoiding Injekt.get<Json>() due to compiler issues
|
||||
// val json = Injekt.getInstance<Json>(fullType<Json>().type)
|
||||
this.use {
|
||||
val responseBody = it.body?.string().orEmpty()
|
||||
return json.decodeFromString(responseBody)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class UserAgentInterceptor : Interceptor {
|
||||
val newRequest = originalRequest
|
||||
.newBuilder()
|
||||
.removeHeader("User-Agent")
|
||||
.addHeader("User-Agent", HttpSource.DEFAULT_USERAGENT)
|
||||
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
|
||||
.build()
|
||||
chain.proceed(newRequest)
|
||||
} else {
|
||||
|
||||
@@ -19,18 +19,19 @@ import eu.kanade.tachiyomi.source.online.english.HBrowse
|
||||
import eu.kanade.tachiyomi.source.online.english.HentaiCafe
|
||||
import eu.kanade.tachiyomi.source.online.english.Pururin
|
||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
||||
import exh.EH_SOURCE_ID
|
||||
import exh.EIGHTMUSES_SOURCE_ID
|
||||
import exh.EXH_SOURCE_ID
|
||||
import exh.HBROWSE_SOURCE_ID
|
||||
import exh.HENTAI_CAFE_SOURCE_ID
|
||||
import exh.PERV_EDEN_EN_SOURCE_ID
|
||||
import exh.PERV_EDEN_IT_SOURCE_ID
|
||||
import exh.PURURIN_SOURCE_ID
|
||||
import exh.TSUMINO_SOURCE_ID
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.source.EH_SOURCE_ID
|
||||
import exh.source.EIGHTMUSES_SOURCE_ID
|
||||
import exh.source.EXH_SOURCE_ID
|
||||
import exh.source.EnhancedHttpSource
|
||||
import exh.source.HBROWSE_SOURCE_ID
|
||||
import exh.source.HENTAI_CAFE_SOURCE_ID
|
||||
import exh.source.PERV_EDEN_EN_SOURCE_ID
|
||||
import exh.source.PERV_EDEN_IT_SOURCE_ID
|
||||
import exh.source.PURURIN_SOURCE_ID
|
||||
import exh.source.TSUMINO_SOURCE_ID
|
||||
import exh.source.handleSourceLibrary
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -119,8 +120,14 @@ open class SourceManager(private val context: Context) {
|
||||
source,
|
||||
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
|
||||
)
|
||||
val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.sourceId }
|
||||
currentDelegatedSources.plusAssign(map)
|
||||
|
||||
currentDelegatedSources[enhancedSource.originalSource.id] = DelegatedSource(
|
||||
enhancedSource.originalSource.name,
|
||||
enhancedSource.originalSource.id,
|
||||
enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName,
|
||||
(enhancedSource.enhancedSource as DelegatedHttpSource)::class,
|
||||
delegate.factory
|
||||
)
|
||||
enhancedSource
|
||||
} else source
|
||||
|
||||
@@ -137,6 +144,9 @@ open class SourceManager(private val context: Context) {
|
||||
|
||||
internal fun unregisterSource(source: Source) {
|
||||
sourcesMap.remove(source.id)
|
||||
// SY -->
|
||||
currentDelegatedSources.remove(source.id)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
private fun createInternalSources(): List<Source> = listOf(
|
||||
@@ -155,7 +165,7 @@ open class SourceManager(private val context: Context) {
|
||||
}
|
||||
// SY <--
|
||||
|
||||
private inner class StubSource(override val id: Long) : Source {
|
||||
inner class StubSource(override val id: Long) : Source {
|
||||
|
||||
override val name: String
|
||||
get() = id.toString()
|
||||
@@ -250,7 +260,7 @@ open class SourceManager(private val context: Context) {
|
||||
)
|
||||
).associateBy { it.originalSourceQualifiedClassName }
|
||||
|
||||
var currentDelegatedSources = mutableMapOf<Long, DelegatedSource>()
|
||||
val currentDelegatedSources = ListenMutableMap(mutableMapOf<Long, DelegatedSource>(), ::handleSourceLibrary)
|
||||
|
||||
data class DelegatedSource(
|
||||
val sourceName: String,
|
||||
@@ -260,5 +270,47 @@ open class SourceManager(private val context: Context) {
|
||||
val factory: Boolean = false
|
||||
)
|
||||
}
|
||||
|
||||
class ListenMutableMap<K, V>(private val internalMap: MutableMap<K, V>, val listener: () -> Unit) : MutableMap<K, V> {
|
||||
override val size: Int
|
||||
get() = internalMap.size
|
||||
override fun containsKey(key: K): Boolean = internalMap.containsKey(key)
|
||||
override fun containsValue(value: V): Boolean = internalMap.containsValue(value)
|
||||
override fun get(key: K): V? = get(key)
|
||||
override fun isEmpty(): Boolean = internalMap.isEmpty()
|
||||
override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
|
||||
get() = internalMap.entries
|
||||
override val keys: MutableSet<K>
|
||||
get() = internalMap.keys
|
||||
override val values: MutableCollection<V>
|
||||
get() = internalMap.values
|
||||
override fun clear() {
|
||||
val clearResult = internalMap.clear()
|
||||
listener()
|
||||
return clearResult
|
||||
}
|
||||
|
||||
override fun put(key: K, value: V): V? {
|
||||
val putResult = internalMap.put(key, value)
|
||||
if (putResult == null) {
|
||||
listener()
|
||||
}
|
||||
return putResult
|
||||
}
|
||||
|
||||
override fun putAll(from: Map<out K, V>) {
|
||||
internalMap.putAll(from)
|
||||
listener()
|
||||
}
|
||||
|
||||
override fun remove(key: K): V? {
|
||||
val removeResult = internalMap.remove(key)
|
||||
if (removeResult != null) {
|
||||
listener()
|
||||
}
|
||||
return removeResult
|
||||
}
|
||||
}
|
||||
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ open class Page(
|
||||
// SY -->
|
||||
var imageUrl = imageUrl
|
||||
get() {
|
||||
if (field == null) return null
|
||||
return DataSaver().compress(field!!)
|
||||
return field?.let { DataSaver.compress(it) }
|
||||
}
|
||||
// SY <--
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
|
||||
interface BrowseSourceFilterHeader {
|
||||
fun getFilterHeader(controller: Controller): RecyclerView.Adapter<*>
|
||||
interface BrowseSourceFilterHeader : CatalogueSource {
|
||||
fun getFilterHeader(controller: BaseController<*>): RecyclerView.Adapter<*>
|
||||
}
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import rx.Observable
|
||||
|
||||
interface FollowsSource {
|
||||
fun fetchFollows(): Observable<MangasPage>
|
||||
interface FollowsSource : CatalogueSource {
|
||||
suspend fun fetchFollows(): MangasPage
|
||||
|
||||
/**
|
||||
* Returns a list of all Follows retrieved by Coroutines
|
||||
*
|
||||
* @param SManga all smanga found for user
|
||||
*/
|
||||
fun fetchAllFollows(forceHd: Boolean = false): Flow<List<Pair<SManga, RaisedSearchMetadata>>>
|
||||
suspend fun fetchAllFollows(forceHd: Boolean = false): List<Pair<SManga, RaisedSearchMetadata>>
|
||||
|
||||
/**
|
||||
* updates the follow status for a manga
|
||||
*/
|
||||
fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Flow<Boolean>
|
||||
suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean
|
||||
|
||||
/**
|
||||
* Get a MdList Track of the manga
|
||||
*/
|
||||
fun fetchTrackingInfo(url: String): Flow<Track>
|
||||
suspend fun fetchTrackingInfo(url: String): Track
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", DEFAULT_USERAGENT)
|
||||
add("User-Agent", DEFAULT_USER_AGENT)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -415,6 +415,6 @@ abstract class HttpSource : CatalogueSource {
|
||||
// EXH <--
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)"
|
||||
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.app.Activity
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
interface LoginSource {
|
||||
interface LoginSource : Source {
|
||||
val needsLogin: Boolean
|
||||
|
||||
fun isLogged(): Boolean
|
||||
|
||||
@@ -11,8 +11,11 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||
import exh.util.executeOnIO
|
||||
import rx.Completable
|
||||
import rx.Single
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.reflect.KClass
|
||||
@@ -33,6 +36,8 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
*/
|
||||
fun parseIntoMetadata(metadata: M, input: I)
|
||||
|
||||
suspend fun parseInfoIntoMetadata(metadata: M, input: I) = parseIntoMetadata(metadata, input)
|
||||
|
||||
/**
|
||||
* Use reflection to create a new instance of metadata
|
||||
*/
|
||||
@@ -46,6 +51,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
*
|
||||
* Will also save the metadata to the DB if possible
|
||||
*/
|
||||
@Deprecated("Use the MangaInfo variant")
|
||||
fun parseToManga(manga: SManga, input: I): Completable {
|
||||
val mangaId = manga.id
|
||||
val metaObservable = if (mangaId != null) {
|
||||
@@ -71,6 +77,22 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun parseToManga(manga: MangaInfo, input: I): MangaInfo {
|
||||
val mangaId = manga.id()
|
||||
val metadata = if (mangaId != null) {
|
||||
val flatMetadata = db.getFlatMetadataForManga(mangaId).executeOnIO()
|
||||
flatMetadata?.raise(metaClass) ?: newMetaInstance()
|
||||
} else newMetaInstance()
|
||||
|
||||
parseInfoIntoMetadata(metadata, input)
|
||||
if (mangaId != null) {
|
||||
metadata.mangaId = mangaId
|
||||
db.insertFlatMetadataAsync(metadata.flatten()).await()
|
||||
}
|
||||
|
||||
return metadata.createMangaInfo(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to first get the metadata from the DB. If the metadata is not in the DB, calls the input
|
||||
* producer and parses the metadata from the input
|
||||
@@ -78,6 +100,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
* If the metadata needs to be parsed from the input producer, the resulting parsed metadata will
|
||||
* also be saved to the DB.
|
||||
*/
|
||||
@Deprecated("use fetchOrLoadMetadata made for MangaInfo")
|
||||
fun getOrLoadMetadata(mangaId: Long?, inputProducer: () -> Single<I>): Single<M> {
|
||||
val metaObservable = if (mangaId != null) {
|
||||
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
|
||||
@@ -103,8 +126,34 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to first get the metadata from the DB. If the metadata is not in the DB, calls the input
|
||||
* producer and parses the metadata from the input
|
||||
*
|
||||
* If the metadata needs to be parsed from the input producer, the resulting parsed metadata will
|
||||
* also be saved to the DB.
|
||||
*/
|
||||
suspend fun fetchOrLoadMetadata(mangaId: Long?, inputProducer: suspend () -> I): M {
|
||||
val meta = if (mangaId != null) {
|
||||
val flatMetadata = db.getFlatMetadataForManga(mangaId).executeOnIO()
|
||||
flatMetadata?.raise(metaClass)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return meta ?: inputProducer().let { input ->
|
||||
val newMeta = newMetaInstance()
|
||||
parseInfoIntoMetadata(newMeta, input)
|
||||
if (mangaId != null) {
|
||||
newMeta.mangaId = mangaId
|
||||
db.insertFlatMetadataAsync(newMeta.flatten()).await().let { newMeta }
|
||||
} else newMeta
|
||||
}
|
||||
}
|
||||
|
||||
fun getDescriptionAdapter(controller: MangaController): RecyclerView.Adapter<*>?
|
||||
|
||||
suspend fun MangaInfo.id() = db.getManga(key, id).executeOnIO()?.id
|
||||
val SManga.id get() = (this as? Manga)?.id
|
||||
val SChapter.mangaId get() = (this as? Chapter)?.manga_id
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
interface NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
|
||||
interface NamespaceSource : Source
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.source.Source
|
||||
|
||||
interface RandomMangaSource {
|
||||
fun fetchRandomMangaUrl(): Flow<String>
|
||||
interface RandomMangaSource : Source {
|
||||
suspend fun fetchRandomMangaUrl(): String
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user