mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-27 11:37:51 +02:00
Compare commits
147 Commits
Author | SHA1 | Date | |
---|---|---|---|
c9e1e6e020 | |||
ade73e6892 | |||
ee2aae7e3a | |||
b6011d4cf5 | |||
a31c6ff875 | |||
69baaac27e | |||
b16a90e9d9 | |||
f31aa622c0 | |||
4578edf157 | |||
33df35db1b | |||
093ddd776b | |||
da10b27219 | |||
5b4ed6f926 | |||
8fc467652d | |||
7971b64d57 | |||
9c1e2c3c45 | |||
909917e133 | |||
3b6c37a30b | |||
4a6e2a5d99 | |||
6cf84256fe | |||
876831480a | |||
aebc9a3b9e | |||
7b28614c37 | |||
4524c705da | |||
1f70be688a | |||
500eedaab7 | |||
2d2ff0a29d | |||
6d0689fe6c | |||
0b3dda18d3 | |||
09a8a494a0 | |||
11ac4df5d7 | |||
d352405ba6 | |||
a81609fd2c | |||
bf05952582 | |||
596a24fce8 | |||
9f20e40257 | |||
8be67a4431 | |||
58a2f7a874 | |||
cb92143613 | |||
08e26aa30d | |||
8e3ffe87b8 | |||
20e2bf9682 | |||
8512f97386 | |||
3ce880bc62 | |||
72ae243fa2 | |||
91829b0e7d | |||
7c3cd10696 | |||
24bdee626f | |||
6a30a75e3e | |||
ccdc336112 | |||
a4b71f4d11 | |||
c3f61e86b7 | |||
d8d93ee344 | |||
8ffff44454 | |||
568b90d0b4 | |||
46e09d174b | |||
1698a85e99 | |||
c9b62209c2 | |||
b280d6a76b | |||
29993e6412 | |||
2a5edf4547 | |||
d58c517a6c | |||
50136c319f | |||
2fb3b50535 | |||
4171e87b4b | |||
60b3036037 | |||
dfb2487640 | |||
97454ca162 | |||
4200409f79 | |||
b6a06189fb | |||
be521804c8 | |||
e95fcf6172 | |||
fbd2235a51 | |||
31b1b83606 | |||
a5d4f63281 | |||
328f9a70d3 | |||
df2b1dbeb1 | |||
f768393a4b | |||
c0a0d60c87 | |||
9cf5a4cac0 | |||
f21a030cf8 | |||
74e3d387eb | |||
8f83f497d5 | |||
6999fa858e | |||
8c1bedf796 | |||
1090c04fe3 | |||
33b04427d5 | |||
f7bb356abd | |||
e16bf0698e | |||
e6190683dd | |||
e08e41ae0d | |||
5f1a89df63 | |||
f15df40a54 | |||
a32e0e4ec5 | |||
3e8ac6b2d0 | |||
50a773f456 | |||
42484d718a | |||
81887000a8 | |||
987473df44 | |||
3680eb0bf5 | |||
3dbdc495e7 | |||
466515c801 | |||
e198f7e671 | |||
5fe1799dab | |||
ce7118084a | |||
06786322ca | |||
130b7501d1 | |||
864f001c3e | |||
1553ce973f | |||
72811e59f5 | |||
4c1da3575b | |||
05c0516a57 | |||
fe6dff9086 | |||
b6df5e6ee6 | |||
3ee5774870 | |||
c8fbb96f49 | |||
143303f7df | |||
585f7ec17d | |||
9beeca652f | |||
cd92569355 | |||
a82e1d0e45 | |||
5ad06df4ac | |||
5cfd5da338 | |||
b1d7167112 | |||
a475ecec4d | |||
5c98e020f4 | |||
eed295587d | |||
237af4b07d | |||
658860fdff | |||
21ba371a32 | |||
589160242e | |||
4de8b6e9a8 | |||
e79d536f33 | |||
9e90096328 | |||
f0a382c21a | |||
682a2c7546 | |||
2d1e85f280 | |||
dbec4fc15e | |||
95cd77e749 | |||
1f8126e2af | |||
86db7497e9 | |||
172305fc6a | |||
dad9dcd742 | |||
59b90a94d0 | |||
93fc5944f3 | |||
7039216eae | |||
7ba898f701 |
@ -5,13 +5,16 @@ android:
|
|||||||
- tools
|
- tools
|
||||||
|
|
||||||
# The BuildTools version used by your project
|
# The BuildTools version used by your project
|
||||||
- build-tools-23.0.3
|
- build-tools-24.0.2
|
||||||
- android-23
|
- android-24
|
||||||
- extra-android-m2repository
|
- extra-android-m2repository
|
||||||
- extra-google-m2repository
|
- extra-google-m2repository
|
||||||
- extra-android-support
|
- extra-android-support
|
||||||
- extra-google-google_play_services
|
- extra-google-google_play_services
|
||||||
|
|
||||||
|
jdk:
|
||||||
|
- oraclejdk8
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- chmod +x gradlew
|
- chmod +x gradlew
|
||||||
#Build, and run tests
|
#Build, and run tests
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
| Build | Download | Auto Update |
|
| Build | Download | Auto Update |
|
||||||
|-------|----------|-------------|
|
|-------|----------|-------------|
|
||||||
| [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-debug-versions) |
|
| [](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [](https://travis-ci.org/inorichi/tachiyomi) | [](https://github.com/inorichi/tachiyomi/releases) [](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) |
|
||||||
|
|
||||||
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
2
app/.gitignore
vendored
2
app/.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
/build
|
/build
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
.idea
|
custom.gradle
|
122
app/build.gradle
122
app/build.gradle
@ -4,6 +4,10 @@ apply plugin: 'com.android.application'
|
|||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
|
||||||
|
if (file("custom.gradle").exists()) {
|
||||||
|
apply from: "custom.gradle"
|
||||||
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
// Git is needed in your system PATH for these commands to work.
|
// 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
|
// If it's not installed, you can return a random value as a workaround
|
||||||
@ -29,18 +33,17 @@ def includeUpdater() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 23
|
compileSdkVersion 24
|
||||||
buildToolsVersion "23.0.3"
|
buildToolsVersion "24.0.2"
|
||||||
publishNonDefault true
|
publishNonDefault true
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "eu.kanade.tachiyomi"
|
applicationId "eu.kanade.tachiyomi"
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 23
|
targetSdkVersion 24
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 8
|
versionCode 12
|
||||||
versionCode project.findProperty('versionCode')?.toInteger() ?: 8
|
versionName "0.3.1"
|
||||||
versionName "0.2.2"
|
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@ -48,16 +51,22 @@ android {
|
|||||||
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi", "armeabi-v7a", "x86"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
versionNameSuffix ".${getCommitCount()}"
|
versionNameSuffix "-${getCommitCount()}"
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
|
multiDexEnabled true
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
|
multiDexEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,62 +88,54 @@ android {
|
|||||||
main.java.srcDirs += 'src/main/kotlin'
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
}
|
}
|
||||||
|
|
||||||
// http://stackoverflow.com/questions/32759529/androidhttpclient-not-found-when-running-robolectric
|
|
||||||
useLibrary 'org.apache.http.legacy'
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
kapt {
|
|
||||||
generateStubs = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
final SUPPORT_LIBRARY_VERSION = '23.4.0'
|
|
||||||
final DAGGER_VERSION = '2.4'
|
|
||||||
final RETROFIT_VERSION = '2.0.2'
|
|
||||||
final NUCLEUS_VERSION = '3.0.0'
|
|
||||||
final STORIO_VERSION = '1.8.0'
|
|
||||||
final MOCKITO_VERSION = '1.10.19'
|
|
||||||
|
|
||||||
// Modified dependencies
|
// Modified dependencies
|
||||||
compile 'com.github.inorichi:subsampling-scale-image-view:421fb81'
|
compile 'com.github.inorichi:subsampling-scale-image-view:2d9c854'
|
||||||
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
|
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION"
|
final support_library_version = '24.2.1'
|
||||||
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:support-v4:$support_library_version"
|
||||||
compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:appcompat-v7:$support_library_version"
|
||||||
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:cardview-v7:$support_library_version"
|
||||||
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:design:$support_library_version"
|
||||||
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:recyclerview-v7:$support_library_version"
|
||||||
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:support-annotations:$support_library_version"
|
||||||
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:customtabs:$support_library_version"
|
||||||
compile "com.android.support:customtabs:$SUPPORT_LIBRARY_VERSION"
|
|
||||||
|
compile 'com.android.support:multidex:1.0.1'
|
||||||
|
|
||||||
|
compile 'com.google.android.gms:play-services-gcm:9.6.1'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.2.0'
|
compile 'io.reactivex:rxandroid:1.2.1'
|
||||||
compile 'io.reactivex:rxjava:1.1.5'
|
compile 'io.reactivex:rxjava:1.2.1'
|
||||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
compile "com.squareup.okhttp3:okhttp:3.3.1"
|
compile "com.squareup.okhttp3:okhttp:3.4.1"
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
|
final retrofit_version = '2.1.0'
|
||||||
compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
|
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION"
|
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||||
|
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
|
|
||||||
// IO
|
// IO
|
||||||
compile 'com.squareup.okio:okio:1.8.0'
|
compile 'com.squareup.okio:okio:1.10.0'
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
compile 'com.google.code.gson:gson:2.6.2'
|
compile 'com.google.code.gson:gson:2.7'
|
||||||
|
compile 'com.github.salomonbrys.kotson:kotson:2.4.0'
|
||||||
|
|
||||||
// YAML
|
// YAML
|
||||||
compile 'org.yaml:snakeyaml:1.17'
|
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
compile 'com.squareup.duktape:duktape-android:0.9.5'
|
compile 'com.squareup.duktape:duktape-android:1.0.0'
|
||||||
|
|
||||||
// Disk cache
|
// Disk cache
|
||||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||||
@ -146,52 +147,53 @@ dependencies {
|
|||||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
compile "com.pushtorefresh.storio:sqlite:1.11.0"
|
||||||
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
|
|
||||||
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
|
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
compile "info.android15.nucleus:nucleus:$NUCLEUS_VERSION"
|
final nucleus_version = '3.0.0'
|
||||||
compile "info.android15.nucleus:nucleus-support-v4:$NUCLEUS_VERSION"
|
compile "info.android15.nucleus:nucleus:$nucleus_version"
|
||||||
compile "info.android15.nucleus:nucleus-support-v7:$NUCLEUS_VERSION"
|
compile "info.android15.nucleus:nucleus-support-v4:$nucleus_version"
|
||||||
|
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
compile "com.google.dagger:dagger:$DAGGER_VERSION"
|
compile "uy.kohesive.injekt:injekt-core:1.16.1"
|
||||||
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
|
||||||
provided 'org.glassfish:javax.annotation:10.0-b28'
|
|
||||||
|
|
||||||
// Image library
|
// Image library
|
||||||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
compile 'com.github.bumptech.glide:glide:3.7.0'
|
||||||
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
|
||||||
|
// Transformations
|
||||||
|
compile 'jp.wasabeef:glide-transformations:2.0.1'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
compile 'com.jakewharton.timber:timber:4.1.2'
|
compile 'com.jakewharton.timber:timber:4.3.1'
|
||||||
|
|
||||||
// Crash reports
|
// Crash reports
|
||||||
compile 'ch.acra:acra:4.8.5'
|
compile 'ch.acra:acra:4.9.1'
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
|
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||||
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||||
compile 'eu.davidea:flexible-adapter:4.2.0'
|
compile 'eu.davidea:flexible-adapter:4.2.0'
|
||||||
compile 'com.nononsenseapps:filepicker:2.5.2'
|
compile 'com.nononsenseapps:filepicker:2.5.2'
|
||||||
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
||||||
compile 'com.afollestad.material-dialogs:core:0.8.5.9'
|
compile 'com.afollestad.material-dialogs:core:0.9.0.2'
|
||||||
|
compile 'net.xpece.android:support-preference:1.0.3'
|
||||||
|
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||||
|
compile 'de.hdodenhof:circleimageview:2.1.0'
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
testCompile 'org.assertj:assertj-core:1.7.1'
|
testCompile 'org.assertj:assertj-core:1.7.1'
|
||||||
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION"
|
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||||
testCompile('org.robolectric:robolectric:3.0') {
|
testCompile 'org.robolectric:robolectric:3.1.2'
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
testCompile 'org.robolectric:shadows-multidex:3.1.2'
|
||||||
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
|
testCompile 'org.robolectric:shadows-play-services:3.1.2'
|
||||||
}
|
|
||||||
|
|
||||||
kaptTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.0.2'
|
ext.kotlin_version = '1.0.4'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@ -1,6 +1,6 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-keep class eu.kanade.tachiyomi.injection.** { *; }
|
-keep class eu.kanade.tachiyomi.**
|
||||||
|
|
||||||
# OkHttp
|
# OkHttp
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest
|
||||||
package="eu.kanade.tachiyomi">
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="eu.kanade.tachiyomi">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
@ -8,6 +10,8 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
|
||||||
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
@ -18,8 +22,7 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:theme="@style/Theme.Tachiyomi" >
|
android:theme="@style/Theme.Tachiyomi" >
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity">
|
||||||
android:theme="@style/Theme.BrandedLaunch">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@ -28,7 +31,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.manga.MangaActivity"
|
android:name=".ui.manga.MangaActivity"
|
||||||
android:parentActivityName=".ui.main.MainActivity" >
|
android:parentActivityName=".ui.main.MainActivity"
|
||||||
|
android:exported="true">
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
@ -59,47 +63,33 @@
|
|||||||
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
<service android:name=".data.mangasync.UpdateMangaSyncService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
||||||
<receiver
|
<service
|
||||||
android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
|
android:name=".data.library.LibraryUpdateTrigger"
|
||||||
android:enabled="false">
|
android:exported="true"
|
||||||
|
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</service>
|
||||||
|
|
||||||
<receiver
|
<service
|
||||||
android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected"
|
android:name=".data.updater.UpdateCheckerService"
|
||||||
android:enabled="false">
|
android:exported="true"
|
||||||
|
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
|
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</service>
|
||||||
|
|
||||||
|
<service android:name=".data.updater.UpdateDownloaderService"
|
||||||
|
android:exported="false"/>
|
||||||
|
|
||||||
|
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
|
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".data.updater.UpdateDownloader$InstallOnReceived">
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".data.library.LibraryUpdateAlarm">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
|
||||||
<action android:name="eu.kanade.UPDATE_LIBRARY" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".data.updater.UpdateDownloaderAlarm">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
|
||||||
<action android:name="eu.kanade.CHECK_UPDATE"/>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||||
android:value="GlideModule" />
|
android:value="GlideModule" />
|
||||||
|
@ -2,14 +2,13 @@ package eu.kanade.tachiyomi
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import android.support.multidex.MultiDex
|
||||||
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector
|
|
||||||
import eu.kanade.tachiyomi.injection.component.AppComponent
|
|
||||||
import eu.kanade.tachiyomi.injection.component.DaggerAppComponent
|
|
||||||
import eu.kanade.tachiyomi.injection.module.AppModule
|
|
||||||
import org.acra.ACRA
|
import org.acra.ACRA
|
||||||
import org.acra.annotation.ReportsCrashes
|
import org.acra.annotation.ReportsCrashes
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.InjektScope
|
||||||
|
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||||
|
|
||||||
@ReportsCrashes(
|
@ReportsCrashes(
|
||||||
formUri = "http://tachiyomi.kanade.eu/crash_report",
|
formUri = "http://tachiyomi.kanade.eu/crash_report",
|
||||||
@ -20,43 +19,25 @@ import timber.log.Timber
|
|||||||
)
|
)
|
||||||
open class App : Application() {
|
open class App : Application() {
|
||||||
|
|
||||||
lateinit var component: AppComponent
|
|
||||||
private set
|
|
||||||
|
|
||||||
lateinit var componentReflection: ComponentReflectionInjector<AppComponent>
|
|
||||||
private set
|
|
||||||
|
|
||||||
var appTheme = 0
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
Injekt = InjektScope(DefaultRegistrar())
|
||||||
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
component = prepareAppComponent().build()
|
|
||||||
|
|
||||||
componentReflection = ComponentReflectionInjector(AppComponent::class.java, component)
|
|
||||||
|
|
||||||
setupTheme()
|
|
||||||
setupAcra()
|
setupAcra()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupTheme() {
|
override fun attachBaseContext(base: Context) {
|
||||||
appTheme = PreferencesHelper.getTheme(this)
|
super.attachBaseContext(base)
|
||||||
}
|
if (BuildConfig.DEBUG) {
|
||||||
|
MultiDex.install(this)
|
||||||
protected open fun prepareAppComponent(): DaggerAppComponent.Builder {
|
}
|
||||||
return DaggerAppComponent.builder()
|
|
||||||
.appModule(AppModule(this))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupAcra() {
|
protected open fun setupAcra() {
|
||||||
ACRA.init(this)
|
ACRA.init(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun get(context: Context): App {
|
|
||||||
return context.applicationContext as App
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
41
app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
Normal file
41
app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||||
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
|
import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
|
|
||||||
|
class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
|
|
||||||
|
addSingletonFactory { PreferencesHelper(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { DatabaseHelper(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { ChapterCache(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { CoverCache(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { NetworkHelper(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { SourceManager(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { DownloadManager(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { MangaSyncManager(app) }
|
||||||
|
|
||||||
|
addSingletonFactory { Gson() }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,14 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.*
|
import com.google.gson.*
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
|
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
|
||||||
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
|
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.lang.reflect.Type
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -191,8 +190,7 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
private fun restoreCategories(jsonCategories: JsonArray) {
|
private fun restoreCategories(jsonCategories: JsonArray) {
|
||||||
// Get categories from file and from db
|
// Get categories from file and from db
|
||||||
val dbCategories = db.getCategories().executeAsBlocking()
|
val dbCategories = db.getCategories().executeAsBlocking()
|
||||||
val backupCategories = getArrayOrEmpty<Category>(jsonCategories,
|
val backupCategories = gson.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||||
object : TypeToken<List<Category>>() {}.type)
|
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
for (category in backupCategories) {
|
for (category in backupCategories) {
|
||||||
@ -224,17 +222,13 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
|
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
|
||||||
*/
|
*/
|
||||||
private fun restoreMangas(jsonMangas: JsonArray) {
|
private fun restoreMangas(jsonMangas: JsonArray) {
|
||||||
val chapterToken = object : TypeToken<List<Chapter>>() {}.type
|
|
||||||
val mangaSyncToken = object : TypeToken<List<MangaSync>>() {}.type
|
|
||||||
val categoriesNamesToken = object : TypeToken<List<String>>() {}.type
|
|
||||||
|
|
||||||
for (backupManga in jsonMangas) {
|
for (backupManga in jsonMangas) {
|
||||||
// Map every entry to objects
|
// Map every entry to objects
|
||||||
val element = backupManga.asJsonObject
|
val element = backupManga.asJsonObject
|
||||||
val manga = gson.fromJson(element.get(MANGA), Manga::class.java)
|
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
|
||||||
val chapters = getArrayOrEmpty<Chapter>(element.get(CHAPTERS), chapterToken)
|
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
|
||||||
val sync = getArrayOrEmpty<MangaSync>(element.get(MANGA_SYNC), mangaSyncToken)
|
val sync = gson.fromJson<List<MangaSyncImpl>>(element.get(MANGA_SYNC) ?: JsonArray())
|
||||||
val categories = getArrayOrEmpty<String>(element.get(CATEGORIES), categoriesNamesToken)
|
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
|
||||||
|
|
||||||
// Restore everything related to this manga
|
// Restore everything related to this manga
|
||||||
restoreManga(manga)
|
restoreManga(manga)
|
||||||
@ -340,7 +334,7 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
|
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
|
||||||
// Fix foreign keys with the current manga id
|
// Fix foreign keys with the current manga id
|
||||||
for (mangaSync in sync) {
|
for (mangaSync in sync) {
|
||||||
mangaSync.manga_id = manga.id
|
mangaSync.manga_id = manga.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
|
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
|
||||||
@ -367,15 +361,4 @@ class BackupManager(private val db: DatabaseHelper) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of items from a json element, or an empty list if the element is null.
|
|
||||||
*
|
|
||||||
* @param element the json to be mapped to a list of items.
|
|
||||||
* @param type the gson mapping to restore the list.
|
|
||||||
* @return a list of items.
|
|
||||||
*/
|
|
||||||
private fun <T> getArrayOrEmpty(element: JsonElement?, type: Type): List<T> {
|
|
||||||
return gson.fromJson<List<T>>(element, type) ?: ArrayList<T>()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.*
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
import eu.kanade.tachiyomi.data.database.queries.*
|
import eu.kanade.tachiyomi.data.database.queries.*
|
||||||
|
|
||||||
@ -9,15 +10,16 @@ import eu.kanade.tachiyomi.data.database.queries.*
|
|||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context)
|
open class DatabaseHelper(context: Context)
|
||||||
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries {
|
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||||
|
|
||||||
override val db = DefaultStorIOSQLite.builder()
|
override val db = DefaultStorIOSQLite.builder()
|
||||||
.sqliteOpenHelper(DbOpenHelper(context))
|
.sqliteOpenHelper(DbOpenHelper(context))
|
||||||
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
|
.addTypeMapping(Manga::class.java, MangaTypeMapping())
|
||||||
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
|
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
|
||||||
.addTypeMapping(MangaSync::class.java, MangaSyncSQLiteTypeMapping())
|
.addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping())
|
||||||
.addTypeMapping(Category::class.java, CategorySQLiteTypeMapping())
|
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||||
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
|
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||||
|
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
||||||
|
@ -3,23 +3,23 @@ package eu.kanade.tachiyomi.data.database
|
|||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
|
||||||
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
|
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
|
||||||
internal().beginTransaction()
|
lowLevel().beginTransaction()
|
||||||
try {
|
try {
|
||||||
block()
|
block()
|
||||||
internal().setTransactionSuccessful()
|
lowLevel().setTransactionSuccessful()
|
||||||
} finally {
|
} finally {
|
||||||
internal().endTransaction()
|
lowLevel().endTransaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
||||||
internal().beginTransaction()
|
lowLevel().beginTransaction()
|
||||||
try {
|
try {
|
||||||
val result = block()
|
val result = block()
|
||||||
internal().setTransactionSuccessful()
|
lowLevel().setTransactionSuccessful()
|
||||||
return result
|
return result
|
||||||
} finally {
|
} finally {
|
||||||
internal().endTransaction()
|
lowLevel().endTransaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 2
|
const val DATABASE_VERSION = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||||
@ -26,11 +26,13 @@ class DbOpenHelper(context: Context)
|
|||||||
execSQL(MangaSyncTable.createTableQuery)
|
execSQL(MangaSyncTable.createTableQuery)
|
||||||
execSQL(CategoryTable.createTableQuery)
|
execSQL(CategoryTable.createTableQuery)
|
||||||
execSQL(MangaCategoryTable.createTableQuery)
|
execSQL(MangaCategoryTable.createTableQuery)
|
||||||
|
execSQL(HistoryTable.createTableQuery)
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
execSQL(MangaTable.createUrlIndexQuery)
|
execSQL(MangaTable.createUrlIndexQuery)
|
||||||
execSQL(MangaTable.createFavoriteIndexQuery)
|
execSQL(MangaTable.createFavoriteIndexQuery)
|
||||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
execSQL(ChapterTable.createMangaIdIndexQuery)
|
||||||
|
execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
@ -41,10 +43,15 @@ class DbOpenHelper(context: Context)
|
|||||||
db.execSQL("""UPDATE mangas SET thumbnail_url =
|
db.execSQL("""UPDATE mangas SET thumbnail_url =
|
||||||
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
|
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 3) {
|
||||||
|
// Initialize history tables
|
||||||
|
db.execSQL(HistoryTable.createTableQuery)
|
||||||
|
db.execSQL(HistoryTable.createChapterIdIndexQuery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SQLiteDatabase) {
|
override fun onConfigure(db: SQLiteDatabase) {
|
||||||
db.setForeignKeyConstraintsEnabled(true)
|
db.setForeignKeyConstraintsEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_FLAGS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
|
||||||
|
|
||||||
|
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
|
||||||
|
CategoryPutResolver(),
|
||||||
|
CategoryGetResolver(),
|
||||||
|
CategoryDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class CategoryPutResolver : DefaultPutResolver<Category>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
||||||
|
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
|
||||||
|
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||||
|
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
||||||
|
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_CHAPTER_NUMBER
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_FETCH
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_UPLOAD
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
|
||||||
|
|
||||||
|
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
|
||||||
|
ChapterPutResolver(),
|
||||||
|
ChapterGetResolver(),
|
||||||
|
ChapterDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: Chapter) = ContentValues(10).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_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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
||||||
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
|
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
||||||
|
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
||||||
|
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
||||||
|
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
||||||
|
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
||||||
|
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
||||||
|
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
|
||||||
|
|
||||||
|
class HistoryTypeMapping : SQLiteTypeMapping<History>(
|
||||||
|
HistoryPutResolver(),
|
||||||
|
HistoryGetResolver(),
|
||||||
|
HistoryDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
open class HistoryPutResolver : DefaultPutResolver<History>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): History = History().apply {
|
||||||
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
|
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
||||||
|
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
||||||
|
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_CATEGORY_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
|
||||||
|
|
||||||
|
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
|
||||||
|
MangaCategoryPutResolver(),
|
||||||
|
MangaCategoryGetResolver(),
|
||||||
|
MangaCategoryDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
||||||
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
|
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_LAST_CHAPTER_READ
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_REMOTE_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SCORE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_STATUS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SYNC_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TITLE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TOTAL_CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.TABLE
|
||||||
|
|
||||||
|
class MangaSyncTypeMapping : SQLiteTypeMapping<MangaSync>(
|
||||||
|
MangaSyncPutResolver(),
|
||||||
|
MangaSyncGetResolver(),
|
||||||
|
MangaSyncDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: MangaSync) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: MangaSync) = ContentValues(9).apply {
|
||||||
|
put(COL_ID, obj.id)
|
||||||
|
put(COL_MANGA_ID, obj.manga_id)
|
||||||
|
put(COL_SYNC_ID, obj.sync_id)
|
||||||
|
put(COL_REMOTE_ID, obj.remote_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_SCORE, obj.score)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().apply {
|
||||||
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
|
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
||||||
|
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
||||||
|
remote_id = cursor.getInt(cursor.getColumnIndex(COL_REMOTE_ID))
|
||||||
|
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||||
|
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
||||||
|
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
||||||
|
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||||
|
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaSyncDeleteResolver : DefaultDeleteResolver<MangaSync>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: MangaSync) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_TITLE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_URL
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
|
||||||
|
|
||||||
|
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
|
||||||
|
MangaPutResolver(),
|
||||||
|
MangaGetResolver(),
|
||||||
|
MangaDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
|
||||||
|
put(COL_ID, obj.id)
|
||||||
|
put(COL_SOURCE, obj.source)
|
||||||
|
put(COL_URL, obj.url)
|
||||||
|
put(COL_ARTIST, obj.artist)
|
||||||
|
put(COL_AUTHOR, obj.author)
|
||||||
|
put(COL_DESCRIPTION, obj.description)
|
||||||
|
put(COL_GENRE, obj.genre)
|
||||||
|
put(COL_TITLE, obj.title)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class MangaGetResolver : DefaultGetResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
|
||||||
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
|
source = cursor.getInt(cursor.getColumnIndex(COL_SOURCE))
|
||||||
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
|
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
||||||
|
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
||||||
|
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
|
||||||
|
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
|
||||||
|
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||||
|
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||||
|
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
||||||
|
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||||
|
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||||
|
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||||
|
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
@ -1,57 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models;
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
|
||||||
|
|
||||||
@StorIOSQLiteType(table = CategoryTable.TABLE)
|
|
||||||
public class Category implements Serializable {
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COL_ID, key = true)
|
|
||||||
public Integer id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COL_NAME)
|
|
||||||
public String name;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COL_ORDER)
|
|
||||||
public int order;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COL_FLAGS)
|
|
||||||
public int flags;
|
|
||||||
|
|
||||||
public Category() {}
|
|
||||||
|
|
||||||
public static Category create(String name) {
|
|
||||||
Category c = new Category();
|
|
||||||
c.name = name;
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Category createDefault() {
|
|
||||||
Category c = create("Default");
|
|
||||||
c.id = 0;
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNameLower() {
|
|
||||||
return name.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
Category category = (Category) o;
|
|
||||||
|
|
||||||
return name.equals(category.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return name.hashCode();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface Category : Serializable {
|
||||||
|
|
||||||
|
var id: Int?
|
||||||
|
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
var order: Int
|
||||||
|
|
||||||
|
var flags: Int
|
||||||
|
|
||||||
|
val nameLower: String
|
||||||
|
get() = name.toLowerCase()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun create(name: String): Category = CategoryImpl().apply {
|
||||||
|
this.name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDefault(): Category = create("Default").apply { id = 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class CategoryImpl : Category {
|
||||||
|
|
||||||
|
override var id: Int? = null
|
||||||
|
|
||||||
|
override lateinit var name: String
|
||||||
|
|
||||||
|
override var order: Int = 0
|
||||||
|
|
||||||
|
override var flags: Int = 0
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
|
val category = other as Category
|
||||||
|
|
||||||
|
return name == category.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return name.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,94 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models;
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
|
||||||
import eu.kanade.tachiyomi.util.UrlUtil;
|
|
||||||
|
|
||||||
@StorIOSQLiteType(table = ChapterTable.TABLE)
|
|
||||||
public class Chapter implements Serializable {
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_ID, key = true)
|
|
||||||
public Long id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_MANGA_ID)
|
|
||||||
public Long manga_id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_URL)
|
|
||||||
public String url;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_NAME)
|
|
||||||
public String name;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_READ)
|
|
||||||
public boolean read;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_LAST_PAGE_READ)
|
|
||||||
public int last_page_read;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_DATE_FETCH)
|
|
||||||
public long date_fetch;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_DATE_UPLOAD)
|
|
||||||
public long date_upload;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_CHAPTER_NUMBER)
|
|
||||||
public float chapter_number;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_SOURCE_ORDER)
|
|
||||||
public int source_order;
|
|
||||||
|
|
||||||
public int status;
|
|
||||||
|
|
||||||
private transient List<Page> pages;
|
|
||||||
|
|
||||||
public Chapter() {}
|
|
||||||
|
|
||||||
public void setUrl(String url) {
|
|
||||||
this.url = UrlUtil.getPath(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
Chapter chapter = (Chapter) o;
|
|
||||||
|
|
||||||
return url.equals(chapter.url);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return url.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Chapter create() {
|
|
||||||
Chapter chapter = new Chapter();
|
|
||||||
chapter.chapter_number = -1;
|
|
||||||
return chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Page> getPages() {
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPages(List<Page> pages) {
|
|
||||||
this.pages = pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDownloaded() {
|
|
||||||
return status == Download.DOWNLOADED;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRecognizedNumber() {
|
|
||||||
return chapter_number >= 0f;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,36 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface Chapter : Serializable {
|
||||||
|
|
||||||
|
var id: Long?
|
||||||
|
|
||||||
|
var manga_id: Long?
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
var read: Boolean
|
||||||
|
|
||||||
|
var last_page_read: Int
|
||||||
|
|
||||||
|
var date_fetch: Long
|
||||||
|
|
||||||
|
var date_upload: Long
|
||||||
|
|
||||||
|
var chapter_number: Float
|
||||||
|
|
||||||
|
var source_order: Int
|
||||||
|
|
||||||
|
val isRecognizedNumber: Boolean
|
||||||
|
get() = chapter_number >= 0f
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun create(): Chapter = ChapterImpl().apply {
|
||||||
|
chapter_number = -1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class ChapterImpl : Chapter {
|
||||||
|
|
||||||
|
override var id: Long? = null
|
||||||
|
|
||||||
|
override var manga_id: Long? = null
|
||||||
|
|
||||||
|
override lateinit var url: String
|
||||||
|
|
||||||
|
override lateinit var name: String
|
||||||
|
|
||||||
|
override var read: Boolean = false
|
||||||
|
|
||||||
|
override var last_page_read: Int = 0
|
||||||
|
|
||||||
|
override var date_fetch: Long = 0
|
||||||
|
|
||||||
|
override var date_upload: Long = 0
|
||||||
|
|
||||||
|
override var chapter_number: Float = 0f
|
||||||
|
|
||||||
|
override var source_order: Int = 0
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
|
val chapter = other as Chapter
|
||||||
|
|
||||||
|
return url == chapter.url
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return url.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object containing the history statistics of a chapter
|
||||||
|
*/
|
||||||
|
class History : Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of history object.
|
||||||
|
*/
|
||||||
|
var id: Long? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter id of history object.
|
||||||
|
*/
|
||||||
|
var chapter_id: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last time chapter was read in time long format
|
||||||
|
*/
|
||||||
|
var last_read: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total time chapter was read - todo not yet implemented
|
||||||
|
*/
|
||||||
|
var time_read: Long = 0
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History constructor
|
||||||
|
*
|
||||||
|
* @param chapter chapter object
|
||||||
|
* @return history object
|
||||||
|
*/
|
||||||
|
fun create(chapter: Chapter): History {
|
||||||
|
val history = History()
|
||||||
|
history.chapter_id = chapter.id!!
|
||||||
|
return history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,213 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.R;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
|
||||||
import eu.kanade.tachiyomi.util.UrlUtil;
|
|
||||||
|
|
||||||
@StorIOSQLiteType(table = MangaTable.TABLE)
|
|
||||||
public class Manga implements Serializable {
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_ID, key = true)
|
|
||||||
public Long id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_SOURCE)
|
|
||||||
public int source;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_URL)
|
|
||||||
public String url;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_ARTIST)
|
|
||||||
public String artist;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_AUTHOR)
|
|
||||||
public String author;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_DESCRIPTION)
|
|
||||||
public String description;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_GENRE)
|
|
||||||
public String genre;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_TITLE)
|
|
||||||
public String title;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_STATUS)
|
|
||||||
public int status;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_THUMBNAIL_URL)
|
|
||||||
public String thumbnail_url;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_FAVORITE)
|
|
||||||
public boolean favorite;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_LAST_UPDATE)
|
|
||||||
public long last_update;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_INITIALIZED)
|
|
||||||
public boolean initialized;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_VIEWER)
|
|
||||||
public int viewer;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_CHAPTER_FLAGS)
|
|
||||||
public int chapter_flags;
|
|
||||||
|
|
||||||
public transient int unread;
|
|
||||||
|
|
||||||
public transient int category;
|
|
||||||
|
|
||||||
public static final int UNKNOWN = 0;
|
|
||||||
public static final int ONGOING = 1;
|
|
||||||
public static final int COMPLETED = 2;
|
|
||||||
public static final int LICENSED = 3;
|
|
||||||
|
|
||||||
public static final int SORT_DESC = 0x00000000;
|
|
||||||
public static final int SORT_ASC = 0x00000001;
|
|
||||||
public static final int SORT_MASK = 0x00000001;
|
|
||||||
|
|
||||||
// Generic filter that does not filter anything
|
|
||||||
public static final int SHOW_ALL = 0x00000000;
|
|
||||||
|
|
||||||
public static final int SHOW_UNREAD = 0x00000002;
|
|
||||||
public static final int SHOW_READ = 0x00000004;
|
|
||||||
public static final int READ_MASK = 0x00000006;
|
|
||||||
|
|
||||||
public static final int SHOW_DOWNLOADED = 0x00000008;
|
|
||||||
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
|
|
||||||
public static final int DOWNLOADED_MASK = 0x00000018;
|
|
||||||
|
|
||||||
public static final int SORTING_SOURCE = 0x00000000;
|
|
||||||
public static final int SORTING_NUMBER = 0x00000100;
|
|
||||||
public static final int SORTING_MASK = 0x00000100;
|
|
||||||
|
|
||||||
public static final int DISPLAY_NAME = 0x00000000;
|
|
||||||
public static final int DISPLAY_NUMBER = 0x00100000;
|
|
||||||
public static final int DISPLAY_MASK = 0x00100000;
|
|
||||||
|
|
||||||
public Manga() {}
|
|
||||||
|
|
||||||
public static Manga create(String pathUrl) {
|
|
||||||
Manga m = new Manga();
|
|
||||||
m.url = pathUrl;
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Manga create(String pathUrl, int source) {
|
|
||||||
Manga m = new Manga();
|
|
||||||
m.url = pathUrl;
|
|
||||||
m.source = source;
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUrl(String url) {
|
|
||||||
this.url = UrlUtil.getPath(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void copyFrom(Manga other) {
|
|
||||||
if (other.title != null)
|
|
||||||
title = other.title;
|
|
||||||
|
|
||||||
if (other.author != null)
|
|
||||||
author = other.author;
|
|
||||||
|
|
||||||
if (other.artist != null)
|
|
||||||
artist = other.artist;
|
|
||||||
|
|
||||||
if (other.url != null)
|
|
||||||
url = other.url;
|
|
||||||
|
|
||||||
if (other.description != null)
|
|
||||||
description = other.description;
|
|
||||||
|
|
||||||
if (other.genre != null)
|
|
||||||
genre = other.genre;
|
|
||||||
|
|
||||||
if (other.thumbnail_url != null)
|
|
||||||
thumbnail_url = other.thumbnail_url;
|
|
||||||
|
|
||||||
status = other.status;
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStatus(Context context) {
|
|
||||||
switch (status) {
|
|
||||||
case ONGOING:
|
|
||||||
return context.getString(R.string.ongoing);
|
|
||||||
case COMPLETED:
|
|
||||||
return context.getString(R.string.completed);
|
|
||||||
case LICENSED:
|
|
||||||
return context.getString(R.string.licensed);
|
|
||||||
default:
|
|
||||||
return context.getString(R.string.unknown);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setChapterOrder(int order) {
|
|
||||||
setFlags(order, SORT_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDisplayMode(int mode) {
|
|
||||||
setFlags(mode, DISPLAY_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReadFilter(int filter) {
|
|
||||||
setFlags(filter, READ_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDownloadedFilter(int filter) {
|
|
||||||
setFlags(filter, DOWNLOADED_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSorting(int sort) {
|
|
||||||
setFlags(sort, SORTING_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setFlags(int flag, int mask) {
|
|
||||||
chapter_flags = (chapter_flags & ~mask) | (flag & mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean sortDescending() {
|
|
||||||
return (chapter_flags & SORT_MASK) == SORT_DESC;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
|
||||||
public int getDisplayMode() {
|
|
||||||
return chapter_flags & DISPLAY_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getReadFilter() {
|
|
||||||
return chapter_flags & READ_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDownloadedFilter() {
|
|
||||||
return chapter_flags & DOWNLOADED_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSorting() {
|
|
||||||
return chapter_flags & SORTING_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
Manga manga = (Manga) o;
|
|
||||||
|
|
||||||
return url.equals(manga.url);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return url.hashCode();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,131 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface Manga : Serializable {
|
||||||
|
|
||||||
|
var id: Long?
|
||||||
|
|
||||||
|
var source: Int
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
|
||||||
|
var artist: String?
|
||||||
|
|
||||||
|
var author: String?
|
||||||
|
|
||||||
|
var description: String?
|
||||||
|
|
||||||
|
var genre: String?
|
||||||
|
|
||||||
|
var status: Int
|
||||||
|
|
||||||
|
var thumbnail_url: String?
|
||||||
|
|
||||||
|
var favorite: Boolean
|
||||||
|
|
||||||
|
var last_update: Long
|
||||||
|
|
||||||
|
var initialized: Boolean
|
||||||
|
|
||||||
|
var viewer: Int
|
||||||
|
|
||||||
|
var chapter_flags: Int
|
||||||
|
|
||||||
|
var unread: Int
|
||||||
|
|
||||||
|
var category: Int
|
||||||
|
|
||||||
|
fun copyFrom(other: Manga) {
|
||||||
|
if (other.author != null)
|
||||||
|
author = other.author
|
||||||
|
|
||||||
|
if (other.artist != null)
|
||||||
|
artist = other.artist
|
||||||
|
|
||||||
|
if (other.description != null)
|
||||||
|
description = other.description
|
||||||
|
|
||||||
|
if (other.genre != null)
|
||||||
|
genre = other.genre
|
||||||
|
|
||||||
|
if (other.thumbnail_url != null)
|
||||||
|
thumbnail_url = other.thumbnail_url
|
||||||
|
|
||||||
|
status = other.status
|
||||||
|
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setChapterOrder(order: Int) {
|
||||||
|
setFlags(order, SORT_MASK)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sortDescending(): Boolean {
|
||||||
|
return chapter_flags and SORT_MASK == SORT_DESC
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to display the chapter's title one way or another
|
||||||
|
var displayMode: Int
|
||||||
|
get() = chapter_flags and DISPLAY_MASK
|
||||||
|
set(mode) = setFlags(mode, DISPLAY_MASK)
|
||||||
|
|
||||||
|
var readFilter: Int
|
||||||
|
get() = chapter_flags and READ_MASK
|
||||||
|
set(filter) = setFlags(filter, READ_MASK)
|
||||||
|
|
||||||
|
var downloadedFilter: Int
|
||||||
|
get() = chapter_flags and DOWNLOADED_MASK
|
||||||
|
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
||||||
|
|
||||||
|
var sorting: Int
|
||||||
|
get() = chapter_flags and SORTING_MASK
|
||||||
|
set(sort) = setFlags(sort, SORTING_MASK)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val UNKNOWN = 0
|
||||||
|
const val ONGOING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val LICENSED = 3
|
||||||
|
|
||||||
|
const val SORT_DESC = 0x00000000
|
||||||
|
const val SORT_ASC = 0x00000001
|
||||||
|
const val SORT_MASK = 0x00000001
|
||||||
|
|
||||||
|
// Generic filter that does not filter anything
|
||||||
|
const val SHOW_ALL = 0x00000000
|
||||||
|
|
||||||
|
const val SHOW_UNREAD = 0x00000002
|
||||||
|
const val SHOW_READ = 0x00000004
|
||||||
|
const val READ_MASK = 0x00000006
|
||||||
|
|
||||||
|
const val SHOW_DOWNLOADED = 0x00000008
|
||||||
|
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
||||||
|
const val DOWNLOADED_MASK = 0x00000018
|
||||||
|
|
||||||
|
const val SORTING_SOURCE = 0x00000000
|
||||||
|
const val SORTING_NUMBER = 0x00000100
|
||||||
|
const val SORTING_MASK = 0x00000100
|
||||||
|
|
||||||
|
const val DISPLAY_NAME = 0x00000000
|
||||||
|
const val DISPLAY_NUMBER = 0x00100000
|
||||||
|
const val DISPLAY_MASK = 0x00100000
|
||||||
|
|
||||||
|
fun create(source: Int): Manga = MangaImpl().apply {
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
|
|
||||||
|
fun create(pathUrl: String, source: Int = 0): Manga = MangaImpl().apply {
|
||||||
|
url = pathUrl
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,29 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models;
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
|
||||||
|
|
||||||
@StorIOSQLiteType(table = MangaCategoryTable.TABLE)
|
|
||||||
public class MangaCategory {
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_ID, key = true)
|
|
||||||
public Long id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_MANGA_ID)
|
|
||||||
public long manga_id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_CATEGORY_ID)
|
|
||||||
public int category_id;
|
|
||||||
|
|
||||||
public MangaCategory() {}
|
|
||||||
|
|
||||||
public static MangaCategory create(Manga manga, Category category) {
|
|
||||||
MangaCategory mc = new MangaCategory();
|
|
||||||
mc.manga_id = manga.id;
|
|
||||||
mc.category_id = category.id;
|
|
||||||
return mc;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,21 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class MangaCategory {
|
||||||
|
|
||||||
|
var id: Long? = null
|
||||||
|
|
||||||
|
var manga_id: Long = 0
|
||||||
|
|
||||||
|
var category_id: Int = 0
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun create(manga: Manga, category: Category): MangaCategory {
|
||||||
|
val mc = MangaCategory()
|
||||||
|
mc.manga_id = manga.id!!
|
||||||
|
mc.category_id = category.id!!
|
||||||
|
return mc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,12 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models;
|
|
||||||
|
|
||||||
public class MangaChapter {
|
|
||||||
|
|
||||||
public Manga manga;
|
|
||||||
public Chapter chapter;
|
|
||||||
|
|
||||||
public MangaChapter(Manga manga, Chapter chapter) {
|
|
||||||
this.manga = manga;
|
|
||||||
this.chapter = chapter;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class MangaChapter(val manga: Manga, val chapter: Chapter)
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object containing manga, chapter and history
|
||||||
|
*
|
||||||
|
* @param manga object containing manga
|
||||||
|
* @param chapter object containing chater
|
||||||
|
* @param history object containing history
|
||||||
|
*/
|
||||||
|
class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
@ -0,0 +1,53 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class MangaImpl : Manga {
|
||||||
|
|
||||||
|
override var id: Long? = null
|
||||||
|
|
||||||
|
override var source: Int = 0
|
||||||
|
|
||||||
|
override lateinit var url: String
|
||||||
|
|
||||||
|
override lateinit var title: String
|
||||||
|
|
||||||
|
override var artist: String? = null
|
||||||
|
|
||||||
|
override var author: String? = null
|
||||||
|
|
||||||
|
override var description: String? = null
|
||||||
|
|
||||||
|
override var genre: String? = null
|
||||||
|
|
||||||
|
override var status: Int = 0
|
||||||
|
|
||||||
|
override var thumbnail_url: String? = null
|
||||||
|
|
||||||
|
override var favorite: Boolean = false
|
||||||
|
|
||||||
|
override var last_update: Long = 0
|
||||||
|
|
||||||
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
|
override var viewer: Int = 0
|
||||||
|
|
||||||
|
override var chapter_flags: Int = 0
|
||||||
|
|
||||||
|
@Transient override var unread: Int = 0
|
||||||
|
|
||||||
|
@Transient override var category: Int = 0
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
|
val manga = other as Manga
|
||||||
|
|
||||||
|
return url == manga.url
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return url.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,78 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models;
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
|
||||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService;
|
|
||||||
|
|
||||||
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
|
|
||||||
public class MangaSync implements Serializable {
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_ID, key = true)
|
|
||||||
public Long id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_MANGA_ID)
|
|
||||||
public long manga_id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_SYNC_ID)
|
|
||||||
public int sync_id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_REMOTE_ID)
|
|
||||||
public int remote_id;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_TITLE)
|
|
||||||
public String title;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_LAST_CHAPTER_READ)
|
|
||||||
public int last_chapter_read;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_TOTAL_CHAPTERS)
|
|
||||||
public int total_chapters;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_SCORE)
|
|
||||||
public float score;
|
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_STATUS)
|
|
||||||
public int status;
|
|
||||||
|
|
||||||
public boolean update;
|
|
||||||
|
|
||||||
public static MangaSync create() {
|
|
||||||
return new MangaSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MangaSync create(MangaSyncService service) {
|
|
||||||
MangaSync mangasync = new MangaSync();
|
|
||||||
mangasync.sync_id = service.getId();
|
|
||||||
return mangasync;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void copyPersonalFrom(MangaSync other) {
|
|
||||||
last_chapter_read = other.last_chapter_read;
|
|
||||||
score = other.score;
|
|
||||||
status = other.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
MangaSync mangaSync = (MangaSync) o;
|
|
||||||
|
|
||||||
if (manga_id != mangaSync.manga_id) return false;
|
|
||||||
if (sync_id != mangaSync.sync_id) return false;
|
|
||||||
return remote_id == mangaSync.remote_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
int result = (int) (manga_id ^ (manga_id >>> 32));
|
|
||||||
result = 31 * result + sync_id;
|
|
||||||
result = 31 * result + remote_id;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface MangaSync : Serializable {
|
||||||
|
|
||||||
|
var id: Long?
|
||||||
|
|
||||||
|
var manga_id: Long
|
||||||
|
|
||||||
|
var sync_id: Int
|
||||||
|
|
||||||
|
var remote_id: Int
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
|
||||||
|
var last_chapter_read: Int
|
||||||
|
|
||||||
|
var total_chapters: Int
|
||||||
|
|
||||||
|
var score: Float
|
||||||
|
|
||||||
|
var status: Int
|
||||||
|
|
||||||
|
var update: Boolean
|
||||||
|
|
||||||
|
fun copyPersonalFrom(other: MangaSync) {
|
||||||
|
last_chapter_read = other.last_chapter_read
|
||||||
|
score = other.score
|
||||||
|
status = other.status
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun create(serviceId: Int): MangaSync = MangaSyncImpl().apply {
|
||||||
|
sync_id = serviceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
class MangaSyncImpl : MangaSync {
|
||||||
|
|
||||||
|
override var id: Long? = null
|
||||||
|
|
||||||
|
override var manga_id: Long = 0
|
||||||
|
|
||||||
|
override var sync_id: Int = 0
|
||||||
|
|
||||||
|
override var remote_id: Int = 0
|
||||||
|
|
||||||
|
override lateinit var title: String
|
||||||
|
|
||||||
|
override var last_chapter_read: Int = 0
|
||||||
|
|
||||||
|
override var total_chapters: Int = 0
|
||||||
|
|
||||||
|
override var score: Float = 0f
|
||||||
|
|
||||||
|
override var status: Int = 0
|
||||||
|
|
||||||
|
override var update: Boolean = false
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
|
val mangaSync = other as MangaSync
|
||||||
|
|
||||||
|
if (manga_id != mangaSync.manga_id) return false
|
||||||
|
if (sync_id != mangaSync.sync_id) return false
|
||||||
|
return remote_id == mangaSync.remote_id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
||||||
|
result = 31 * result + sync_id
|
||||||
|
result = 31 * result + remote_id
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
@ -34,80 +33,6 @@ interface ChapterQueries : DbProvider {
|
|||||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getNextChapter(chapter: Chapter): PreparedGetObject<Chapter> {
|
|
||||||
// Add a delta to the chapter number, because binary decimal representation
|
|
||||||
// can retrieve the same chapter again
|
|
||||||
val chapterNumber = chapter.chapter_number + 0.00001
|
|
||||||
|
|
||||||
return db.get()
|
|
||||||
.`object`(Chapter::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where("${ChapterTable.COL_MANGA_ID} = ? AND " +
|
|
||||||
"${ChapterTable.COL_CHAPTER_NUMBER} > ? AND " +
|
|
||||||
"${ChapterTable.COL_CHAPTER_NUMBER} <= ?")
|
|
||||||
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
|
|
||||||
.orderBy(ChapterTable.COL_CHAPTER_NUMBER)
|
|
||||||
.limit(1)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getNextChapterBySource(chapter: Chapter) = db.get()
|
|
||||||
.`object`(Chapter::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where("""${ChapterTable.COL_MANGA_ID} = ? AND
|
|
||||||
${ChapterTable.COL_SOURCE_ORDER} < ?""")
|
|
||||||
.whereArgs(chapter.manga_id, chapter.source_order)
|
|
||||||
.orderBy("${ChapterTable.COL_SOURCE_ORDER} DESC")
|
|
||||||
.limit(1)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getPreviousChapter(chapter: Chapter): PreparedGetObject<Chapter> {
|
|
||||||
// Add a delta to the chapter number, because binary decimal representation
|
|
||||||
// can retrieve the same chapter again
|
|
||||||
val chapterNumber = chapter.chapter_number - 0.00001
|
|
||||||
|
|
||||||
return db.get()
|
|
||||||
.`object`(Chapter::class.java)
|
|
||||||
.withQuery(Query.builder().table(ChapterTable.TABLE)
|
|
||||||
.where("${ChapterTable.COL_MANGA_ID} = ? AND " +
|
|
||||||
"${ChapterTable.COL_CHAPTER_NUMBER} < ? AND " +
|
|
||||||
"${ChapterTable.COL_CHAPTER_NUMBER} >= ?")
|
|
||||||
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
|
|
||||||
.orderBy("${ChapterTable.COL_CHAPTER_NUMBER} DESC")
|
|
||||||
.limit(1)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPreviousChapterBySource(chapter: Chapter) = db.get()
|
|
||||||
.`object`(Chapter::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where("""${ChapterTable.COL_MANGA_ID} = ? AND
|
|
||||||
${ChapterTable.COL_SOURCE_ORDER} > ?""")
|
|
||||||
.whereArgs(chapter.manga_id, chapter.source_order)
|
|
||||||
.orderBy(ChapterTable.COL_SOURCE_ORDER)
|
|
||||||
.limit(1)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getNextUnreadChapter(manga: Manga) = db.get()
|
|
||||||
.`object`(Chapter::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where("${ChapterTable.COL_MANGA_ID} = ? AND " +
|
|
||||||
"${ChapterTable.COL_READ} = ? AND " +
|
|
||||||
"${ChapterTable.COL_CHAPTER_NUMBER} >= ?")
|
|
||||||
.whereArgs(manga.id, 0, 0)
|
|
||||||
.orderBy(ChapterTable.COL_CHAPTER_NUMBER)
|
|
||||||
.limit(1)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||||
|
|
||||||
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface HistoryQueries : DbProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert history into database
|
||||||
|
* @param history object containing history information
|
||||||
|
*/
|
||||||
|
fun insertHistory(history: History) = db.put().`object`(history).prepare()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns history of recent manga containing last read chapter
|
||||||
|
* @param date recent date range
|
||||||
|
*/
|
||||||
|
fun getRecentManga(date: Date) = db.get()
|
||||||
|
.listOfObjects(MangaChapterHistory::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getRecentMangasQuery())
|
||||||
|
.args(date.time)
|
||||||
|
.observesTables(HistoryTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
||||||
|
.listOfObjects(History::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getHistoryByMangaId())
|
||||||
|
.args(mangaId)
|
||||||
|
.observesTables(HistoryTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the history last read.
|
||||||
|
* Inserts history object if not yet in database
|
||||||
|
* @param history history object
|
||||||
|
*/
|
||||||
|
fun updateHistoryLastRead(history: History) = db.put()
|
||||||
|
.`object`(history)
|
||||||
|
.withPutResolver(HistoryLastReadPutResolver())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the history last read.
|
||||||
|
* Inserts history object if not yet in database
|
||||||
|
* @param historyList history object list
|
||||||
|
*/
|
||||||
|
fun updateHistoryLastRead(historyList: List<History>) = db.put()
|
||||||
|
.objects(historyList)
|
||||||
|
.withPutResolver(HistoryLastReadPutResolver())
|
||||||
|
.prepare()
|
||||||
|
}
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.database.queries
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
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.ChapterTable as Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||||
|
|
||||||
@ -39,6 +40,38 @@ fun getRecentsQuery() = """
|
|||||||
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the recently read chapters of manga from the library up to a date.
|
||||||
|
* The max_last_read table contains the most recent chapters grouped by manga
|
||||||
|
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
|
||||||
|
* and are read after the given time period
|
||||||
|
* @return return limit is 25
|
||||||
|
*/
|
||||||
|
fun getRecentMangasQuery() = """
|
||||||
|
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
|
JOIN ${History.TABLE}
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
|
JOIN (
|
||||||
|
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
|
||||||
|
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
|
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
|
||||||
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
||||||
|
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
|
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
|
||||||
|
LIMIT 25
|
||||||
|
"""
|
||||||
|
|
||||||
|
fun getHistoryByMangaId() = """
|
||||||
|
SELECT ${History.TABLE}.*
|
||||||
|
FROM ${History.TABLE}
|
||||||
|
JOIN ${Chapter.TABLE}
|
||||||
|
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
|
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||||
|
"""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the categories for a manga.
|
* Query to get the categories for a manga.
|
||||||
|
@ -15,7 +15,7 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
|||||||
val updateQuery = mapToUpdateQuery(chapter)
|
val updateQuery = mapToUpdateQuery(chapter)
|
||||||
val contentValues = mapToContentValues(chapter)
|
val contentValues = mapToContentValues(chapter)
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
|||||||
val updateQuery = mapToUpdateQuery(chapter)
|
val updateQuery = mapToUpdateQuery(chapter)
|
||||||
val contentValues = mapToContentValues(chapter)
|
val contentValues = mapToContentValues(chapter)
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.support.annotation.NonNull
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
|
|
||||||
|
class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates last_read time of chapter
|
||||||
|
*/
|
||||||
|
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(history)
|
||||||
|
|
||||||
|
val cursor = db.lowLevel().query(Query.builder()
|
||||||
|
.table(updateQuery.table())
|
||||||
|
.where(updateQuery.where())
|
||||||
|
.whereArgs(updateQuery.whereArgs())
|
||||||
|
.build())
|
||||||
|
|
||||||
|
val putResult: PutResult
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (cursor.count == 0) {
|
||||||
|
val insertQuery = mapToInsertQuery(history)
|
||||||
|
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
||||||
|
putResult = PutResult.newInsertResult(insertedId, insertQuery.table())
|
||||||
|
} else {
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
|
||||||
|
putResult = PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
putResult
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates update query
|
||||||
|
* @param obj history object
|
||||||
|
*/
|
||||||
|
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||||
|
.table(HistoryTable.TABLE)
|
||||||
|
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||||
|
.whereArgs(obj.chapter_id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create content query
|
||||||
|
* @param history object
|
||||||
|
*/
|
||||||
|
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
|
||||||
|
put(HistoryTable.COL_LAST_READ, history.last_read)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,12 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
|
class LibraryMangaGetResolver : MangaGetResolver() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val INSTANCE = LibraryMangaGetResolver()
|
val INSTANCE = LibraryMangaGetResolver()
|
||||||
|
@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.data.database.resolvers
|
|||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterStorIOSQLiteGetResolver
|
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
|
|
||||||
|
|
||||||
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
||||||
|
|
||||||
@ -12,15 +12,15 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
|||||||
val INSTANCE = MangaChapterGetResolver()
|
val INSTANCE = MangaChapterGetResolver()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mangaGetResolver = MangaStorIOSQLiteGetResolver()
|
private val mangaGetResolver = MangaGetResolver()
|
||||||
|
|
||||||
private val chapterGetResolver = ChapterStorIOSQLiteGetResolver()
|
private val chapterGetResolver = ChapterGetResolver()
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaChapter {
|
override fun mapFromCursor(cursor: Cursor): MangaChapter {
|
||||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||||
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"));
|
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
||||||
|
|
||||||
return MangaChapter(manga, chapter)
|
return MangaChapter(manga, chapter)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||||
|
|
||||||
|
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
|
||||||
|
companion object {
|
||||||
|
val INSTANCE = MangaChapterHistoryGetResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manga get resolver
|
||||||
|
*/
|
||||||
|
private val mangaGetResolver = MangaGetResolver()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter get resolver
|
||||||
|
*/
|
||||||
|
private val chapterResolver = ChapterGetResolver()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* History get resolver
|
||||||
|
*/
|
||||||
|
private val historyGetResolver = HistoryGetResolver()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map correct objects from cursor result
|
||||||
|
*/
|
||||||
|
override fun mapFromCursor(cursor: Cursor): MangaChapterHistory {
|
||||||
|
// Get manga object
|
||||||
|
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||||
|
|
||||||
|
// Get chapter object
|
||||||
|
val chapter = chapterResolver.mapFromCursor(cursor)
|
||||||
|
|
||||||
|
// Get history object
|
||||||
|
val history = historyGetResolver.mapFromCursor(cursor)
|
||||||
|
|
||||||
|
// Make certain column conflicts are dealt with
|
||||||
|
manga.id = chapter.manga_id
|
||||||
|
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
||||||
|
chapter.id = history.chapter_id
|
||||||
|
|
||||||
|
// Return result
|
||||||
|
return MangaChapterHistory(manga, chapter, history)
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
|
|||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
val contentValues = mapToContentValues(manga)
|
val contentValues = mapToContentValues(manga)
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.tables
|
||||||
|
|
||||||
|
object HistoryTable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
const val TABLE = "history"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id column name
|
||||||
|
*/
|
||||||
|
const val COL_ID = "${TABLE}_id"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter id column name
|
||||||
|
*/
|
||||||
|
const val COL_CHAPTER_ID = "${TABLE}_chapter_id"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last read column name
|
||||||
|
*/
|
||||||
|
const val COL_LAST_READ = "${TABLE}_last_read"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time read column name
|
||||||
|
*/
|
||||||
|
const val COL_TIME_READ = "${TABLE}_time_read"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* query to create history table
|
||||||
|
*/
|
||||||
|
val createTableQuery: String
|
||||||
|
get() = """CREATE TABLE $TABLE(
|
||||||
|
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
|
||||||
|
$COL_LAST_READ LONG,
|
||||||
|
$COL_TIME_READ LONG,
|
||||||
|
FOREIGN KEY($COL_CHAPTER_ID) REFERENCES ${ChapterTable.TABLE} (${ChapterTable.COL_ID})
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)"""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* query to index history chapter id
|
||||||
|
*/
|
||||||
|
val createChapterIdIndexQuery: String
|
||||||
|
get() = "CREATE INDEX ${TABLE}_${COL_CHAPTER_ID}_index ON $TABLE($COL_CHAPTER_ID)"
|
||||||
|
}
|
@ -16,10 +16,7 @@ import eu.kanade.tachiyomi.data.source.Source
|
|||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
import eu.kanade.tachiyomi.util.DiskUtils
|
import eu.kanade.tachiyomi.util.*
|
||||||
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
|
|
||||||
import eu.kanade.tachiyomi.util.UrlUtil
|
|
||||||
import eu.kanade.tachiyomi.util.saveImageTo
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -27,12 +24,17 @@ import rx.schedulers.Schedulers
|
|||||||
import rx.subjects.BehaviorSubject
|
import rx.subjects.BehaviorSubject
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileReader
|
import java.io.FileReader
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class DownloadManager(private val context: Context, private val sourceManager: SourceManager, private val preferences: PreferencesHelper) {
|
class DownloadManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
) {
|
||||||
|
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
|
|
||||||
@ -78,10 +80,10 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
if (areAllDownloadsFinished()) {
|
if (areAllDownloadsFinished()) {
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
}
|
}
|
||||||
}, { e ->
|
}, { error ->
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
Timber.e(e, e.message)
|
Timber.e(error)
|
||||||
downloadNotifier.onError(e.message)
|
downloadNotifier.onError(error.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
@ -185,7 +187,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
private fun downloadChapter(download: Download): Observable<Download> {
|
private fun downloadChapter(download: Download): Observable<Download> {
|
||||||
DiskUtils.createDirectory(download.directory)
|
DiskUtils.createDirectory(download.directory)
|
||||||
|
|
||||||
val pageListObservable = if (download.pages == null)
|
val pageListObservable: Observable<List<Page>> = if (download.pages == null)
|
||||||
// Pull page list from network and add them to download object
|
// Pull page list from network and add them to download object
|
||||||
download.source.fetchPageListFromNetwork(download.chapter)
|
download.source.fetchPageListFromNetwork(download.chapter)
|
||||||
.doOnNext { pages ->
|
.doOnNext { pages ->
|
||||||
@ -258,21 +260,20 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> {
|
private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> {
|
||||||
page.status = Page.DOWNLOAD_IMAGE
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
return source.imageResponse(page)
|
return source.imageResponse(page)
|
||||||
.flatMap {
|
.map {
|
||||||
|
val file = File(directory, filename)
|
||||||
try {
|
try {
|
||||||
val file = File(directory, filename)
|
|
||||||
file.parentFile.mkdirs()
|
file.parentFile.mkdirs()
|
||||||
it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage())
|
it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
it.body().close()
|
it.close()
|
||||||
|
file.delete()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
Observable.just(page)
|
page
|
||||||
}
|
|
||||||
.retryWhen {
|
|
||||||
it.zipWith(Observable.range(1, 3)) { errors, retries -> retries }
|
|
||||||
.flatMap { retries -> Observable.timer((retries * 2).toLong(), TimeUnit.SECONDS) }
|
|
||||||
}
|
}
|
||||||
|
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
|
||||||
|
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
|
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
|
||||||
@ -323,7 +324,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
var actualProgress = 0
|
var actualProgress = 0
|
||||||
var status = Download.DOWNLOADED
|
var status = Download.DOWNLOADED
|
||||||
// If any page has an error, the download result will be error
|
// If any page has an error, the download result will be error
|
||||||
for (page in download.pages) {
|
for (page in download.pages!!) {
|
||||||
actualProgress += page.progress
|
actualProgress += page.progress
|
||||||
if (page.status != Page.READY) {
|
if (page.status != Page.READY) {
|
||||||
status = Download.ERROR
|
status = Download.ERROR
|
||||||
@ -368,15 +369,15 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
try {
|
try {
|
||||||
it.write(gson.toJson(pages).toByteArray())
|
it.write(gson.toJson(pages).toByteArray())
|
||||||
it.flush()
|
it.flush()
|
||||||
} catch (e: Exception) {
|
} catch (error: Exception) {
|
||||||
Timber.e(e, e.message)
|
Timber.e(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shortcut for the method above
|
// Shortcut for the method above
|
||||||
private fun savePageList(download: Download) {
|
private fun savePageList(download: Download) {
|
||||||
savePageList(download.source, download.manga, download.chapter, download.pages)
|
savePageList(download.source, download.manga, download.chapter, download.pages!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
|
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
|
||||||
|
@ -79,7 +79,7 @@ class DownloadNotifier(private val context: Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (download != null && download.pages.size == download.downloadedImages) {
|
if (download != null && download.pages!!.size == download.downloadedImages) {
|
||||||
onComplete(download)
|
onComplete(download)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -96,6 +96,10 @@ class DownloadNotifier(private val context: Context) {
|
|||||||
if (multipleDownloadThreads) {
|
if (multipleDownloadThreads) {
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
|
|
||||||
|
// Reset the queue size if the download progress is negative
|
||||||
|
if ((initialQueueSize - queue.size) < 0)
|
||||||
|
initialQueueSize = queue.size
|
||||||
|
|
||||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
setContentText(context.getString(R.string.chapter_downloading_progress)
|
||||||
.format(initialQueueSize - queue.size, initialQueueSize))
|
.format(initialQueueSize - queue.size, initialQueueSize))
|
||||||
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
|
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
|
||||||
@ -107,8 +111,8 @@ class DownloadNotifier(private val context: Context) {
|
|||||||
setContentTitle(it.chapter.name)
|
setContentTitle(it.chapter.name)
|
||||||
|
|
||||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
setContentText(context.getString(R.string.chapter_downloading_progress)
|
||||||
.format(it.downloadedImages, it.pages.size))
|
.format(it.downloadedImages, it.pages!!.size))
|
||||||
setProgress(it.pages.size, it.downloadedImages, false)
|
setProgress(it.pages!!.size, it.downloadedImages, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,6 +165,9 @@ class DownloadNotifier(private val context: Context) {
|
|||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
|
context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
|
||||||
|
|
||||||
|
// Reset download information
|
||||||
|
onClear()
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,13 @@ import android.os.IBinder
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
|
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
|
||||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import javax.inject.Inject
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class DownloadService : Service() {
|
class DownloadService : Service() {
|
||||||
|
|
||||||
@ -29,8 +28,8 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject lateinit var downloadManager: DownloadManager
|
val downloadManager: DownloadManager by injectLazy()
|
||||||
@Inject lateinit var preferences: PreferencesHelper
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private var networkChangeSubscription: Subscription? = null
|
private var networkChangeSubscription: Subscription? = null
|
||||||
@ -39,7 +38,6 @@ class DownloadService : Service() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
App.get(this).component.inject(this)
|
|
||||||
|
|
||||||
createWakeLock()
|
createWakeLock()
|
||||||
|
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.download.model;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource;
|
|
||||||
import rx.subjects.PublishSubject;
|
|
||||||
|
|
||||||
public class Download {
|
|
||||||
public OnlineSource source;
|
|
||||||
public Manga manga;
|
|
||||||
public Chapter chapter;
|
|
||||||
public List<Page> pages;
|
|
||||||
public File directory;
|
|
||||||
|
|
||||||
public transient volatile int totalProgress;
|
|
||||||
public transient volatile int downloadedImages;
|
|
||||||
private transient volatile int status;
|
|
||||||
|
|
||||||
private transient PublishSubject<Download> statusSubject;
|
|
||||||
|
|
||||||
public static final int NOT_DOWNLOADED = 0;
|
|
||||||
public static final int QUEUE = 1;
|
|
||||||
public static final int DOWNLOADING = 2;
|
|
||||||
public static final int DOWNLOADED = 3;
|
|
||||||
public static final int ERROR = 4;
|
|
||||||
|
|
||||||
|
|
||||||
public Download(OnlineSource source, Manga manga, Chapter chapter) {
|
|
||||||
this.source = source;
|
|
||||||
this.manga = manga;
|
|
||||||
this.chapter = chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(int status) {
|
|
||||||
this.status = status;
|
|
||||||
notifyStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatusSubject(PublishSubject<Download> subject) {
|
|
||||||
this.statusSubject = subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyStatus() {
|
|
||||||
if (statusSubject != null)
|
|
||||||
statusSubject.onNext(this);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.download.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class Download(val source: OnlineSource, val manga: Manga, val chapter: Chapter) {
|
||||||
|
|
||||||
|
lateinit var directory: File
|
||||||
|
|
||||||
|
var pages: List<Page>? = null
|
||||||
|
|
||||||
|
@Volatile @Transient var totalProgress: Int = 0
|
||||||
|
|
||||||
|
@Volatile @Transient var downloadedImages: Int = 0
|
||||||
|
|
||||||
|
@Volatile @Transient var status: Int = 0
|
||||||
|
set(status) {
|
||||||
|
field = status
|
||||||
|
statusSubject?.onNext(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient private var statusSubject: PublishSubject<Download>? = null
|
||||||
|
|
||||||
|
fun setStatusSubject(subject: PublishSubject<Download>?) {
|
||||||
|
statusSubject = subject
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val NOT_DOWNLOADED = 0
|
||||||
|
const val QUEUE = 1
|
||||||
|
const val DOWNLOADING = 2
|
||||||
|
const val DOWNLOADED = 3
|
||||||
|
const val ERROR = 4
|
||||||
|
}
|
||||||
|
}
|
@ -6,29 +6,41 @@ import rx.Observable
|
|||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
class DownloadQueue : CopyOnWriteArrayList<Download>() {
|
class DownloadQueue(private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
|
||||||
|
: List<Download> by queue {
|
||||||
|
|
||||||
private val statusSubject = PublishSubject.create<Download>()
|
private val statusSubject = PublishSubject.create<Download>()
|
||||||
|
|
||||||
override fun add(download: Download): Boolean {
|
private val removeSubject = PublishSubject.create<Download>()
|
||||||
|
|
||||||
|
fun add(download: Download): Boolean {
|
||||||
download.setStatusSubject(statusSubject)
|
download.setStatusSubject(statusSubject)
|
||||||
download.status = Download.QUEUE
|
download.status = Download.QUEUE
|
||||||
return super.add(download)
|
return queue.add(download)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun del(download: Download) {
|
fun del(download: Download) {
|
||||||
super.remove(download)
|
val removed = queue.remove(download)
|
||||||
download.setStatusSubject(null)
|
download.setStatusSubject(null)
|
||||||
|
if (removed) {
|
||||||
|
removeSubject.onNext(download)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun del(chapter: Chapter) {
|
fun del(chapter: Chapter) {
|
||||||
find { it.chapter.id == chapter.id }?.let { del(it) }
|
find { it.chapter.id == chapter.id }?.let { del(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActiveDownloads() =
|
fun clear() {
|
||||||
|
queue.forEach { del(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getActiveDownloads(): Observable<Download> =
|
||||||
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
|
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
|
||||||
|
|
||||||
fun getStatusObservable() = statusSubject.onBackpressureBuffer()
|
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
|
||||||
|
|
||||||
|
fun getRemovedObservable(): Observable<Download> = removeSubject.onBackpressureBuffer()
|
||||||
|
|
||||||
fun getProgressObservable(): Observable<Download> {
|
fun getProgressObservable(): Observable<Download> {
|
||||||
return statusSubject.onBackpressureBuffer()
|
return statusSubject.onBackpressureBuffer()
|
||||||
|
@ -7,28 +7,26 @@ import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
|||||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
import com.bumptech.glide.module.GlideModule
|
import com.bumptech.glide.module.GlideModule
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class used to update Glide module settings
|
* Class used to update Glide module settings
|
||||||
*/
|
*/
|
||||||
class AppGlideModule : GlideModule {
|
class AppGlideModule : GlideModule {
|
||||||
|
|
||||||
@Inject lateinit var networkHelper: NetworkHelper
|
|
||||||
|
|
||||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||||
// Set the cache size of Glide to 15 MiB
|
// Set the cache size of Glide to 15 MiB
|
||||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
|
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerComponents(context: Context, glide: Glide) {
|
override fun registerComponents(context: Context, glide: Glide) {
|
||||||
App.get(context).component.inject(this)
|
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
||||||
glide.register(GlideUrl::class.java, InputStream::class.java,
|
|
||||||
OkHttpUrlLoader.Factory(networkHelper.client))
|
glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory)
|
||||||
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import com.bumptech.glide.Priority
|
|||||||
import com.bumptech.glide.load.data.DataFetcher
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileNotFoundException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,17 +26,32 @@ class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
|||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun loadData(priority: Priority): InputStream? {
|
override fun loadData(priority: Priority): InputStream? {
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
if (!file.exists()) {
|
synchronized(file) {
|
||||||
file.parentFile.mkdirs()
|
if (!file.exists()) {
|
||||||
networkFetcher.loadData(priority)?.let {
|
val tmpFile = File(file.path + ".tmp")
|
||||||
it.use { input ->
|
try {
|
||||||
file.outputStream().use { output ->
|
// Retrieve source stream.
|
||||||
input.copyTo(output)
|
val input = networkFetcher.loadData(priority)
|
||||||
|
?: throw Exception("Couldn't open source stream")
|
||||||
|
|
||||||
|
// Retrieve destination stream, create parent folders if needed.
|
||||||
|
val output = try {
|
||||||
|
tmpFile.outputStream()
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
tmpFile.parentFile.mkdirs()
|
||||||
|
tmpFile.outputStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy the file and rename to the original.
|
||||||
|
input.use { output.use { input.copyTo(output) } }
|
||||||
|
tmpFile.renameTo(file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
tmpFile.delete()
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return FileInputStream(file)
|
return file.inputStream()
|
||||||
} else {
|
} else {
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
file.delete()
|
file.delete()
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.LruCache
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
import com.bumptech.glide.load.model.*
|
import com.bumptech.glide.load.model.*
|
||||||
import com.bumptech.glide.load.model.stream.StreamModelLoader
|
import com.bumptech.glide.load.model.stream.StreamModelLoader
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||||
@ -30,12 +30,12 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
/**
|
/**
|
||||||
* Cover cache where persistent covers are stored.
|
* Cover cache where persistent covers are stored.
|
||||||
*/
|
*/
|
||||||
@Inject lateinit var coverCache: CoverCache
|
val coverCache: CoverCache by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source manager.
|
* Source manager.
|
||||||
*/
|
*/
|
||||||
@Inject lateinit var sourceManager: SourceManager
|
val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base network loader.
|
* Base network loader.
|
||||||
@ -47,17 +47,13 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
|
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
|
||||||
* and the file where it should be stored in case the manga is a favorite.
|
* and the file where it should be stored in case the manga is a favorite.
|
||||||
*/
|
*/
|
||||||
private val modelCache = ModelCache<String, Pair<GlideUrl, File>>(100)
|
private val lruCache = LruCache<String, Pair<GlideUrl, File>>(100)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map where request headers are stored for a source.
|
* Map where request headers are stored for a source.
|
||||||
*/
|
*/
|
||||||
private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
|
private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
|
||||||
|
|
||||||
init {
|
|
||||||
App.get(context).component.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory class for creating [MangaModelLoader] instances.
|
* Factory class for creating [MangaModelLoader] instances.
|
||||||
*/
|
*/
|
||||||
@ -79,6 +75,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
override fun getResourceFetcher(manga: Manga,
|
override fun getResourceFetcher(manga: Manga,
|
||||||
width: Int,
|
width: Int,
|
||||||
height: Int): DataFetcher<InputStream>? {
|
height: Int): DataFetcher<InputStream>? {
|
||||||
|
|
||||||
// Check thumbnail is not null or empty
|
// Check thumbnail is not null or empty
|
||||||
val url = manga.thumbnail_url
|
val url = manga.thumbnail_url
|
||||||
if (url.isNullOrEmpty()) {
|
if (url.isNullOrEmpty()) {
|
||||||
@ -87,9 +84,9 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
|
|
||||||
// Obtain the request url and the file for this url from the LRU cache, or calculate it
|
// Obtain the request url and the file for this url from the LRU cache, or calculate it
|
||||||
// and add them to the cache.
|
// and add them to the cache.
|
||||||
val (glideUrl, file) = modelCache.get(url, width, height) ?:
|
val (glideUrl, file) = lruCache.get(url) ?:
|
||||||
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url)).apply {
|
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url!!)).apply {
|
||||||
modelCache.put(url, width, height, this)
|
lruCache.put(url, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the network fetcher for this request url.
|
// Get the network fetcher for this request url.
|
||||||
@ -108,7 +105,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
val source = sourceManager.get(manga.source) as? OnlineSource ?: return LazyHeaders.DEFAULT
|
val source = sourceManager.get(manga.source) as? OnlineSource ?: return LazyHeaders.DEFAULT
|
||||||
return cachedHeaders.getOrPut(manga.source) {
|
return cachedHeaders.getOrPut(manga.source) {
|
||||||
LazyHeaders.Builder().apply {
|
LazyHeaders.Builder().apply {
|
||||||
setHeader("User-Agent", null as? String)
|
val nullStr: String? = null
|
||||||
|
setHeader("User-Agent", nullStr)
|
||||||
for ((key, value) in source.headers.toMultimap()) {
|
for ((key, value) in source.headers.toMultimap()) {
|
||||||
addHeader(key, value[0])
|
addHeader(key, value[0])
|
||||||
}
|
}
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
|
||||||
|
|
||||||
import android.app.AlarmManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.SystemClock
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.util.alarmManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is used to update the library by firing an alarm after a specified time.
|
|
||||||
* It has a receiver reacting to system's boot and the intent fired by this alarm.
|
|
||||||
* See [onReceive] for more information.
|
|
||||||
*/
|
|
||||||
class LibraryUpdateAlarm : BroadcastReceiver() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val LIBRARY_UPDATE_ACTION = "eu.kanade.UPDATE_LIBRARY"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the alarm to run the intent that updates the library.
|
|
||||||
* @param context the application context.
|
|
||||||
* @param intervalInHours the time in hours when it will be executed. Defaults to the
|
|
||||||
* value stored in preferences.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
@JvmOverloads
|
|
||||||
fun startAlarm(context: Context,
|
|
||||||
intervalInHours: Int = PreferencesHelper.getLibraryUpdateInterval(context)) {
|
|
||||||
// Stop previous running alarms if needed, and do not restart it if the interval is 0.
|
|
||||||
stopAlarm(context)
|
|
||||||
if (intervalInHours == 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
// Get the time the alarm should fire the event to update.
|
|
||||||
val intervalInMillis = intervalInHours * 60 * 60 * 1000
|
|
||||||
val nextRun = SystemClock.elapsedRealtime() + intervalInMillis
|
|
||||||
|
|
||||||
// Start the alarm.
|
|
||||||
val pendingIntent = getPendingIntent(context)
|
|
||||||
context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
|
||||||
nextRun, intervalInMillis.toLong(), pendingIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the alarm if it's running.
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
|
||||||
fun stopAlarm(context: Context) {
|
|
||||||
val pendingIntent = getPendingIntent(context)
|
|
||||||
context.alarmManager.cancel(pendingIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the intent the alarm should run when it's fired.
|
|
||||||
* @param context the application context.
|
|
||||||
* @return the intent that will run when the alarm is fired.
|
|
||||||
*/
|
|
||||||
private fun getPendingIntent(context: Context): PendingIntent {
|
|
||||||
val intent = Intent(context, LibraryUpdateAlarm::class.java)
|
|
||||||
intent.action = LIBRARY_UPDATE_ACTION
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the intents received by this [BroadcastReceiver].
|
|
||||||
* @param context the application context.
|
|
||||||
* @param intent the intent to process.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
when (intent.action) {
|
|
||||||
// Start the alarm when the system is booted.
|
|
||||||
Intent.ACTION_BOOT_COMPLETED -> startAlarm(context)
|
|
||||||
// Update the library when the alarm fires an event.
|
|
||||||
LIBRARY_UPDATE_ACTION -> LibraryUpdateService.start(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -5,28 +5,32 @@ import android.app.Service
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
|
|
||||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.Constants
|
import eu.kanade.tachiyomi.Constants
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.*
|
import eu.kanade.tachiyomi.util.AndroidComponentUtil
|
||||||
|
import eu.kanade.tachiyomi.util.notification
|
||||||
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class will take care of updating the chapters of the manga from the library. It can be
|
* This class will take care of updating the chapters of the manga from the library. It can be
|
||||||
@ -41,17 +45,17 @@ class LibraryUpdateService : Service() {
|
|||||||
/**
|
/**
|
||||||
* Database helper.
|
* Database helper.
|
||||||
*/
|
*/
|
||||||
@Inject lateinit var db: DatabaseHelper
|
val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source manager.
|
* Source manager.
|
||||||
*/
|
*/
|
||||||
@Inject lateinit var sourceManager: SourceManager
|
val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preferences.
|
* Preferences.
|
||||||
*/
|
*/
|
||||||
@Inject lateinit var preferences: PreferencesHelper
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wake lock that will be held until the service is destroyed.
|
* Wake lock that will be held until the service is destroyed.
|
||||||
@ -69,18 +73,20 @@ class LibraryUpdateService : Service() {
|
|||||||
private val notificationId: Int
|
private val notificationId: Int
|
||||||
get() = Constants.NOTIFICATION_LIBRARY_ID
|
get() = Constants.NOTIFICATION_LIBRARY_ID
|
||||||
|
|
||||||
|
private var notificationBitmap: Bitmap? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* Key for manual library update.
|
|
||||||
*/
|
|
||||||
const val UPDATE_IS_MANUAL = "is_manual"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for category to update.
|
* Key for category to update.
|
||||||
*/
|
*/
|
||||||
const val UPDATE_CATEGORY = "category"
|
const val UPDATE_CATEGORY = "category"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key for updating the details instead of the chapters.
|
||||||
|
*/
|
||||||
|
const val UPDATE_DETAILS = "details"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
*
|
*
|
||||||
@ -96,13 +102,13 @@ class LibraryUpdateService : Service() {
|
|||||||
* running.
|
* running.
|
||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param isManual whether the update has been manually triggered.
|
* @param category a specific category to update, or null for global update.
|
||||||
* @param category a specific category to update, or null for all in the library.
|
* @param details whether to update the details instead of the list of chapters.
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, isManual: Boolean = false, category: Category? = null) {
|
fun start(context: Context, category: Category? = null, details: Boolean = false) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
putExtra(UPDATE_IS_MANUAL, isManual)
|
putExtra(UPDATE_DETAILS, details)
|
||||||
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
|
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
|
||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
@ -126,7 +132,6 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
App.get(this).component.inject(this)
|
|
||||||
createAndAcquireWakeLock()
|
createAndAcquireWakeLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +141,8 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
LibraryUpdateAlarm.startAlarm(this)
|
notificationBitmap?.recycle()
|
||||||
|
notificationBitmap = null
|
||||||
destroyWakeLock()
|
destroyWakeLock()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@ -157,61 +163,36 @@ class LibraryUpdateService : Service() {
|
|||||||
* @return the start value of the command.
|
* @return the start value of the command.
|
||||||
*/
|
*/
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null) return Service.START_NOT_STICKY
|
||||||
// Get connectivity status
|
|
||||||
val connection = ReactiveNetwork().getConnectivityStatus(this, true)
|
|
||||||
|
|
||||||
// Get library update restrictions
|
|
||||||
val restrictions = preferences.libraryUpdateRestriction()
|
|
||||||
|
|
||||||
// Check if users updates library manual
|
|
||||||
val isManualUpdate = intent?.getBooleanExtra(UPDATE_IS_MANUAL, false) ?: false
|
|
||||||
|
|
||||||
// Whether to cancel the update.
|
|
||||||
var cancelUpdate = false
|
|
||||||
|
|
||||||
// Check if device has internet connection
|
|
||||||
// Check if device has wifi connection if only wifi is enabled
|
|
||||||
if (connection == ConnectivityStatus.OFFLINE || (!isManualUpdate && "wifi" in restrictions
|
|
||||||
&& connection != ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET)) {
|
|
||||||
|
|
||||||
if (isManualUpdate) {
|
|
||||||
toast(R.string.notification_no_connection_title)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable library update when connection available
|
|
||||||
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true)
|
|
||||||
cancelUpdate = true
|
|
||||||
}
|
|
||||||
if (!isManualUpdate && "ac" in restrictions && !DeviceUtil.isPowerConnected(this)) {
|
|
||||||
AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, true)
|
|
||||||
cancelUpdate = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelUpdate) {
|
|
||||||
stopSelf(startId)
|
|
||||||
return Service.START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop enabled components.
|
|
||||||
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, false)
|
|
||||||
AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, false)
|
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
// Unsubscribe from any previous subscription if needed.
|
||||||
subscription?.unsubscribe()
|
subscription?.unsubscribe()
|
||||||
|
|
||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||||
subscription = Observable.defer { updateMangaList(getMangaToUpdate(intent)) }
|
subscription = Observable
|
||||||
|
.defer {
|
||||||
|
if (notificationBitmap == null) {
|
||||||
|
notificationBitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaList = getMangaToUpdate(intent)
|
||||||
|
|
||||||
|
// Update either chapter list or manga details.
|
||||||
|
if (!intent.getBooleanExtra(UPDATE_DETAILS, false))
|
||||||
|
updateChapterList(mangaList)
|
||||||
|
else
|
||||||
|
updateDetails(mangaList)
|
||||||
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({},
|
.subscribe({
|
||||||
{
|
}, {
|
||||||
showNotification(getString(R.string.notification_update_error), "")
|
showNotification(getString(R.string.notification_update_error), "")
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}, {
|
}, {
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Service.START_STICKY
|
return Service.START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -220,19 +201,26 @@ class LibraryUpdateService : Service() {
|
|||||||
* @param intent the update intent.
|
* @param intent the update intent.
|
||||||
* @return a list of manga to update
|
* @return a list of manga to update
|
||||||
*/
|
*/
|
||||||
fun getMangaToUpdate(intent: Intent?): List<Manga> {
|
fun getMangaToUpdate(intent: Intent): List<Manga> {
|
||||||
val categoryId = intent?.getIntExtra(UPDATE_CATEGORY, -1) ?: -1
|
val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
|
||||||
|
|
||||||
var toUpdate = if (categoryId != -1)
|
var listToUpdate = if (categoryId != -1)
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
||||||
else
|
else {
|
||||||
db.getFavoriteMangas().executeAsBlocking()
|
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map { it.toInt() }
|
||||||
|
if (categoriesToUpdate.isNotEmpty())
|
||||||
if (preferences.updateOnlyNonCompleted()) {
|
db.getLibraryMangas().executeAsBlocking()
|
||||||
toUpdate = toUpdate.filter { it.status != Manga.COMPLETED }
|
.filter { it.category in categoriesToUpdate }
|
||||||
|
.distinctBy { it.id }
|
||||||
|
else
|
||||||
|
db.getFavoriteMangas().executeAsBlocking().distinctBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
return toUpdate
|
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
|
||||||
|
listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED }
|
||||||
|
}
|
||||||
|
|
||||||
|
return listToUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -244,7 +232,7 @@ class LibraryUpdateService : Service() {
|
|||||||
* @param mangaToUpdate the list to update
|
* @param mangaToUpdate the list to update
|
||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
*/
|
*/
|
||||||
fun updateMangaList(mangaToUpdate: List<Manga>): Observable<Manga> {
|
fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> {
|
||||||
// Initialize the variables holding the progress of the updates.
|
// Initialize the variables holding the progress of the updates.
|
||||||
val count = AtomicInteger(0)
|
val count = AtomicInteger(0)
|
||||||
val newUpdates = ArrayList<Manga>()
|
val newUpdates = ArrayList<Manga>()
|
||||||
@ -279,6 +267,7 @@ class LibraryUpdateService : Service() {
|
|||||||
} else {
|
} else {
|
||||||
showResultNotification(newUpdates, failedUpdates)
|
showResultNotification(newUpdates, failedUpdates)
|
||||||
}
|
}
|
||||||
|
LibraryUpdateTrigger.setupTask(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,6 +283,43 @@ class LibraryUpdateService : Service() {
|
|||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that updates the details of the given list of manga. It's called in a background
|
||||||
|
* thread, so it's safe to do heavy operations or network calls here.
|
||||||
|
* For each manga it calls [updateManga] and updates the notification showing the current
|
||||||
|
* progress.
|
||||||
|
*
|
||||||
|
* @param mangaToUpdate the list to update
|
||||||
|
* @return an observable delivering the progress of each update.
|
||||||
|
*/
|
||||||
|
fun updateDetails(mangaToUpdate: List<Manga>): Observable<Manga> {
|
||||||
|
// Initialize the variables holding the progress of the updates.
|
||||||
|
val count = AtomicInteger(0)
|
||||||
|
|
||||||
|
val cancelIntent = PendingIntent.getBroadcast(this, 0,
|
||||||
|
Intent(this, CancelUpdateReceiver::class.java), 0)
|
||||||
|
|
||||||
|
// Emit each manga and update it sequentially.
|
||||||
|
return Observable.from(mangaToUpdate)
|
||||||
|
// Notify manga that will update.
|
||||||
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
|
||||||
|
// Update the details of the manga.
|
||||||
|
.concatMap { manga ->
|
||||||
|
val source = sourceManager.get(manga.source) as? OnlineSource
|
||||||
|
?: return@concatMap Observable.empty<Manga>()
|
||||||
|
|
||||||
|
source.fetchMangaDetails(manga)
|
||||||
|
.doOnNext { networkManga ->
|
||||||
|
manga.copyFrom(networkManga)
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
.onErrorReturn { manga }
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
cancelNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the text that will be displayed in the notification when there are new chapters.
|
* Returns the text that will be displayed in the notification when there are new chapters.
|
||||||
*
|
*
|
||||||
@ -352,6 +378,7 @@ class LibraryUpdateService : Service() {
|
|||||||
private fun showNotification(title: String, body: String) {
|
private fun showNotification(title: String, body: String) {
|
||||||
notificationManager.notify(notificationId, notification() {
|
notificationManager.notify(notificationId, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(title)
|
setContentTitle(title)
|
||||||
setContentText(body)
|
setContentText(body)
|
||||||
})
|
})
|
||||||
@ -367,6 +394,7 @@ class LibraryUpdateService : Service() {
|
|||||||
private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
|
private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
|
||||||
notificationManager.notify(notificationId, notification() {
|
notificationManager.notify(notificationId, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(manga.title)
|
setContentTitle(manga.title)
|
||||||
setProgress(total, current, false)
|
setProgress(total, current, false)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
@ -387,6 +415,7 @@ class LibraryUpdateService : Service() {
|
|||||||
|
|
||||||
notificationManager.notify(notificationId, notification() {
|
notificationManager.notify(notificationId, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
|
setLargeIcon(notificationBitmap)
|
||||||
setContentTitle(title)
|
setContentTitle(title)
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
||||||
setContentIntent(notificationIntent)
|
setContentIntent(notificationIntent)
|
||||||
@ -411,41 +440,6 @@ class LibraryUpdateService : Service() {
|
|||||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that triggers the library to update when a connection is available. It receives
|
|
||||||
* network changes.
|
|
||||||
*/
|
|
||||||
class SyncOnConnectionAvailable : BroadcastReceiver() {
|
|
||||||
/**
|
|
||||||
* Method called when a network change occurs.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
* @param intent the intent received.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (DeviceUtil.isNetworkConnected(context)) {
|
|
||||||
AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
|
|
||||||
start(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that triggers the library to update when connected to power.
|
|
||||||
*/
|
|
||||||
class SyncOnPowerConnected: BroadcastReceiver() {
|
|
||||||
/**
|
|
||||||
* Method called when AC is connected.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
* @param intent the intent received.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
|
|
||||||
start(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that stops updating the library.
|
* Class that stops updating the library.
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.google.android.gms.gcm.*
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class LibraryUpdateTrigger : GcmTaskService() {
|
||||||
|
|
||||||
|
override fun onInitializeTasks() {
|
||||||
|
setupTask(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRunTask(params: TaskParams): Int {
|
||||||
|
LibraryUpdateService.start(this)
|
||||||
|
return GcmNetworkManager.RESULT_SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
val interval = prefInterval ?: preferences.libraryUpdateInterval().getOrDefault()
|
||||||
|
if (interval > 0) {
|
||||||
|
val restrictions = preferences.libraryUpdateRestriction()
|
||||||
|
val acRestriction = "ac" in restrictions
|
||||||
|
val wifiRestriction = if ("wifi" in restrictions)
|
||||||
|
Task.NETWORK_STATE_UNMETERED
|
||||||
|
else
|
||||||
|
Task.NETWORK_STATE_ANY
|
||||||
|
|
||||||
|
val task = PeriodicTask.Builder()
|
||||||
|
.setService(LibraryUpdateTrigger::class.java)
|
||||||
|
.setTag("Library periodic update")
|
||||||
|
.setPeriod(interval * 60 * 60L)
|
||||||
|
.setFlex(5 * 60)
|
||||||
|
.setRequiredNetwork(wifiRestriction)
|
||||||
|
.setRequiresCharging(acRestriction)
|
||||||
|
.setUpdateCurrent(true)
|
||||||
|
.setPersisted(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
GcmNetworkManager.getInstance(context).schedule(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelTask(context: Context) {
|
||||||
|
GcmNetworkManager.getInstance(context).cancelAllTasks(LibraryUpdateTrigger::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,21 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync
|
package eu.kanade.tachiyomi.data.mangasync
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
|
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
|
||||||
|
|
||||||
class MangaSyncManager(private val context: Context) {
|
class MangaSyncManager(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MYANIMELIST = 1
|
const val MYANIMELIST = 1
|
||||||
|
const val ANILIST = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||||
|
|
||||||
|
val aniList = Anilist(context, ANILIST)
|
||||||
|
|
||||||
|
// TODO enable anilist
|
||||||
val services = listOf(myAnimeList)
|
val services = listOf(myAnimeList)
|
||||||
|
|
||||||
fun getService(id: Int) = services.find { it.id == id }
|
fun getService(id: Int) = services.find { it.id == id }
|
||||||
|
@ -2,23 +2,18 @@ package eu.kanade.tachiyomi.data.mangasync
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.support.annotation.CallSuper
|
import android.support.annotation.CallSuper
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import javax.inject.Inject
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class MangaSyncService(private val context: Context, val id: Int) {
|
abstract class MangaSyncService(private val context: Context, val id: Int) {
|
||||||
|
|
||||||
@Inject lateinit var preferences: PreferencesHelper
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
@Inject lateinit var networkService: NetworkHelper
|
val networkService: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
init {
|
|
||||||
App.get(context).component.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
open val client: OkHttpClient
|
open val client: OkHttpClient
|
||||||
get() = networkService.client
|
get() = networkService.client
|
||||||
|
@ -4,25 +4,23 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import javax.inject.Inject
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class UpdateMangaSyncService : Service() {
|
class UpdateMangaSyncService : Service() {
|
||||||
|
|
||||||
@Inject lateinit var syncManager: MangaSyncManager
|
val syncManager: MangaSyncManager by injectLazy()
|
||||||
@Inject lateinit var db: DatabaseHelper
|
val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
private lateinit var subscriptions: CompositeSubscription
|
private lateinit var subscriptions: CompositeSubscription
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
App.get(this).component.inject(this)
|
|
||||||
subscriptions = CompositeSubscription()
|
subscriptions = CompositeSubscription()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
||||||
|
import rx.Completable
|
||||||
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class Anilist(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLAN_TO_READ = 5
|
||||||
|
|
||||||
|
const val DEFAULT_STATUS = READING
|
||||||
|
const val DEFAULT_SCORE = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override val name = "AniList"
|
||||||
|
|
||||||
|
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
|
||||||
|
|
||||||
|
private val api by lazy {
|
||||||
|
AnilistApi.createService(networkService.client.newBuilder()
|
||||||
|
.addInterceptor(interceptor)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
|
fun login(authCode: String): Completable {
|
||||||
|
// Create a new api with the default client to avoid request interceptions.
|
||||||
|
return AnilistApi.createService(client)
|
||||||
|
// Request the access token from the API with the authorization code.
|
||||||
|
.requestAccessToken(authCode)
|
||||||
|
// Save the token in the interceptor.
|
||||||
|
.doOnNext { interceptor.setAuth(it) }
|
||||||
|
// Obtain the authenticated user from the API.
|
||||||
|
.zipWith(api.getCurrentUser().map { it["id"].toString() })
|
||||||
|
{ oauth, user -> Pair(user, oauth.refresh_token!!) }
|
||||||
|
// Save service credentials (username and refresh token).
|
||||||
|
.doOnNext { saveCredentials(it.first, it.second) }
|
||||||
|
// Logout on any error.
|
||||||
|
.doOnError { logout() }
|
||||||
|
.toCompletable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout() {
|
||||||
|
super.logout()
|
||||||
|
interceptor.setAuth(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String): Observable<List<MangaSync>> {
|
||||||
|
return api.search(query, 1)
|
||||||
|
.flatMap { Observable.from(it) }
|
||||||
|
.filter { it.type != "Novel" }
|
||||||
|
.map { it.toMangaSync() }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getList(): Observable<List<MangaSync>> {
|
||||||
|
return api.getList(getUsername())
|
||||||
|
.flatMap { Observable.from(it.flatten()) }
|
||||||
|
.map { it.toMangaSync() }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(manga: MangaSync): Observable<MangaSync> {
|
||||||
|
return api.addManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
||||||
|
manga.score.toInt())
|
||||||
|
.doOnNext { it.body().close() }
|
||||||
|
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
||||||
|
.doOnError { Timber.e(it, it.message) }
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(manga: MangaSync): Observable<MangaSync> {
|
||||||
|
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
||||||
|
manga.status = COMPLETED
|
||||||
|
}
|
||||||
|
return api.updateManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
|
||||||
|
manga.score.toInt())
|
||||||
|
.doOnNext { it.body().close() }
|
||||||
|
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
||||||
|
.doOnError { Timber.e(it, it.message) }
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(manga: MangaSync): Observable<MangaSync> {
|
||||||
|
return getList()
|
||||||
|
.flatMap { userlist ->
|
||||||
|
manga.sync_id = id
|
||||||
|
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
|
||||||
|
if (mangaFromList != null) {
|
||||||
|
manga.copyPersonalFrom(mangaFromList)
|
||||||
|
update(manga)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
manga.score = DEFAULT_SCORE.toFloat()
|
||||||
|
manga.status = DEFAULT_STATUS
|
||||||
|
add(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String = with(context) {
|
||||||
|
when (status) {
|
||||||
|
READING -> getString(R.string.reading)
|
||||||
|
COMPLETED -> getString(R.string.completed)
|
||||||
|
ON_HOLD -> getString(R.string.on_hold)
|
||||||
|
DROPPED -> getString(R.string.dropped)
|
||||||
|
PLAN_TO_READ -> getString(R.string.plan_to_read)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MangaSync.getAnilistStatus() = when (status) {
|
||||||
|
READING -> "reading"
|
||||||
|
COMPLETED -> "completed"
|
||||||
|
ON_HOLD -> "on-hold"
|
||||||
|
DROPPED -> "dropped"
|
||||||
|
PLAN_TO_READ -> "plan to read"
|
||||||
|
else -> throw NotImplementedError("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALManga
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALUserLists
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
||||||
|
import eu.kanade.tachiyomi.data.network.POST
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import retrofit2.http.*
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
interface AnilistApi {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val clientId = "tachiyomi-hrtje"
|
||||||
|
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
|
||||||
|
private const val clientUrl = "tachiyomi://anilist-auth"
|
||||||
|
private const val baseUrl = "https://anilist.co/api/"
|
||||||
|
|
||||||
|
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
|
||||||
|
.appendQueryParameter("grant_type", "authorization_code")
|
||||||
|
.appendQueryParameter("client_id", clientId)
|
||||||
|
.appendQueryParameter("redirect_uri", clientUrl)
|
||||||
|
.appendQueryParameter("response_type", "code")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
|
||||||
|
body = FormBody.Builder()
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("client_secret", clientSecret)
|
||||||
|
.add("refresh_token", token)
|
||||||
|
.build())
|
||||||
|
|
||||||
|
fun createService(client: OkHttpClient) = Retrofit.Builder()
|
||||||
|
.baseUrl(baseUrl)
|
||||||
|
.client(client)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(AnilistApi::class.java)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("auth/access_token")
|
||||||
|
fun requestAccessToken(
|
||||||
|
@Field("code") code: String,
|
||||||
|
@Field("grant_type") grant_type: String = "authorization_code",
|
||||||
|
@Field("client_id") client_id: String = clientId,
|
||||||
|
@Field("client_secret") client_secret: String = clientSecret,
|
||||||
|
@Field("redirect_uri") redirect_uri: String = clientUrl)
|
||||||
|
: Observable<OAuth>
|
||||||
|
|
||||||
|
@GET("user")
|
||||||
|
fun getCurrentUser(): Observable<JsonObject>
|
||||||
|
|
||||||
|
@GET("manga/search/{query}")
|
||||||
|
fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>>
|
||||||
|
|
||||||
|
@GET("user/{username}/mangalist")
|
||||||
|
fun getList(@Path("username") username: String): Observable<ALUserLists>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@PUT("mangalist")
|
||||||
|
fun addManga(
|
||||||
|
@Field("id") id: Int,
|
||||||
|
@Field("chapters_read") chapters_read: Int,
|
||||||
|
@Field("list_status") list_status: String,
|
||||||
|
@Field("score_raw") score_raw: Int)
|
||||||
|
: Observable<Response<ResponseBody>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@PUT("mangalist")
|
||||||
|
fun updateManga(
|
||||||
|
@Field("id") id: Int,
|
||||||
|
@Field("chapters_read") chapters_read: Int,
|
||||||
|
@Field("list_status") list_status: String,
|
||||||
|
@Field("score_raw") score_raw: Int)
|
||||||
|
: Observable<Response<ResponseBody>>
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth object used for authenticated requests.
|
||||||
|
*
|
||||||
|
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||||
|
* before its original expiration date.
|
||||||
|
*/
|
||||||
|
private var oauth: OAuth? = null
|
||||||
|
set(value) {
|
||||||
|
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
if (refreshToken.isNullOrEmpty()) {
|
||||||
|
throw Exception("Not authenticated with Anilist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh access token if null or expired.
|
||||||
|
if (oauth == null || oauth!!.isExpired()) {
|
||||||
|
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
|
||||||
|
oauth = if (response.isSuccessful) {
|
||||||
|
Gson().fromJson(response.body().string(), OAuth::class.java)
|
||||||
|
} else {
|
||||||
|
response.close()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw on null auth.
|
||||||
|
if (oauth == null) {
|
||||||
|
throw Exception("Access token wasn't refreshed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the authorization header to the original request.
|
||||||
|
val authRequest = originalRequest.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(authRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||||
|
* and the oauth object.
|
||||||
|
*/
|
||||||
|
fun setAuth(oauth: OAuth?) {
|
||||||
|
refreshToken = oauth?.refresh_token
|
||||||
|
this.oauth = oauth
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||||
|
|
||||||
|
data class ALManga(
|
||||||
|
val id: Int,
|
||||||
|
val title_romaji: String,
|
||||||
|
val type: String,
|
||||||
|
val total_chapters: Int) {
|
||||||
|
|
||||||
|
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
||||||
|
remote_id = this@ALManga.id
|
||||||
|
title = title_romaji
|
||||||
|
total_chapters = this@ALManga.total_chapters
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||||
|
|
||||||
|
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
|
||||||
|
|
||||||
|
fun flatten() = lists.values.flatten()
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
|
||||||
|
|
||||||
|
data class ALUserManga(
|
||||||
|
val id: Int,
|
||||||
|
val list_status: String,
|
||||||
|
val score_raw: Int,
|
||||||
|
val chapters_read: Int,
|
||||||
|
val manga: ALManga) {
|
||||||
|
|
||||||
|
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
|
||||||
|
remote_id = manga.id
|
||||||
|
status = getMangaSyncStatus()
|
||||||
|
score = score_raw.toFloat()
|
||||||
|
last_chapter_read = chapters_read
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMangaSyncStatus() = when (list_status) {
|
||||||
|
"reading" -> Anilist.READING
|
||||||
|
"completed" -> Anilist.COMPLETED
|
||||||
|
"on-hold" -> Anilist.ON_HOLD
|
||||||
|
"dropped" -> Anilist.DROPPED
|
||||||
|
"plan to read" -> Anilist.PLAN_TO_READ
|
||||||
|
else -> throw NotImplementedError("Unknown status")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.anilist.model
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val expires: Long,
|
||||||
|
val expires_in: Long,
|
||||||
|
val refresh_token: String?) {
|
||||||
|
|
||||||
|
fun isExpired() = System.currentTimeMillis() > expires
|
||||||
|
}
|
@ -26,7 +26,7 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
|
|||||||
private lateinit var headers: Headers
|
private lateinit var headers: Headers
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val BASE_URL = "http://myanimelist.net"
|
val BASE_URL = "https://myanimelist.net"
|
||||||
|
|
||||||
private val ENTRY_TAG = "entry"
|
private val ENTRY_TAG = "entry"
|
||||||
private val CHAPTER_TAG = "chapter"
|
private val CHAPTER_TAG = "chapter"
|
||||||
@ -97,8 +97,8 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
|
|||||||
.flatMap { Observable.from(it.select("entry")) }
|
.flatMap { Observable.from(it.select("entry")) }
|
||||||
.filter { it.select("type").text() != "Novel" }
|
.filter { it.select("type").text() != "Novel" }
|
||||||
.map {
|
.map {
|
||||||
MangaSync.create(this).apply {
|
MangaSync.create(id).apply {
|
||||||
title = it.selectText("title")
|
title = it.selectText("title")!!
|
||||||
remote_id = it.selectInt("id")
|
remote_id = it.selectInt("id")
|
||||||
total_chapters = it.selectInt("chapters")
|
total_chapters = it.selectInt("chapters")
|
||||||
}
|
}
|
||||||
@ -114,8 +114,8 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
|
|||||||
.map { Jsoup.parse(it.body().string()) }
|
.map { Jsoup.parse(it.body().string()) }
|
||||||
.flatMap { Observable.from(it.select("manga")) }
|
.flatMap { Observable.from(it.select("manga")) }
|
||||||
.map {
|
.map {
|
||||||
MangaSync.create(this).apply {
|
MangaSync.create(id).apply {
|
||||||
title = it.selectText("series_title")
|
title = it.selectText("series_title")!!
|
||||||
remote_id = it.selectInt("series_mangadb_id")
|
remote_id = it.selectInt("series_mangadb_id")
|
||||||
last_chapter_read = it.selectInt("my_read_chapters")
|
last_chapter_read = it.selectInt("my_read_chapters")
|
||||||
status = it.selectInt("my_status")
|
status = it.selectInt("my_status")
|
||||||
|
@ -55,13 +55,12 @@ class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interc
|
|||||||
|
|
||||||
val js = operation
|
val js = operation
|
||||||
//language=RegExp
|
//language=RegExp
|
||||||
.replace(Regex("""a\.value =(.+?) \+ .+?;"""), "$1")
|
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
||||||
//language=RegExp
|
//language=RegExp
|
||||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||||
.replace("\n", "")
|
.replace("\n", "")
|
||||||
|
|
||||||
// Duktape can only return strings, so the result has to be converted to string first
|
val result = (duktape.evaluate(js) as Double).toInt()
|
||||||
val result = duktape.evaluate("$js.toString()").toInt()
|
|
||||||
|
|
||||||
val answer = "${result + domain.length}"
|
val answer = "${result + domain.length}"
|
||||||
|
|
||||||
|
@ -5,24 +5,49 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subscriptions.Subscriptions
|
import rx.Producer
|
||||||
import java.io.IOException
|
import rx.Subscription
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
fun Call.asObservable(): Observable<Response> {
|
fun Call.asObservable(): Observable<Response> {
|
||||||
return Observable.create { subscriber ->
|
return Observable.create { subscriber ->
|
||||||
subscriber.add(Subscriptions.create { cancel() })
|
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||||
|
val call = if (!isExecuted) this else {
|
||||||
|
// TODO use clone method in OkHttp 3.5
|
||||||
|
val field = javaClass.getDeclaredField("client").apply { isAccessible = true }
|
||||||
|
val client = field.get(this) as OkHttpClient
|
||||||
|
client.newCall(request())
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
||||||
val response = execute()
|
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
|
||||||
if (!subscriber.isUnsubscribed) {
|
override fun request(n: Long) {
|
||||||
subscriber.onNext(response)
|
if (n == 0L || !compareAndSet(false, true)) return
|
||||||
subscriber.onCompleted()
|
|
||||||
|
try {
|
||||||
|
val response = call.execute()
|
||||||
|
if (!subscriber.isUnsubscribed) {
|
||||||
|
subscriber.onNext(response)
|
||||||
|
subscriber.onCompleted()
|
||||||
|
}
|
||||||
|
} catch (error: Exception) {
|
||||||
|
if (!subscriber.isUnsubscribed) {
|
||||||
|
subscriber.onError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error: IOException) {
|
|
||||||
if (!subscriber.isUnsubscribed) {
|
override fun unsubscribe() {
|
||||||
subscriber.onError(error)
|
call.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isUnsubscribed(): Boolean {
|
||||||
|
return call.isCanceled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subscriber.add(requestArbiter)
|
||||||
|
subscriber.setProducer(requestArbiter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,13 +10,15 @@ import eu.kanade.tachiyomi.R
|
|||||||
*/
|
*/
|
||||||
class PreferenceKeys(context: Context) {
|
class PreferenceKeys(context: Context) {
|
||||||
|
|
||||||
|
val theme = context.getString(R.string.pref_theme_key)
|
||||||
|
|
||||||
val rotation = context.getString(R.string.pref_rotation_type_key)
|
val rotation = context.getString(R.string.pref_rotation_type_key)
|
||||||
|
|
||||||
val enableTransitions = context.getString(R.string.pref_enable_transitions_key)
|
val enableTransitions = context.getString(R.string.pref_enable_transitions_key)
|
||||||
|
|
||||||
val showPageNumber = context.getString(R.string.pref_show_page_number_key)
|
val showPageNumber = context.getString(R.string.pref_show_page_number_key)
|
||||||
|
|
||||||
val hideStatusBar = context.getString(R.string.pref_hide_status_bar_key)
|
val fullscreen = context.getString(R.string.pref_fullscreen_key)
|
||||||
|
|
||||||
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key)
|
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key)
|
||||||
|
|
||||||
@ -24,6 +26,10 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
|
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
|
||||||
|
|
||||||
|
val colorFilter = context.getString(R.string.pref_color_filter_key)
|
||||||
|
|
||||||
|
val colorFilterValue = context.getString(R.string.pref_color_filter_value_key)
|
||||||
|
|
||||||
val defaultViewer = context.getString(R.string.pref_default_viewer_key)
|
val defaultViewer = context.getString(R.string.pref_default_viewer_key)
|
||||||
|
|
||||||
val imageScaleType = context.getString(R.string.pref_image_scale_type_key)
|
val imageScaleType = context.getString(R.string.pref_image_scale_type_key)
|
||||||
@ -54,8 +60,6 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
|
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
|
||||||
|
|
||||||
val seamlessMode = context.getString(R.string.pref_seamless_mode_key)
|
|
||||||
|
|
||||||
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
|
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
|
||||||
|
|
||||||
val enabledLanguages = context.getString(R.string.pref_source_languages)
|
val enabledLanguages = context.getString(R.string.pref_source_languages)
|
||||||
@ -66,9 +70,7 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
|
val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
|
||||||
|
|
||||||
val removeAfterRead = context.getString(R.string.pref_remove_after_read_key)
|
val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key)
|
||||||
|
|
||||||
val removeAfterReadPrevious = context.getString(R.string.pref_remove_after_read_previous_key)
|
|
||||||
|
|
||||||
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
|
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
|
||||||
|
|
||||||
@ -76,10 +78,16 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key)
|
val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key)
|
||||||
|
|
||||||
|
val libraryUpdateCategories = context.getString(R.string.pref_library_update_categories_key)
|
||||||
|
|
||||||
val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
|
val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
|
||||||
|
|
||||||
val filterUnread = context.getString(R.string.pref_filter_unread_key)
|
val filterUnread = context.getString(R.string.pref_filter_unread_key)
|
||||||
|
|
||||||
|
val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key)
|
||||||
|
|
||||||
|
val startScreen = context.getString(R.string.pref_start_screen_key)
|
||||||
|
|
||||||
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
|
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
|
||||||
|
|
||||||
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
|
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
|
||||||
@ -88,4 +96,6 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
|
||||||
|
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import java.io.IOException
|
|||||||
|
|
||||||
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
||||||
|
|
||||||
class PreferencesHelper(private val context: Context) {
|
class PreferencesHelper(context: Context) {
|
||||||
|
|
||||||
val keys = PreferenceKeys(context)
|
val keys = PreferenceKeys(context)
|
||||||
|
|
||||||
@ -32,28 +32,11 @@ class PreferencesHelper(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
fun startScreen() = prefs.getInt(keys.startScreen, 1)
|
||||||
|
|
||||||
fun getLibraryUpdateInterval(context: Context): Int {
|
fun clear() = prefs.edit().clear().apply()
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
|
|
||||||
context.getString(R.string.pref_library_update_interval_key), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAutomaticUpdateStatus(context: Context): Boolean {
|
fun theme() = prefs.getInt(keys.theme, 1)
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
|
|
||||||
context.getString(R.string.pref_enable_automatic_updates), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getTheme(context: Context): Int {
|
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
|
|
||||||
context.getString(R.string.pref_theme_key), 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear() {
|
|
||||||
prefs.edit().clear().apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
|
fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
|
||||||
|
|
||||||
@ -61,13 +44,17 @@ class PreferencesHelper(private val context: Context) {
|
|||||||
|
|
||||||
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
|
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
|
||||||
|
|
||||||
fun hideStatusBar() = rxPrefs.getBoolean(keys.hideStatusBar, true)
|
fun fullscreen() = rxPrefs.getBoolean(keys.fullscreen, true)
|
||||||
|
|
||||||
fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
|
fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
|
||||||
|
|
||||||
fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false)
|
fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false)
|
||||||
|
|
||||||
fun customBrightnessValue() = rxPrefs.getFloat(keys.customBrightnessValue, 0f)
|
fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0)
|
||||||
|
|
||||||
|
fun colorFilter() = rxPrefs.getBoolean(keys.colorFilter, false)
|
||||||
|
|
||||||
|
fun colorFilterValue() = rxPrefs.getInteger(keys.colorFilterValue, 0)
|
||||||
|
|
||||||
fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
|
fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
|
||||||
|
|
||||||
@ -101,8 +88,6 @@ class PreferencesHelper(private val context: Context) {
|
|||||||
|
|
||||||
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
|
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
|
||||||
|
|
||||||
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
|
|
||||||
|
|
||||||
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
||||||
|
|
||||||
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN"))
|
fun enabledLanguages() = rxPrefs.getStringSet(keys.enabledLanguages, setOf("EN"))
|
||||||
@ -135,9 +120,7 @@ class PreferencesHelper(private val context: Context) {
|
|||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
fun removeAfterRead() = prefs.getBoolean(keys.removeAfterRead, false)
|
fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1)
|
||||||
|
|
||||||
fun removeAfterReadPrevious() = prefs.getBoolean(keys.removeAfterReadPrevious, false)
|
|
||||||
|
|
||||||
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
|
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
|
||||||
|
|
||||||
@ -145,8 +128,16 @@ class PreferencesHelper(private val context: Context) {
|
|||||||
|
|
||||||
fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
|
fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
|
||||||
|
|
||||||
|
fun libraryUpdateCategories() = rxPrefs.getStringSet(keys.libraryUpdateCategories, emptySet())
|
||||||
|
|
||||||
|
fun libraryAsList() = rxPrefs.getBoolean(keys.libraryAsList, false)
|
||||||
|
|
||||||
fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
|
fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
|
||||||
|
|
||||||
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
|
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
|
||||||
|
|
||||||
|
fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
|
||||||
|
|
||||||
|
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,8 @@ package eu.kanade.tachiyomi.data.source
|
|||||||
|
|
||||||
class Language(val code: String, val lang: String)
|
class Language(val code: String, val lang: String)
|
||||||
|
|
||||||
|
val DE = Language("DE", "German")
|
||||||
val EN = Language("EN", "English")
|
val EN = Language("EN", "English")
|
||||||
val RU = Language("RU", "Russian")
|
val RU = Language("RU", "Russian")
|
||||||
|
|
||||||
fun getLanguages() = listOf(EN, RU)
|
fun getLanguages() = listOf(DE, EN, RU)
|
@ -47,5 +47,4 @@ interface Source {
|
|||||||
* @param page the page.
|
* @param page the page.
|
||||||
*/
|
*/
|
||||||
fun fetchImage(page: Page): Observable<Page>
|
fun fetchImage(page: Page): Observable<Page>
|
||||||
|
|
||||||
}
|
}
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.online.english.*
|
import eu.kanade.tachiyomi.data.source.online.english.*
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.german.WieManga
|
||||||
import eu.kanade.tachiyomi.data.source.online.russian.Mangachan
|
import eu.kanade.tachiyomi.data.source.online.russian.Mangachan
|
||||||
import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga
|
import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga
|
||||||
import eu.kanade.tachiyomi.data.source.online.russian.Readmanga
|
import eu.kanade.tachiyomi.data.source.online.russian.Readmanga
|
||||||
@ -17,18 +18,7 @@ import java.io.File
|
|||||||
|
|
||||||
open class SourceManager(private val context: Context) {
|
open class SourceManager(private val context: Context) {
|
||||||
|
|
||||||
val BATOTO = 1
|
private val sourcesMap = createSources()
|
||||||
val MANGAHERE = 2
|
|
||||||
val MANGAFOX = 3
|
|
||||||
val KISSMANGA = 4
|
|
||||||
val READMANGA = 5
|
|
||||||
val MINTMANGA = 6
|
|
||||||
val MANGACHAN = 7
|
|
||||||
val READMANGATODAY = 8
|
|
||||||
|
|
||||||
val LAST_SOURCE = 8
|
|
||||||
|
|
||||||
val sourcesMap = createSources()
|
|
||||||
|
|
||||||
open fun get(sourceKey: Int): Source? {
|
open fun get(sourceKey: Int): Source? {
|
||||||
return sourcesMap[sourceKey]
|
return sourcesMap[sourceKey]
|
||||||
@ -36,22 +26,21 @@ open class SourceManager(private val context: Context) {
|
|||||||
|
|
||||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
|
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
|
||||||
|
|
||||||
private fun createSource(id: Int): Source? = when (id) {
|
private fun createOnlineSourceList(): List<Source> = listOf(
|
||||||
BATOTO -> Batoto(context, id)
|
Batoto(1),
|
||||||
KISSMANGA -> Kissmanga(context, id)
|
Mangahere(2),
|
||||||
MANGAHERE -> Mangahere(context, id)
|
Mangafox(3),
|
||||||
MANGAFOX -> Mangafox(context, id)
|
Kissmanga(4),
|
||||||
READMANGA -> Readmanga(context, id)
|
Readmanga(5),
|
||||||
MINTMANGA -> Mintmanga(context, id)
|
Mintmanga(6),
|
||||||
MANGACHAN -> Mangachan(context, id)
|
Mangachan(7),
|
||||||
READMANGATODAY -> Readmangatoday(context, id)
|
Readmangatoday(8),
|
||||||
else -> null
|
Mangasee(9),
|
||||||
}
|
WieManga(10)
|
||||||
|
)
|
||||||
|
|
||||||
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
|
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
|
||||||
for (i in 1..LAST_SOURCE) {
|
createOnlineSourceList().forEach { put(it.id, it) }
|
||||||
createSource(i)?.let { put(i, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||||
File.separator + context.getString(R.string.app_name), "parsers")
|
File.separator + context.getString(R.string.app_name), "parsers")
|
||||||
@ -61,7 +50,7 @@ open class SourceManager(private val context: Context) {
|
|||||||
for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
|
for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
|
||||||
try {
|
try {
|
||||||
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
|
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
|
||||||
YamlOnlineSource(context, map).let { put(it.id, it) }
|
YamlOnlineSource(map).let { put(it.id, it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("Error loading source from file. Bad format?")
|
Timber.e("Error loading source from file. Bad format?")
|
||||||
}
|
}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.model;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
|
|
||||||
public class MangasPage {
|
|
||||||
|
|
||||||
public List<Manga> mangas;
|
|
||||||
public int page;
|
|
||||||
public String url;
|
|
||||||
public String nextPageUrl;
|
|
||||||
|
|
||||||
public MangasPage(int page) {
|
|
||||||
this.page = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,13 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
|
||||||
|
class MangasPage(val page: Int) {
|
||||||
|
|
||||||
|
val mangas: MutableList<Manga> = mutableListOf()
|
||||||
|
|
||||||
|
lateinit var url: String
|
||||||
|
|
||||||
|
var nextPageUrl: String? = null
|
||||||
|
|
||||||
|
}
|
@ -1,105 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.model;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
|
||||||
import eu.kanade.tachiyomi.data.network.ProgressListener;
|
|
||||||
import rx.subjects.PublishSubject;
|
|
||||||
|
|
||||||
public class Page implements ProgressListener {
|
|
||||||
|
|
||||||
private int pageNumber;
|
|
||||||
private String url;
|
|
||||||
private String imageUrl;
|
|
||||||
private transient Chapter chapter;
|
|
||||||
private transient String imagePath;
|
|
||||||
private transient volatile int status;
|
|
||||||
private transient volatile int progress;
|
|
||||||
|
|
||||||
private transient PublishSubject<Integer> statusSubject;
|
|
||||||
|
|
||||||
public static final int QUEUE = 0;
|
|
||||||
public static final int LOAD_PAGE = 1;
|
|
||||||
public static final int DOWNLOAD_IMAGE = 2;
|
|
||||||
public static final int READY = 3;
|
|
||||||
public static final int ERROR = 4;
|
|
||||||
|
|
||||||
public Page(int pageNumber, String url) {
|
|
||||||
this(pageNumber, url, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Page(int pageNumber, String url, String imageUrl) {
|
|
||||||
this(pageNumber, url, imageUrl, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Page(int pageNumber, String url, String imageUrl, String imagePath) {
|
|
||||||
this.pageNumber = pageNumber;
|
|
||||||
this.url = url;
|
|
||||||
this.imageUrl = imageUrl;
|
|
||||||
this.imagePath = imagePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPageNumber() {
|
|
||||||
return pageNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUrl() {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getImageUrl() {
|
|
||||||
return imageUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setImageUrl(String imageUrl) {
|
|
||||||
this.imageUrl = imageUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getImagePath() {
|
|
||||||
return imagePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setImagePath(String imagePath) {
|
|
||||||
this.imagePath = imagePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(int status) {
|
|
||||||
this.status = status;
|
|
||||||
if (statusSubject != null)
|
|
||||||
statusSubject.onNext(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getProgress() {
|
|
||||||
return progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProgress(int value) {
|
|
||||||
progress = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void update(long bytesRead, long contentLength, boolean done) {
|
|
||||||
progress = (int) ((100 * bytesRead) / contentLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatusSubject(PublishSubject<Integer> subject) {
|
|
||||||
this.statusSubject = subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Chapter getChapter() {
|
|
||||||
return chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setChapter(Chapter chapter) {
|
|
||||||
this.chapter = chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isLastPage() {
|
|
||||||
List<Page> chapterPages = chapter.getPages();
|
|
||||||
return chapterPages.size() -1 == pageNumber;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.network.ProgressListener
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
|
||||||
|
import rx.subjects.Subject
|
||||||
|
|
||||||
|
class Page(
|
||||||
|
val pageNumber: Int,
|
||||||
|
val url: String,
|
||||||
|
var imageUrl: String? = null,
|
||||||
|
@Transient var imagePath: String? = null
|
||||||
|
) : ProgressListener {
|
||||||
|
|
||||||
|
@Transient lateinit var chapter: ReaderChapter
|
||||||
|
|
||||||
|
@Transient @Volatile var status: Int = 0
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
statusSubject?.onNext(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient @Volatile var progress: Int = 0
|
||||||
|
|
||||||
|
@Transient private var statusSubject: Subject<Int, Int>? = null
|
||||||
|
|
||||||
|
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||||
|
progress = (100 * bytesRead / contentLength).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStatusSubject(subject: Subject<Int, Int>?) {
|
||||||
|
this.statusSubject = subject
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val QUEUE = 0
|
||||||
|
const val LOAD_PAGE = 1
|
||||||
|
const val DOWNLOAD_IMAGE = 2
|
||||||
|
const val READY = 3
|
||||||
|
const val ERROR = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
package eu.kanade.tachiyomi.data.source.online
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -14,31 +12,33 @@ import eu.kanade.tachiyomi.data.source.Language
|
|||||||
import eu.kanade.tachiyomi.data.source.Source
|
import eu.kanade.tachiyomi.data.source.Source
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import okhttp3.*
|
import eu.kanade.tachiyomi.util.UrlUtil
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import javax.inject.Inject
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple implementation for sources from a website.
|
* A simple implementation for sources from a website.
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
*/
|
||||||
abstract class OnlineSource(context: Context) : Source {
|
abstract class OnlineSource() : Source {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Network service.
|
* Network service.
|
||||||
*/
|
*/
|
||||||
@Inject lateinit var network: NetworkHelper
|
val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chapter cache.
|
* Chapter cache.
|
||||||
*/
|
*/
|
||||||
@Inject lateinit var chapterCache: ChapterCache
|
val chapterCache: ChapterCache by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preferences helper.
|
* Preferences helper.
|
||||||
*/
|
*/
|
||||||
@Inject lateinit var preferences: PreferencesHelper
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||||
@ -50,22 +50,27 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
*/
|
*/
|
||||||
abstract val lang: Language
|
abstract val lang: Language
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the source has support for latest updates.
|
||||||
|
*/
|
||||||
|
abstract val supportsLatest : Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Headers used for requests.
|
* Headers used for requests.
|
||||||
*/
|
*/
|
||||||
val headers by lazy { headersBuilder().build() }
|
val headers by lazy { headersBuilder().build() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genre filters.
|
||||||
|
*/
|
||||||
|
val filters by lazy { getFilterList() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default network client for doing requests.
|
* Default network client for doing requests.
|
||||||
*/
|
*/
|
||||||
open val client: OkHttpClient
|
open val client: OkHttpClient
|
||||||
get() = network.client
|
get() = network.client
|
||||||
|
|
||||||
init {
|
|
||||||
// Inject dependencies.
|
|
||||||
App.get(context).component.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||||
*/
|
*/
|
||||||
@ -89,10 +94,8 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
.newCall(popularMangaRequest(page))
|
.newCall(popularMangaRequest(page))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
page.apply {
|
popularMangaParse(response, page)
|
||||||
mangas = mutableListOf<Manga>()
|
page
|
||||||
popularMangaParse(response, this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -130,14 +133,12 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
* the current page and the next page url.
|
* the current page and the next page url.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
open fun fetchSearchManga(page: MangasPage, query: String): Observable<MangasPage> = client
|
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = client
|
||||||
.newCall(searchMangaRequest(page, query))
|
.newCall(searchMangaRequest(page, query, filters))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
page.apply {
|
searchMangaParse(response, page, query, filters)
|
||||||
mangas = mutableListOf<Manga>()
|
page
|
||||||
searchMangaParse(response, this, query)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -147,9 +148,9 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
* @param page the page object.
|
* @param page the page object.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
open protected fun searchMangaRequest(page: MangasPage, query: String): Request {
|
open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||||
if (page.page == 1) {
|
if (page.page == 1) {
|
||||||
page.url = searchMangaInitialUrl(query)
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
}
|
}
|
||||||
return GET(page.url, headers)
|
return GET(page.url, headers)
|
||||||
}
|
}
|
||||||
@ -159,7 +160,7 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
*
|
*
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
abstract protected fun searchMangaInitialUrl(query: String): String
|
abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter>): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the response from the site. It should add a list of manga and the absolute url to the
|
* Parse the response from the site. It should add a list of manga and the absolute url to the
|
||||||
@ -169,7 +170,38 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
* @param page the page object to be filled.
|
* @param page the page object to be filled.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String)
|
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of latest manga.
|
||||||
|
*/
|
||||||
|
open fun fetchLatestUpdates(page: MangasPage): Observable<MangasPage> = client
|
||||||
|
.newCall(latestUpdatesRequest(page))
|
||||||
|
.asObservable()
|
||||||
|
.map { response ->
|
||||||
|
latestUpdatesParse(response, page)
|
||||||
|
page
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for latest manga given the page.
|
||||||
|
*/
|
||||||
|
open protected fun latestUpdatesRequest(page: MangasPage): Request {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = latestUpdatesInitialUrl()
|
||||||
|
}
|
||||||
|
return GET(page.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the absolute url of the first page to latest manga.
|
||||||
|
*/
|
||||||
|
abstract protected fun latestUpdatesInitialUrl(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as [popularMangaParse], but for latest manga.
|
||||||
|
*/
|
||||||
|
abstract protected fun latestUpdatesParse(response: Response, page: MangasPage)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||||
@ -193,7 +225,7 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
*
|
*
|
||||||
* @param manga the manga to be updated.
|
* @param manga the manga to be updated.
|
||||||
*/
|
*/
|
||||||
open protected fun mangaDetailsRequest(manga: Manga): Request {
|
open fun mangaDetailsRequest(manga: Manga): Request {
|
||||||
return GET(baseUrl + manga.url, headers)
|
return GET(baseUrl + manga.url, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,7 +395,7 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
* @param page the chapter whose page list has to be fetched
|
* @param page the chapter whose page list has to be fetched
|
||||||
*/
|
*/
|
||||||
open protected fun imageRequest(page: Page): Request {
|
open protected fun imageRequest(page: Page): Request {
|
||||||
return GET(page.imageUrl, headers)
|
return GET(page.imageUrl!!, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -373,20 +405,18 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
* @param page the page.
|
* @param page the page.
|
||||||
*/
|
*/
|
||||||
fun getCachedImage(page: Page): Observable<Page> {
|
fun getCachedImage(page: Page): Observable<Page> {
|
||||||
val pageObservable = Observable.just(page)
|
val imageUrl = page.imageUrl ?: return Observable.just(page)
|
||||||
if (page.imageUrl.isNullOrEmpty())
|
|
||||||
return pageObservable
|
|
||||||
|
|
||||||
return pageObservable
|
return Observable.just(page)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
if (!chapterCache.isImageInCache(page.imageUrl)) {
|
if (!chapterCache.isImageInCache(imageUrl)) {
|
||||||
cacheImage(page)
|
cacheImage(page)
|
||||||
} else {
|
} else {
|
||||||
Observable.just(page)
|
Observable.just(page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
page.imagePath = chapterCache.getImagePath(page.imageUrl)
|
page.imagePath = chapterCache.getImagePath(imageUrl)
|
||||||
page.status = Page.READY
|
page.status = Page.READY
|
||||||
}
|
}
|
||||||
.doOnError { page.status = Page.ERROR }
|
.doOnError { page.status = Page.ERROR }
|
||||||
@ -401,32 +431,13 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
private fun cacheImage(page: Page): Observable<Page> {
|
private fun cacheImage(page: Page): Observable<Page> {
|
||||||
page.status = Page.DOWNLOAD_IMAGE
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
return imageResponse(page)
|
return imageResponse(page)
|
||||||
.doOnNext { chapterCache.putImageToCache(page.imageUrl, it, preferences.reencodeImage()) }
|
.doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it, preferences.reencodeImage()) }
|
||||||
.map { page }
|
.map { page }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Utility methods
|
// Utility methods
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an absolute url from a href.
|
|
||||||
*
|
|
||||||
* Ex:
|
|
||||||
* href="http://example.com/foo" url="http://example.com" -> http://example.com/foo
|
|
||||||
* href="/mypath" url="http://example.com/foo" -> http://example.com/mypath
|
|
||||||
* href="bar" url="http://example.com/foo" -> http://example.com/bar
|
|
||||||
* href="bar" url="http://example.com/foo/" -> http://example.com/foo/bar
|
|
||||||
*
|
|
||||||
* @param href the href attribute from the html.
|
|
||||||
* @param url the requested url.
|
|
||||||
*/
|
|
||||||
fun getAbsoluteUrl(href: String, url: HttpUrl) = when {
|
|
||||||
href.startsWith("http://") || href.startsWith("https://") -> href
|
|
||||||
href.startsWith("/") -> url.newBuilder().encodedPath("/").fragment(null).query(null)
|
|
||||||
.toString() + href.substring(1)
|
|
||||||
else -> url.toString().substringBeforeLast('/') + "/$href"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchAllImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
|
fun fetchAllImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
|
||||||
.filter { !it.imageUrl.isNullOrEmpty() }
|
.filter { !it.imageUrl.isNullOrEmpty() }
|
||||||
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
||||||
@ -441,9 +452,21 @@ abstract class OnlineSource(context: Context) : Source {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Chapter.setUrlWithoutDomain(url: String) {
|
||||||
|
this.url = UrlUtil.getPath(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Manga.setUrlWithoutDomain(url: String) {
|
||||||
|
this.url = UrlUtil.getPath(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Overridable method to allow custom parsing.
|
// Overridable method to allow custom parsing.
|
||||||
open fun parseChapterNumber(chapter: Chapter) {
|
open fun parseChapterNumber(chapter: Chapter) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Filter(val id: String, val name: String)
|
||||||
|
|
||||||
|
open fun getFilterList(): List<Filter> = emptyList()
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,18 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
package eu.kanade.tachiyomi.data.source.online
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
*/
|
||||||
abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
abstract class ParsedOnlineSource() : OnlineSource() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the response from the site and fills [page].
|
* Parse the response from the site and fills [page].
|
||||||
@ -24,19 +21,16 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
* @param page the page object to be filled.
|
* @param page the page object to be filled.
|
||||||
*/
|
*/
|
||||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
override fun popularMangaParse(response: Response, page: MangasPage) {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val document = response.asJsoup()
|
||||||
for (element in document.select(popularMangaSelector())) {
|
for (element in document.select(popularMangaSelector())) {
|
||||||
Manga().apply {
|
Manga.create(id).apply {
|
||||||
source = this@ParsedOnlineSource.id
|
|
||||||
popularMangaFromElement(element, this)
|
popularMangaFromElement(element, this)
|
||||||
page.mangas.add(this)
|
page.mangas.add(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
popularMangaNextPageSelector()?.let { selector ->
|
popularMangaNextPageSelector()?.let { selector ->
|
||||||
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||||
getAbsoluteUrl(it, response.request().url())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,20 +61,17 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
* @param page the page object to be filled.
|
* @param page the page object to be filled.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
*/
|
*/
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val document = response.asJsoup()
|
||||||
for (element in document.select(searchMangaSelector())) {
|
for (element in document.select(searchMangaSelector())) {
|
||||||
Manga().apply {
|
Manga.create(id).apply {
|
||||||
source = this@ParsedOnlineSource.id
|
|
||||||
searchMangaFromElement(element, this)
|
searchMangaFromElement(element, this)
|
||||||
page.mangas.add(this)
|
page.mangas.add(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchMangaNextPageSelector()?.let { selector ->
|
searchMangaNextPageSelector()?.let { selector ->
|
||||||
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||||
getAbsoluteUrl(it, response.request().url())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +95,38 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
*/
|
*/
|
||||||
abstract protected fun searchMangaNextPageSelector(): String?
|
abstract protected fun searchMangaNextPageSelector(): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site for latest updates and fills [page].
|
||||||
|
*/
|
||||||
|
override fun latestUpdatesParse(response: Response, page: MangasPage) {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
for (element in document.select(latestUpdatesSelector())) {
|
||||||
|
Manga.create(id).apply {
|
||||||
|
latestUpdatesFromElement(element, this)
|
||||||
|
page.mangas.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
latestUpdatesNextPageSelector()?.let { selector ->
|
||||||
|
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector similar to [popularMangaSelector], but for latest updates.
|
||||||
|
*/
|
||||||
|
abstract protected fun latestUpdatesSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills [manga] with the given [element]. For latest updates.
|
||||||
|
*/
|
||||||
|
abstract protected fun latestUpdatesFromElement(element: Element, manga: Manga)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns the <a> tag, like [popularMangaNextPageSelector].
|
||||||
|
*/
|
||||||
|
abstract protected fun latestUpdatesNextPageSelector(): String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the response from the site and fills the details of [manga].
|
* Parse the response from the site and fills the details of [manga].
|
||||||
*
|
*
|
||||||
@ -111,7 +134,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
* @param manga the manga to fill.
|
* @param manga the manga to fill.
|
||||||
*/
|
*/
|
||||||
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
||||||
mangaDetailsParse(Jsoup.parse(response.body().string()), manga)
|
mangaDetailsParse(response.asJsoup(), manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,7 +152,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
* @param chapters the list of chapters to fill.
|
* @param chapters the list of chapters to fill.
|
||||||
*/
|
*/
|
||||||
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val document = response.asJsoup()
|
||||||
|
|
||||||
for (element in document.select(chapterListSelector())) {
|
for (element in document.select(chapterListSelector())) {
|
||||||
Chapter.create().apply {
|
Chapter.create().apply {
|
||||||
@ -159,7 +182,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
* @param pages the list of pages to fill.
|
* @param pages the list of pages to fill.
|
||||||
*/
|
*/
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||||
pageListParse(Jsoup.parse(response.body().string()), pages)
|
pageListParse(response.asJsoup(), pages)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -176,7 +199,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
override fun imageUrlParse(response: Response): String {
|
override fun imageUrlParse(response: Response): String {
|
||||||
return imageUrlParse(Jsoup.parse(response.body().string()))
|
return imageUrlParse(response.asJsoup())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -185,5 +208,4 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) {
|
|||||||
* @param document the parsed document.
|
* @param document the parsed document.
|
||||||
*/
|
*/
|
||||||
abstract protected fun imageUrlParse(document: Document): String
|
abstract protected fun imageUrlParse(document: Document): String
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
package eu.kanade.tachiyomi.data.source.online
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.data.network.GET
|
||||||
@ -8,6 +7,8 @@ import eu.kanade.tachiyomi.data.network.POST
|
|||||||
import eu.kanade.tachiyomi.data.source.getLanguages
|
import eu.kanade.tachiyomi.data.source.getLanguages
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import eu.kanade.tachiyomi.util.attrOrText
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
@ -15,7 +16,7 @@ import org.jsoup.nodes.Element
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(context) {
|
class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
||||||
|
|
||||||
val map = YamlSourceNode(mappings)
|
val map = YamlSourceNode(mappings)
|
||||||
|
|
||||||
@ -30,6 +31,8 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
getLanguages().find { code == it.code }!!
|
getLanguages().find { code == it.code }!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val supportsLatest = map.latestupdates != null
|
||||||
|
|
||||||
override val client = when(map.client) {
|
override val client = when(map.client) {
|
||||||
"cloudflare" -> network.cloudflareClient
|
"cloudflare" -> network.cloudflareClient
|
||||||
else -> network.client
|
else -> network.client
|
||||||
@ -52,26 +55,23 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
override fun popularMangaInitialUrl() = map.popular.url
|
override fun popularMangaInitialUrl() = map.popular.url
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
override fun popularMangaParse(response: Response, page: MangasPage) {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val document = response.asJsoup()
|
||||||
for (element in document.select(map.popular.manga_css)) {
|
for (element in document.select(map.popular.manga_css)) {
|
||||||
Manga().apply {
|
Manga.create(id).apply {
|
||||||
source = this@YamlOnlineSource.id
|
|
||||||
title = element.text()
|
title = element.text()
|
||||||
setUrl(element.attr("href"))
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
page.mangas.add(this)
|
page.mangas.add(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
map.popular.next_url_css?.let { selector ->
|
map.popular.next_url_css?.let { selector ->
|
||||||
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||||
getAbsoluteUrl(it, response.request().url())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||||
if (page.page == 1) {
|
if (page.page == 1) {
|
||||||
page.url = searchMangaInitialUrl(query)
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
}
|
}
|
||||||
return when (map.search.method?.toLowerCase()) {
|
return when (map.search.method?.toLowerCase()) {
|
||||||
"post" -> POST(page.url, headers, map.search.createForm())
|
"post" -> POST(page.url, headers, map.search.createForm())
|
||||||
@ -79,28 +79,52 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = map.search.url.replace("\$query", query)
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = map.search.url.replace("\$query", query)
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val document = response.asJsoup()
|
||||||
for (element in document.select(map.search.manga_css)) {
|
for (element in document.select(map.search.manga_css)) {
|
||||||
Manga().apply {
|
Manga.create(id).apply {
|
||||||
source = this@YamlOnlineSource.id
|
|
||||||
title = element.text()
|
title = element.text()
|
||||||
setUrl(element.attr("href"))
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
page.mangas.add(this)
|
page.mangas.add(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
map.search.next_url_css?.let { selector ->
|
map.search.next_url_css?.let { selector ->
|
||||||
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||||
getAbsoluteUrl(it, response.request().url())
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: MangasPage): Request {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = latestUpdatesInitialUrl()
|
||||||
|
}
|
||||||
|
return when (map.latestupdates!!.method?.toLowerCase()) {
|
||||||
|
"post" -> POST(page.url, headers, map.latestupdates.createForm())
|
||||||
|
else -> GET(page.url, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesInitialUrl() = map.latestupdates!!.url
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response, page: MangasPage) {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
for (element in document.select(map.latestupdates!!.manga_css)) {
|
||||||
|
Manga.create(id).apply {
|
||||||
|
title = element.text()
|
||||||
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
|
page.mangas.add(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
map.latestupdates.next_url_css?.let { selector ->
|
||||||
|
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val document = response.asJsoup()
|
||||||
with(map.manga) {
|
with(map.manga) {
|
||||||
val pool = parts.get(document)
|
val pool = parts.get(document)
|
||||||
|
|
||||||
@ -114,7 +138,7 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val document = response.asJsoup()
|
||||||
with(map.chapters) {
|
with(map.chapters) {
|
||||||
val pool = emptyMap<String, Element>()
|
val pool = emptyMap<String, Element>()
|
||||||
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
|
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
|
||||||
@ -123,7 +147,7 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
val chapter = Chapter.create()
|
val chapter = Chapter.create()
|
||||||
element.select(title).first().let {
|
element.select(title).first().let {
|
||||||
chapter.name = it.text()
|
chapter.name = it.text()
|
||||||
chapter.setUrl(it.attr("href"))
|
chapter.setUrlWithoutDomain(it.attr("href"))
|
||||||
}
|
}
|
||||||
val dateElement = element.select(date?.select).first()
|
val dateElement = element.select(date?.select).first()
|
||||||
chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
|
chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
|
||||||
@ -133,33 +157,59 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val body = response.body().string()
|
||||||
|
val url = response.request().url().toString()
|
||||||
|
|
||||||
|
// TODO lazy initialization in Kotlin 1.1
|
||||||
|
val document = Jsoup.parse(body, url)
|
||||||
|
|
||||||
with(map.pages) {
|
with(map.pages) {
|
||||||
val url = response.request().url().toString()
|
// Capture a list of values where page urls will be resolved.
|
||||||
pages_css?.let {
|
val capturedPages = if (pages_regex != null)
|
||||||
for (element in document.select(it)) {
|
pages_regex!!.toRegex().findAll(body).map { it.value }.toList()
|
||||||
val value = element.attr(pages_attr)
|
else if (pages_css != null)
|
||||||
val pageUrl = replace?.let { url.replace(it.toRegex(), replacement!!.replace("\$value", value)) } ?: value
|
document.select(pages_css).map { it.attrOrText(pages_attr!!) }
|
||||||
pages.add(Page(pages.size, pageUrl))
|
else
|
||||||
}
|
null
|
||||||
|
|
||||||
|
// For each captured value, obtain the url and create a new page.
|
||||||
|
capturedPages?.forEach { value ->
|
||||||
|
// If the captured value isn't an url, we have to use replaces with the chapter url.
|
||||||
|
val pageUrl = if (replace != null && replacement != null)
|
||||||
|
url.replace(replace!!.toRegex(), replacement!!.replace("\$value", value))
|
||||||
|
else
|
||||||
|
value
|
||||||
|
|
||||||
|
pages.add(Page(pages.size, pageUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
for ((i, element) in document.select(image_css).withIndex()) {
|
// Capture a list of images.
|
||||||
pages.getOrNull(i)?.imageUrl = element.attr(image_attr).let {
|
val capturedImages = if (image_regex != null)
|
||||||
getAbsoluteUrl(it, response.request().url())
|
image_regex!!.toRegex().findAll(body).map { it.groups[1]?.value }.toList()
|
||||||
}
|
else if (image_css != null)
|
||||||
|
document.select(image_css).map { it.absUrl(image_attr) }
|
||||||
|
else
|
||||||
|
null
|
||||||
|
|
||||||
|
// Assign the image url to each page
|
||||||
|
capturedImages?.forEachIndexed { i, url ->
|
||||||
|
val page = pages.getOrElse(i) { Page(i, "").apply { pages.add(this) } }
|
||||||
|
page.imageUrl = url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
override fun imageUrlParse(response: Response): String {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val body = response.body().string()
|
||||||
return with(map.pages) {
|
val url = response.request().url().toString()
|
||||||
document.select(image_css).first().attr(image_attr).let {
|
|
||||||
getAbsoluteUrl(it, response.request().url())
|
with(map.pages) {
|
||||||
}
|
return if (image_regex != null)
|
||||||
|
image_regex!!.toRegex().find(body)!!.groups[1]!!.value
|
||||||
|
else if (image_css != null)
|
||||||
|
Jsoup.parse(body, url).select(image_css).first().absUrl(image_attr)
|
||||||
|
else
|
||||||
|
throw Exception("image_regex and image_css are null")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,8 @@ class YamlSourceNode(uncheckedMap: Map<*, *>) {
|
|||||||
|
|
||||||
val popular = PopularNode(toMap(map["popular"])!!)
|
val popular = PopularNode(toMap(map["popular"])!!)
|
||||||
|
|
||||||
|
val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) }
|
||||||
|
|
||||||
val search = SearchNode(toMap(map["search"])!!)
|
val search = SearchNode(toMap(map["search"])!!)
|
||||||
|
|
||||||
val manga = MangaNode(toMap(map["manga"])!!)
|
val manga = MangaNode(toMap(map["manga"])!!)
|
||||||
@ -73,6 +75,17 @@ class PopularNode(override val map: Map<String, Any?>): RequestableNode {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LatestUpdatesNode(override val map: Map<String, Any?>): RequestableNode {
|
||||||
|
|
||||||
|
val manga_css: String by map
|
||||||
|
|
||||||
|
val next_url_css: String?
|
||||||
|
get() = map["next_url_css"] as? String
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SearchNode(override val map: Map<String, Any?>): RequestableNode {
|
class SearchNode(override val map: Map<String, Any?>): RequestableNode {
|
||||||
|
|
||||||
val manga_css: String by map
|
val manga_css: String by map
|
||||||
@ -194,6 +207,9 @@ class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
|||||||
|
|
||||||
class PagesNode(private val map: Map<String, Any?>) {
|
class PagesNode(private val map: Map<String, Any?>) {
|
||||||
|
|
||||||
|
val pages_regex: String?
|
||||||
|
get() = map["pages_regex"] as? String
|
||||||
|
|
||||||
val pages_css: String?
|
val pages_css: String?
|
||||||
get() = map["pages_css"] as? String
|
get() = map["pages_css"] as? String
|
||||||
|
|
||||||
@ -206,7 +222,11 @@ class PagesNode(private val map: Map<String, Any?>) {
|
|||||||
val replacement: String?
|
val replacement: String?
|
||||||
get() = map["url_replacement"] as? String
|
get() = map["url_replacement"] as? String
|
||||||
|
|
||||||
val image_css: String by map
|
val image_regex: String?
|
||||||
|
get() = map["image_regex"] as? String
|
||||||
|
|
||||||
|
val image_css: String?
|
||||||
|
get() = map["image_css"] as? String
|
||||||
|
|
||||||
val image_attr: String
|
val image_attr: String
|
||||||
get() = map["image_attr"] as? String ?: "src"
|
get() = map["image_attr"] as? String ?: "src"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
@ -14,11 +13,11 @@ import eu.kanade.tachiyomi.data.source.model.MangasPage
|
|||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -28,7 +27,7 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(context), LoginSource {
|
class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||||
|
|
||||||
override val name = "Batoto"
|
override val name = "Batoto"
|
||||||
|
|
||||||
@ -36,6 +35,8 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
private val datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*")
|
private val datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*")
|
||||||
|
|
||||||
private val dateFields = HashMap<String, Int>().apply {
|
private val dateFields = HashMap<String, Int>().apply {
|
||||||
@ -59,11 +60,12 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/search_ajax?order_cond=views&order=desc&p=1"
|
override fun popularMangaInitialUrl() = "$baseUrl/search_ajax?order_cond=views&order=desc&p=1"
|
||||||
|
|
||||||
|
override fun latestUpdatesInitialUrl() = "$baseUrl/search_ajax?order_cond=update&order=desc&p=1"
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
override fun popularMangaParse(response: Response, page: MangasPage) {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val document = response.asJsoup()
|
||||||
for (element in document.select(popularMangaSelector())) {
|
for (element in document.select(popularMangaSelector())) {
|
||||||
Manga().apply {
|
Manga.create(id).apply {
|
||||||
source = this@Batoto.id
|
|
||||||
popularMangaFromElement(element, this)
|
popularMangaFromElement(element, this)
|
||||||
page.mangas.add(this)
|
page.mangas.add(this)
|
||||||
}
|
}
|
||||||
@ -74,31 +76,64 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaSelector() = "tr:not([id]):not([class])"
|
override fun latestUpdatesParse(response: Response, page: MangasPage) {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
for (element in document.select(latestUpdatesSelector())) {
|
||||||
|
Manga.create(id).apply {
|
||||||
|
latestUpdatesFromElement(element, this)
|
||||||
|
page.mangas.add(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page.nextPageUrl = document.select(latestUpdatesNextPageSelector()).first()?.let {
|
||||||
|
"$baseUrl/search_ajax?order_cond=update&order=desc&p=${page.page + 1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "tr:has(a)"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "tr:has(a)"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("a[href^=http://bato.to]").first().let {
|
element.select("a[href^=http://bato.to]").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text().trim()
|
manga.title = it.text().trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "#show_more_row"
|
override fun popularMangaNextPageSelector() = "#show_more_row"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=1"
|
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1&genre_cond=and&genres=${getFilterParams(filters)}"
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
|
private fun getFilterParams(filters: List<Filter>): String = filters
|
||||||
|
.map {
|
||||||
|
";i" + it.id
|
||||||
|
}.joinToString()
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
|
}
|
||||||
|
return GET(page.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
||||||
|
val document = response.asJsoup()
|
||||||
for (element in document.select(searchMangaSelector())) {
|
for (element in document.select(searchMangaSelector())) {
|
||||||
Manga().apply {
|
Manga.create(id).apply {
|
||||||
source = this@Batoto.id
|
|
||||||
searchMangaFromElement(element, this)
|
searchMangaFromElement(element, this)
|
||||||
page.mangas.add(this)
|
page.mangas.add(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
||||||
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}"
|
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}&order_cond=views&order=desc&genre_cond=and&genres=" + getFilterParams(filters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +176,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
throw Exception(notice)
|
throw Exception(notice)
|
||||||
}
|
}
|
||||||
|
|
||||||
val document = Jsoup.parse(body)
|
val document = response.asJsoup(body)
|
||||||
|
|
||||||
for (element in document.select(chapterListSelector())) {
|
for (element in document.select(chapterListSelector())) {
|
||||||
Chapter.create().apply {
|
Chapter.create().apply {
|
||||||
@ -156,7 +191,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
val urlElement = element.select("a[href^=http://bato.to/reader").first()
|
val urlElement = element.select("a[href^=http://bato.to/reader").first()
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
chapter.name = urlElement.text()
|
chapter.name = urlElement.text()
|
||||||
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
||||||
parseDateFromElement(it)
|
parseDateFromElement(it)
|
||||||
@ -213,7 +248,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
val start = pageUrl.indexOf("#") + 1
|
val start = pageUrl.indexOf("#") + 1
|
||||||
val end = pageUrl.indexOf("_", start)
|
val end = pageUrl.indexOf("_", start)
|
||||||
val id = pageUrl.substring(start, end)
|
val id = pageUrl.substring(start, end)
|
||||||
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end+1)}", pageHeaders)
|
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end + 1)}", pageHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String {
|
override fun imageUrlParse(document: Document): String {
|
||||||
@ -221,13 +256,13 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String) =
|
override fun login(username: String, password: String) =
|
||||||
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers))
|
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers))
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.flatMap { doLogin(it.body().string(), username, password) }
|
.flatMap { doLogin(it, username, password) }
|
||||||
.map { isAuthenticationSuccessful(it) }
|
.map { isAuthenticationSuccessful(it) }
|
||||||
|
|
||||||
private fun doLogin(response: String, username: String, password: String): Observable<Response> {
|
private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
|
||||||
val doc = Jsoup.parse(response)
|
val doc = response.asJsoup()
|
||||||
val form = doc.select("#login").first()
|
val form = doc.select("#login").first()
|
||||||
val url = form.attr("action")
|
val url = form.attr("action")
|
||||||
val authKey = form.select("input[name=auth_key]").first()
|
val authKey = form.select("input[name=auth_key]").first()
|
||||||
@ -244,7 +279,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isAuthenticationSuccessful(response: Response) =
|
override fun isAuthenticationSuccessful(response: Response) =
|
||||||
response.priorResponse() != null && response.priorResponse().code() == 302
|
response.priorResponse() != null && response.priorResponse().code() == 302
|
||||||
|
|
||||||
override fun isLogged(): Boolean {
|
override fun isLogged(): Boolean {
|
||||||
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
||||||
@ -266,4 +301,49 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
|
||||||
|
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Filter("${id}", "${el.textContent.trim()}")`
|
||||||
|
// }).join(',\n')
|
||||||
|
// on https://bato.to/search
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
Filter("40", "4-Koma"),
|
||||||
|
Filter("1", "Action"),
|
||||||
|
Filter("2", "Adventure"),
|
||||||
|
Filter("39", "Award Winning"),
|
||||||
|
Filter("3", "Comedy"),
|
||||||
|
Filter("41", "Cooking"),
|
||||||
|
Filter("9", "Doujinshi"),
|
||||||
|
Filter("10", "Drama"),
|
||||||
|
Filter("12", "Ecchi"),
|
||||||
|
Filter("13", "Fantasy"),
|
||||||
|
Filter("15", "Gender Bender"),
|
||||||
|
Filter("17", "Harem"),
|
||||||
|
Filter("20", "Historical"),
|
||||||
|
Filter("22", "Horror"),
|
||||||
|
Filter("34", "Josei"),
|
||||||
|
Filter("27", "Martial Arts"),
|
||||||
|
Filter("30", "Mecha"),
|
||||||
|
Filter("42", "Medical"),
|
||||||
|
Filter("37", "Music"),
|
||||||
|
Filter("4", "Mystery"),
|
||||||
|
Filter("38", "Oneshot"),
|
||||||
|
Filter("5", "Psychological"),
|
||||||
|
Filter("6", "Romance"),
|
||||||
|
Filter("7", "School Life"),
|
||||||
|
Filter("8", "Sci-fi"),
|
||||||
|
Filter("32", "Seinen"),
|
||||||
|
Filter("35", "Shoujo"),
|
||||||
|
Filter("16", "Shoujo Ai"),
|
||||||
|
Filter("33", "Shounen"),
|
||||||
|
Filter("19", "Shounen Ai"),
|
||||||
|
Filter("21", "Slice of Life"),
|
||||||
|
Filter("23", "Smut"),
|
||||||
|
Filter("25", "Sports"),
|
||||||
|
Filter("26", "Supernatural"),
|
||||||
|
Filter("28", "Tragedy"),
|
||||||
|
Filter("36", "Webtoon"),
|
||||||
|
Filter("29", "Yaoi"),
|
||||||
|
Filter("31", "Yuri")
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
@ -19,7 +19,7 @@ import org.jsoup.nodes.Element
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
||||||
|
|
||||||
override val name = "Kissmanga"
|
override val name = "Kissmanga"
|
||||||
|
|
||||||
@ -27,37 +27,52 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
override val client: OkHttpClient get() = network.cloudflareClient
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/MangaList/MostPopular"
|
override fun popularMangaInitialUrl() = "$baseUrl/MangaList/MostPopular"
|
||||||
|
|
||||||
|
override fun latestUpdatesInitialUrl() = "http://kissmanga.com/MangaList/LatestUpdate"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "table.listing tr:gt(1)"
|
override fun popularMangaSelector() = "table.listing tr:gt(1)"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "table.listing tr:gt(1)"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("td a:eq(0)").first().let {
|
element.select("td a:eq(0)").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text()
|
manga.title = it.text()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
override fun latestUpdatesNextPageSelector(): String = "ul.pager > li > a:contains(Next)"
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
||||||
if (page.page == 1) {
|
if (page.page == 1) {
|
||||||
page.url = searchMangaInitialUrl(query)
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
val form = FormBody.Builder().apply {
|
val form = FormBody.Builder().apply {
|
||||||
add("authorArtist", "")
|
add("authorArtist", "")
|
||||||
add("mangaName", query)
|
add("mangaName", query)
|
||||||
add("status", "")
|
add("status", "")
|
||||||
add("genres", "")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return POST(page.url, headers, form)
|
this@Kissmanga.filters.forEach { filter ->
|
||||||
|
add("genres", if (filter in filters) "1" else "0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return POST(page.url, headers, form.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/AdvanceSearch"
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/AdvanceSearch"
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
@ -73,7 +88,7 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
|
manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
|
||||||
manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
|
manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
|
||||||
manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text()
|
manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text()
|
||||||
manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it)}
|
manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) }
|
||||||
manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
|
manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +103,7 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
val urlElement = element.select("a").first()
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
chapter.name = urlElement.text()
|
chapter.name = urlElement.text()
|
||||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||||
SimpleDateFormat("MM/dd/yyyy").parse(it).time
|
SimpleDateFormat("MM/dd/yyyy").parse(it).time
|
||||||
@ -109,10 +124,59 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Not used
|
// Not used
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
|
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||||
|
}
|
||||||
|
|
||||||
override fun imageUrlRequest(page: Page) = GET(page.url)
|
override fun imageUrlRequest(page: Page) = GET(page.url)
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
// $("select[name=\"genres\"]").map((i,el) => `Filter("${i}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
||||||
|
// on http://kissmanga.com/AdvanceSearch
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
Filter("0", "Action"),
|
||||||
|
Filter("1", "Adult"),
|
||||||
|
Filter("2", "Adventure"),
|
||||||
|
Filter("3", "Comedy"),
|
||||||
|
Filter("4", "Comic"),
|
||||||
|
Filter("5", "Cooking"),
|
||||||
|
Filter("6", "Doujinshi"),
|
||||||
|
Filter("7", "Drama"),
|
||||||
|
Filter("8", "Ecchi"),
|
||||||
|
Filter("9", "Fantasy"),
|
||||||
|
Filter("10", "Gender Bender"),
|
||||||
|
Filter("11", "Harem"),
|
||||||
|
Filter("12", "Historical"),
|
||||||
|
Filter("13", "Horror"),
|
||||||
|
Filter("14", "Josei"),
|
||||||
|
Filter("15", "Lolicon"),
|
||||||
|
Filter("16", "Manga"),
|
||||||
|
Filter("17", "Manhua"),
|
||||||
|
Filter("18", "Manhwa"),
|
||||||
|
Filter("19", "Martial Arts"),
|
||||||
|
Filter("20", "Mature"),
|
||||||
|
Filter("21", "Mecha"),
|
||||||
|
Filter("22", "Medical"),
|
||||||
|
Filter("23", "Music"),
|
||||||
|
Filter("24", "Mystery"),
|
||||||
|
Filter("25", "One shot"),
|
||||||
|
Filter("26", "Psychological"),
|
||||||
|
Filter("27", "Romance"),
|
||||||
|
Filter("28", "School Life"),
|
||||||
|
Filter("29", "Sci-fi"),
|
||||||
|
Filter("30", "Seinen"),
|
||||||
|
Filter("31", "Shotacon"),
|
||||||
|
Filter("32", "Shoujo"),
|
||||||
|
Filter("33", "Shoujo Ai"),
|
||||||
|
Filter("34", "Shounen"),
|
||||||
|
Filter("35", "Shounen Ai"),
|
||||||
|
Filter("36", "Slice of Life"),
|
||||||
|
Filter("37", "Smut"),
|
||||||
|
Filter("38", "Sports"),
|
||||||
|
Filter("39", "Supernatural"),
|
||||||
|
Filter("40", "Tragedy"),
|
||||||
|
Filter("41", "Webtoon"),
|
||||||
|
Filter("42", "Yaoi"),
|
||||||
|
Filter("43", "Yuri")
|
||||||
|
)
|
||||||
}
|
}
|
@ -1,21 +1,20 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
import eu.kanade.tachiyomi.data.source.EN
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
||||||
|
|
||||||
override val name = "Mangafox"
|
override val name = "Mangafox"
|
||||||
|
|
||||||
@ -23,27 +22,39 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
|
|||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
|
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
|
||||||
|
|
||||||
|
override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?latest"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
|
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div#mangalist > ul.list > li"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("a.title").first().let {
|
element.select("a.title").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text()
|
manga.title = it.text()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) =
|
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
||||||
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1"
|
|
||||||
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||||
|
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1&${filters.map { it.id + "=1" }.joinToString("&")}"
|
||||||
|
|
||||||
override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)"
|
override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)"
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("a.series_preview").first().let {
|
element.select("a.series_preview").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text()
|
manga.title = it.text()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,7 +85,7 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
|
|||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
val urlElement = element.select("a.tips").first()
|
val urlElement = element.select("a.tips").first()
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
chapter.name = urlElement.text()
|
chapter.name = urlElement.text()
|
||||||
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
}
|
}
|
||||||
@ -105,10 +116,10 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||||
val document = Jsoup.parse(response.body().string())
|
val document = response.asJsoup()
|
||||||
|
|
||||||
val url = response.request().url().toString().substringBeforeLast('/')
|
val url = response.request().url().toString().substringBeforeLast('/')
|
||||||
document.select("select.m").first().select("option:not([value=0])").forEach {
|
document.select("select.m").first()?.select("option:not([value=0])")?.forEach {
|
||||||
pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
|
pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,4 +129,44 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont
|
|||||||
|
|
||||||
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
||||||
|
|
||||||
|
// $('select.genres').map((i,el)=>`Filter("${$(el).attr('name')}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
||||||
|
// on http://kissmanga.com/AdvanceSearch
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
Filter("genres[Action]", "Action"),
|
||||||
|
Filter("genres[Adult]", "Adult"),
|
||||||
|
Filter("genres[Adventure]", "Adventure"),
|
||||||
|
Filter("genres[Comedy]", "Comedy"),
|
||||||
|
Filter("genres[Doujinshi]", "Doujinshi"),
|
||||||
|
Filter("genres[Drama]", "Drama"),
|
||||||
|
Filter("genres[Ecchi]", "Ecchi"),
|
||||||
|
Filter("genres[Fantasy]", "Fantasy"),
|
||||||
|
Filter("genres[Gender Bender]", "Gender Bender"),
|
||||||
|
Filter("genres[Harem]", "Harem"),
|
||||||
|
Filter("genres[Historical]", "Historical"),
|
||||||
|
Filter("genres[Horror]", "Horror"),
|
||||||
|
Filter("genres[Josei]", "Josei"),
|
||||||
|
Filter("genres[Martial Arts]", "Martial Arts"),
|
||||||
|
Filter("genres[Mature]", "Mature"),
|
||||||
|
Filter("genres[Mecha]", "Mecha"),
|
||||||
|
Filter("genres[Mystery]", "Mystery"),
|
||||||
|
Filter("genres[One Shot]", "One Shot"),
|
||||||
|
Filter("genres[Psychological]", "Psychological"),
|
||||||
|
Filter("genres[Romance]", "Romance"),
|
||||||
|
Filter("genres[School Life]", "School Life"),
|
||||||
|
Filter("genres[Sci-fi]", "Sci-fi"),
|
||||||
|
Filter("genres[Seinen]", "Seinen"),
|
||||||
|
Filter("genres[Shoujo]", "Shoujo"),
|
||||||
|
Filter("genres[Shoujo Ai]", "Shoujo Ai"),
|
||||||
|
Filter("genres[Shounen]", "Shounen"),
|
||||||
|
Filter("genres[Shounen Ai]", "Shounen Ai"),
|
||||||
|
Filter("genres[Slice of Life]", "Slice of Life"),
|
||||||
|
Filter("genres[Smut]", "Smut"),
|
||||||
|
Filter("genres[Sports]", "Sports"),
|
||||||
|
Filter("genres[Supernatural]", "Supernatural"),
|
||||||
|
Filter("genres[Tragedy]", "Tragedy"),
|
||||||
|
Filter("genres[Webtoons]", "Webtoons"),
|
||||||
|
Filter("genres[Yaoi]", "Yaoi"),
|
||||||
|
Filter("genres[Yuri]", "Yuri")
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
@ -13,7 +13,7 @@ import java.text.ParseException
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
||||||
|
|
||||||
override val name = "Mangahere"
|
override val name = "Mangahere"
|
||||||
|
|
||||||
@ -21,27 +21,38 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = "$baseUrl/directory/?views.za"
|
||||||
|
|
||||||
|
override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?last_chapter_time.za"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("div.title > a").first().let {
|
element.select("div.title > a").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text()
|
manga.title = it.text()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) =
|
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
||||||
"$baseUrl/search.php?name=$query&page=1&sort=views&order=za"
|
|
||||||
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search.php?name=$query&page=1&sort=views&order=za&${filters.map { it.id + "=1" }.joinToString("&")}&advopts=1"
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("a.manga_info").first().let {
|
element.select("a.manga_info").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text()
|
manga.title = it.text()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,7 +82,7 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
val urlElement = element.select("a").first()
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
chapter.name = urlElement.text()
|
chapter.name = urlElement.text()
|
||||||
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
}
|
}
|
||||||
@ -102,7 +113,7 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||||
document.select("select.wid60").first().getElementsByTag("option").forEach {
|
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
||||||
pages.add(Page(pages.size, it.attr("value")))
|
pages.add(Page(pages.size, it.attr("value")))
|
||||||
}
|
}
|
||||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||||
@ -110,4 +121,41 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
|
|
||||||
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
||||||
|
|
||||||
|
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Filter("${el.getAttribute('name')}", "${el.nextSibling.nextSibling.textContent.trim()}")`).join(',\n')
|
||||||
|
// http://www.mangahere.co/advsearch.htm
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
Filter("genres[Action]", "Action"),
|
||||||
|
Filter("genres[Adventure]", "Adventure"),
|
||||||
|
Filter("genres[Comedy]", "Comedy"),
|
||||||
|
Filter("genres[Doujinshi]", "Doujinshi"),
|
||||||
|
Filter("genres[Drama]", "Drama"),
|
||||||
|
Filter("genres[Ecchi]", "Ecchi"),
|
||||||
|
Filter("genres[Fantasy]", "Fantasy"),
|
||||||
|
Filter("genres[Gender Bender]", "Gender Bender"),
|
||||||
|
Filter("genres[Harem]", "Harem"),
|
||||||
|
Filter("genres[Historical]", "Historical"),
|
||||||
|
Filter("genres[Horror]", "Horror"),
|
||||||
|
Filter("genres[Josei]", "Josei"),
|
||||||
|
Filter("genres[Martial Arts]", "Martial Arts"),
|
||||||
|
Filter("genres[Mature]", "Mature"),
|
||||||
|
Filter("genres[Mecha]", "Mecha"),
|
||||||
|
Filter("genres[Mystery]", "Mystery"),
|
||||||
|
Filter("genres[One Shot]", "One Shot"),
|
||||||
|
Filter("genres[Psychological]", "Psychological"),
|
||||||
|
Filter("genres[Romance]", "Romance"),
|
||||||
|
Filter("genres[School Life]", "School Life"),
|
||||||
|
Filter("genres[Sci-fi]", "Sci-fi"),
|
||||||
|
Filter("genres[Seinen]", "Seinen"),
|
||||||
|
Filter("genres[Shoujo]", "Shoujo"),
|
||||||
|
Filter("genres[Shoujo Ai]", "Shoujo Ai"),
|
||||||
|
Filter("genres[Shounen]", "Shounen"),
|
||||||
|
Filter("genres[Shounen Ai]", "Shounen Ai"),
|
||||||
|
Filter("genres[Slice of Life]", "Slice of Life"),
|
||||||
|
Filter("genres[Sports]", "Sports"),
|
||||||
|
Filter("genres[Supernatural]", "Supernatural"),
|
||||||
|
Filter("genres[Tragedy]", "Tragedy"),
|
||||||
|
Filter("genres[Yaoi]", "Yaoi"),
|
||||||
|
Filter("genres[Yuri]", "Yuri")
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,190 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.english
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.EN
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.util.*
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||||
|
|
||||||
|
override val name = "Mangasee"
|
||||||
|
|
||||||
|
override val baseUrl = "http://www.mangasee.co"
|
||||||
|
|
||||||
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
|
override val supportsLatest = false
|
||||||
|
|
||||||
|
private val datePattern = Pattern.compile("(\\d+)\\s+(.*?)s? (from now|ago).*")
|
||||||
|
|
||||||
|
private val dateFields = HashMap<String, Int>().apply {
|
||||||
|
put("second", Calendar.SECOND)
|
||||||
|
put("minute", Calendar.MINUTE)
|
||||||
|
put("hour", Calendar.HOUR)
|
||||||
|
put("day", Calendar.DATE)
|
||||||
|
put("week", Calendar.WEEK_OF_YEAR)
|
||||||
|
put("month", Calendar.MONTH)
|
||||||
|
put("year", Calendar.YEAR)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val dateRelationFields = HashMap<String, Int>().apply {
|
||||||
|
put("from now", 1)
|
||||||
|
put("ago", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = "$baseUrl/search_result.php?Action=Yes&order=popularity&numResultPerPage=20&sort=desc"
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.well > table > tbody > tr"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("td > h2 > a").first().let {
|
||||||
|
manga.setUrlWithoutDomain("/${it.attr("href")}")
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "ul.pagination > li > a:contains(Next)"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||||
|
"$baseUrl/advanced-search/result.php?sortBy=alphabet&direction=ASC&textOnly=no&resPerPage=20&page=1&seriesName=$query&${filters.map { it.id + "=Yes" }.joinToString("&")}"
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = "div.row > div > div > div > h1"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
element.select("a").first().let {
|
||||||
|
manga.setUrlWithoutDomain("/${it.attr("href")}")
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = "ul.pagination > li > a:contains(Next)"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
|
val detailElement = document.select("div.well > div.row").first()
|
||||||
|
|
||||||
|
manga.author = detailElement.select("a[href^=../search_result.php?author_name=]").first()?.text()
|
||||||
|
manga.genre = detailElement.select("div > div.row > div:has(b:contains(Genre:)) > a").map { it.text() }.joinToString()
|
||||||
|
manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
|
||||||
|
manga.status = detailElement.select("div > div.row > div:has(b:contains(Scanlation Status:))").first()?.text().orEmpty().let { parseStatus(it) }
|
||||||
|
manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing") -> Manga.ONGOING
|
||||||
|
status.contains("Completed") -> Manga.COMPLETED
|
||||||
|
else -> Manga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "div.row > div > div.row > div > div.row:has(a.chapter_link[alt])"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
chapter.setUrlWithoutDomain("/${urlElement.attr("href")}")
|
||||||
|
chapter.name = urlElement.text()
|
||||||
|
chapter.date_upload = element.select("span").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterDate(dateAsString: String): Long {
|
||||||
|
val m = datePattern.matcher(dateAsString)
|
||||||
|
|
||||||
|
if (m.matches()) {
|
||||||
|
val amount = Integer.parseInt(m.group(1))
|
||||||
|
val unit = m.group(2)
|
||||||
|
val relation = m.group(3)
|
||||||
|
|
||||||
|
return Calendar.getInstance().apply {
|
||||||
|
add(dateFields[unit]!!, dateRelationFields[relation]!! * amount)
|
||||||
|
}.time.time
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val url = response.request().url().toString().substringBeforeLast('/')
|
||||||
|
|
||||||
|
val series = document.select("input[name=series]").first().attr("value")
|
||||||
|
val chapter = document.select("input[name=chapter]").first().attr("value")
|
||||||
|
val index = document.select("input[name=index]").first().attr("value")
|
||||||
|
|
||||||
|
document.select("select[name=page] > option").forEach {
|
||||||
|
pages.add(Page(pages.size, "$url/?series=$series&chapter=$chapter&index=$index&page=${pages.size + 1}"))
|
||||||
|
}
|
||||||
|
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not used, overrides parent.
|
||||||
|
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = document.select("div > a > img").attr("src")
|
||||||
|
|
||||||
|
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||||
|
// http://mangasee.co/advanced-search/
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
Filter("Action", "Action"),
|
||||||
|
Filter("Adult", "Adult"),
|
||||||
|
Filter("Adventure", "Adventure"),
|
||||||
|
Filter("Comedy", "Comedy"),
|
||||||
|
Filter("Doujinshi", "Doujinshi"),
|
||||||
|
Filter("Drama", "Drama"),
|
||||||
|
Filter("Ecchi", "Ecchi"),
|
||||||
|
Filter("Fantasy", "Fantasy"),
|
||||||
|
Filter("Gender_Bender", "Gender Bender"),
|
||||||
|
Filter("Harem", "Harem"),
|
||||||
|
Filter("Hentai", "Hentai"),
|
||||||
|
Filter("Historical", "Historical"),
|
||||||
|
Filter("Horror", "Horror"),
|
||||||
|
Filter("Josei", "Josei"),
|
||||||
|
Filter("Lolicon", "Lolicon"),
|
||||||
|
Filter("Martial_Arts", "Martial Arts"),
|
||||||
|
Filter("Mature", "Mature"),
|
||||||
|
Filter("Mecha", "Mecha"),
|
||||||
|
Filter("Mystery", "Mystery"),
|
||||||
|
Filter("Psychological", "Psychological"),
|
||||||
|
Filter("Romance", "Romance"),
|
||||||
|
Filter("School_Life", "School Life"),
|
||||||
|
Filter("Sci-fi", "Sci-fi"),
|
||||||
|
Filter("Seinen", "Seinen"),
|
||||||
|
Filter("Shotacon", "Shotacon"),
|
||||||
|
Filter("Shoujo", "Shoujo"),
|
||||||
|
Filter("Shoujo_Ai", "Shoujo Ai"),
|
||||||
|
Filter("Shounen", "Shounen"),
|
||||||
|
Filter("Shounen_Ai", "Shounen Ai"),
|
||||||
|
Filter("Slice_of_Life", "Slice of Life"),
|
||||||
|
Filter("Smut", "Smut"),
|
||||||
|
Filter("Sports", "Sports"),
|
||||||
|
Filter("Supernatural", "Supernatural"),
|
||||||
|
Filter("Tragedy", "Tragedy"),
|
||||||
|
Filter("Yaoi", "Yaoi"),
|
||||||
|
Filter("Yuri", "Yuri")
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun latestUpdatesInitialUrl(): String {
|
||||||
|
throw UnsupportedOperationException("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String {
|
||||||
|
throw UnsupportedOperationException("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
|
throw UnsupportedOperationException("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String {
|
||||||
|
throw UnsupportedOperationException("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,13 +8,16 @@ import eu.kanade.tachiyomi.data.source.EN
|
|||||||
import eu.kanade.tachiyomi.data.source.Language
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
||||||
|
|
||||||
override val name = "ReadMangaToday"
|
override val name = "ReadMangaToday"
|
||||||
|
|
||||||
@ -22,39 +25,56 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc
|
|||||||
|
|
||||||
override val lang: Language get() = EN
|
override val lang: Language get() = EN
|
||||||
|
|
||||||
|
override val supportsLatest = false
|
||||||
|
|
||||||
|
override val client: OkHttpClient get() = network.cloudflareClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search only returns data with this set
|
||||||
|
*/
|
||||||
|
override fun headersBuilder() = Headers.Builder().apply {
|
||||||
|
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/"
|
override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
|
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("div.title > h2 > a").first().let {
|
element.select("div.title > h2 > a").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.attr("title")
|
manga.title = it.attr("title")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) =
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||||
"$baseUrl/search"
|
"$baseUrl/service/advanced_search"
|
||||||
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<OnlineSource.Filter>): Request {
|
||||||
if (page.page == 1) {
|
if (page.page == 1) {
|
||||||
page.url = searchMangaInitialUrl(query)
|
page.url = searchMangaInitialUrl(query, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
var builder = okhttp3.FormBody.Builder()
|
val builder = okhttp3.FormBody.Builder()
|
||||||
builder.add("query", query)
|
builder.add("manga-name", query)
|
||||||
|
builder.add("type", "all")
|
||||||
|
builder.add("status", "both")
|
||||||
|
for (filter in filters) {
|
||||||
|
builder.add("include[]", filter.id)
|
||||||
|
}
|
||||||
|
|
||||||
return POST(page.url, headers, builder.build())
|
return POST(page.url, headers, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.content-list > div.style-list > div.box"
|
override fun searchMangaSelector() = "div.style-list > div.box"
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("div.title > h2 > a").first().let {
|
element.select("div.title > h2 > a").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.attr("title")
|
manga.title = it.attr("title")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,7 +103,7 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc
|
|||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
val urlElement = element.select("a").first()
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
chapter.name = urlElement.select("span.val").text()
|
chapter.name = urlElement.select("span.val").text()
|
||||||
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
}
|
}
|
||||||
@ -124,4 +144,60 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc
|
|||||||
|
|
||||||
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
||||||
|
|
||||||
|
// [...document.querySelectorAll("ul.manga-cat span")].map(el => `Filter("${el.getAttribute('data-id')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||||
|
// http://www.readmanga.today/advanced-search
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
Filter("2", "Action"),
|
||||||
|
Filter("4", "Adventure"),
|
||||||
|
Filter("5", "Comedy"),
|
||||||
|
Filter("6", "Doujinshi"),
|
||||||
|
Filter("7", "Drama"),
|
||||||
|
Filter("8", "Ecchi"),
|
||||||
|
Filter("9", "Fantasy"),
|
||||||
|
Filter("10", "Gender Bender"),
|
||||||
|
Filter("11", "Harem"),
|
||||||
|
Filter("12", "Historical"),
|
||||||
|
Filter("13", "Horror"),
|
||||||
|
Filter("14", "Josei"),
|
||||||
|
Filter("15", "Lolicon"),
|
||||||
|
Filter("16", "Martial Arts"),
|
||||||
|
Filter("17", "Mature"),
|
||||||
|
Filter("18", "Mecha"),
|
||||||
|
Filter("19", "Mystery"),
|
||||||
|
Filter("20", "One shot"),
|
||||||
|
Filter("21", "Psychological"),
|
||||||
|
Filter("22", "Romance"),
|
||||||
|
Filter("23", "School Life"),
|
||||||
|
Filter("24", "Sci-fi"),
|
||||||
|
Filter("25", "Seinen"),
|
||||||
|
Filter("26", "Shotacon"),
|
||||||
|
Filter("27", "Shoujo"),
|
||||||
|
Filter("28", "Shoujo Ai"),
|
||||||
|
Filter("29", "Shounen"),
|
||||||
|
Filter("30", "Shounen Ai"),
|
||||||
|
Filter("31", "Slice of Life"),
|
||||||
|
Filter("32", "Smut"),
|
||||||
|
Filter("33", "Sports"),
|
||||||
|
Filter("34", "Supernatural"),
|
||||||
|
Filter("35", "Tragedy"),
|
||||||
|
Filter("36", "Yaoi"),
|
||||||
|
Filter("37", "Yuri")
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun latestUpdatesInitialUrl(): String {
|
||||||
|
throw UnsupportedOperationException("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String {
|
||||||
|
throw UnsupportedOperationException("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
|
throw UnsupportedOperationException("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String {
|
||||||
|
throw UnsupportedOperationException("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.german
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.source.DE
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
|
class WieManga(override val id: Int) : ParsedOnlineSource() {
|
||||||
|
|
||||||
|
override val name = "Wie Manga!"
|
||||||
|
|
||||||
|
override val baseUrl = "http://www.wiemanga.com"
|
||||||
|
|
||||||
|
override val lang: Language get() = DE
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun popularMangaInitialUrl() = "$baseUrl/list/Hot-Book/"
|
||||||
|
|
||||||
|
override fun latestUpdatesInitialUrl() = "$baseUrl/list/New-Update/"
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = ".booklist td > div"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = ".booklist td > div"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
val image = element.select("dt img")
|
||||||
|
val title = element.select("dd a:first-child")
|
||||||
|
|
||||||
|
manga.setUrlWithoutDomain(title.attr("href"))
|
||||||
|
manga.title = title.text()
|
||||||
|
manga.thumbnail_url = image.attr("src")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = null
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = null
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search/?wd=$query"
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = ".searchresult td > div"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
|
val image = element.select(".resultimg img")
|
||||||
|
val title = element.select(".resultbookname")
|
||||||
|
|
||||||
|
manga.setUrlWithoutDomain(title.attr("href"))
|
||||||
|
manga.title = title.text()
|
||||||
|
manga.thumbnail_url = image.attr("src")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = ".pagetor a.l"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
|
val imageElement = document.select(".bookmessgae tr > td:nth-child(1)").first()
|
||||||
|
val infoElement = document.select(".bookmessgae tr > td:nth-child(2)").first()
|
||||||
|
|
||||||
|
manga.author = infoElement.select("dd:nth-of-type(2) a").first()?.text()
|
||||||
|
manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text()
|
||||||
|
manga.description = infoElement.select("dl > dt:last-child").first()?.text()?.replaceFirst("Beschreibung", "")
|
||||||
|
manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
|
||||||
|
|
||||||
|
if (manga.author == "RSS")
|
||||||
|
manga.author = null
|
||||||
|
|
||||||
|
if (manga.artist == "RSS")
|
||||||
|
manga.artist = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
|
val urlElement = element.select(".col1 a").first()
|
||||||
|
val dateElement = element.select(".col3 a").first()
|
||||||
|
|
||||||
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.text()
|
||||||
|
chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterDate(date: String): Long {
|
||||||
|
return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
document.select("select#page").first().select("option").forEach {
|
||||||
|
pages.add(Page(pages.size, it.attr("value")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
|
||||||
|
|
||||||
|
}
|
@ -13,32 +13,41 @@ import org.jsoup.nodes.Element
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||||
|
|
||||||
override val name = "Mangachan"
|
override val name = "Mangachan"
|
||||||
|
|
||||||
override val baseUrl = "http://mangachan.ru"
|
override val baseUrl = "http://mangachan.me"
|
||||||
|
|
||||||
override val lang: Language get() = RU
|
override val lang: Language get() = RU
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
|
override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/?do=search&subaction=search&story=$query"
|
override fun latestUpdatesInitialUrl() = "$baseUrl/manga/new"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/?do=search&subaction=search&story=$query"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.content_row"
|
override fun popularMangaSelector() = "div.content_row"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.content_row"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("h2 > a").first().let {
|
element.select("h2 > a").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.text()
|
manga.title = it.text()
|
||||||
}
|
}
|
||||||
element.select("img").first().let {
|
}
|
||||||
manga.thumbnail_url = baseUrl + it.attr("src")
|
|
||||||
}
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "a:contains(Вперед)"
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
@ -50,11 +59,13 @@ class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
val infoElement = document.select("table.mangatitle").first()
|
val infoElement = document.select("table.mangatitle").first()
|
||||||
val descElement = document.select("div#description").first()
|
val descElement = document.select("div#description").first()
|
||||||
|
val imgElement = document.select("img#cover").first()
|
||||||
|
|
||||||
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
|
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
|
||||||
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
|
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
|
||||||
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
|
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
|
||||||
manga.description = descElement.textNodes().first().text()
|
manga.description = descElement.textNodes().first().text()
|
||||||
|
manga.thumbnail_url = baseUrl + imgElement.attr("src")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseStatus(element: String): Int {
|
private fun parseStatus(element: String): Int {
|
||||||
@ -70,7 +81,7 @@ class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
val urlElement = element.select("a").first()
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
chapter.name = urlElement.text()
|
chapter.name = urlElement.text()
|
||||||
chapter.date_upload = element.select("div.date").first()?.text()?.let {
|
chapter.date_upload = element.select("div.date").first()?.text()?.let {
|
||||||
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
|
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
|
||||||
@ -92,4 +103,5 @@ class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
}
|
}
|
@ -14,7 +14,7 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
class Mintmanga(override val id: Int) : ParsedOnlineSource() {
|
||||||
|
|
||||||
override val name = "Mintmanga"
|
override val name = "Mintmanga"
|
||||||
|
|
||||||
@ -22,28 +22,42 @@ class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
|
|
||||||
override val lang: Language get() = RU
|
override val lang: Language get() = RU
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
|
override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||||
|
"$baseUrl/search?q=$query&${filters.map { it.id + "=in" }.joinToString("&")}"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.desc"
|
override fun popularMangaSelector() = "div.desc"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.desc"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("h3 > a").first().let {
|
element.select("h3 > a").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.attr("title")
|
manga.title = it.attr("title")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
popularMangaFromElement(element, manga)
|
popularMangaFromElement(element, manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
// max 200 results
|
||||||
|
override fun searchMangaNextPageSelector() = null
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
val infoElement = document.select("div.leftContent").first()
|
val infoElement = document.select("div.leftContent").first()
|
||||||
@ -69,7 +83,7 @@ class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
val urlElement = element.select("a").first()
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href") + "?mature=1")
|
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
|
||||||
chapter.name = urlElement.text().replace(" новое", "")
|
chapter.name = urlElement.text().replace(" новое", "")
|
||||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
||||||
@ -84,14 +98,14 @@ class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
val html = response.body().string()
|
val html = response.body().string()
|
||||||
val beginIndex = html.indexOf("rm_h.init( [")
|
val beginIndex = html.indexOf("rm_h.init( [")
|
||||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("[\"\']+".toRegex(), "")
|
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||||
|
|
||||||
val p = Pattern.compile("auto/[\\w/]+,http://[\\w.]+/,/[\\w./]+.(png|jpg)+")
|
val p = Pattern.compile("'.+?','.+?',\".+?\"")
|
||||||
val m = p.matcher(trimmedHtml)
|
val m = p.matcher(trimmedHtml)
|
||||||
|
|
||||||
var i = 0
|
var i = 0
|
||||||
while (m.find()) {
|
while (m.find()) {
|
||||||
val urlParts = m.group().split(',')
|
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
||||||
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,4 +113,55 @@ class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||||
|
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||||
|
* return `Filter("${id}", "${el.textContent.trim()}")` }).join(',\n')
|
||||||
|
* on http://mintmanga.com/search
|
||||||
|
*/
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
Filter("el_2220", "арт"),
|
||||||
|
Filter("el_1353", "бара"),
|
||||||
|
Filter("el_1346", "боевик"),
|
||||||
|
Filter("el_1334", "боевые искусства"),
|
||||||
|
Filter("el_1339", "вампиры"),
|
||||||
|
Filter("el_1333", "гарем"),
|
||||||
|
Filter("el_1347", "гендерная интрига"),
|
||||||
|
Filter("el_1337", "героическое фэнтези"),
|
||||||
|
Filter("el_1343", "детектив"),
|
||||||
|
Filter("el_1349", "дзёсэй"),
|
||||||
|
Filter("el_1332", "додзинси"),
|
||||||
|
Filter("el_1310", "драма"),
|
||||||
|
Filter("el_5229", "игра"),
|
||||||
|
Filter("el_1311", "история"),
|
||||||
|
Filter("el_1351", "киберпанк"),
|
||||||
|
Filter("el_1328", "комедия"),
|
||||||
|
Filter("el_1318", "меха"),
|
||||||
|
Filter("el_1324", "мистика"),
|
||||||
|
Filter("el_1325", "научная фантастика"),
|
||||||
|
Filter("el_1327", "повседневность"),
|
||||||
|
Filter("el_1342", "постапокалиптика"),
|
||||||
|
Filter("el_1322", "приключения"),
|
||||||
|
Filter("el_1335", "психология"),
|
||||||
|
Filter("el_1313", "романтика"),
|
||||||
|
Filter("el_1316", "самурайский боевик"),
|
||||||
|
Filter("el_1350", "сверхъестественное"),
|
||||||
|
Filter("el_1314", "сёдзё"),
|
||||||
|
Filter("el_1320", "сёдзё-ай"),
|
||||||
|
Filter("el_1326", "сёнэн"),
|
||||||
|
Filter("el_1330", "сёнэн-ай"),
|
||||||
|
Filter("el_1321", "спорт"),
|
||||||
|
Filter("el_1329", "сэйнэн"),
|
||||||
|
Filter("el_1344", "трагедия"),
|
||||||
|
Filter("el_1341", "триллер"),
|
||||||
|
Filter("el_1317", "ужасы"),
|
||||||
|
Filter("el_1331", "фантастика"),
|
||||||
|
Filter("el_1323", "фэнтези"),
|
||||||
|
Filter("el_1319", "школа"),
|
||||||
|
Filter("el_1340", "эротика"),
|
||||||
|
Filter("el_1354", "этти"),
|
||||||
|
Filter("el_1315", "юри"),
|
||||||
|
Filter("el_1336", "яой")
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
@ -14,7 +14,7 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
class Readmanga(override val id: Int) : ParsedOnlineSource() {
|
||||||
|
|
||||||
override val name = "Readmanga"
|
override val name = "Readmanga"
|
||||||
|
|
||||||
@ -22,28 +22,45 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
|
|
||||||
override val lang: Language get() = RU
|
override val lang: Language get() = RU
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
|
override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
|
||||||
|
|
||||||
|
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
||||||
|
"$baseUrl/search?q=$query&${filters.map { it.id + "=in" }.joinToString("&")}"
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.desc"
|
override fun popularMangaSelector() = "div.desc"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.desc"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||||
element.select("h3 > a").first().let {
|
element.select("h3 > a").first().let {
|
||||||
manga.setUrl(it.attr("href"))
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
manga.title = it.attr("title")
|
manga.title = it.attr("title")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||||
|
popularMangaFromElement(element, manga)
|
||||||
|
}
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||||
popularMangaFromElement(element, manga)
|
element.select("h3 > a").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.attr("title")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
// max 200 results
|
||||||
|
override fun searchMangaNextPageSelector() = null
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||||
val infoElement = document.select("div.leftContent").first()
|
val infoElement = document.select("div.leftContent").first()
|
||||||
@ -69,7 +86,7 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||||
val urlElement = element.select("a").first()
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href") + "?mature=1")
|
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
|
||||||
chapter.name = urlElement.text().replace(" новое", "")
|
chapter.name = urlElement.text().replace(" новое", "")
|
||||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
||||||
@ -84,14 +101,14 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
val html = response.body().string()
|
val html = response.body().string()
|
||||||
val beginIndex = html.indexOf("rm_h.init( [")
|
val beginIndex = html.indexOf("rm_h.init( [")
|
||||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("[\"\']+".toRegex(), "")
|
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||||
|
|
||||||
val p = Pattern.compile("auto/[\\w/]+,http://[\\w.]+/,/[\\w./]+.(png|jpg)+")
|
val p = Pattern.compile("'.+?','.+?',\".+?\"")
|
||||||
val m = p.matcher(trimmedHtml)
|
val m = p.matcher(trimmedHtml)
|
||||||
|
|
||||||
var i = 0
|
var i = 0
|
||||||
while (m.find()) {
|
while (m.find()) {
|
||||||
val urlParts = m.group().split(',')
|
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
||||||
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,4 +116,53 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con
|
|||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||||
|
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||||
|
* return `Filter("${id}", "${el.textContent.trim()}")` }).join(',\n')
|
||||||
|
* on http://readmanga.me/search
|
||||||
|
*/
|
||||||
|
override fun getFilterList(): List<Filter> = listOf(
|
||||||
|
Filter("el_5685", "арт"),
|
||||||
|
Filter("el_2155", "боевик"),
|
||||||
|
Filter("el_2143", "боевые искусства"),
|
||||||
|
Filter("el_2148", "вампиры"),
|
||||||
|
Filter("el_2142", "гарем"),
|
||||||
|
Filter("el_2156", "гендерная интрига"),
|
||||||
|
Filter("el_2146", "героическое фэнтези"),
|
||||||
|
Filter("el_2152", "детектив"),
|
||||||
|
Filter("el_2158", "дзёсэй"),
|
||||||
|
Filter("el_2141", "додзинси"),
|
||||||
|
Filter("el_2118", "драма"),
|
||||||
|
Filter("el_2154", "игра"),
|
||||||
|
Filter("el_2119", "история"),
|
||||||
|
Filter("el_2137", "кодомо"),
|
||||||
|
Filter("el_2136", "комедия"),
|
||||||
|
Filter("el_2147", "махо-сёдзё"),
|
||||||
|
Filter("el_2126", "меха"),
|
||||||
|
Filter("el_2132", "мистика"),
|
||||||
|
Filter("el_2133", "научная фантастика"),
|
||||||
|
Filter("el_2135", "повседневность"),
|
||||||
|
Filter("el_2151", "постапокалиптика"),
|
||||||
|
Filter("el_2130", "приключения"),
|
||||||
|
Filter("el_2144", "психология"),
|
||||||
|
Filter("el_2121", "романтика"),
|
||||||
|
Filter("el_2124", "самурайский боевик"),
|
||||||
|
Filter("el_2159", "сверхъестественное"),
|
||||||
|
Filter("el_2122", "сёдзё"),
|
||||||
|
Filter("el_2128", "сёдзё-ай"),
|
||||||
|
Filter("el_2134", "сёнэн"),
|
||||||
|
Filter("el_2139", "сёнэн-ай"),
|
||||||
|
Filter("el_2129", "спорт"),
|
||||||
|
Filter("el_2138", "сэйнэн"),
|
||||||
|
Filter("el_2153", "трагедия"),
|
||||||
|
Filter("el_2150", "триллер"),
|
||||||
|
Filter("el_2125", "ужасы"),
|
||||||
|
Filter("el_2140", "фантастика"),
|
||||||
|
Filter("el_2131", "фэнтези"),
|
||||||
|
Filter("el_2127", "школа"),
|
||||||
|
Filter("el_2149", "этти"),
|
||||||
|
Filter("el_2123", "юри")
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
@ -6,7 +6,6 @@ import retrofit2.converter.gson.GsonConverterFactory
|
|||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to connect with the Github API.
|
* Used to connect with the Github API.
|
||||||
*/
|
*/
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import android.content.Context
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.util.toast
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
|
class GithubUpdateChecker() {
|
||||||
|
|
||||||
class GithubUpdateChecker(private val context: Context) {
|
private val service: GithubService = GithubService.create()
|
||||||
|
|
||||||
val service: GithubService = GithubService.create()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns observable containing release information
|
* Returns observable containing release information
|
||||||
*/
|
*/
|
||||||
fun checkForApplicationUpdate(): Observable<GithubRelease> {
|
fun checkForUpdate(): Observable<GithubUpdateResult> {
|
||||||
context.toast(R.string.update_check_look_for_updates)
|
return service.getLatestVersion().map { release ->
|
||||||
return service.getLatestVersion()
|
val newVersion = release.version.replace("[^\\d.]".toRegex(), "")
|
||||||
|
|
||||||
|
// Check if latest version is different from current version
|
||||||
|
if (newVersion != BuildConfig.VERSION_NAME) {
|
||||||
|
GithubUpdateResult.NewUpdate(release)
|
||||||
|
} else {
|
||||||
|
GithubUpdateResult.NoNewUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
sealed class GithubUpdateResult {
|
||||||
|
|
||||||
|
class NewUpdate(val release: GithubRelease): GithubUpdateResult()
|
||||||
|
class NoNewUpdate(): GithubUpdateResult()
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.support.v4.app.NotificationCompat
|
||||||
|
import com.google.android.gms.gcm.*
|
||||||
|
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class UpdateCheckerService : GcmTaskService() {
|
||||||
|
|
||||||
|
override fun onInitializeTasks() {
|
||||||
|
val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
if (preferences.automaticUpdates()) {
|
||||||
|
setupTask(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRunTask(params: TaskParams): Int {
|
||||||
|
return checkVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkVersion(): Int {
|
||||||
|
return GithubUpdateChecker()
|
||||||
|
.checkForUpdate()
|
||||||
|
.map { result ->
|
||||||
|
if (result is GithubUpdateResult.NewUpdate) {
|
||||||
|
val url = result.release.downloadLink
|
||||||
|
|
||||||
|
NotificationCompat.Builder(this).update {
|
||||||
|
setContentTitle(getString(R.string.app_name))
|
||||||
|
setContentText(getString(R.string.update_check_notification_update_available))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
// Download action
|
||||||
|
addAction(android.R.drawable.stat_sys_download_done,
|
||||||
|
getString(R.string.action_download),
|
||||||
|
UpdateNotificationReceiver.downloadApkIntent(
|
||||||
|
this@UpdateCheckerService, url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GcmNetworkManager.RESULT_SUCCESS
|
||||||
|
}
|
||||||
|
.onErrorReturn { GcmNetworkManager.RESULT_FAILURE }
|
||||||
|
// Sadly, the task needs to be synchronous.
|
||||||
|
.toBlocking()
|
||||||
|
.single()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
|
||||||
|
block()
|
||||||
|
notificationManager.notify(NOTIFICATION_UPDATER_ID, build())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun setupTask(context: Context) {
|
||||||
|
val task = PeriodicTask.Builder()
|
||||||
|
.setService(UpdateCheckerService::class.java)
|
||||||
|
.setTag("Updater")
|
||||||
|
// 24 hours
|
||||||
|
.setPeriod(24 * 60 * 60)
|
||||||
|
// Run between the last two hours
|
||||||
|
.setFlex(2 * 60 * 60)
|
||||||
|
.setRequiredNetwork(Task.NETWORK_STATE_CONNECTED)
|
||||||
|
.setPersisted(true)
|
||||||
|
.setUpdateCurrent(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
GcmNetworkManager.getInstance(context).schedule(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelTask(context: Context) {
|
||||||
|
GcmNetworkManager.getInstance(context).cancelAllTasks(UpdateCheckerService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,206 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.AsyncTask
|
|
||||||
import android.support.v4.app.NotificationCompat
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.Constants
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.data.network.ProgressListener
|
|
||||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
|
||||||
import eu.kanade.tachiyomi.util.saveTo
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class UpdateDownloader(private val context: Context) :
|
|
||||||
AsyncTask<String, Int, UpdateDownloader.DownloadResult>() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Prompt user with apk install intent
|
|
||||||
* @param context context
|
|
||||||
* @param file file of apk that is installed
|
|
||||||
*/
|
|
||||||
fun installAPK(context: Context, file: File) {
|
|
||||||
// Prompt install interface
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive")
|
|
||||||
// Without this flag android returned a intent error!
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject lateinit var network: NetworkHelper
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default download dir
|
|
||||||
*/
|
|
||||||
private val apkFile = File(context.externalCacheDir, "update.apk")
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification builder
|
|
||||||
*/
|
|
||||||
private val notificationBuilder = NotificationCompat.Builder(context)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Id of the notification
|
|
||||||
*/
|
|
||||||
private val notificationId: Int
|
|
||||||
get() = Constants.NOTIFICATION_UPDATER_ID
|
|
||||||
|
|
||||||
init {
|
|
||||||
App.get(context).component.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class containing download result
|
|
||||||
* @param url url of file
|
|
||||||
* @param successful status of download
|
|
||||||
*/
|
|
||||||
class DownloadResult(var url: String, var successful: Boolean)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before downloading
|
|
||||||
*/
|
|
||||||
override fun onPreExecute() {
|
|
||||||
// Create download notification
|
|
||||||
with(notificationBuilder) {
|
|
||||||
setContentTitle(context.getString(R.string.update_check_notification_file_download))
|
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun doInBackground(vararg params: String?): DownloadResult {
|
|
||||||
// Initialize information array containing path and url to file.
|
|
||||||
val result = DownloadResult(params[0]!!, false)
|
|
||||||
|
|
||||||
// Progress of the download
|
|
||||||
var savedProgress = 0
|
|
||||||
|
|
||||||
val progressListener = object : ProgressListener {
|
|
||||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
|
||||||
val progress = (100 * bytesRead / contentLength).toInt()
|
|
||||||
if (progress > savedProgress) {
|
|
||||||
savedProgress = progress
|
|
||||||
publishProgress(progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Make the request and download the file
|
|
||||||
val response = network.client.newCallWithProgress(GET(result.url), progressListener).execute()
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
response.body().source().saveTo(apkFile)
|
|
||||||
// Set download successful
|
|
||||||
result.successful = true
|
|
||||||
} else {
|
|
||||||
response.close()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, e.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when progress is updated
|
|
||||||
* @param values values containing progress
|
|
||||||
*/
|
|
||||||
override fun onProgressUpdate(vararg values: Int?) {
|
|
||||||
// Notify notification manager to update notification
|
|
||||||
values.getOrNull(0)?.let {
|
|
||||||
notificationBuilder.setProgress(100, it, false)
|
|
||||||
// Displays the progress bar on notification
|
|
||||||
context.notificationManager.notify(notificationId, notificationBuilder.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when download done
|
|
||||||
* @param result string containing download information
|
|
||||||
*/
|
|
||||||
override fun onPostExecute(result: DownloadResult) {
|
|
||||||
with(notificationBuilder) {
|
|
||||||
if (result.successful) {
|
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
|
||||||
addAction(R.drawable.ic_system_update_grey_24dp_img, context.getString(R.string.action_install),
|
|
||||||
getInstallOnReceivedIntent(InstallOnReceived.INSTALL_APK, apkFile.absolutePath))
|
|
||||||
addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel),
|
|
||||||
getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION))
|
|
||||||
} else {
|
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_error))
|
|
||||||
addAction(R.drawable.ic_refresh_grey_24dp_img, context.getString(R.string.action_retry),
|
|
||||||
getInstallOnReceivedIntent(InstallOnReceived.RETRY_DOWNLOAD, result.url))
|
|
||||||
addAction(R.drawable.ic_clear_grey_24dp_img, context.getString(R.string.action_cancel),
|
|
||||||
getInstallOnReceivedIntent(InstallOnReceived.CANCEL_NOTIFICATION))
|
|
||||||
}
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
setProgress(0, 0, false)
|
|
||||||
}
|
|
||||||
val notification = notificationBuilder.build()
|
|
||||||
notification.flags = Notification.FLAG_NO_CLEAR
|
|
||||||
context.notificationManager.notify(notificationId, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns broadcast intent
|
|
||||||
* @param action action name of broadcast intent
|
|
||||||
* @param path path of file | url of file
|
|
||||||
* @return broadcast intent
|
|
||||||
*/
|
|
||||||
fun getInstallOnReceivedIntent(action: String, path: String = ""): PendingIntent {
|
|
||||||
val intent = Intent(context, InstallOnReceived::class.java).apply {
|
|
||||||
this.action = action
|
|
||||||
putExtra(InstallOnReceived.FILE_LOCATION, path)
|
|
||||||
}
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BroadcastEvent used to install apk or retry download
|
|
||||||
*/
|
|
||||||
class InstallOnReceived : BroadcastReceiver() {
|
|
||||||
companion object {
|
|
||||||
// Install apk action
|
|
||||||
const val INSTALL_APK = "eu.kanade.INSTALL_APK"
|
|
||||||
|
|
||||||
// Retry download action
|
|
||||||
const val RETRY_DOWNLOAD = "eu.kanade.RETRY_DOWNLOAD"
|
|
||||||
|
|
||||||
// Retry download action
|
|
||||||
const val CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
|
|
||||||
|
|
||||||
// Absolute path of file || URL of file
|
|
||||||
const val FILE_LOCATION = "file_location"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
when (intent.action) {
|
|
||||||
// Install apk.
|
|
||||||
INSTALL_APK -> UpdateDownloader.installAPK(context, File(intent.getStringExtra(FILE_LOCATION)))
|
|
||||||
// Retry download.
|
|
||||||
RETRY_DOWNLOAD -> UpdateDownloader(context).execute(intent.getStringExtra(FILE_LOCATION))
|
|
||||||
|
|
||||||
CANCEL_NOTIFICATION -> context.notificationManager.cancel(Constants.NOTIFICATION_UPDATER_ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user