mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-28 12:07:52 +02:00
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
0cffb9e503 |
5
.github/CONTRIBUTING.md
vendored
5
.github/CONTRIBUTING.md
vendored
@ -1,20 +1,19 @@
|
|||||||
# Bugs
|
# Bugs
|
||||||
* Include version (Setting > About > Version)
|
* Include version (Setting > About > Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Dev version is equal to the number of commits as seen in the main page
|
* Debug version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
|
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
|
||||||
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
* For large logs use http://pastebin.com/ (or similar)
|
||||||
* For multipart issues **use list** like this:
|
* For multipart issues use list like this:
|
||||||
* [x] Done
|
* [x] Done
|
||||||
* [ ] Not done
|
* [ ] Not done
|
||||||
```
|
```
|
||||||
* [x] Done
|
* [x] Done
|
||||||
* [ ] Not done
|
* [ ] Not done
|
||||||
```
|
```
|
||||||
* Don't put together too many unrelated requests into one issue
|
|
||||||
|
|
||||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ android:
|
|||||||
before_script:
|
before_script:
|
||||||
- chmod +x gradlew
|
- chmod +x gradlew
|
||||||
#Build, and run tests
|
#Build, and run tests
|
||||||
script: "./gradlew clean buildDebug"
|
script: "./gradlew clean assembleDebug testDebugUnitTest"
|
||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
before_cache:
|
before_cache:
|
||||||
|
@ -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-debug-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)
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ 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
|
||||||
getCommitCount = {
|
getCommitCount = {
|
||||||
return 'git rev-list --count HEAD'.execute().text.trim()
|
return 'git rev-list --count origin/master'.execute().text.trim()
|
||||||
// return "1"
|
// return "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,9 +38,8 @@ android {
|
|||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 23
|
targetSdkVersion 23
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 8
|
versionCode 7
|
||||||
versionCode project.findProperty('versionCode')?.toInteger() ?: 8
|
versionName "0.2.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()}\""
|
||||||
@ -89,10 +88,10 @@ kapt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
final SUPPORT_LIBRARY_VERSION = '23.4.0'
|
final SUPPORT_LIBRARY_VERSION = '23.3.0'
|
||||||
final DAGGER_VERSION = '2.4'
|
final DAGGER_VERSION = '2.2'
|
||||||
final RETROFIT_VERSION = '2.0.2'
|
final OKHTTP_VERSION = '3.2.0'
|
||||||
final NUCLEUS_VERSION = '3.0.0'
|
final RETROFIT_VERSION = '2.0.1'
|
||||||
final STORIO_VERSION = '1.8.0'
|
final STORIO_VERSION = '1.8.0'
|
||||||
final MOCKITO_VERSION = '1.10.19'
|
final MOCKITO_VERSION = '1.10.19'
|
||||||
|
|
||||||
@ -107,17 +106,18 @@ dependencies {
|
|||||||
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
|
||||||
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
|
||||||
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
|
||||||
|
compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION"
|
||||||
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
|
||||||
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
|
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
|
||||||
compile "com.android.support:customtabs:$SUPPORT_LIBRARY_VERSION"
|
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.2.0'
|
compile 'io.reactivex:rxandroid:1.1.0'
|
||||||
compile 'io.reactivex:rxjava:1.1.5'
|
compile 'io.reactivex:rxjava:1.1.1'
|
||||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
compile "com.squareup.okhttp3:okhttp:3.3.1"
|
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
|
||||||
|
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
|
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
|
||||||
@ -125,25 +125,16 @@ dependencies {
|
|||||||
compile "com.squareup.retrofit2:adapter-rxjava:$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.7.0'
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
compile 'com.google.code.gson:gson:2.6.2'
|
compile 'com.google.code.gson:gson:2.6.2'
|
||||||
|
|
||||||
// YAML
|
|
||||||
compile 'org.yaml:snakeyaml:1.17'
|
|
||||||
|
|
||||||
// JavaScript engine
|
|
||||||
compile 'com.squareup.duktape:duktape-android:0.9.5'
|
|
||||||
|
|
||||||
// Disk cache
|
// Disk cache
|
||||||
compile 'com.jakewharton:disklrucache:2.0.2'
|
compile 'com.jakewharton:disklrucache:2.0.2'
|
||||||
|
|
||||||
// Parse HTML
|
// Parse HTML
|
||||||
compile 'org.jsoup:jsoup:1.9.2'
|
compile 'org.jsoup:jsoup:1.8.3'
|
||||||
|
|
||||||
// Changelog
|
|
||||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
||||||
@ -151,9 +142,7 @@ dependencies {
|
|||||||
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
|
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
compile "info.android15.nucleus:nucleus:$NUCLEUS_VERSION"
|
compile 'info.android15.nucleus:nucleus:3.0.0-beta'
|
||||||
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 "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||||
@ -162,7 +151,6 @@ dependencies {
|
|||||||
|
|
||||||
// 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'
|
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
compile 'com.jakewharton.timber:timber:4.1.2'
|
compile 'com.jakewharton.timber:timber:4.1.2'
|
||||||
@ -175,7 +163,9 @@ dependencies {
|
|||||||
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.github.afollestad.material-dialogs:core:0.8.5.5@aar') {
|
||||||
|
transitive = true
|
||||||
|
}
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
@ -191,7 +181,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.0.2'
|
ext.kotlin_version = '1.0.1'
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
8
app/proguard-rules.pro
vendored
8
app/proguard-rules.pro
vendored
@ -119,11 +119,3 @@
|
|||||||
# Keep the support library
|
# Keep the support library
|
||||||
-keep class org.acra.** { *; }
|
-keep class org.acra.** { *; }
|
||||||
-keep interface org.acra.** { *; }
|
-keep interface org.acra.** { *; }
|
||||||
|
|
||||||
# SnakeYaml
|
|
||||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
|
||||||
-keep class org.yaml.snakeyaml.** { public protected private *; }
|
|
||||||
-dontwarn org.yaml.snakeyaml.**
|
|
||||||
|
|
||||||
# Duktape
|
|
||||||
-keep class com.squareup.duktape.** { *; }
|
|
@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule"
|
||||||
android:value="GlideModule" />
|
android:value="GlideModule" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi
|
|
||||||
|
|
||||||
object Constants {
|
|
||||||
const val NOTIFICATION_LIBRARY_ID = 1
|
|
||||||
const val NOTIFICATION_UPDATER_ID = 2
|
|
||||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
|
|
||||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
|
|
||||||
}
|
|
@ -1,6 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
import com.bumptech.glide.load.model.LazyHeaders
|
||||||
|
import com.bumptech.glide.request.animation.GlideAnimation
|
||||||
|
import com.bumptech.glide.request.target.SimpleTarget
|
||||||
import eu.kanade.tachiyomi.util.DiskUtils
|
import eu.kanade.tachiyomi.util.DiskUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -22,19 +27,80 @@ class CoverCache(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
|
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the cover with Glide and save the file.
|
||||||
|
* @param thumbnailUrl url of thumbnail.
|
||||||
|
* @param headers headers included in Glide request.
|
||||||
|
* @param onReady function to call when the image is ready
|
||||||
|
*/
|
||||||
|
fun save(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)? = null) {
|
||||||
|
// Check if url is empty.
|
||||||
|
if (thumbnailUrl.isNullOrEmpty())
|
||||||
|
return
|
||||||
|
|
||||||
|
// Download the cover with Glide and save the file.
|
||||||
|
val url = GlideUrl(thumbnailUrl, headers)
|
||||||
|
Glide.with(context)
|
||||||
|
.load(url)
|
||||||
|
.downloadOnly(object : SimpleTarget<File>() {
|
||||||
|
override fun onResourceReady(resource: File, anim: GlideAnimation<in File>) {
|
||||||
|
try {
|
||||||
|
// Copy the cover from Glide's cache to local cache.
|
||||||
|
copyToCache(thumbnailUrl!!, resource)
|
||||||
|
|
||||||
|
onReady?.invoke(resource)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save or load the image from cache
|
||||||
|
* @param thumbnailUrl the thumbnail url.
|
||||||
|
* @param headers headers included in Glide request.
|
||||||
|
* @param onReady function to call when the image is ready
|
||||||
|
*/
|
||||||
|
fun saveOrLoadFromCache(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)?) {
|
||||||
|
// Check if url is empty.
|
||||||
|
if (thumbnailUrl.isNullOrEmpty())
|
||||||
|
return
|
||||||
|
|
||||||
|
// If file exist load it otherwise save it.
|
||||||
|
val localCover = getCoverFromCache(thumbnailUrl!!)
|
||||||
|
if (localCover.exists()) {
|
||||||
|
onReady?.invoke(localCover)
|
||||||
|
} else {
|
||||||
|
save(thumbnailUrl, headers, onReady)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cover from cache.
|
* Returns the cover from cache.
|
||||||
*
|
|
||||||
* @param thumbnailUrl the thumbnail url.
|
* @param thumbnailUrl the thumbnail url.
|
||||||
* @return cover image.
|
* @return cover image.
|
||||||
*/
|
*/
|
||||||
fun getCoverFile(thumbnailUrl: String): File {
|
private fun getCoverFromCache(thumbnailUrl: String): File {
|
||||||
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the given file to this cache.
|
||||||
|
* @param thumbnailUrl url of thumbnail.
|
||||||
|
* @param sourceFile the source file of the cover image.
|
||||||
|
* @throws IOException if there's any error.
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun copyToCache(thumbnailUrl: String, sourceFile: File) {
|
||||||
|
// Get destination file.
|
||||||
|
val destFile = getCoverFromCache(thumbnailUrl)
|
||||||
|
|
||||||
|
sourceFile.copyTo(destFile, overwrite = true)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy the given stream to this cache.
|
* Copy the given stream to this cache.
|
||||||
*
|
|
||||||
* @param thumbnailUrl url of the thumbnail.
|
* @param thumbnailUrl url of the thumbnail.
|
||||||
* @param inputStream the stream to copy.
|
* @param inputStream the stream to copy.
|
||||||
* @throws IOException if there's any error.
|
* @throws IOException if there's any error.
|
||||||
@ -42,14 +108,13 @@ class CoverCache(private val context: Context) {
|
|||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
|
||||||
// Get destination file.
|
// Get destination file.
|
||||||
val destFile = getCoverFile(thumbnailUrl)
|
val destFile = getCoverFromCache(thumbnailUrl)
|
||||||
|
|
||||||
destFile.outputStream().use { inputStream.copyTo(it) }
|
destFile.outputStream().use { inputStream.copyTo(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the cover file from the cache.
|
* Delete the cover file from the cache.
|
||||||
*
|
|
||||||
* @param thumbnailUrl the thumbnail url.
|
* @param thumbnailUrl the thumbnail url.
|
||||||
* @return status of deletion.
|
* @return status of deletion.
|
||||||
*/
|
*/
|
||||||
@ -59,7 +124,7 @@ class CoverCache(private val context: Context) {
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Remove file.
|
// Remove file.
|
||||||
val file = getCoverFile(thumbnailUrl!!)
|
val file = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
|
||||||
return file.exists() && file.delete()
|
return file.exists() && file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.kt
vendored
Normal file
22
app/src/main/java/eu/kanade/tachiyomi/data/cache/CoverGlideModule.kt
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.GlideBuilder
|
||||||
|
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||||
|
import com.bumptech.glide.module.GlideModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to update Glide module settings
|
||||||
|
*/
|
||||||
|
class CoverGlideModule : GlideModule {
|
||||||
|
|
||||||
|
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||||
|
// Set the cache size of Glide to 15 MiB
|
||||||
|
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerComponents(context: Context, glide: Glide) {
|
||||||
|
// Nothing to see here!
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,26 @@
|
|||||||
package eu.kanade.tachiyomi.data.database
|
package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Pair
|
||||||
|
import com.pushtorefresh.storio.Queries
|
||||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
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.resolvers.LibraryMangaGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.*
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
|
import eu.kanade.tachiyomi.util.ChapterRecognition
|
||||||
|
import rx.Observable
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
open class DatabaseHelper(context: Context) {
|
||||||
* This class provides operations to manage the database through its interfaces.
|
|
||||||
*/
|
|
||||||
open class DatabaseHelper(context: Context)
|
|
||||||
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries {
|
|
||||||
|
|
||||||
override val db = DefaultStorIOSQLite.builder()
|
val db = DefaultStorIOSQLite.builder()
|
||||||
.sqliteOpenHelper(DbOpenHelper(context))
|
.sqliteOpenHelper(DbOpenHelper(context))
|
||||||
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
|
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
|
||||||
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
|
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
|
||||||
@ -20,6 +29,287 @@ open class DatabaseHelper(context: Context)
|
|||||||
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
|
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
|
inline fun inTransaction(func: DatabaseHelper.() -> Unit) {
|
||||||
|
db.internal().beginTransaction()
|
||||||
|
try {
|
||||||
|
func()
|
||||||
|
db.internal().setTransactionSuccessful()
|
||||||
|
} finally {
|
||||||
|
db.internal().endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mangas related queries
|
||||||
|
|
||||||
|
fun getMangas() = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getLibraryMangas() = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(libraryQuery)
|
||||||
|
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
open fun getFavoriteMangas() = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COLUMN_FAVORITE} = ?")
|
||||||
|
.whereArgs(1)
|
||||||
|
.orderBy(MangaTable.COLUMN_TITLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getManga(url: String, sourceId: Int) = db.get()
|
||||||
|
.`object`(Manga::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COLUMN_URL} = ? AND ${MangaTable.COLUMN_SOURCE} = ?")
|
||||||
|
.whereArgs(url, sourceId)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getManga(id: Long) = db.get()
|
||||||
|
.`object`(Manga::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COLUMN_ID} = ?")
|
||||||
|
.whereArgs(id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||||
|
|
||||||
|
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||||
|
|
||||||
|
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
|
|
||||||
|
fun deleteMangasNotInLibrary() = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COLUMN_FAVORITE} = ?")
|
||||||
|
.whereArgs(0)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
|
||||||
|
// Chapters related queries
|
||||||
|
|
||||||
|
fun getChapters(manga: Manga) = db.get()
|
||||||
|
.listOfObjects(Chapter::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COLUMN_MANGA_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getRecentChapters(date: Date) = db.get()
|
||||||
|
.listOfObjects(MangaChapter::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getRecentsQuery(date))
|
||||||
|
.observesTables(ChapterTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||||
|
.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.COLUMN_MANGA_ID} = ? AND " +
|
||||||
|
"${ChapterTable.COLUMN_CHAPTER_NUMBER} > ? AND " +
|
||||||
|
"${ChapterTable.COLUMN_CHAPTER_NUMBER} <= ?")
|
||||||
|
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
|
||||||
|
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
|
||||||
|
.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.COLUMN_MANGA_ID} = ? AND " +
|
||||||
|
"${ChapterTable.COLUMN_CHAPTER_NUMBER} < ? AND " +
|
||||||
|
"${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
|
||||||
|
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
|
||||||
|
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + " DESC")
|
||||||
|
.limit(1)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNextUnreadChapter(manga: Manga) = db.get()
|
||||||
|
.`object`(Chapter::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COLUMN_MANGA_ID} = ? AND " +
|
||||||
|
"${ChapterTable.COLUMN_READ} = ? AND " +
|
||||||
|
"${ChapterTable.COLUMN_CHAPTER_NUMBER} >= ?")
|
||||||
|
.whereArgs(manga.id, 0, 0)
|
||||||
|
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
|
||||||
|
.limit(1)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
|
||||||
|
|
||||||
|
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
||||||
|
|
||||||
|
// Add new chapters or delete if the source deletes them
|
||||||
|
open fun insertOrRemoveChapters(manga: Manga, sourceChapters: List<Chapter>, source: Source): Observable<Pair<Int, Int>> {
|
||||||
|
val dbChapters = getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
val newChapters = Observable.from(sourceChapters)
|
||||||
|
.filter { it !in dbChapters }
|
||||||
|
.doOnNext { c ->
|
||||||
|
c.manga_id = manga.id
|
||||||
|
source.parseChapterNumber(c)
|
||||||
|
ChapterRecognition.parseChapterNumber(c, manga)
|
||||||
|
}.toList()
|
||||||
|
|
||||||
|
val deletedChapters = Observable.from(dbChapters)
|
||||||
|
.filter { it !in sourceChapters }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
return Observable.zip(newChapters, deletedChapters) { toAdd, toDelete ->
|
||||||
|
var added = 0
|
||||||
|
var deleted = 0
|
||||||
|
var readded = 0
|
||||||
|
|
||||||
|
inTransaction {
|
||||||
|
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||||
|
if (!toDelete.isEmpty()) {
|
||||||
|
for (c in toDelete) {
|
||||||
|
if (c.read) {
|
||||||
|
deletedReadChapterNumbers.add(c.chapter_number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deleted = deleteChapters(toDelete).executeAsBlocking().results().size
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toAdd.isEmpty()) {
|
||||||
|
// Set the date fetch for new items in reverse order to allow another sorting method.
|
||||||
|
// Sources MUST return the chapters from most to less recent, which is common.
|
||||||
|
var now = Date().time
|
||||||
|
|
||||||
|
for (i in toAdd.indices.reversed()) {
|
||||||
|
val c = toAdd[i]
|
||||||
|
c.date_fetch = now++
|
||||||
|
// Try to mark already read chapters as read when the source deletes them
|
||||||
|
if (c.chapter_number != -1f && c.chapter_number in deletedReadChapterNumbers) {
|
||||||
|
c.read = true
|
||||||
|
readded++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
added = insertChapters(toAdd).executeAsBlocking().numberOfInserts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Pair.create(added - readded, deleted - readded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
|
||||||
|
|
||||||
|
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
||||||
|
|
||||||
|
// Manga sync related queries
|
||||||
|
|
||||||
|
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
|
||||||
|
.`object`(MangaSync::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(MangaSyncTable.TABLE)
|
||||||
|
.where("${MangaSyncTable.COLUMN_MANGA_ID} = ? AND " +
|
||||||
|
"${MangaSyncTable.COLUMN_SYNC_ID} = ?")
|
||||||
|
.whereArgs(manga.id, sync.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getMangasSync(manga: Manga) = db.get()
|
||||||
|
.listOfObjects(MangaSync::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(MangaSyncTable.TABLE)
|
||||||
|
.where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
|
||||||
|
|
||||||
|
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
|
||||||
|
|
||||||
|
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
|
||||||
|
|
||||||
|
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(MangaSyncTable.TABLE)
|
||||||
|
.where("${MangaSyncTable.COLUMN_MANGA_ID} = ?")
|
||||||
|
.whereArgs(manga.id)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
// Categories related queries
|
||||||
|
|
||||||
|
fun getCategories() = db.get()
|
||||||
|
.listOfObjects(Category::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(CategoryTable.TABLE)
|
||||||
|
.orderBy(CategoryTable.COLUMN_ORDER)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getCategoriesForManga(manga: Manga) = db.get()
|
||||||
|
.listOfObjects(Category::class.java)
|
||||||
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(getCategoriesForMangaQuery(manga))
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
||||||
|
|
||||||
|
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
|
||||||
|
|
||||||
|
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
|
||||||
|
|
||||||
|
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
|
||||||
|
|
||||||
|
fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
|
||||||
|
|
||||||
|
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
|
||||||
|
|
||||||
|
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
|
||||||
|
.byQuery(DeleteQuery.builder()
|
||||||
|
.table(MangaCategoryTable.TABLE)
|
||||||
|
.where("${MangaCategoryTable.COLUMN_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
||||||
|
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
|
||||||
|
inTransaction {
|
||||||
|
deleteOldMangasCategories(mangas).executeAsBlocking()
|
||||||
|
insertMangasCategories(mangasCategories).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
|
||||||
|
|
||||||
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
|
|
||||||
internal().beginTransaction()
|
|
||||||
try {
|
|
||||||
block()
|
|
||||||
internal().setTransactionSuccessful()
|
|
||||||
} finally {
|
|
||||||
internal().endTransaction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
|
|
||||||
internal().beginTransaction()
|
|
||||||
try {
|
|
||||||
val result = block()
|
|
||||||
internal().setTransactionSuccessful()
|
|
||||||
return result
|
|
||||||
} finally {
|
|
||||||
internal().endTransaction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,8 +5,7 @@ import android.database.sqlite.SQLiteDatabase
|
|||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import eu.kanade.tachiyomi.data.database.tables.*
|
import eu.kanade.tachiyomi.data.database.tables.*
|
||||||
|
|
||||||
class DbOpenHelper(context: Context)
|
class DbOpenHelper(context: Context) : SQLiteOpenHelper(context, DbOpenHelper.DATABASE_NAME, null, DbOpenHelper.DATABASE_VERSION) {
|
||||||
: SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
@ -17,30 +16,24 @@ class DbOpenHelper(context: Context)
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 2
|
const val DATABASE_VERSION = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
override fun onCreate(db: SQLiteDatabase) = with(db) {
|
||||||
execSQL(MangaTable.createTableQuery)
|
execSQL(MangaTable.getCreateTableQuery())
|
||||||
execSQL(ChapterTable.createTableQuery)
|
execSQL(ChapterTable.getCreateTableQuery())
|
||||||
execSQL(MangaSyncTable.createTableQuery)
|
execSQL(MangaSyncTable.getCreateTableQuery())
|
||||||
execSQL(CategoryTable.createTableQuery)
|
execSQL(CategoryTable.getCreateTableQuery())
|
||||||
execSQL(MangaCategoryTable.createTableQuery)
|
execSQL(MangaCategoryTable.getCreateTableQuery())
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
execSQL(MangaTable.createUrlIndexQuery)
|
execSQL(MangaTable.getCreateUrlIndexQuery())
|
||||||
execSQL(MangaTable.createFavoriteIndexQuery)
|
execSQL(MangaTable.getCreateFavoriteIndexQuery())
|
||||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
execSQL(ChapterTable.getCreateMangaIdIndexQuery())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
if (oldVersion < 2) {
|
|
||||||
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
|
|
||||||
|
|
||||||
// Fix kissmanga covers after supporting cloudflare
|
|
||||||
db.execSQL("""UPDATE mangas SET thumbnail_url =
|
|
||||||
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SQLiteDatabase) {
|
override fun onConfigure(db: SQLiteDatabase) {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
|
||||||
|
|
||||||
interface DbProvider {
|
|
||||||
|
|
||||||
val db: DefaultStorIOSQLite
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,54 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga as MangaModel
|
||||||
|
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.MangaCategoryTable as MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the manga from the library, with their categories and unread count.
|
||||||
|
*/
|
||||||
|
val libraryQuery =
|
||||||
|
"SELECT M.*, COALESCE(MC.${MangaCategory.COLUMN_CATEGORY_ID}, 0) AS ${Manga.COLUMN_CATEGORY} " +
|
||||||
|
"FROM (" +
|
||||||
|
"SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COLUMN_UNREAD} " +
|
||||||
|
"FROM ${Manga.TABLE} " +
|
||||||
|
"LEFT JOIN (" +
|
||||||
|
"SELECT ${Chapter.COLUMN_MANGA_ID}, COUNT(*) AS unread " +
|
||||||
|
"FROM ${Chapter.TABLE} " +
|
||||||
|
"WHERE ${Chapter.COLUMN_READ} = 0 " +
|
||||||
|
"GROUP BY ${Chapter.COLUMN_MANGA_ID}" +
|
||||||
|
") AS C " +
|
||||||
|
"ON ${Manga.COLUMN_ID} = C.${Chapter.COLUMN_MANGA_ID} " +
|
||||||
|
"WHERE ${Manga.COLUMN_FAVORITE} = 1 " +
|
||||||
|
"GROUP BY ${Manga.COLUMN_ID} " +
|
||||||
|
"ORDER BY ${Manga.COLUMN_TITLE}" +
|
||||||
|
") AS M " +
|
||||||
|
"LEFT JOIN (" +
|
||||||
|
"SELECT * FROM ${MangaCategory.TABLE}) AS MC " +
|
||||||
|
"ON MC.${MangaCategory.COLUMN_MANGA_ID} = M.${Manga.COLUMN_ID}"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the recent chapters of manga from the library up to a date.
|
||||||
|
*
|
||||||
|
* @param date the delimiting date.
|
||||||
|
*/
|
||||||
|
fun getRecentsQuery(date: Date): String =
|
||||||
|
"SELECT ${Manga.TABLE}.${Manga.COLUMN_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} " +
|
||||||
|
"ON ${Manga.TABLE}.${Manga.COLUMN_ID} = ${Chapter.TABLE}.${Chapter.COLUMN_MANGA_ID} " +
|
||||||
|
"WHERE ${Manga.COLUMN_FAVORITE} = 1 AND ${Chapter.COLUMN_DATE_UPLOAD} > ${date.time} " +
|
||||||
|
"ORDER BY ${Chapter.COLUMN_DATE_UPLOAD} DESC"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query to get the categorias for a manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga.
|
||||||
|
*/
|
||||||
|
fun getCategoriesForMangaQuery(manga: MangaModel) =
|
||||||
|
"SELECT ${Category.TABLE}.* FROM ${Category.TABLE} " +
|
||||||
|
"JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COLUMN_ID} = " +
|
||||||
|
"${MangaCategory.TABLE}.${MangaCategory.COLUMN_CATEGORY_ID} " +
|
||||||
|
"WHERE ${MangaCategory.COLUMN_MANGA_ID} = ${manga.id}"
|
@ -10,16 +10,16 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
|||||||
@StorIOSQLiteType(table = CategoryTable.TABLE)
|
@StorIOSQLiteType(table = CategoryTable.TABLE)
|
||||||
public class Category implements Serializable {
|
public class Category implements Serializable {
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COL_ID, key = true)
|
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ID, key = true)
|
||||||
public Integer id;
|
public Integer id;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COL_NAME)
|
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_NAME)
|
||||||
public String name;
|
public String name;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COL_ORDER)
|
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ORDER)
|
||||||
public int order;
|
public int order;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = CategoryTable.COL_FLAGS)
|
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_FLAGS)
|
||||||
public int flags;
|
public int flags;
|
||||||
|
|
||||||
public Category() {}
|
public Category() {}
|
||||||
|
@ -14,36 +14,33 @@ import eu.kanade.tachiyomi.util.UrlUtil;
|
|||||||
@StorIOSQLiteType(table = ChapterTable.TABLE)
|
@StorIOSQLiteType(table = ChapterTable.TABLE)
|
||||||
public class Chapter implements Serializable {
|
public class Chapter implements Serializable {
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_ID, key = true)
|
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_ID, key = true)
|
||||||
public Long id;
|
public Long id;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_MANGA_ID)
|
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_MANGA_ID)
|
||||||
public Long manga_id;
|
public Long manga_id;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_URL)
|
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_URL)
|
||||||
public String url;
|
public String url;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_NAME)
|
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_NAME)
|
||||||
public String name;
|
public String name;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_READ)
|
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_READ)
|
||||||
public boolean read;
|
public boolean read;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_LAST_PAGE_READ)
|
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_LAST_PAGE_READ)
|
||||||
public int last_page_read;
|
public int last_page_read;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_DATE_FETCH)
|
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_FETCH)
|
||||||
public long date_fetch;
|
public long date_fetch;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_DATE_UPLOAD)
|
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_UPLOAD)
|
||||||
public long date_upload;
|
public long date_upload;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_CHAPTER_NUMBER)
|
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_CHAPTER_NUMBER)
|
||||||
public float chapter_number;
|
public float chapter_number;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = ChapterTable.COL_SOURCE_ORDER)
|
|
||||||
public int source_order;
|
|
||||||
|
|
||||||
public int status;
|
public int status;
|
||||||
|
|
||||||
private transient List<Page> pages;
|
private transient List<Page> pages;
|
||||||
@ -87,8 +84,4 @@ public class Chapter implements Serializable {
|
|||||||
public boolean isDownloaded() {
|
public boolean isDownloaded() {
|
||||||
return status == Download.DOWNLOADED;
|
return status == Download.DOWNLOADED;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRecognizedNumber() {
|
|
||||||
return chapter_number >= 0f;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -14,49 +14,49 @@ import eu.kanade.tachiyomi.util.UrlUtil;
|
|||||||
@StorIOSQLiteType(table = MangaTable.TABLE)
|
@StorIOSQLiteType(table = MangaTable.TABLE)
|
||||||
public class Manga implements Serializable {
|
public class Manga implements Serializable {
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_ID, key = true)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ID, key = true)
|
||||||
public Long id;
|
public Long id;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_SOURCE)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_SOURCE)
|
||||||
public int source;
|
public int source;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_URL)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_URL)
|
||||||
public String url;
|
public String url;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_ARTIST)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ARTIST)
|
||||||
public String artist;
|
public String artist;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_AUTHOR)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_AUTHOR)
|
||||||
public String author;
|
public String author;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_DESCRIPTION)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_DESCRIPTION)
|
||||||
public String description;
|
public String description;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_GENRE)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_GENRE)
|
||||||
public String genre;
|
public String genre;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_TITLE)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_TITLE)
|
||||||
public String title;
|
public String title;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_STATUS)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_STATUS)
|
||||||
public int status;
|
public int status;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_THUMBNAIL_URL)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_THUMBNAIL_URL)
|
||||||
public String thumbnail_url;
|
public String thumbnail_url;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_FAVORITE)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_FAVORITE)
|
||||||
public boolean favorite;
|
public boolean favorite;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_LAST_UPDATE)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_LAST_UPDATE)
|
||||||
public long last_update;
|
public long last_update;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_INITIALIZED)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_INITIALIZED)
|
||||||
public boolean initialized;
|
public boolean initialized;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_VIEWER)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_VIEWER)
|
||||||
public int viewer;
|
public int viewer;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaTable.COL_CHAPTER_FLAGS)
|
@StorIOSQLiteColumn(name = MangaTable.COLUMN_CHAPTER_FLAGS)
|
||||||
public int chapter_flags;
|
public int chapter_flags;
|
||||||
|
|
||||||
public transient int unread;
|
public transient int unread;
|
||||||
@ -68,13 +68,10 @@ public class Manga implements Serializable {
|
|||||||
public static final int COMPLETED = 2;
|
public static final int COMPLETED = 2;
|
||||||
public static final int LICENSED = 3;
|
public static final int LICENSED = 3;
|
||||||
|
|
||||||
public static final int SORT_DESC = 0x00000000;
|
public static final int SORT_AZ = 0x00000000;
|
||||||
public static final int SORT_ASC = 0x00000001;
|
public static final int SORT_ZA = 0x00000001;
|
||||||
public static final int SORT_MASK = 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_UNREAD = 0x00000002;
|
||||||
public static final int SHOW_READ = 0x00000004;
|
public static final int SHOW_READ = 0x00000004;
|
||||||
public static final int READ_MASK = 0x00000006;
|
public static final int READ_MASK = 0x00000006;
|
||||||
@ -83,9 +80,8 @@ public class Manga implements Serializable {
|
|||||||
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
|
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
|
||||||
public static final int DOWNLOADED_MASK = 0x00000018;
|
public static final int DOWNLOADED_MASK = 0x00000018;
|
||||||
|
|
||||||
public static final int SORTING_SOURCE = 0x00000000;
|
// Generic filter that does not filter anything
|
||||||
public static final int SORTING_NUMBER = 0x00000100;
|
public static final int SHOW_ALL = 0x00000000;
|
||||||
public static final int SORTING_MASK = 0x00000100;
|
|
||||||
|
|
||||||
public static final int DISPLAY_NAME = 0x00000000;
|
public static final int DISPLAY_NAME = 0x00000000;
|
||||||
public static final int DISPLAY_NUMBER = 0x00100000;
|
public static final int DISPLAY_NUMBER = 0x00100000;
|
||||||
@ -166,16 +162,12 @@ public class Manga implements Serializable {
|
|||||||
setFlags(filter, DOWNLOADED_MASK);
|
setFlags(filter, DOWNLOADED_MASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSorting(int sort) {
|
|
||||||
setFlags(sort, SORTING_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setFlags(int flag, int mask) {
|
private void setFlags(int flag, int mask) {
|
||||||
chapter_flags = (chapter_flags & ~mask) | (flag & mask);
|
chapter_flags = (chapter_flags & ~mask) | (flag & mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean sortDescending() {
|
public boolean sortChaptersAZ() {
|
||||||
return (chapter_flags & SORT_MASK) == SORT_DESC;
|
return (chapter_flags & SORT_MASK) == SORT_AZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
// Used to display the chapter's title one way or another
|
||||||
@ -191,10 +183,6 @@ public class Manga implements Serializable {
|
|||||||
return chapter_flags & DOWNLOADED_MASK;
|
return chapter_flags & DOWNLOADED_MASK;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getSorting() {
|
|
||||||
return chapter_flags & SORTING_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
|
@ -8,13 +8,13 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
|||||||
@StorIOSQLiteType(table = MangaCategoryTable.TABLE)
|
@StorIOSQLiteType(table = MangaCategoryTable.TABLE)
|
||||||
public class MangaCategory {
|
public class MangaCategory {
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_ID, key = true)
|
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_ID, key = true)
|
||||||
public Long id;
|
public Long id;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_MANGA_ID)
|
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_MANGA_ID)
|
||||||
public long manga_id;
|
public long manga_id;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaCategoryTable.COL_CATEGORY_ID)
|
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_CATEGORY_ID)
|
||||||
public int category_id;
|
public int category_id;
|
||||||
|
|
||||||
public MangaCategory() {}
|
public MangaCategory() {}
|
||||||
|
@ -6,36 +6,36 @@ import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService;
|
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||||
|
|
||||||
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
|
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
|
||||||
public class MangaSync implements Serializable {
|
public class MangaSync implements Serializable {
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_ID, key = true)
|
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_ID, key = true)
|
||||||
public Long id;
|
public Long id;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_MANGA_ID)
|
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_MANGA_ID)
|
||||||
public long manga_id;
|
public long manga_id;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_SYNC_ID)
|
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_SYNC_ID)
|
||||||
public int sync_id;
|
public int sync_id;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_REMOTE_ID)
|
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_REMOTE_ID)
|
||||||
public int remote_id;
|
public int remote_id;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_TITLE)
|
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_TITLE)
|
||||||
public String title;
|
public String title;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_LAST_CHAPTER_READ)
|
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_LAST_CHAPTER_READ)
|
||||||
public int last_chapter_read;
|
public int last_chapter_read;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_TOTAL_CHAPTERS)
|
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_TOTAL_CHAPTERS)
|
||||||
public int total_chapters;
|
public int total_chapters;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_SCORE)
|
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_SCORE)
|
||||||
public float score;
|
public float score;
|
||||||
|
|
||||||
@StorIOSQLiteColumn(name = MangaSyncTable.COL_STATUS)
|
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_STATUS)
|
||||||
public int status;
|
public int status;
|
||||||
|
|
||||||
public boolean update;
|
public boolean update;
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
|
||||||
|
|
||||||
interface CategoryQueries : DbProvider {
|
|
||||||
|
|
||||||
fun getCategories() = db.get()
|
|
||||||
.listOfObjects(Category::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(CategoryTable.TABLE)
|
|
||||||
.orderBy(CategoryTable.COL_ORDER)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getCategoriesForManga(manga: Manga) = db.get()
|
|
||||||
.listOfObjects(Category::class.java)
|
|
||||||
.withQuery(RawQuery.builder()
|
|
||||||
.query(getCategoriesForMangaQuery())
|
|
||||||
.args(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
|
||||||
|
|
||||||
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
|
|
||||||
|
|
||||||
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
|
|
||||||
|
|
||||||
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
|
|
||||||
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
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.RawQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
interface ChapterQueries : DbProvider {
|
|
||||||
|
|
||||||
fun getChapters(manga: Manga) = db.get()
|
|
||||||
.listOfObjects(Chapter::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where("${ChapterTable.COL_MANGA_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getRecentChapters(date: Date) = db.get()
|
|
||||||
.listOfObjects(MangaChapter::class.java)
|
|
||||||
.withQuery(RawQuery.builder()
|
|
||||||
.query(getRecentsQuery())
|
|
||||||
.args(date.time)
|
|
||||||
.observesTables(ChapterTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
|
||||||
.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 insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
|
|
||||||
|
|
||||||
fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
|
|
||||||
|
|
||||||
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
|
|
||||||
|
|
||||||
fun updateChapterProgress(chapter: Chapter) = db.put()
|
|
||||||
.`object`(chapter)
|
|
||||||
.withPutResolver(ChapterProgressPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
|
|
||||||
.objects(chapters)
|
|
||||||
.withPutResolver(ChapterProgressPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
|
|
||||||
.objects(chapters)
|
|
||||||
.withPutResolver(ChapterSourceOrderPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.Queries
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
|
||||||
import eu.kanade.tachiyomi.data.database.inTransaction
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
|
||||||
|
|
||||||
interface MangaCategoryQueries : DbProvider {
|
|
||||||
|
|
||||||
fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
|
|
||||||
|
|
||||||
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
|
|
||||||
|
|
||||||
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(MangaCategoryTable.TABLE)
|
|
||||||
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
|
|
||||||
.whereArgs(*mangas.map { it.id }.toTypedArray())
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
|
|
||||||
db.inTransaction {
|
|
||||||
deleteOldMangasCategories(mangas).executeAsBlocking()
|
|
||||||
insertMangasCategories(mangasCategories).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|
||||||
|
|
||||||
interface MangaQueries : DbProvider {
|
|
||||||
|
|
||||||
fun getMangas() = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getLibraryMangas() = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(RawQuery.builder()
|
|
||||||
.query(libraryQuery)
|
|
||||||
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
|
|
||||||
.build())
|
|
||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
open fun getFavoriteMangas() = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
|
||||||
.whereArgs(1)
|
|
||||||
.orderBy(MangaTable.COL_TITLE)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getManga(url: String, sourceId: Int) = db.get()
|
|
||||||
.`object`(Manga::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
|
|
||||||
.whereArgs(url, sourceId)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getManga(id: Long) = db.get()
|
|
||||||
.`object`(Manga::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
|
||||||
|
|
||||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
|
||||||
|
|
||||||
fun updateFlags(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaFlagsPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
|
||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
|
||||||
|
|
||||||
fun deleteMangasNotInLibrary() = db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
|
||||||
.whereArgs(0)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
|
||||||
|
|
||||||
interface MangaSyncQueries : DbProvider {
|
|
||||||
|
|
||||||
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
|
|
||||||
.`object`(MangaSync::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
|
|
||||||
"${MangaSyncTable.COL_SYNC_ID} = ?")
|
|
||||||
.whereArgs(manga.id, sync.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getMangasSync(manga: Manga) = db.get()
|
|
||||||
.listOfObjects(MangaSync::class.java)
|
|
||||||
.withQuery(Query.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
|
|
||||||
|
|
||||||
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
|
|
||||||
|
|
||||||
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
|
|
||||||
|
|
||||||
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
|
|
||||||
.byQuery(DeleteQuery.builder()
|
|
||||||
.table(MangaSyncTable.TABLE)
|
|
||||||
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
|
||||||
|
|
||||||
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.MangaCategoryTable as MangaCategory
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query to get the manga from the library, with their categories and unread count.
|
|
||||||
*/
|
|
||||||
val libraryQuery = """
|
|
||||||
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
|
|
||||||
FROM (
|
|
||||||
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
|
|
||||||
FROM ${Manga.TABLE}
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
|
|
||||||
FROM ${Chapter.TABLE}
|
|
||||||
WHERE ${Chapter.COL_READ} = 0
|
|
||||||
GROUP BY ${Chapter.COL_MANGA_ID}
|
|
||||||
) AS C
|
|
||||||
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
|
|
||||||
WHERE ${Manga.COL_FAVORITE} = 1
|
|
||||||
GROUP BY ${Manga.COL_ID}
|
|
||||||
ORDER BY ${Manga.COL_TITLE}
|
|
||||||
) AS M
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT * FROM ${MangaCategory.TABLE}) AS MC
|
|
||||||
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
|
||||||
"""
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query to get the recent chapters of manga from the library up to a date.
|
|
||||||
*/
|
|
||||||
fun getRecentsQuery() = """
|
|
||||||
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
|
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
|
||||||
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
|
|
||||||
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query to get the categories for a manga.
|
|
||||||
*/
|
|
||||||
fun getCategoriesForMangaQuery() = """
|
|
||||||
SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
|
|
||||||
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
|
|
||||||
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
|
||||||
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
|
||||||
"""
|
|
@ -1,34 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
|
||||||
|
|
||||||
class ChapterProgressPutResolver : PutResolver<Chapter>() {
|
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
|
||||||
val updateQuery = mapToUpdateQuery(chapter)
|
|
||||||
val contentValues = mapToContentValues(chapter)
|
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
|
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where("${ChapterTable.COL_ID} = ?")
|
|
||||||
.whereArgs(chapter.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(2).apply {
|
|
||||||
put(ChapterTable.COL_READ, chapter.read)
|
|
||||||
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
|
||||||
|
|
||||||
class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
|
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
|
||||||
val updateQuery = mapToUpdateQuery(chapter)
|
|
||||||
val contentValues = mapToContentValues(chapter)
|
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
|
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
|
|
||||||
.whereArgs(chapter.url, chapter.manga_id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
|
|
||||||
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -15,10 +15,10 @@ class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
|
|||||||
override fun mapFromCursor(cursor: Cursor): Manga {
|
override fun mapFromCursor(cursor: Cursor): Manga {
|
||||||
val manga = super.mapFromCursor(cursor)
|
val manga = super.mapFromCursor(cursor)
|
||||||
|
|
||||||
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
|
val unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD)
|
||||||
manga.unread = cursor.getInt(unreadColumn)
|
manga.unread = cursor.getInt(unreadColumn)
|
||||||
|
|
||||||
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
|
val categoryColumn = cursor.getColumnIndex(MangaTable.COLUMN_CATEGORY)
|
||||||
manga.category = cursor.getInt(categoryColumn)
|
manga.category = cursor.getInt(categoryColumn)
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
|
||||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|
||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|
||||||
|
|
||||||
class MangaFlagsPutResolver : PutResolver<Manga>() {
|
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
|
||||||
val contentValues = mapToContentValues(manga)
|
|
||||||
|
|
||||||
val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
|
|
||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
|
||||||
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.tables;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public class CategoryTable {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String TABLE = "categories";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_ID = "_id";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_NAME = "name";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_ORDER = "sort";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_FLAGS = "flags";
|
||||||
|
|
||||||
|
// This is just class with Meta Data, we don't need instances
|
||||||
|
private CategoryTable() {
|
||||||
|
throw new IllegalStateException("No instances please");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better than static final field -> allows VM to unload useless String
|
||||||
|
// Because you need this string only once per application life on the device
|
||||||
|
@NonNull
|
||||||
|
public static String getCreateTableQuery() {
|
||||||
|
return "CREATE TABLE " + TABLE + "("
|
||||||
|
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||||
|
+ COLUMN_NAME + " TEXT NOT NULL, "
|
||||||
|
+ COLUMN_ORDER + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_FLAGS + " INTEGER NOT NULL"
|
||||||
|
+ ");";
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
|
||||||
|
|
||||||
object CategoryTable {
|
|
||||||
|
|
||||||
const val TABLE = "categories"
|
|
||||||
|
|
||||||
const val COL_ID = "_id"
|
|
||||||
|
|
||||||
const val COL_NAME = "name"
|
|
||||||
|
|
||||||
const val COL_ORDER = "sort"
|
|
||||||
|
|
||||||
const val COL_FLAGS = "flags"
|
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() = """CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_NAME TEXT NOT NULL,
|
|
||||||
$COL_ORDER INTEGER NOT NULL,
|
|
||||||
$COL_FLAGS INTEGER NOT NULL
|
|
||||||
)"""
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,59 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.tables;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public class ChapterTable {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String TABLE = "chapters";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_ID = "_id";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_MANGA_ID = "manga_id";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_URL = "url";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_NAME = "name";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_READ = "read";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_DATE_FETCH = "date_fetch";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_DATE_UPLOAD = "date_upload";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_LAST_PAGE_READ = "last_page_read";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_CHAPTER_NUMBER = "chapter_number";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String getCreateTableQuery() {
|
||||||
|
return "CREATE TABLE " + TABLE + "("
|
||||||
|
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||||
|
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_URL + " TEXT NOT NULL, "
|
||||||
|
+ COLUMN_NAME + " TEXT NOT NULL, "
|
||||||
|
+ COLUMN_READ + " BOOLEAN NOT NULL, "
|
||||||
|
+ COLUMN_LAST_PAGE_READ + " INT NOT NULL, "
|
||||||
|
+ COLUMN_CHAPTER_NUMBER + " FLOAT NOT NULL, "
|
||||||
|
+ COLUMN_DATE_FETCH + " LONG NOT NULL, "
|
||||||
|
+ COLUMN_DATE_UPLOAD + " LONG NOT NULL, "
|
||||||
|
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
|
||||||
|
+ "ON DELETE CASCADE"
|
||||||
|
+ ");";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCreateMangaIdIndexQuery() {
|
||||||
|
return "CREATE INDEX " + TABLE + "_" + COLUMN_MANGA_ID + "_index ON " + TABLE + "(" + COLUMN_MANGA_ID + ");";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,49 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
|
||||||
|
|
||||||
object ChapterTable {
|
|
||||||
|
|
||||||
const val TABLE = "chapters"
|
|
||||||
|
|
||||||
const val COL_ID = "_id"
|
|
||||||
|
|
||||||
const val COL_MANGA_ID = "manga_id"
|
|
||||||
|
|
||||||
const val COL_URL = "url"
|
|
||||||
|
|
||||||
const val COL_NAME = "name"
|
|
||||||
|
|
||||||
const val COL_READ = "read"
|
|
||||||
|
|
||||||
const val COL_DATE_FETCH = "date_fetch"
|
|
||||||
|
|
||||||
const val COL_DATE_UPLOAD = "date_upload"
|
|
||||||
|
|
||||||
const val COL_LAST_PAGE_READ = "last_page_read"
|
|
||||||
|
|
||||||
const val COL_CHAPTER_NUMBER = "chapter_number"
|
|
||||||
|
|
||||||
const val COL_SOURCE_ORDER = "source_order"
|
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() = """CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
|
||||||
$COL_URL TEXT NOT NULL,
|
|
||||||
$COL_NAME TEXT NOT NULL,
|
|
||||||
$COL_READ BOOLEAN NOT NULL,
|
|
||||||
$COL_LAST_PAGE_READ INT NOT NULL,
|
|
||||||
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
|
|
||||||
$COL_SOURCE_ORDER INTEGER NOT NULL,
|
|
||||||
$COL_DATE_FETCH LONG NOT NULL,
|
|
||||||
$COL_DATE_UPLOAD LONG NOT NULL,
|
|
||||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
|
||||||
ON DELETE CASCADE
|
|
||||||
)"""
|
|
||||||
|
|
||||||
val createMangaIdIndexQuery: String
|
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)"
|
|
||||||
|
|
||||||
val sourceOrderUpdateQuery: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.tables;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public class MangaCategoryTable {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String TABLE = "mangas_categories";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_ID = "_id";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_MANGA_ID = "manga_id";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_CATEGORY_ID = "category_id";
|
||||||
|
|
||||||
|
// This is just class with Meta Data, we don't need instances
|
||||||
|
private MangaCategoryTable() {
|
||||||
|
throw new IllegalStateException("No instances please");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better than static final field -> allows VM to unload useless String
|
||||||
|
// Because you need this string only once per application life on the device
|
||||||
|
@NonNull
|
||||||
|
public static String getCreateTableQuery() {
|
||||||
|
return "CREATE TABLE " + TABLE + "("
|
||||||
|
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||||
|
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_CATEGORY_ID + " INTEGER NOT NULL, "
|
||||||
|
+ "FOREIGN KEY(" + COLUMN_CATEGORY_ID + ") REFERENCES " + CategoryTable.TABLE + "(" + CategoryTable.COLUMN_ID + ") "
|
||||||
|
+ "ON DELETE CASCADE, "
|
||||||
|
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
|
||||||
|
+ "ON DELETE CASCADE"
|
||||||
|
+ ");";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
|
||||||
|
|
||||||
object MangaCategoryTable {
|
|
||||||
|
|
||||||
const val TABLE = "mangas_categories"
|
|
||||||
|
|
||||||
const val COL_ID = "_id"
|
|
||||||
|
|
||||||
const val COL_MANGA_ID = "manga_id"
|
|
||||||
|
|
||||||
const val COL_CATEGORY_ID = "category_id"
|
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() = """CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
|
||||||
$COL_CATEGORY_ID INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY($COL_CATEGORY_ID) REFERENCES ${CategoryTable.TABLE} (${CategoryTable.COL_ID})
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
|
||||||
ON DELETE CASCADE
|
|
||||||
)"""
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,45 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.tables;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public class MangaSyncTable {
|
||||||
|
|
||||||
|
public static final String TABLE = "manga_sync";
|
||||||
|
|
||||||
|
public static final String COLUMN_ID = "_id";
|
||||||
|
|
||||||
|
public static final String COLUMN_MANGA_ID = "manga_id";
|
||||||
|
|
||||||
|
public static final String COLUMN_SYNC_ID = "sync_id";
|
||||||
|
|
||||||
|
public static final String COLUMN_REMOTE_ID = "remote_id";
|
||||||
|
|
||||||
|
public static final String COLUMN_TITLE = "title";
|
||||||
|
|
||||||
|
public static final String COLUMN_LAST_CHAPTER_READ = "last_chapter_read";
|
||||||
|
|
||||||
|
public static final String COLUMN_STATUS = "status";
|
||||||
|
|
||||||
|
public static final String COLUMN_SCORE = "score";
|
||||||
|
|
||||||
|
public static final String COLUMN_TOTAL_CHAPTERS = "total_chapters";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String getCreateTableQuery() {
|
||||||
|
return "CREATE TABLE " + TABLE + "("
|
||||||
|
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||||
|
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_SYNC_ID + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_REMOTE_ID + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_TITLE + " TEXT NOT NULL, "
|
||||||
|
+ COLUMN_LAST_CHAPTER_READ + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_TOTAL_CHAPTERS + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_STATUS + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_SCORE + " FLOAT NOT NULL, "
|
||||||
|
+ "UNIQUE (" + COLUMN_MANGA_ID + ", " + COLUMN_SYNC_ID + ") ON CONFLICT REPLACE, "
|
||||||
|
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
|
||||||
|
+ "ON DELETE CASCADE"
|
||||||
|
+ ");";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
|
||||||
|
|
||||||
object MangaSyncTable {
|
|
||||||
|
|
||||||
const val TABLE = "manga_sync"
|
|
||||||
|
|
||||||
const val COL_ID = "_id"
|
|
||||||
|
|
||||||
const val COL_MANGA_ID = "manga_id"
|
|
||||||
|
|
||||||
const val COL_SYNC_ID = "sync_id"
|
|
||||||
|
|
||||||
const val COL_REMOTE_ID = "remote_id"
|
|
||||||
|
|
||||||
const val COL_TITLE = "title"
|
|
||||||
|
|
||||||
const val COL_LAST_CHAPTER_READ = "last_chapter_read"
|
|
||||||
|
|
||||||
const val COL_STATUS = "status"
|
|
||||||
|
|
||||||
const val COL_SCORE = "score"
|
|
||||||
|
|
||||||
const val COL_TOTAL_CHAPTERS = "total_chapters"
|
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() = """CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
|
||||||
$COL_SYNC_ID INTEGER NOT NULL,
|
|
||||||
$COL_REMOTE_ID INTEGER NOT NULL,
|
|
||||||
$COL_TITLE TEXT NOT NULL,
|
|
||||||
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
|
||||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
|
||||||
$COL_STATUS INTEGER NOT NULL,
|
|
||||||
$COL_SCORE FLOAT NOT NULL,
|
|
||||||
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
|
|
||||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
|
||||||
ON DELETE CASCADE
|
|
||||||
)"""
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,98 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.tables;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public class MangaTable {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String TABLE = "mangas";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_ID = "_id";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_SOURCE = "source";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_URL = "url";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_ARTIST = "artist";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_AUTHOR = "author" ;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_DESCRIPTION = "description";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_GENRE = "genre";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_TITLE = "title";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_STATUS = "status";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_THUMBNAIL_URL = "thumbnail_url";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_FAVORITE = "favorite";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_LAST_UPDATE = "last_update";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_INITIALIZED = "initialized";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_VIEWER = "viewer";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_CHAPTER_FLAGS = "chapter_flags";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_UNREAD = "unread";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static final String COLUMN_CATEGORY = "category";
|
||||||
|
|
||||||
|
// This is just class with Meta Data, we don't need instances
|
||||||
|
private MangaTable() {
|
||||||
|
throw new IllegalStateException("No instances please");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better than static final field -> allows VM to unload useless String
|
||||||
|
// Because you need this string only once per application life on the device
|
||||||
|
@NonNull
|
||||||
|
public static String getCreateTableQuery() {
|
||||||
|
return "CREATE TABLE " + TABLE + "("
|
||||||
|
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
|
||||||
|
+ COLUMN_SOURCE + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_URL + " TEXT NOT NULL, "
|
||||||
|
+ COLUMN_ARTIST + " TEXT, "
|
||||||
|
+ COLUMN_AUTHOR + " TEXT, "
|
||||||
|
+ COLUMN_DESCRIPTION + " TEXT, "
|
||||||
|
+ COLUMN_GENRE + " TEXT, "
|
||||||
|
+ COLUMN_TITLE + " TEXT NOT NULL, "
|
||||||
|
+ COLUMN_STATUS + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_THUMBNAIL_URL + " TEXT, "
|
||||||
|
+ COLUMN_FAVORITE + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_LAST_UPDATE + " LONG, "
|
||||||
|
+ COLUMN_INITIALIZED + " BOOLEAN NOT NULL, "
|
||||||
|
+ COLUMN_VIEWER + " INTEGER NOT NULL, "
|
||||||
|
+ COLUMN_CHAPTER_FLAGS + " INTEGER NOT NULL"
|
||||||
|
+ ");";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCreateUrlIndexQuery() {
|
||||||
|
return "CREATE INDEX " + TABLE + "_" + COLUMN_URL + "_index ON " + TABLE + "(" + COLUMN_URL + ");";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCreateFavoriteIndexQuery() {
|
||||||
|
return "CREATE INDEX " + TABLE + "_" + COLUMN_FAVORITE + "_index ON " + TABLE + "(" + COLUMN_FAVORITE + ");";
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,65 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.tables
|
|
||||||
|
|
||||||
object MangaTable {
|
|
||||||
|
|
||||||
const val TABLE = "mangas"
|
|
||||||
|
|
||||||
const val COL_ID = "_id"
|
|
||||||
|
|
||||||
const val COL_SOURCE = "source"
|
|
||||||
|
|
||||||
const val COL_URL = "url"
|
|
||||||
|
|
||||||
const val COL_ARTIST = "artist"
|
|
||||||
|
|
||||||
const val COL_AUTHOR = "author"
|
|
||||||
|
|
||||||
const val COL_DESCRIPTION = "description"
|
|
||||||
|
|
||||||
const val COL_GENRE = "genre"
|
|
||||||
|
|
||||||
const val COL_TITLE = "title"
|
|
||||||
|
|
||||||
const val COL_STATUS = "status"
|
|
||||||
|
|
||||||
const val COL_THUMBNAIL_URL = "thumbnail_url"
|
|
||||||
|
|
||||||
const val COL_FAVORITE = "favorite"
|
|
||||||
|
|
||||||
const val COL_LAST_UPDATE = "last_update"
|
|
||||||
|
|
||||||
const val COL_INITIALIZED = "initialized"
|
|
||||||
|
|
||||||
const val COL_VIEWER = "viewer"
|
|
||||||
|
|
||||||
const val COL_CHAPTER_FLAGS = "chapter_flags"
|
|
||||||
|
|
||||||
const val COL_UNREAD = "unread"
|
|
||||||
|
|
||||||
const val COL_CATEGORY = "category"
|
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() = """CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_SOURCE INTEGER NOT NULL,
|
|
||||||
$COL_URL TEXT NOT NULL,
|
|
||||||
$COL_ARTIST TEXT,
|
|
||||||
$COL_AUTHOR TEXT,
|
|
||||||
$COL_DESCRIPTION TEXT,
|
|
||||||
$COL_GENRE TEXT,
|
|
||||||
$COL_TITLE TEXT NOT NULL,
|
|
||||||
$COL_STATUS INTEGER NOT NULL,
|
|
||||||
$COL_THUMBNAIL_URL TEXT,
|
|
||||||
$COL_FAVORITE INTEGER NOT NULL,
|
|
||||||
$COL_LAST_UPDATE LONG,
|
|
||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
|
||||||
$COL_VIEWER INTEGER NOT NULL,
|
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL
|
|
||||||
)"""
|
|
||||||
|
|
||||||
val createUrlIndexQuery: String
|
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_URL}_index ON $TABLE($COL_URL)"
|
|
||||||
|
|
||||||
val createFavoriteIndexQuery: String
|
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE)"
|
|
||||||
}
|
|
@ -5,21 +5,17 @@ import android.net.Uri
|
|||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
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.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
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.preference.getOrDefault
|
||||||
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.base.Source
|
||||||
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.event.DownloadChaptersEvent
|
||||||
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
|
||||||
@ -40,8 +36,6 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
val runningSubject = BehaviorSubject.create<Boolean>()
|
val runningSubject = BehaviorSubject.create<Boolean>()
|
||||||
private var downloadsSubscription: Subscription? = null
|
private var downloadsSubscription: Subscription? = null
|
||||||
|
|
||||||
val downloadNotifier by lazy { DownloadNotifier(context) }
|
|
||||||
|
|
||||||
private val threadsSubject = BehaviorSubject.create<Int>()
|
private val threadsSubject = BehaviorSubject.create<Int>()
|
||||||
private var threadsSubscription: Subscription? = null
|
private var threadsSubscription: Subscription? = null
|
||||||
|
|
||||||
@ -51,37 +45,27 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
|
|
||||||
val PAGE_LIST_FILE = "index.json"
|
val PAGE_LIST_FILE = "index.json"
|
||||||
|
|
||||||
@Volatile var isRunning: Boolean = false
|
@Volatile private var isRunning: Boolean = false
|
||||||
private set
|
|
||||||
|
|
||||||
private fun initializeSubscriptions() {
|
private fun initializeSubscriptions() {
|
||||||
|
|
||||||
downloadsSubscription?.unsubscribe()
|
downloadsSubscription?.unsubscribe()
|
||||||
|
|
||||||
threadsSubscription = preferences.downloadThreads().asObservable()
|
threadsSubscription = preferences.downloadThreads().asObservable()
|
||||||
.subscribe {
|
.subscribe { threadsSubject.onNext(it) }
|
||||||
threadsSubject.onNext(it)
|
|
||||||
downloadNotifier.multipleDownloadThreads = it > 1
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) }
|
downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) }
|
||||||
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
|
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
|
||||||
.onBackpressureBuffer()
|
.onBackpressureBuffer()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({
|
.map { download -> areAllDownloadsFinished() }
|
||||||
// Delete successful downloads from queue
|
.subscribe({ finished ->
|
||||||
if (it.status == Download.DOWNLOADED) {
|
if (finished!!) {
|
||||||
// remove downloaded chapter from queue
|
|
||||||
queue.del(it)
|
|
||||||
downloadNotifier.onProgressChange(queue)
|
|
||||||
}
|
|
||||||
if (areAllDownloadsFinished()) {
|
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
}
|
}
|
||||||
}, { e ->
|
}, { e ->
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
Timber.e(e, e.message)
|
Timber.e(e, e.message)
|
||||||
downloadNotifier.onError(e.message)
|
context.toast(e.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
@ -107,18 +91,16 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a download object for every chapter and add them to the downloads queue
|
// Create a download object for every chapter in the event and add them to the downloads queue
|
||||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
fun onDownloadChaptersEvent(event: DownloadChaptersEvent) {
|
||||||
val source = sourceManager.get(manga.source) as? OnlineSource ?: return
|
val manga = event.manga
|
||||||
|
val source = sourceManager.get(manga.source)
|
||||||
// Add chapters to queue from the start
|
|
||||||
val sortedChapters = chapters.sortedByDescending { it.source_order }
|
|
||||||
|
|
||||||
// Used to avoid downloading chapters with the same name
|
// Used to avoid downloading chapters with the same name
|
||||||
val addedChapters = ArrayList<String>()
|
val addedChapters = ArrayList<String>()
|
||||||
val pending = ArrayList<Download>()
|
val pending = ArrayList<Download>()
|
||||||
|
|
||||||
for (chapter in sortedChapters) {
|
for (chapter in event.chapters) {
|
||||||
if (addedChapters.contains(chapter.name))
|
if (addedChapters.contains(chapter.name))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -130,12 +112,6 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
pending.add(download)
|
pending.add(download)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize queue size
|
|
||||||
downloadNotifier.initialQueueSize = queue.size
|
|
||||||
// Show notification
|
|
||||||
downloadNotifier.onProgressChange(queue)
|
|
||||||
|
|
||||||
if (isRunning) downloadsQueueSubject.onNext(pending)
|
if (isRunning) downloadsQueueSubject.onNext(pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +163,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
|
|
||||||
val pageListObservable = if (download.pages == null)
|
val pageListObservable = 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.pullPageListFromNetwork(download.chapter.url)
|
||||||
.doOnNext { pages ->
|
.doOnNext { pages ->
|
||||||
download.pages = pages
|
download.pages = pages
|
||||||
savePageList(download)
|
savePageList(download)
|
||||||
@ -196,20 +172,15 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
// Or if the page list already exists, start from the file
|
// Or if the page list already exists, start from the file
|
||||||
Observable.just(download.pages)
|
Observable.just(download.pages)
|
||||||
|
|
||||||
return Observable.defer {
|
return Observable.defer { pageListObservable
|
||||||
pageListObservable
|
|
||||||
.doOnNext { pages ->
|
.doOnNext { pages ->
|
||||||
download.downloadedImages = 0
|
download.downloadedImages = 0
|
||||||
download.status = Download.DOWNLOADING
|
download.status = Download.DOWNLOADING
|
||||||
}
|
}
|
||||||
// Get all the URLs to the source images, fetch pages if necessary
|
// Get all the URLs to the source images, fetch pages if necessary
|
||||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
.flatMap { download.source.getAllImageUrlsFromPageList(it) }
|
||||||
// Start downloading images, consider we can have downloaded images already
|
// Start downloading images, consider we can have downloaded images already
|
||||||
.concatMap { page -> getOrDownloadImage(page, download) }
|
.concatMap { page -> getOrDownloadImage(page, download) }
|
||||||
// Do when page is downloaded.
|
|
||||||
.doOnNext {
|
|
||||||
downloadNotifier.onProgressChange(download, queue)
|
|
||||||
}
|
|
||||||
// Do after download completes
|
// Do after download completes
|
||||||
.doOnCompleted { onDownloadCompleted(download) }
|
.doOnCompleted { onDownloadCompleted(download) }
|
||||||
.toList()
|
.toList()
|
||||||
@ -217,7 +188,6 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
// If the page list threw, it will resume here
|
// If the page list threw, it will resume here
|
||||||
.onErrorResumeNext { error ->
|
.onErrorResumeNext { error ->
|
||||||
download.status = Download.ERROR
|
download.status = Download.ERROR
|
||||||
downloadNotifier.onError(error.message, download.chapter.name)
|
|
||||||
Observable.just(download)
|
Observable.just(download)
|
||||||
}
|
}
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
@ -255,9 +225,9 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save image on disk
|
// Save image on disk
|
||||||
private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> {
|
private fun downloadImage(page: Page, source: Source, directory: File, filename: String): Observable<Page> {
|
||||||
page.status = Page.DOWNLOAD_IMAGE
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
return source.imageResponse(page)
|
return source.getImageProgressResponse(page)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
try {
|
try {
|
||||||
val file = File(directory, filename)
|
val file = File(directory, filename)
|
||||||
@ -325,18 +295,18 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
// 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
|
|
||||||
downloadNotifier.onError(context.getString(R.string.download_notifier_page_ready_error), download.chapter.name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Ensure that the chapter folder has all the images
|
// Ensure that the chapter folder has all the images
|
||||||
if (!isChapterDownloaded(download.directory, download.pages)) {
|
if (!isChapterDownloaded(download.directory, download.pages)) {
|
||||||
status = Download.ERROR
|
status = Download.ERROR
|
||||||
downloadNotifier.onError(context.getString(R.string.download_notifier_page_error), download.chapter.name)
|
|
||||||
}
|
}
|
||||||
download.totalProgress = actualProgress
|
download.totalProgress = actualProgress
|
||||||
download.status = status
|
download.status = status
|
||||||
|
// Delete successful downloads from queue after notifying
|
||||||
|
if (status == Download.DOWNLOADED) {
|
||||||
|
queue.del(download)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the page list from the chapter's directory if it exists, null otherwise
|
// Return the page list from the chapter's directory if it exists, null otherwise
|
||||||
@ -380,7 +350,7 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
|
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
|
||||||
val mangaRelativePath = source.toString() +
|
val mangaRelativePath = source.visibleName +
|
||||||
File.separator +
|
File.separator +
|
||||||
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
|
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
|
||||||
|
|
||||||
@ -431,19 +401,13 @@ class DownloadManager(private val context: Context, private val sourceManager: S
|
|||||||
return !pending.isEmpty()
|
return !pending.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopDownloads(errorMessage: String? = null) {
|
fun stopDownloads() {
|
||||||
destroySubscriptions()
|
destroySubscriptions()
|
||||||
for (download in queue) {
|
for (download in queue) {
|
||||||
if (download.status == Download.DOWNLOADING) {
|
if (download.status == Download.DOWNLOADING) {
|
||||||
download.status = Download.ERROR
|
download.status = Download.ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
errorMessage?.let { downloadNotifier.onError(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearQueue() {
|
|
||||||
queue.clear()
|
|
||||||
downloadNotifier.onClear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,166 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.support.v4.app.NotificationCompat
|
|
||||||
import eu.kanade.tachiyomi.Constants
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
|
||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
|
|
||||||
*
|
|
||||||
* @param context context of application
|
|
||||||
*/
|
|
||||||
class DownloadNotifier(private val context: Context) {
|
|
||||||
/**
|
|
||||||
* Notification builder.
|
|
||||||
*/
|
|
||||||
private val notificationBuilder = NotificationCompat.Builder(context)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Id of the notification.
|
|
||||||
*/
|
|
||||||
private val notificationId: Int
|
|
||||||
get() = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status of download. Used for correct notification icon.
|
|
||||||
*/
|
|
||||||
private var isDownloading = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The size of queue on start download.
|
|
||||||
*/
|
|
||||||
internal var initialQueueSize = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simultaneous download setting > 1.
|
|
||||||
*/
|
|
||||||
internal var multipleDownloadThreads = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when download progress changes.
|
|
||||||
* Note: Only accepted when multi download active.
|
|
||||||
*
|
|
||||||
* @param queue the queue containing downloads.
|
|
||||||
*/
|
|
||||||
internal fun onProgressChange(queue: DownloadQueue) {
|
|
||||||
if (multipleDownloadThreads) {
|
|
||||||
doOnProgressChange(null, queue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when download progress changes
|
|
||||||
* Note: Only accepted when single download active
|
|
||||||
*
|
|
||||||
* @param download download object containing download information
|
|
||||||
* @param queue the queue containing downloads
|
|
||||||
*/
|
|
||||||
internal fun onProgressChange(download: Download, queue: DownloadQueue) {
|
|
||||||
if (!multipleDownloadThreads) {
|
|
||||||
doOnProgressChange(download, queue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show notification progress of chapter
|
|
||||||
*
|
|
||||||
* @param download download object containing download information
|
|
||||||
* @param queue the queue containing downloads
|
|
||||||
*/
|
|
||||||
private fun doOnProgressChange(download: Download?, queue: DownloadQueue) {
|
|
||||||
// Check if download is completed
|
|
||||||
if (multipleDownloadThreads) {
|
|
||||||
if (queue.isEmpty()) {
|
|
||||||
onComplete(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (download != null && download.pages.size == download.downloadedImages) {
|
|
||||||
onComplete(download)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create notification
|
|
||||||
with (notificationBuilder) {
|
|
||||||
// Check if icon needs refresh
|
|
||||||
if (!isDownloading) {
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
|
||||||
isDownloading = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (multipleDownloadThreads) {
|
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
|
||||||
|
|
||||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
|
||||||
.format(initialQueueSize - queue.size, initialQueueSize))
|
|
||||||
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
|
|
||||||
} else {
|
|
||||||
download?.let {
|
|
||||||
if (it.chapter.name.length >= 33)
|
|
||||||
setContentTitle(it.chapter.name.slice(IntRange(0, 30)).plus("..."))
|
|
||||||
else
|
|
||||||
setContentTitle(it.chapter.name)
|
|
||||||
|
|
||||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
|
||||||
.format(it.downloadedImages, it.pages.size))
|
|
||||||
setProgress(it.pages.size, it.downloadedImages, false)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Displays the progress bar on notification
|
|
||||||
context.notificationManager.notify(notificationId, notificationBuilder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when chapter is downloaded
|
|
||||||
*
|
|
||||||
* @param download download object containing download information
|
|
||||||
*/
|
|
||||||
private fun onComplete(download: Download?) {
|
|
||||||
// Create notification.
|
|
||||||
with(notificationBuilder) {
|
|
||||||
setContentTitle(download?.chapter?.name ?: context.getString(R.string.app_name))
|
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
setProgress(0, 0, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show notification.
|
|
||||||
context.notificationManager.notify(notificationId, notificationBuilder.build())
|
|
||||||
|
|
||||||
// Reset initial values
|
|
||||||
isDownloading = false
|
|
||||||
initialQueueSize = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the notification message
|
|
||||||
*/
|
|
||||||
internal fun onClear() {
|
|
||||||
context.notificationManager.cancel(notificationId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called on error while downloading chapter
|
|
||||||
*
|
|
||||||
* @param error string containing error information
|
|
||||||
* @param chapter string containing chapter title
|
|
||||||
*/
|
|
||||||
internal fun onError(error: String? = null, chapter: String? = null) {
|
|
||||||
// Create notification
|
|
||||||
with(notificationBuilder) {
|
|
||||||
setContentTitle(chapter ?: context.getString(R.string.download_notifier_title_error))
|
|
||||||
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
|
||||||
setProgress(0, 0, false)
|
|
||||||
}
|
|
||||||
context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
|
|
||||||
isDownloading = false
|
|
||||||
}
|
|
||||||
}
|
|
@ -82,12 +82,12 @@ class DownloadService : Service() {
|
|||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
} else if (isRunning) {
|
} else if (isRunning) {
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
downloadManager.stopDownloads()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
downloadManager.stopDownloads()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,12 @@ import java.util.List;
|
|||||||
|
|
||||||
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.base.Source;
|
||||||
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 rx.subjects.PublishSubject;
|
import rx.subjects.PublishSubject;
|
||||||
|
|
||||||
public class Download {
|
public class Download {
|
||||||
public OnlineSource source;
|
public Source source;
|
||||||
public Manga manga;
|
public Manga manga;
|
||||||
public Chapter chapter;
|
public Chapter chapter;
|
||||||
public List<Page> pages;
|
public List<Page> pages;
|
||||||
@ -29,7 +29,7 @@ public class Download {
|
|||||||
public static final int ERROR = 4;
|
public static final int ERROR = 4;
|
||||||
|
|
||||||
|
|
||||||
public Download(OnlineSource source, Manga manga, Chapter chapter) {
|
public Download(Source source, Manga manga, Chapter chapter) {
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.manga = manga;
|
this.manga = manga;
|
||||||
this.chapter = chapter;
|
this.chapter = chapter;
|
||||||
|
@ -22,7 +22,12 @@ class DownloadQueue : CopyOnWriteArrayList<Download>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun del(chapter: Chapter) {
|
fun del(chapter: Chapter) {
|
||||||
find { it.chapter.id == chapter.id }?.let { del(it) }
|
for (download in this) {
|
||||||
|
if (download.chapter.id == chapter.id) {
|
||||||
|
del(download)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActiveDownloads() =
|
fun getActiveDownloads() =
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.GlideBuilder
|
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
|
||||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
|
||||||
import com.bumptech.glide.module.GlideModule
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
|
||||||
import java.io.InputStream
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to update Glide module settings
|
|
||||||
*/
|
|
||||||
class AppGlideModule : GlideModule {
|
|
||||||
|
|
||||||
@Inject lateinit var networkHelper: NetworkHelper
|
|
||||||
|
|
||||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
|
||||||
// Set the cache size of Glide to 15 MiB
|
|
||||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerComponents(context: Context, glide: Glide) {
|
|
||||||
App.get(context).component.inject(this)
|
|
||||||
glide.register(GlideUrl::class.java, InputStream::class.java,
|
|
||||||
OkHttpUrlLoader.Factory(networkHelper.client))
|
|
||||||
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import com.bumptech.glide.Priority
|
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [DataFetcher] for loading a cover of a manga depending on its favorite status.
|
|
||||||
* If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it
|
|
||||||
* fallbacks to network and copies it to the cache.
|
|
||||||
* If the manga is not favorite, it tries to delete the cover from our cache and always fallback
|
|
||||||
* to network for fetching.
|
|
||||||
*
|
|
||||||
* @param networkFetcher the network fetcher for this cover.
|
|
||||||
* @param file the file where this cover should be. It may exists or not.
|
|
||||||
* @param manga the manga of the cover to load.
|
|
||||||
*/
|
|
||||||
class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
|
||||||
private val file: File,
|
|
||||||
private val manga: Manga)
|
|
||||||
: DataFetcher<InputStream> {
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
override fun loadData(priority: Priority): InputStream? {
|
|
||||||
if (manga.favorite) {
|
|
||||||
if (!file.exists()) {
|
|
||||||
file.parentFile.mkdirs()
|
|
||||||
networkFetcher.loadData(priority)?.let {
|
|
||||||
it.use { input ->
|
|
||||||
file.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FileInputStream(file)
|
|
||||||
} else {
|
|
||||||
if (file.exists()) {
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
return networkFetcher.loadData(priority)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the id for this manga's cover.
|
|
||||||
*
|
|
||||||
* Appending the file's modified date to the url, we can force Glide to skip its memory and disk
|
|
||||||
* lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when
|
|
||||||
* the file has changed. If the file doesn't exist it will append a 0.
|
|
||||||
*/
|
|
||||||
override fun getId(): String {
|
|
||||||
return manga.thumbnail_url + file.lastModified()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
networkFetcher.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cleanup() {
|
|
||||||
networkFetcher.cleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,119 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
|
||||||
import com.bumptech.glide.load.model.*
|
|
||||||
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.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
|
||||||
import java.io.File
|
|
||||||
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.
|
|
||||||
* Coupled with [MangaDataFetcher], this class allows to implement the following flow:
|
|
||||||
*
|
|
||||||
* - Check in RAM LRU.
|
|
||||||
* - Check in disk LRU.
|
|
||||||
* - Check in this module.
|
|
||||||
* - Fetch from the network connection.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
|
||||||
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cover cache where persistent covers are stored.
|
|
||||||
*/
|
|
||||||
@Inject lateinit var coverCache: CoverCache
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
@Inject lateinit var sourceManager: SourceManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base network loader.
|
|
||||||
*/
|
|
||||||
private val baseLoader = Glide.buildModelLoader(GlideUrl::class.java,
|
|
||||||
InputStream::class.java, context)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
private val modelCache = ModelCache<String, Pair<GlideUrl, File>>(100)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map where request headers are stored for a source.
|
|
||||||
*/
|
|
||||||
private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
App.get(context).component.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory class for creating [MangaModelLoader] instances.
|
|
||||||
*/
|
|
||||||
class Factory : ModelLoaderFactory<Manga, InputStream> {
|
|
||||||
|
|
||||||
override fun build(context: Context, factories: GenericLoaderFactory)
|
|
||||||
= MangaModelLoader(context)
|
|
||||||
|
|
||||||
override fun teardown() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a [MangaDataFetcher] for the given manga or null if the url is empty.
|
|
||||||
*
|
|
||||||
* @param manga the model.
|
|
||||||
* @param width the width of the view where the resource will be loaded.
|
|
||||||
* @param height the height of the view where the resource will be loaded.
|
|
||||||
*/
|
|
||||||
override fun getResourceFetcher(manga: Manga,
|
|
||||||
width: Int,
|
|
||||||
height: Int): DataFetcher<InputStream>? {
|
|
||||||
// Check thumbnail is not null or empty
|
|
||||||
val url = manga.thumbnail_url
|
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtain the request url and the file for this url from the LRU cache, or calculate it
|
|
||||||
// and add them to the cache.
|
|
||||||
val (glideUrl, file) = modelCache.get(url, width, height) ?:
|
|
||||||
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url)).apply {
|
|
||||||
modelCache.put(url, width, height, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the network fetcher for this request url.
|
|
||||||
val networkFetcher = baseLoader.getResourceFetcher(glideUrl, width, height)
|
|
||||||
|
|
||||||
// Return an instance of our fetcher providing the needed elements.
|
|
||||||
return MangaDataFetcher(networkFetcher, file, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request headers for a source copying its OkHttp headers and caching them.
|
|
||||||
*
|
|
||||||
* @param manga the model.
|
|
||||||
*/
|
|
||||||
fun getHeaders(manga: Manga): Headers {
|
|
||||||
val source = sourceManager.get(manga.source) as? OnlineSource ?: return LazyHeaders.DEFAULT
|
|
||||||
return cachedHeaders.getOrPut(manga.source) {
|
|
||||||
LazyHeaders.Builder().apply {
|
|
||||||
setHeader("User-Agent", null as? String)
|
|
||||||
for ((key, value) in source.headers.toMultimap()) {
|
|
||||||
addHeader(key, value[0])
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -8,17 +8,15 @@ import android.content.Intent
|
|||||||
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 android.util.Pair
|
||||||
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.App
|
||||||
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.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
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.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.*
|
import eu.kanade.tachiyomi.util.*
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -38,52 +36,38 @@ import javax.inject.Inject
|
|||||||
*/
|
*/
|
||||||
class LibraryUpdateService : Service() {
|
class LibraryUpdateService : Service() {
|
||||||
|
|
||||||
/**
|
// Dependencies injected through dagger.
|
||||||
* Database helper.
|
|
||||||
*/
|
|
||||||
@Inject lateinit var db: DatabaseHelper
|
@Inject lateinit var db: DatabaseHelper
|
||||||
|
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
@Inject lateinit var sourceManager: SourceManager
|
@Inject lateinit var sourceManager: SourceManager
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences.
|
|
||||||
*/
|
|
||||||
@Inject lateinit var preferences: PreferencesHelper
|
@Inject lateinit var preferences: PreferencesHelper
|
||||||
|
|
||||||
/**
|
// Wake lock that will be held until the service is destroyed.
|
||||||
* Wake lock that will be held until the service is destroyed.
|
|
||||||
*/
|
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
/**
|
// Subscription where the update is done.
|
||||||
* Subscription where the update is done.
|
|
||||||
*/
|
|
||||||
private var subscription: Subscription? = null
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Id of the library update notification.
|
|
||||||
*/
|
|
||||||
private val notificationId: Int
|
|
||||||
get() = Constants.NOTIFICATION_LIBRARY_ID
|
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
val UPDATE_NOTIFICATION_ID = 1
|
||||||
* Key for manual library update.
|
|
||||||
*/
|
// Intent key for manual library update
|
||||||
const val UPDATE_IS_MANUAL = "is_manual"
|
val UPDATE_IS_MANUAL = "is_manual"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for category to update.
|
* Get the start intent for [LibraryUpdateService].
|
||||||
|
* @param context the application context.
|
||||||
|
* @param isManual true when user triggers library update.
|
||||||
|
* @return the intent of the service.
|
||||||
*/
|
*/
|
||||||
const val UPDATE_CATEGORY = "category"
|
fun getIntent(context: Context, isManual: Boolean = false): Intent {
|
||||||
|
return Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
|
putExtra(UPDATE_IS_MANUAL, isManual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the status of the service.
|
* Returns the status of the service.
|
||||||
*
|
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @return true if the service is running, false otherwise.
|
* @return true if the service is running, false otherwise.
|
||||||
*/
|
*/
|
||||||
@ -92,30 +76,19 @@ class LibraryUpdateService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the service. It will be started only if there isn't another instance already
|
* Static method to start the service. It will be started only if there isn't another
|
||||||
* running.
|
* instance already 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 all in the library.
|
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, isManual: Boolean = false, category: Category? = null) {
|
@JvmStatic
|
||||||
|
fun start(context: Context, isForced: Boolean = false) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
context.startService(getIntent(context, isForced))
|
||||||
putExtra(UPDATE_IS_MANUAL, isManual)
|
|
||||||
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
|
|
||||||
}
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the service.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
|
||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
context.stopService(Intent(context, LibraryUpdateService::class.java))
|
context.stopService(getIntent(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -131,7 +104,7 @@ class LibraryUpdateService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service is destroyed. It destroys the running subscription, resets
|
* Method called when the service is destroyed. It destroy the running subscription, resets
|
||||||
* the alarm and release the wake lock.
|
* the alarm and release the wake lock.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@ -148,9 +121,9 @@ class LibraryUpdateService : Service() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method called when the service receives an intent.
|
* Method called when the service receives an intent.
|
||||||
*
|
|
||||||
* @param intent the start intent from.
|
* @param intent the start intent from.
|
||||||
* @param flags the flags of the command.
|
* @param flags the flags of the command.
|
||||||
* @param startId the start id of this command.
|
* @param startId the start id of this command.
|
||||||
@ -172,7 +145,7 @@ class LibraryUpdateService : Service() {
|
|||||||
|
|
||||||
// Check if device has internet connection
|
// Check if device has internet connection
|
||||||
// Check if device has wifi connection if only wifi is enabled
|
// Check if device has wifi connection if only wifi is enabled
|
||||||
if (connection == ConnectivityStatus.OFFLINE || (!isManualUpdate && "wifi" in restrictions
|
if (connection == ConnectivityStatus.OFFLINE || ("wifi" in restrictions
|
||||||
&& connection != ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET)) {
|
&& connection != ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET)) {
|
||||||
|
|
||||||
if (isManualUpdate) {
|
if (isManualUpdate) {
|
||||||
@ -201,7 +174,7 @@ class LibraryUpdateService : Service() {
|
|||||||
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 { updateLibrary() }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe({},
|
.subscribe({},
|
||||||
{
|
{
|
||||||
@ -215,36 +188,13 @@ class LibraryUpdateService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of manga to be updated.
|
* Method that updates the library. It's called in a background thread, so it's safe to do
|
||||||
*
|
* heavy operations or network calls here.
|
||||||
* @param intent the update intent.
|
|
||||||
* @return a list of manga to update
|
|
||||||
*/
|
|
||||||
fun getMangaToUpdate(intent: Intent?): List<Manga> {
|
|
||||||
val categoryId = intent?.getIntExtra(UPDATE_CATEGORY, -1) ?: -1
|
|
||||||
|
|
||||||
var toUpdate = if (categoryId != -1)
|
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
|
||||||
else
|
|
||||||
db.getFavoriteMangas().executeAsBlocking()
|
|
||||||
|
|
||||||
if (preferences.updateOnlyNonCompleted()) {
|
|
||||||
toUpdate = toUpdate.filter { it.status != Manga.COMPLETED }
|
|
||||||
}
|
|
||||||
|
|
||||||
return toUpdate
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method that updates 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
|
* For each manga it calls [updateManga] and updates the notification showing the current
|
||||||
* progress.
|
* progress.
|
||||||
*
|
|
||||||
* @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 updateLibrary(): 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>()
|
||||||
@ -253,10 +203,17 @@ class LibraryUpdateService : Service() {
|
|||||||
val cancelIntent = PendingIntent.getBroadcast(this, 0,
|
val cancelIntent = PendingIntent.getBroadcast(this, 0,
|
||||||
Intent(this, CancelUpdateReceiver::class.java), 0)
|
Intent(this, CancelUpdateReceiver::class.java), 0)
|
||||||
|
|
||||||
|
// Get the manga list that is going to be updated.
|
||||||
|
val allLibraryMangas = db.getFavoriteMangas().executeAsBlocking()
|
||||||
|
val toUpdate = if (!preferences.updateOnlyNonCompleted())
|
||||||
|
allLibraryMangas
|
||||||
|
else
|
||||||
|
allLibraryMangas.filter { it.status != Manga.COMPLETED }
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(toUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, toUpdate.size, cancelIntent) }
|
||||||
// Update the chapters of the manga.
|
// Update the chapters of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
updateManga(manga)
|
updateManga(manga)
|
||||||
@ -284,19 +241,18 @@ class LibraryUpdateService : Service() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the chapters for the given manga and adds them to the database.
|
* Updates the chapters for the given manga and adds them to the database.
|
||||||
*
|
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
|
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
|
||||||
val source = sourceManager.get(manga.source) as? OnlineSource ?: return Observable.empty()
|
val source = sourceManager.get(manga.source)
|
||||||
return source.fetchChapterList(manga)
|
return source!!
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.pullChaptersFromNetwork(manga.url)
|
||||||
|
.flatMap { db.insertOrRemoveChapters(manga, it, source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*
|
|
||||||
* @param updates a list of manga that contains new chapters.
|
* @param updates a list of manga that contains new chapters.
|
||||||
* @param failedUpdates a list of manga that failed to update.
|
* @param failedUpdates a list of manga that failed to update.
|
||||||
* @return the body of the notification to display.
|
* @return the body of the notification to display.
|
||||||
@ -345,12 +301,11 @@ class LibraryUpdateService : Service() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification with the given title and body.
|
* Shows the notification with the given title and body.
|
||||||
*
|
|
||||||
* @param title the title of the notification.
|
* @param title the title of the notification.
|
||||||
* @param body the body of the notification.
|
* @param body the body of the notification.
|
||||||
*/
|
*/
|
||||||
private fun showNotification(title: String, body: String) {
|
private fun showNotification(title: String, body: String) {
|
||||||
notificationManager.notify(notificationId, notification() {
|
notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
setContentTitle(title)
|
setContentTitle(title)
|
||||||
setContentText(body)
|
setContentText(body)
|
||||||
@ -359,13 +314,12 @@ class LibraryUpdateService : Service() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification containing the currently updating manga and the progress.
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
*
|
|
||||||
* @param manga the manga that's being updated.
|
* @param manga the manga that's being updated.
|
||||||
* @param current the current progress.
|
* @param current the current progress.
|
||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
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(UPDATE_NOTIFICATION_ID, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
setContentTitle(manga.title)
|
setContentTitle(manga.title)
|
||||||
setProgress(total, current, false)
|
setProgress(total, current, false)
|
||||||
@ -377,7 +331,6 @@ class LibraryUpdateService : Service() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the notification containing the result of the update done by the service.
|
* Shows the notification containing the result of the update done by the service.
|
||||||
*
|
|
||||||
* @param updates a list of manga with new updates.
|
* @param updates a list of manga with new updates.
|
||||||
* @param failed a list of manga that failed to update.
|
* @param failed a list of manga that failed to update.
|
||||||
*/
|
*/
|
||||||
@ -385,7 +338,7 @@ class LibraryUpdateService : Service() {
|
|||||||
val title = getString(R.string.notification_update_completed)
|
val title = getString(R.string.notification_update_completed)
|
||||||
val body = getUpdatedMangasBody(updates, failed)
|
val body = getUpdatedMangasBody(updates, failed)
|
||||||
|
|
||||||
notificationManager.notify(notificationId, notification() {
|
notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() {
|
||||||
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
|
||||||
setContentTitle(title)
|
setContentTitle(title)
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
setStyle(NotificationCompat.BigTextStyle().bigText(body))
|
||||||
@ -398,7 +351,7 @@ class LibraryUpdateService : Service() {
|
|||||||
* Cancels the notification.
|
* Cancels the notification.
|
||||||
*/
|
*/
|
||||||
private fun cancelNotification() {
|
private fun cancelNotification() {
|
||||||
notificationManager.cancel(notificationId)
|
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -418,7 +371,6 @@ class LibraryUpdateService : Service() {
|
|||||||
class SyncOnConnectionAvailable : BroadcastReceiver() {
|
class SyncOnConnectionAvailable : BroadcastReceiver() {
|
||||||
/**
|
/**
|
||||||
* Method called when a network change occurs.
|
* Method called when a network change occurs.
|
||||||
*
|
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param intent the intent received.
|
* @param intent the intent received.
|
||||||
*/
|
*/
|
||||||
@ -436,7 +388,6 @@ class LibraryUpdateService : Service() {
|
|||||||
class SyncOnPowerConnected: BroadcastReceiver() {
|
class SyncOnPowerConnected: BroadcastReceiver() {
|
||||||
/**
|
/**
|
||||||
* Method called when AC is connected.
|
* Method called when AC is connected.
|
||||||
*
|
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
* @param intent the intent received.
|
* @param intent the intent received.
|
||||||
*/
|
*/
|
||||||
@ -457,7 +408,8 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
LibraryUpdateService.stop(context)
|
LibraryUpdateService.stop(context)
|
||||||
context.notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_ID)
|
context.notificationManager.cancel(UPDATE_NOTIFICATION_ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
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.myanimelist.MyAnimeList
|
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList
|
||||||
|
|
||||||
class MangaSyncManager(private val context: Context) {
|
class MangaSyncManager(private val context: Context) {
|
||||||
|
|
||||||
|
val services: List<MangaSyncService>
|
||||||
|
val myAnimeList: MyAnimeList
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MYANIMELIST = 1
|
const val MYANIMELIST = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
init {
|
||||||
|
myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||||
|
services = listOf(myAnimeList)
|
||||||
|
}
|
||||||
|
|
||||||
val services = listOf(myAnimeList)
|
fun getService(id: Int): MangaSyncService = services.find { it.id == id }!!
|
||||||
|
|
||||||
fun getService(id: Int) = services.find { it.id == id }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.support.annotation.CallSuper
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
abstract class MangaSyncService(private val context: Context, val id: Int) {
|
|
||||||
|
|
||||||
@Inject lateinit var preferences: PreferencesHelper
|
|
||||||
@Inject lateinit var networkService: NetworkHelper
|
|
||||||
|
|
||||||
init {
|
|
||||||
App.get(context).component.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
open val client: OkHttpClient
|
|
||||||
get() = networkService.client
|
|
||||||
|
|
||||||
// Name of the manga sync service to display
|
|
||||||
abstract val name: String
|
|
||||||
|
|
||||||
abstract fun login(username: String, password: String): Completable
|
|
||||||
|
|
||||||
open val isLogged: Boolean
|
|
||||||
get() = !getUsername().isEmpty() &&
|
|
||||||
!getPassword().isEmpty()
|
|
||||||
|
|
||||||
abstract fun add(manga: MangaSync): Observable<MangaSync>
|
|
||||||
|
|
||||||
abstract fun update(manga: MangaSync): Observable<MangaSync>
|
|
||||||
|
|
||||||
abstract fun bind(manga: MangaSync): Observable<MangaSync>
|
|
||||||
|
|
||||||
abstract fun getStatus(status: Int): String
|
|
||||||
|
|
||||||
fun saveCredentials(username: String, password: String) {
|
|
||||||
preferences.setMangaSyncCredentials(this, username, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
open fun logout() {
|
|
||||||
preferences.setMangaSyncCredentials(this, "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUsername() = preferences.mangaSyncUsername(this)
|
|
||||||
|
|
||||||
fun getPassword() = preferences.mangaSyncPassword(this)
|
|
||||||
|
|
||||||
}
|
|
@ -48,13 +48,15 @@ class UpdateMangaSyncService : Service() {
|
|||||||
|
|
||||||
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
|
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
|
||||||
val sync = syncManager.getService(mangaSync.sync_id)
|
val sync = syncManager.getService(mangaSync.sync_id)
|
||||||
if (sync == null) {
|
|
||||||
stopSelf(startId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions.add(Observable.defer { sync.update(mangaSync) }
|
subscriptions.add(Observable.defer { sync.update(mangaSync) }
|
||||||
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
|
.flatMap {
|
||||||
|
if (it.isSuccessful) {
|
||||||
|
db.insertMangaSync(mangaSync).asRxObservable()
|
||||||
|
} else {
|
||||||
|
Observable.error(Exception("Could not update manga in remote service"))
|
||||||
|
}
|
||||||
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ stopSelf(startId) },
|
.subscribe({ stopSelf(startId) },
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.base
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.App
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Observable
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
abstract class MangaSyncService(private val context: Context, val id: Int) {
|
||||||
|
|
||||||
|
@Inject lateinit var preferences: PreferencesHelper
|
||||||
|
@Inject lateinit var networkService: NetworkHelper
|
||||||
|
|
||||||
|
init {
|
||||||
|
App.get(context).component.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name of the manga sync service to display
|
||||||
|
abstract val name: String
|
||||||
|
|
||||||
|
abstract fun login(username: String, password: String): Observable<Boolean>
|
||||||
|
|
||||||
|
open val isLogged: Boolean
|
||||||
|
get() = !preferences.mangaSyncUsername(this).isEmpty() &&
|
||||||
|
!preferences.mangaSyncPassword(this).isEmpty()
|
||||||
|
|
||||||
|
abstract fun update(manga: MangaSync): Observable<Response>
|
||||||
|
|
||||||
|
abstract fun add(manga: MangaSync): Observable<Response>
|
||||||
|
|
||||||
|
abstract fun bind(manga: MangaSync): Observable<Response>
|
||||||
|
|
||||||
|
abstract fun getStatus(status: Int): String
|
||||||
|
|
||||||
|
}
|
@ -1,222 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.mangasync.myanimelist
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Xml
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
|
||||||
import eu.kanade.tachiyomi.data.network.asObservable
|
|
||||||
import eu.kanade.tachiyomi.util.selectInt
|
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
|
||||||
import okhttp3.Credentials
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.RequestBody
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.xmlpull.v1.XmlSerializer
|
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import java.io.StringWriter
|
|
||||||
|
|
||||||
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
|
||||||
|
|
||||||
private lateinit var headers: Headers
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val BASE_URL = "http://myanimelist.net"
|
|
||||||
|
|
||||||
private val ENTRY_TAG = "entry"
|
|
||||||
private val CHAPTER_TAG = "chapter"
|
|
||||||
private val SCORE_TAG = "score"
|
|
||||||
private val STATUS_TAG = "status"
|
|
||||||
|
|
||||||
val READING = 1
|
|
||||||
val COMPLETED = 2
|
|
||||||
val ON_HOLD = 3
|
|
||||||
val DROPPED = 4
|
|
||||||
val PLAN_TO_READ = 6
|
|
||||||
|
|
||||||
val DEFAULT_STATUS = READING
|
|
||||||
val DEFAULT_SCORE = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val username = getUsername()
|
|
||||||
val password = getPassword()
|
|
||||||
|
|
||||||
if (!username.isEmpty() && !password.isEmpty()) {
|
|
||||||
createHeaders(username, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val name: String
|
|
||||||
get() = "MyAnimeList"
|
|
||||||
|
|
||||||
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/account/verify_credentials.xml")
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/manga/search.xml")
|
|
||||||
.appendQueryParameter("q", query)
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendPath("malappinfo.php")
|
|
||||||
.appendQueryParameter("u", username)
|
|
||||||
.appendQueryParameter("status", "all")
|
|
||||||
.appendQueryParameter("type", "manga")
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/mangalist/update")
|
|
||||||
.appendPath("${manga.remote_id}.xml")
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
|
|
||||||
.appendEncodedPath("api/mangalist/add")
|
|
||||||
.appendPath("${manga.remote_id}.xml")
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
override fun login(username: String, password: String): Completable {
|
|
||||||
createHeaders(username, password)
|
|
||||||
return client.newCall(GET(getLoginUrl(), headers))
|
|
||||||
.asObservable()
|
|
||||||
.doOnNext { it.close() }
|
|
||||||
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
|
|
||||||
.toCompletable()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(query: String): Observable<List<MangaSync>> {
|
|
||||||
return client.newCall(GET(getSearchUrl(query), headers))
|
|
||||||
.asObservable()
|
|
||||||
.map { Jsoup.parse(it.body().string()) }
|
|
||||||
.flatMap { Observable.from(it.select("entry")) }
|
|
||||||
.filter { it.select("type").text() != "Novel" }
|
|
||||||
.map {
|
|
||||||
MangaSync.create(this).apply {
|
|
||||||
title = it.selectText("title")
|
|
||||||
remote_id = it.selectInt("id")
|
|
||||||
total_chapters = it.selectInt("chapters")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAL doesn't support score with decimals
|
|
||||||
fun getList(): Observable<List<MangaSync>> {
|
|
||||||
return networkService.forceCacheClient
|
|
||||||
.newCall(GET(getListUrl(getUsername()), headers))
|
|
||||||
.asObservable()
|
|
||||||
.map { Jsoup.parse(it.body().string()) }
|
|
||||||
.flatMap { Observable.from(it.select("manga")) }
|
|
||||||
.map {
|
|
||||||
MangaSync.create(this).apply {
|
|
||||||
title = it.selectText("series_title")
|
|
||||||
remote_id = it.selectInt("series_mangadb_id")
|
|
||||||
last_chapter_read = it.selectInt("my_read_chapters")
|
|
||||||
status = it.selectInt("my_status")
|
|
||||||
score = it.selectInt("my_score").toFloat()
|
|
||||||
total_chapters = it.selectInt("series_chapters")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
return Observable.defer {
|
|
||||||
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
|
||||||
manga.status = COMPLETED
|
|
||||||
}
|
|
||||||
client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
|
|
||||||
.asObservable()
|
|
||||||
.doOnNext { it.close() }
|
|
||||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun add(manga: MangaSync): Observable<MangaSync> {
|
|
||||||
return Observable.defer {
|
|
||||||
client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
|
|
||||||
.asObservable()
|
|
||||||
.doOnNext { it.close() }
|
|
||||||
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
|
|
||||||
.map { manga }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
|
|
||||||
val xml = Xml.newSerializer()
|
|
||||||
val writer = StringWriter()
|
|
||||||
|
|
||||||
with(xml) {
|
|
||||||
setOutput(writer)
|
|
||||||
startDocument("UTF-8", false)
|
|
||||||
startTag("", ENTRY_TAG)
|
|
||||||
|
|
||||||
// Last chapter read
|
|
||||||
if (manga.last_chapter_read != 0) {
|
|
||||||
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
|
|
||||||
}
|
|
||||||
// Manga status in the list
|
|
||||||
inTag(STATUS_TAG, manga.status.toString())
|
|
||||||
|
|
||||||
// Manga score
|
|
||||||
inTag(SCORE_TAG, manga.score.toString())
|
|
||||||
|
|
||||||
endTag("", ENTRY_TAG)
|
|
||||||
endDocument()
|
|
||||||
}
|
|
||||||
|
|
||||||
val form = FormBody.Builder()
|
|
||||||
form.add("data", writer.toString())
|
|
||||||
return form.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
|
|
||||||
startTag(namespace, tag)
|
|
||||||
text(body)
|
|
||||||
endTag(namespace, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 -> ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createHeaders(username: String, password: String) {
|
|
||||||
val builder = Headers.Builder()
|
|
||||||
builder.add("Authorization", Credentials.basic(username, password))
|
|
||||||
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
|
||||||
headers = builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,216 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.mangasync.services
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Xml
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaSync
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
||||||
|
import eu.kanade.tachiyomi.data.network.get
|
||||||
|
import eu.kanade.tachiyomi.data.network.post
|
||||||
|
import eu.kanade.tachiyomi.util.selectInt
|
||||||
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
|
import okhttp3.*
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.xmlpull.v1.XmlSerializer
|
||||||
|
import rx.Observable
|
||||||
|
import java.io.StringWriter
|
||||||
|
|
||||||
|
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
|
||||||
|
startTag(namespace, tag)
|
||||||
|
text(body)
|
||||||
|
endTag(namespace, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
|
||||||
|
|
||||||
|
private lateinit var headers: Headers
|
||||||
|
private lateinit var username: String
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val BASE_URL = "http://myanimelist.net"
|
||||||
|
|
||||||
|
private val ENTRY_TAG = "entry"
|
||||||
|
private val CHAPTER_TAG = "chapter"
|
||||||
|
private val SCORE_TAG = "score"
|
||||||
|
private val STATUS_TAG = "status"
|
||||||
|
|
||||||
|
val READING = 1
|
||||||
|
val COMPLETED = 2
|
||||||
|
val ON_HOLD = 3
|
||||||
|
val DROPPED = 4
|
||||||
|
val PLAN_TO_READ = 6
|
||||||
|
|
||||||
|
val DEFAULT_STATUS = READING
|
||||||
|
val DEFAULT_SCORE = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val username = preferences.mangaSyncUsername(this)
|
||||||
|
val password = preferences.mangaSyncPassword(this)
|
||||||
|
|
||||||
|
if (!username.isEmpty() && !password.isEmpty()) {
|
||||||
|
createHeaders(username, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val name: String
|
||||||
|
get() = "MyAnimeList"
|
||||||
|
|
||||||
|
fun getLoginUrl(): String {
|
||||||
|
return Uri.parse(BASE_URL).buildUpon()
|
||||||
|
.appendEncodedPath("api/account/verify_credentials.xml")
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun login(username: String, password: String): Observable<Boolean> {
|
||||||
|
createHeaders(username, password)
|
||||||
|
return networkService.request(get(getLoginUrl(), headers))
|
||||||
|
.map { it.code() == 200 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSearchUrl(query: String): String {
|
||||||
|
return Uri.parse(BASE_URL).buildUpon()
|
||||||
|
.appendEncodedPath("api/manga/search.xml")
|
||||||
|
.appendQueryParameter("q", query)
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun search(query: String): Observable<List<MangaSync>> {
|
||||||
|
return networkService.requestBody(get(getSearchUrl(query), headers))
|
||||||
|
.map { Jsoup.parse(it) }
|
||||||
|
.flatMap { Observable.from(it.select("entry")) }
|
||||||
|
.filter { it.select("type").text() != "Novel" }
|
||||||
|
.map {
|
||||||
|
val manga = MangaSync.create(this)
|
||||||
|
manga.title = it.selectText("title")
|
||||||
|
manga.remote_id = it.selectInt("id")
|
||||||
|
manga.total_chapters = it.selectInt("chapters")
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getListUrl(username: String): String {
|
||||||
|
return Uri.parse(BASE_URL).buildUpon()
|
||||||
|
.appendPath("malappinfo.php")
|
||||||
|
.appendQueryParameter("u", username)
|
||||||
|
.appendQueryParameter("status", "all")
|
||||||
|
.appendQueryParameter("type", "manga")
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAL doesn't support score with decimals
|
||||||
|
fun getList(): Observable<List<MangaSync>> {
|
||||||
|
return networkService.requestBody(get(getListUrl(username), headers), true)
|
||||||
|
.map { Jsoup.parse(it) }
|
||||||
|
.flatMap { Observable.from(it.select("manga")) }
|
||||||
|
.map {
|
||||||
|
val manga = MangaSync.create(this)
|
||||||
|
manga.title = it.selectText("series_title")
|
||||||
|
manga.remote_id = it.selectInt("series_mangadb_id")
|
||||||
|
manga.last_chapter_read = it.selectInt("my_read_chapters")
|
||||||
|
manga.status = it.selectInt("my_status")
|
||||||
|
manga.score = it.selectInt("my_score").toFloat()
|
||||||
|
manga.total_chapters = it.selectInt("series_chapters")
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUpdateUrl(manga: MangaSync): String {
|
||||||
|
return Uri.parse(BASE_URL).buildUpon()
|
||||||
|
.appendEncodedPath("api/mangalist/update")
|
||||||
|
.appendPath(manga.remote_id.toString() + ".xml")
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(manga: MangaSync): Observable<Response> {
|
||||||
|
return Observable.defer {
|
||||||
|
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
|
||||||
|
manga.status = COMPLETED
|
||||||
|
}
|
||||||
|
networkService.request(post(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAddUrl(manga: MangaSync): String {
|
||||||
|
return Uri.parse(BASE_URL).buildUpon()
|
||||||
|
.appendEncodedPath("api/mangalist/add")
|
||||||
|
.appendPath(manga.remote_id.toString() + ".xml")
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(manga: MangaSync): Observable<Response> {
|
||||||
|
return Observable.defer {
|
||||||
|
networkService.request(post(getAddUrl(manga), headers, getMangaPostPayload(manga)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
|
||||||
|
val xml = Xml.newSerializer()
|
||||||
|
val writer = StringWriter()
|
||||||
|
|
||||||
|
with(xml) {
|
||||||
|
setOutput(writer)
|
||||||
|
startDocument("UTF-8", false)
|
||||||
|
startTag("", ENTRY_TAG)
|
||||||
|
|
||||||
|
// Last chapter read
|
||||||
|
if (manga.last_chapter_read != 0) {
|
||||||
|
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
|
||||||
|
}
|
||||||
|
// Manga status in the list
|
||||||
|
inTag(STATUS_TAG, manga.status.toString())
|
||||||
|
|
||||||
|
// Manga score
|
||||||
|
inTag(SCORE_TAG, manga.score.toString())
|
||||||
|
|
||||||
|
endTag("", ENTRY_TAG)
|
||||||
|
endDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
val form = FormBody.Builder()
|
||||||
|
form.add("data", writer.toString())
|
||||||
|
return form.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(manga: MangaSync): Observable<Response> {
|
||||||
|
return getList()
|
||||||
|
.flatMap {
|
||||||
|
manga.sync_id = id
|
||||||
|
for (remoteManga in it) {
|
||||||
|
if (remoteManga.remote_id == manga.remote_id) {
|
||||||
|
// Manga is already in the list
|
||||||
|
manga.copyPersonalFrom(remoteManga)
|
||||||
|
return@flatMap update(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
manga.score = DEFAULT_SCORE.toFloat()
|
||||||
|
manga.status = DEFAULT_STATUS
|
||||||
|
return@flatMap 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 -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createHeaders(username: String, password: String) {
|
||||||
|
this.username = username
|
||||||
|
val builder = Headers.Builder()
|
||||||
|
builder.add("Authorization", Credentials.basic(username, password))
|
||||||
|
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
|
||||||
|
headers = builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,81 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
|
||||||
|
|
||||||
import com.squareup.duktape.Duktape
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
|
|
||||||
class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interceptor {
|
|
||||||
|
|
||||||
//language=RegExp
|
|
||||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
|
||||||
|
|
||||||
//language=RegExp
|
|
||||||
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
|
||||||
|
|
||||||
//language=RegExp
|
|
||||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val response = chain.proceed(chain.request())
|
|
||||||
|
|
||||||
// Check if we already solved a challenge
|
|
||||||
if (response.code() != 503 &&
|
|
||||||
cookies.get(response.request().url()).any { it.name() == "cf_clearance" }) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Cloudflare anti-bot is on
|
|
||||||
if ("URL=/cdn-cgi/" in response.header("Refresh", "")
|
|
||||||
&& response.header("Server", "") == "cloudflare-nginx") {
|
|
||||||
return chain.proceed(resolveChallenge(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveChallenge(response: Response): Request {
|
|
||||||
val duktape = Duktape.create()
|
|
||||||
try {
|
|
||||||
val originalRequest = response.request()
|
|
||||||
val domain = originalRequest.url().host()
|
|
||||||
val content = response.body().string()
|
|
||||||
|
|
||||||
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
|
||||||
Thread.sleep(4000)
|
|
||||||
|
|
||||||
val operation = operationPattern.find(content)?.groups?.get(1)?.value
|
|
||||||
val challenge = challengePattern.find(content)?.groups?.get(1)?.value
|
|
||||||
val pass = passPattern.find(content)?.groups?.get(1)?.value
|
|
||||||
|
|
||||||
if (operation == null || challenge == null || pass == null) {
|
|
||||||
throw RuntimeException("Failed resolving Cloudflare challenge")
|
|
||||||
}
|
|
||||||
|
|
||||||
val js = operation
|
|
||||||
//language=RegExp
|
|
||||||
.replace(Regex("""a\.value =(.+?) \+ .+?;"""), "$1")
|
|
||||||
//language=RegExp
|
|
||||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
|
||||||
.replace("\n", "")
|
|
||||||
|
|
||||||
// Duktape can only return strings, so the result has to be converted to string first
|
|
||||||
val result = duktape.evaluate("$js.toString()").toInt()
|
|
||||||
|
|
||||||
val answer = "${result + domain.length}"
|
|
||||||
|
|
||||||
val url = HttpUrl.parse("http://$domain/cdn-cgi/l/chk_jschl").newBuilder()
|
|
||||||
.addQueryParameter("jschl_vc", challenge)
|
|
||||||
.addQueryParameter("pass", pass)
|
|
||||||
.addQueryParameter("jschl_answer", answer)
|
|
||||||
.toString()
|
|
||||||
|
|
||||||
val referer = originalRequest.url().toString()
|
|
||||||
return GET(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
|
|
||||||
} finally {
|
|
||||||
duktape.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,9 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.data.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import okhttp3.Cache
|
import okhttp3.*
|
||||||
import okhttp3.OkHttpClient
|
import rx.Observable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.CookieManager
|
||||||
|
import java.net.CookiePolicy
|
||||||
|
import java.net.CookieStore
|
||||||
|
|
||||||
class NetworkHelper(context: Context) {
|
class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
@ -11,28 +14,63 @@ class NetworkHelper(context: Context) {
|
|||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
private val cookieManager = PersistentCookieJar(context)
|
private val cookieManager = CookieManager().apply {
|
||||||
|
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||||
|
}
|
||||||
|
|
||||||
val client = OkHttpClient.Builder()
|
private val forceCacheInterceptor = { chain: Interceptor.Chain ->
|
||||||
.cookieJar(cookieManager)
|
|
||||||
.cache(Cache(cacheDir, cacheSize))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val forceCacheClient = client.newBuilder()
|
|
||||||
.addNetworkInterceptor { chain ->
|
|
||||||
val originalResponse = chain.proceed(chain.request())
|
val originalResponse = chain.proceed(chain.request())
|
||||||
originalResponse.newBuilder()
|
originalResponse.newBuilder()
|
||||||
.removeHeader("Pragma")
|
.removeHeader("Pragma")
|
||||||
.header("Cache-Control", "max-age=600")
|
.header("Cache-Control", "max-age=" + 600)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val client = OkHttpClient.Builder()
|
||||||
|
.cookieJar(JavaNetCookieJar(cookieManager))
|
||||||
|
.cache(Cache(cacheDir, cacheSize))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val forceCacheClient = client.newBuilder()
|
||||||
|
.addNetworkInterceptor(forceCacheInterceptor)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val cookies: CookieStore
|
||||||
|
get() = cookieManager.cookieStore
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun request(request: Request, forceCache: Boolean = false): Observable<Response> {
|
||||||
|
return Observable.fromCallable {
|
||||||
|
val c = if (forceCache) forceCacheClient else client
|
||||||
|
c.newCall(request).execute().apply { body().close() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
|
||||||
|
return Observable.fromCallable {
|
||||||
|
val c = if (forceCache) forceCacheClient else client
|
||||||
|
c.newCall(request).execute().body().string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestBodyProgress(request: Request, listener: ProgressListener): Observable<Response> {
|
||||||
|
return Observable.fromCallable { requestBodyProgressBlocking(request, listener) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestBodyProgressBlocking(request: Request, listener: ProgressListener): Response {
|
||||||
|
val progressClient = client.newBuilder()
|
||||||
|
.cache(null)
|
||||||
|
.addNetworkInterceptor { chain ->
|
||||||
|
val originalResponse = chain.proceed(chain.request())
|
||||||
|
originalResponse.newBuilder()
|
||||||
|
.body(ProgressResponseBody(originalResponse.body(), listener))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val cloudflareClient = client.newBuilder()
|
return progressClient.newCall(request).execute()
|
||||||
.addInterceptor(CloudflareInterceptor(cookies))
|
}
|
||||||
.build()
|
|
||||||
|
|
||||||
val cookies: PersistentCookieStore
|
|
||||||
get() = cookieManager.store
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
|
||||||
|
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import rx.subscriptions.Subscriptions
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
fun Call.asObservable(): Observable<Response> {
|
|
||||||
return Observable.create { subscriber ->
|
|
||||||
subscriber.add(Subscriptions.create { cancel() })
|
|
||||||
|
|
||||||
try {
|
|
||||||
val response = execute()
|
|
||||||
if (!subscriber.isUnsubscribed) {
|
|
||||||
subscriber.onNext(response)
|
|
||||||
subscriber.onCompleted()
|
|
||||||
}
|
|
||||||
} catch (error: IOException) {
|
|
||||||
if (!subscriber.isUnsubscribed) {
|
|
||||||
subscriber.onError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
|
||||||
val progressClient = newBuilder()
|
|
||||||
.cache(null)
|
|
||||||
.addNetworkInterceptor { chain ->
|
|
||||||
val originalResponse = chain.proceed(chain.request())
|
|
||||||
originalResponse.newBuilder()
|
|
||||||
.body(ProgressResponseBody(originalResponse.body(), listener))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return progressClient.newCall(request)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
class PersistentCookieJar(context: Context) : CookieJar {
|
|
||||||
|
|
||||||
val store = PersistentCookieStore(context)
|
|
||||||
|
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
|
||||||
store.addAll(url, cookies)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
|
||||||
return store.get(url)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import java.net.URI
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
class PersistentCookieStore(context: Context) {
|
|
||||||
|
|
||||||
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
|
||||||
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
init {
|
|
||||||
for ((key, value) in prefs.all) {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val cookies = value as? Set<String>
|
|
||||||
if (cookies != null) {
|
|
||||||
try {
|
|
||||||
val url = HttpUrl.parse("http://$key")
|
|
||||||
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
|
|
||||||
.filter { !it.hasExpired() }
|
|
||||||
cookieMap.put(key, nonExpiredCookies)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
|
||||||
synchronized(this) {
|
|
||||||
val key = url.uri().host
|
|
||||||
|
|
||||||
// Append or replace the cookies for this domain.
|
|
||||||
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
|
|
||||||
for (cookie in cookies) {
|
|
||||||
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
|
||||||
val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
|
|
||||||
if (pos == -1) {
|
|
||||||
cookiesForDomain.add(cookie)
|
|
||||||
} else {
|
|
||||||
cookiesForDomain[pos] = cookie
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cookieMap.put(key, cookiesForDomain)
|
|
||||||
|
|
||||||
// Get cookies to be stored in disk
|
|
||||||
val newValues = cookiesForDomain.asSequence()
|
|
||||||
.filter { it.persistent() && !it.hasExpired() }
|
|
||||||
.map { it.toString() }
|
|
||||||
.toSet()
|
|
||||||
|
|
||||||
prefs.edit().putStringSet(key, newValues).apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAll() {
|
|
||||||
synchronized(this) {
|
|
||||||
prefs.edit().clear().apply()
|
|
||||||
cookieMap.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(url: HttpUrl) = get(url.uri().host)
|
|
||||||
|
|
||||||
fun get(uri: URI) = get(uri.host)
|
|
||||||
|
|
||||||
private fun get(url: String): List<Cookie> {
|
|
||||||
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
|
|
||||||
|
|
||||||
}
|
|
@ -1,13 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.data.network
|
||||||
|
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import java.util.concurrent.TimeUnit.MINUTES
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, TimeUnit.MINUTES).build()
|
||||||
private val DEFAULT_HEADERS = Headers.Builder().build()
|
private val DEFAULT_HEADERS = Headers.Builder().build()
|
||||||
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
|
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
|
||||||
|
|
||||||
fun GET(url: String,
|
@JvmOverloads
|
||||||
|
fun get(url: String,
|
||||||
headers: Headers = DEFAULT_HEADERS,
|
headers: Headers = DEFAULT_HEADERS,
|
||||||
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
||||||
|
|
||||||
@ -18,7 +19,8 @@ fun GET(url: String,
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun POST(url: String,
|
@JvmOverloads
|
||||||
|
fun post(url: String,
|
||||||
headers: Headers = DEFAULT_HEADERS,
|
headers: Headers = DEFAULT_HEADERS,
|
||||||
body: RequestBody = DEFAULT_BODY,
|
body: RequestBody = DEFAULT_BODY,
|
||||||
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
@ -52,8 +52,6 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
|
||||||
|
|
||||||
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
|
|
||||||
|
|
||||||
val seamlessMode = context.getString(R.string.pref_seamless_mode_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)
|
||||||
@ -82,6 +80,7 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
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"
|
||||||
|
|
||||||
fun syncUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun syncUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
@ -6,8 +6,8 @@ import android.preference.PreferenceManager
|
|||||||
import com.f2prateek.rx.preferences.Preference
|
import com.f2prateek.rx.preferences.Preference
|
||||||
import com.f2prateek.rx.preferences.RxSharedPreferences
|
import com.f2prateek.rx.preferences.RxSharedPreferences
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
@ -97,10 +97,6 @@ class PreferencesHelper(private val context: Context) {
|
|||||||
|
|
||||||
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
|
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
|
||||||
|
|
||||||
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
|
|
||||||
|
|
||||||
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
|
|
||||||
|
|
||||||
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
|
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
|
||||||
|
|
||||||
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source
|
|
||||||
|
|
||||||
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 rx.Observable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
|
||||||
*/
|
|
||||||
interface Source {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Id for the source. Must be unique.
|
|
||||||
*/
|
|
||||||
val id: Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the source.
|
|
||||||
*/
|
|
||||||
val name: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated details for a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga to update.
|
|
||||||
*/
|
|
||||||
fun fetchMangaDetails(manga: Manga): Observable<Manga>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with all the available chapters for a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga to update.
|
|
||||||
*/
|
|
||||||
fun fetchChapterList(manga: Manga): Observable<List<Chapter>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the list of pages a chapter has.
|
|
||||||
*
|
|
||||||
* @param chapter the chapter.
|
|
||||||
*/
|
|
||||||
fun fetchPageList(chapter: Chapter): Observable<List<Page>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the path of the image.
|
|
||||||
*
|
|
||||||
* @param page the page.
|
|
||||||
*/
|
|
||||||
fun fetchImage(page: Page): Observable<Page>
|
|
||||||
|
|
||||||
}
|
|
@ -1,22 +1,21 @@
|
|||||||
package eu.kanade.tachiyomi.data.source
|
package eu.kanade.tachiyomi.data.source
|
||||||
|
|
||||||
import android.Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.data.source.online.english.Batoto
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.online.english.Kissmanga
|
||||||
import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
|
import eu.kanade.tachiyomi.data.source.online.english.Mangafox
|
||||||
import eu.kanade.tachiyomi.data.source.online.english.*
|
import eu.kanade.tachiyomi.data.source.online.english.Mangahere
|
||||||
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;
|
||||||
import eu.kanade.tachiyomi.util.hasPermission
|
import eu.kanade.tachiyomi.data.source.online.english.ReadMangaToday
|
||||||
import org.yaml.snakeyaml.Yaml
|
import java.util.*
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
open class SourceManager(private val context: Context) {
|
open class SourceManager(private val context: Context) {
|
||||||
|
|
||||||
|
val sourcesMap: HashMap<Int, Source>
|
||||||
|
|
||||||
val BATOTO = 1
|
val BATOTO = 1
|
||||||
val MANGAHERE = 2
|
val MANGAHERE = 2
|
||||||
val MANGAFOX = 3
|
val MANGAFOX = 3
|
||||||
@ -28,45 +27,38 @@ open class SourceManager(private val context: Context) {
|
|||||||
|
|
||||||
val LAST_SOURCE = 8
|
val LAST_SOURCE = 8
|
||||||
|
|
||||||
val sourcesMap = createSources()
|
init {
|
||||||
|
sourcesMap = createSourcesMap()
|
||||||
|
}
|
||||||
|
|
||||||
open fun get(sourceKey: Int): Source? {
|
open fun get(sourceKey: Int): Source? {
|
||||||
return sourcesMap[sourceKey]
|
return sourcesMap[sourceKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
|
private fun createSource(sourceKey: Int): Source? = when (sourceKey) {
|
||||||
|
BATOTO -> Batoto(context)
|
||||||
private fun createSource(id: Int): Source? = when (id) {
|
MANGAHERE -> Mangahere(context)
|
||||||
BATOTO -> Batoto(context, id)
|
MANGAFOX -> Mangafox(context)
|
||||||
KISSMANGA -> Kissmanga(context, id)
|
KISSMANGA -> Kissmanga(context)
|
||||||
MANGAHERE -> Mangahere(context, id)
|
READMANGA -> Readmanga(context)
|
||||||
MANGAFOX -> Mangafox(context, id)
|
MINTMANGA -> Mintmanga(context)
|
||||||
READMANGA -> Readmanga(context, id)
|
MANGACHAN -> Mangachan(context)
|
||||||
MINTMANGA -> Mintmanga(context, id)
|
READMANGATODAY -> ReadMangaToday(context)
|
||||||
MANGACHAN -> Mangachan(context, id)
|
|
||||||
READMANGATODAY -> Readmangatoday(context, id)
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
|
private fun createSourcesMap(): HashMap<Int, Source> {
|
||||||
|
val map = HashMap<Int, Source>()
|
||||||
for (i in 1..LAST_SOURCE) {
|
for (i in 1..LAST_SOURCE) {
|
||||||
createSource(i)?.let { put(i, it) }
|
val source = createSource(i)
|
||||||
|
if (source != null) {
|
||||||
|
source.id = i
|
||||||
|
map.put(i, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
fun getSources(): List<Source> = ArrayList(sourcesMap.values)
|
||||||
File.separator + context.getString(R.string.app_name), "parsers")
|
|
||||||
|
|
||||||
if (parsersDir.exists() && context.hasPermission(READ_EXTERNAL_STORAGE)) {
|
|
||||||
val yaml = Yaml()
|
|
||||||
for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
|
|
||||||
try {
|
|
||||||
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
|
|
||||||
YamlOnlineSource(context, map).let { put(it.id, it) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e("Error loading source from file. Bad format?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.base;
|
||||||
|
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
|
||||||
|
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.Language;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||||
|
import okhttp3.Headers;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import rx.Observable;
|
||||||
|
|
||||||
|
public abstract class BaseSource {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
|
||||||
|
// Id of the source
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Language getLang();
|
||||||
|
|
||||||
|
// Name of the source to display
|
||||||
|
public abstract String getName();
|
||||||
|
|
||||||
|
// Name of the source to display with the language
|
||||||
|
public String getVisibleName() {
|
||||||
|
return getName() + " (" + getLang().getCode() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base url of the source, like: http://example.com
|
||||||
|
public abstract String getBaseUrl();
|
||||||
|
|
||||||
|
// True if the source requires a login
|
||||||
|
public abstract boolean isLoginRequired();
|
||||||
|
|
||||||
|
// Return the initial popular mangas URL
|
||||||
|
protected abstract String getInitialPopularMangasUrl();
|
||||||
|
|
||||||
|
// Return the initial search url given a query
|
||||||
|
protected abstract String getInitialSearchUrl(String query);
|
||||||
|
|
||||||
|
// Get the popular list of mangas from the source's parsed document
|
||||||
|
protected abstract List<Manga> parsePopularMangasFromHtml(Document parsedHtml);
|
||||||
|
|
||||||
|
// Get the next popular page URL or null if it's the last
|
||||||
|
protected abstract String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page);
|
||||||
|
|
||||||
|
// Get the searched list of mangas from the source's parsed document
|
||||||
|
protected abstract List<Manga> parseSearchFromHtml(Document parsedHtml);
|
||||||
|
|
||||||
|
// Get the next search page URL or null if it's the last
|
||||||
|
protected abstract String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query);
|
||||||
|
|
||||||
|
// Given the URL of a manga and the result of the request, return the details of the manga
|
||||||
|
protected abstract Manga parseHtmlToManga(String mangaUrl, String unparsedHtml);
|
||||||
|
|
||||||
|
// Given the result of the request to mangas' chapters, return a list of chapters
|
||||||
|
protected abstract List<Chapter> parseHtmlToChapters(String unparsedHtml);
|
||||||
|
|
||||||
|
// Given the result of the request to a chapter, return the list of URLs of the chapter
|
||||||
|
protected abstract List<String> parseHtmlToPageUrls(String unparsedHtml);
|
||||||
|
|
||||||
|
// Given the result of the request to a chapter's page, return the URL of the image of the page
|
||||||
|
protected abstract String parseHtmlToImageUrl(String unparsedHtml);
|
||||||
|
|
||||||
|
|
||||||
|
// Login related methods, shouldn't be overriden if the source doesn't require it
|
||||||
|
public Observable<Boolean> login(String username, String password) {
|
||||||
|
throw new UnsupportedOperationException("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLogged() {
|
||||||
|
throw new UnsupportedOperationException("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isAuthenticationSuccessful(Response response) {
|
||||||
|
throw new UnsupportedOperationException("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default headers, it can be overriden by children or just add new keys
|
||||||
|
protected Headers.Builder headersBuilder() {
|
||||||
|
Headers.Builder builder = new Headers.Builder();
|
||||||
|
builder.add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)");
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getVisibleName();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.base;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public abstract class LoginSource extends Source {
|
||||||
|
|
||||||
|
public LoginSource(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLoginRequired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
236
app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.kt
Normal file
236
app/src/main/java/eu/kanade/tachiyomi/data/source/base/Source.kt
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.base
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.bumptech.glide.load.model.LazyHeaders
|
||||||
|
import eu.kanade.tachiyomi.App
|
||||||
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.data.network.get
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import rx.Observable
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
abstract class Source(context: Context) : BaseSource() {
|
||||||
|
|
||||||
|
@Inject protected lateinit var networkService: NetworkHelper
|
||||||
|
@Inject protected lateinit var chapterCache: ChapterCache
|
||||||
|
@Inject protected lateinit var prefs: PreferencesHelper
|
||||||
|
|
||||||
|
val requestHeaders by lazy { headersBuilder().build() }
|
||||||
|
|
||||||
|
val glideHeaders by lazy { glideHeadersBuilder().build() }
|
||||||
|
|
||||||
|
init {
|
||||||
|
App.get(context).component.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isLoginRequired(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun popularMangaRequest(page: MangasPage): Request {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = initialPopularMangasUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(page.url, requestHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun searchMangaRequest(page: MangasPage, query: String): Request {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = getInitialSearchUrl(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(page.url, requestHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun mangaDetailsRequest(mangaUrl: String): Request {
|
||||||
|
return get(baseUrl + mangaUrl, requestHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun chapterListRequest(mangaUrl: String): Request {
|
||||||
|
return get(baseUrl + mangaUrl, requestHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun pageListRequest(chapterUrl: String): Request {
|
||||||
|
return get(baseUrl + chapterUrl, requestHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun imageUrlRequest(page: Page): Request {
|
||||||
|
return get(page.url, requestHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun imageRequest(page: Page): Request {
|
||||||
|
return get(page.imageUrl, requestHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most popular mangas from the source
|
||||||
|
open fun pullPopularMangasFromNetwork(page: MangasPage): Observable<MangasPage> {
|
||||||
|
return networkService.requestBody(popularMangaRequest(page), true)
|
||||||
|
.map { Jsoup.parse(it) }
|
||||||
|
.doOnNext { doc -> page.mangas = parsePopularMangasFromHtml(doc) }
|
||||||
|
.doOnNext { doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page) }
|
||||||
|
.map { response -> page }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mangas from the source with a query
|
||||||
|
open fun searchMangasFromNetwork(page: MangasPage, query: String): Observable<MangasPage> {
|
||||||
|
return networkService.requestBody(searchMangaRequest(page, query), true)
|
||||||
|
.map { Jsoup.parse(it) }
|
||||||
|
.doOnNext { doc -> page.mangas = parseSearchFromHtml(doc) }
|
||||||
|
.doOnNext { doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query) }
|
||||||
|
.map { response -> page }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get manga details from the source
|
||||||
|
open fun pullMangaFromNetwork(mangaUrl: String): Observable<Manga> {
|
||||||
|
return networkService.requestBody(mangaDetailsRequest(mangaUrl))
|
||||||
|
.flatMap { Observable.just(parseHtmlToManga(mangaUrl, it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get chapter list of a manga from the source
|
||||||
|
open fun pullChaptersFromNetwork(mangaUrl: String): Observable<List<Chapter>> {
|
||||||
|
return networkService.requestBody(chapterListRequest(mangaUrl))
|
||||||
|
.flatMap { unparsedHtml ->
|
||||||
|
val chapters = parseHtmlToChapters(unparsedHtml)
|
||||||
|
if (!chapters.isEmpty())
|
||||||
|
Observable.just(chapters)
|
||||||
|
else
|
||||||
|
Observable.error(Exception("No chapters found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getCachedPageListOrPullFromNetwork(chapterUrl: String): Observable<List<Page>> {
|
||||||
|
return chapterCache.getPageListFromCache(getChapterCacheKey(chapterUrl))
|
||||||
|
.onErrorResumeNext { pullPageListFromNetwork(chapterUrl) }
|
||||||
|
.onBackpressureBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun pullPageListFromNetwork(chapterUrl: String): Observable<List<Page>> {
|
||||||
|
return networkService.requestBody(pageListRequest(chapterUrl))
|
||||||
|
.flatMap { unparsedHtml ->
|
||||||
|
val pages = convertToPages(parseHtmlToPageUrls(unparsedHtml))
|
||||||
|
if (!pages.isEmpty())
|
||||||
|
Observable.just(parseFirstPage(pages, unparsedHtml))
|
||||||
|
else
|
||||||
|
Observable.error(Exception("Page list is empty"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||||
|
return Observable.from(pages)
|
||||||
|
.filter { page -> page.imageUrl != null }
|
||||||
|
.mergeWith(getRemainingImageUrlsFromPageList(pages))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the URLs of the images of a chapter
|
||||||
|
open fun getRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||||
|
return Observable.from(pages)
|
||||||
|
.filter { page -> page.imageUrl == null }
|
||||||
|
.concatMap { getImageUrlFromPage(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getImageUrlFromPage(page: Page): Observable<Page> {
|
||||||
|
page.status = Page.LOAD_PAGE
|
||||||
|
return networkService.requestBody(imageUrlRequest(page))
|
||||||
|
.flatMap { unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)) }
|
||||||
|
.onErrorResumeNext { e ->
|
||||||
|
page.status = Page.ERROR
|
||||||
|
Observable.just<String>(null)
|
||||||
|
}
|
||||||
|
.flatMap { imageUrl ->
|
||||||
|
page.imageUrl = imageUrl
|
||||||
|
Observable.just(page)
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getCachedImage(page: Page): Observable<Page> {
|
||||||
|
val pageObservable = Observable.just(page)
|
||||||
|
if (page.imageUrl == null)
|
||||||
|
return pageObservable
|
||||||
|
|
||||||
|
return pageObservable
|
||||||
|
.flatMap { p ->
|
||||||
|
if (!chapterCache.isImageInCache(page.imageUrl)) {
|
||||||
|
return@flatMap cacheImage(page)
|
||||||
|
}
|
||||||
|
Observable.just(page)
|
||||||
|
}
|
||||||
|
.flatMap { p ->
|
||||||
|
page.imagePath = chapterCache.getImagePath(page.imageUrl)
|
||||||
|
page.status = Page.READY
|
||||||
|
Observable.just(page)
|
||||||
|
}
|
||||||
|
.onErrorResumeNext { e ->
|
||||||
|
page.status = Page.ERROR
|
||||||
|
Observable.just(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cacheImage(page: Page): Observable<Page> {
|
||||||
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
|
return getImageProgressResponse(page)
|
||||||
|
.flatMap { resp ->
|
||||||
|
chapterCache.putImageToCache(page.imageUrl, resp, prefs.reencodeImage())
|
||||||
|
Observable.just(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getImageProgressResponse(page: Page): Observable<Response> {
|
||||||
|
return networkService.requestBodyProgress(imageRequest(page), page)
|
||||||
|
.doOnNext {
|
||||||
|
if (!it.isSuccessful) {
|
||||||
|
it.body().close()
|
||||||
|
throw RuntimeException("Not a valid response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun savePageList(chapterUrl: String, pages: List<Page>?) {
|
||||||
|
if (pages != null)
|
||||||
|
chapterCache.putPageListToCache(getChapterCacheKey(chapterUrl), pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun convertToPages(pageUrls: List<String>): List<Page> {
|
||||||
|
val pages = ArrayList<Page>()
|
||||||
|
for (i in pageUrls.indices) {
|
||||||
|
pages.add(Page(i, pageUrls[i]))
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun parseFirstPage(pages: List<Page>, unparsedHtml: String): List<Page> {
|
||||||
|
val firstImage = parseHtmlToImageUrl(unparsedHtml)
|
||||||
|
pages[0].imageUrl = firstImage
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getChapterCacheKey(chapterUrl: String): String {
|
||||||
|
return "$id$chapterUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overridable method to allow custom parsing.
|
||||||
|
open fun parseChapterNumber(chapter: Chapter) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun glideHeadersBuilder(): LazyHeaders.Builder {
|
||||||
|
val builder = LazyHeaders.Builder()
|
||||||
|
for ((key, value) in requestHeaders.toMultimap()) {
|
||||||
|
builder.addHeader(key, value[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
interface LoginSource : Source {
|
|
||||||
|
|
||||||
fun isLogged(): Boolean
|
|
||||||
|
|
||||||
fun login(username: String, password: String): Observable<Boolean>
|
|
||||||
|
|
||||||
fun isAuthenticationSuccessful(response: Response): Boolean
|
|
||||||
|
|
||||||
}
|
|
@ -1,449 +0,0 @@
|
|||||||
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.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.data.network.asObservable
|
|
||||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import okhttp3.*
|
|
||||||
import rx.Observable
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple implementation for sources from a website.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
|
||||||
abstract class OnlineSource(context: Context) : Source {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Network service.
|
|
||||||
*/
|
|
||||||
@Inject lateinit var network: NetworkHelper
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter cache.
|
|
||||||
*/
|
|
||||||
@Inject lateinit var chapterCache: ChapterCache
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences helper.
|
|
||||||
*/
|
|
||||||
@Inject lateinit var preferences: PreferencesHelper
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
|
||||||
*/
|
|
||||||
abstract val baseUrl: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Language of the source.
|
|
||||||
*/
|
|
||||||
abstract val lang: Language
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Headers used for requests.
|
|
||||||
*/
|
|
||||||
val headers by lazy { headersBuilder().build() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default network client for doing requests.
|
|
||||||
*/
|
|
||||||
open val client: OkHttpClient
|
|
||||||
get() = network.client
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Inject dependencies.
|
|
||||||
App.get(context).component.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
|
||||||
*/
|
|
||||||
open protected fun headersBuilder() = Headers.Builder().apply {
|
|
||||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visible name of the source.
|
|
||||||
*/
|
|
||||||
override fun toString() = "$name (${lang.code})"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param page the page object where the information will be saved, like the list of manga,
|
|
||||||
* the current page and the next page url.
|
|
||||||
*/
|
|
||||||
open fun fetchPopularManga(page: MangasPage): Observable<MangasPage> = client
|
|
||||||
.newCall(popularMangaRequest(page))
|
|
||||||
.asObservable()
|
|
||||||
.map { response ->
|
|
||||||
page.apply {
|
|
||||||
mangas = mutableListOf<Manga>()
|
|
||||||
popularMangaParse(response, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for the popular manga given the page. Override only if it's needed to
|
|
||||||
* send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param page the page object.
|
|
||||||
*/
|
|
||||||
open protected fun popularMangaRequest(page: MangasPage): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = popularMangaInitialUrl()
|
|
||||||
}
|
|
||||||
return GET(page.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the absolute url of the first page to popular manga.
|
|
||||||
*/
|
|
||||||
abstract protected fun popularMangaInitialUrl(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site. It should add a list of manga and the absolute url to the
|
|
||||||
* next page (if it has a next one) to [page].
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param page the page object to be filled.
|
|
||||||
*/
|
|
||||||
abstract protected fun popularMangaParse(response: Response, page: MangasPage)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param page the page object where the information will be saved, like the list of manga,
|
|
||||||
* the current page and the next page url.
|
|
||||||
* @param query the search query.
|
|
||||||
*/
|
|
||||||
open fun fetchSearchManga(page: MangasPage, query: String): Observable<MangasPage> = client
|
|
||||||
.newCall(searchMangaRequest(page, query))
|
|
||||||
.asObservable()
|
|
||||||
.map { response ->
|
|
||||||
page.apply {
|
|
||||||
mangas = mutableListOf<Manga>()
|
|
||||||
searchMangaParse(response, this, query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for the search manga given the page. Override only if it's needed to
|
|
||||||
* send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param page the page object.
|
|
||||||
* @param query the search query.
|
|
||||||
*/
|
|
||||||
open protected fun searchMangaRequest(page: MangasPage, query: String): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = searchMangaInitialUrl(query)
|
|
||||||
}
|
|
||||||
return GET(page.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the absolute url of the first page to popular manga.
|
|
||||||
*
|
|
||||||
* @param query the search query.
|
|
||||||
*/
|
|
||||||
abstract protected fun searchMangaInitialUrl(query: String): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site. It should add a list of manga and the absolute url to the
|
|
||||||
* next page (if it has a next one) to [page].
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param page the page object to be filled.
|
|
||||||
* @param query the search query.
|
|
||||||
*/
|
|
||||||
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param manga the manga to be updated.
|
|
||||||
*/
|
|
||||||
override fun fetchMangaDetails(manga: Manga): Observable<Manga> = client
|
|
||||||
.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservable()
|
|
||||||
.map { response ->
|
|
||||||
Manga.create(manga.url, id).apply {
|
|
||||||
mangaDetailsParse(response, this)
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for updating a manga. Override only if it's needed to override the url,
|
|
||||||
* send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param manga the manga to be updated.
|
|
||||||
*/
|
|
||||||
open protected fun mangaDetailsRequest(manga: Manga): Request {
|
|
||||||
return GET(baseUrl + manga.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site. It should fill [manga].
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param manga the manga whose fields have to be filled.
|
|
||||||
*/
|
|
||||||
abstract protected fun mangaDetailsParse(response: Response, manga: Manga)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param manga the manga to look for chapters.
|
|
||||||
*/
|
|
||||||
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> = client
|
|
||||||
.newCall(chapterListRequest(manga))
|
|
||||||
.asObservable()
|
|
||||||
.map { response ->
|
|
||||||
mutableListOf<Chapter>().apply {
|
|
||||||
chapterListParse(response, this)
|
|
||||||
if (isEmpty()) {
|
|
||||||
throw Exception("No chapters found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for updating the chapter list. Override only if it's needed to override
|
|
||||||
* the url, send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param manga the manga to look for chapters.
|
|
||||||
*/
|
|
||||||
open protected fun chapterListRequest(manga: Manga): Request {
|
|
||||||
return GET(baseUrl + manga.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site. It should fill [chapters].
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param chapters the chapter list to be filled.
|
|
||||||
*/
|
|
||||||
abstract protected fun chapterListParse(response: Response, chapters: MutableList<Chapter>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the page list for a chapter. It tries to return the page list from
|
|
||||||
* the local cache, otherwise fallbacks to network calling [fetchPageListFromNetwork].
|
|
||||||
*
|
|
||||||
* @param chapter the chapter whose page list has to be fetched.
|
|
||||||
*/
|
|
||||||
final override fun fetchPageList(chapter: Chapter): Observable<List<Page>> = chapterCache
|
|
||||||
.getPageListFromCache(getChapterCacheKey(chapter))
|
|
||||||
.onErrorResumeNext { fetchPageListFromNetwork(chapter) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the page list for a chapter. Normally it's not needed to override
|
|
||||||
* this method.
|
|
||||||
*
|
|
||||||
* @param chapter the chapter whose page list has to be fetched.
|
|
||||||
*/
|
|
||||||
open fun fetchPageListFromNetwork(chapter: Chapter): Observable<List<Page>> = client
|
|
||||||
.newCall(pageListRequest(chapter))
|
|
||||||
.asObservable()
|
|
||||||
.map { response ->
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
throw Exception("Webpage sent ${response.code()} code")
|
|
||||||
}
|
|
||||||
mutableListOf<Page>().apply {
|
|
||||||
pageListParse(response, this)
|
|
||||||
if (isEmpty()) {
|
|
||||||
throw Exception("Page list is empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for getting the page list. Override only if it's needed to override the
|
|
||||||
* url, send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param chapter the chapter whose page list has to be fetched
|
|
||||||
*/
|
|
||||||
open protected fun pageListRequest(chapter: Chapter): Request {
|
|
||||||
return GET(baseUrl + chapter.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site. It should fill [pages].
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param pages the page list to be filled.
|
|
||||||
*/
|
|
||||||
abstract protected fun pageListParse(response: Response, pages: MutableList<Page>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the key for the page list to be stored in [ChapterCache].
|
|
||||||
*/
|
|
||||||
private fun getChapterCacheKey(chapter: Chapter) = "$id${chapter.url}"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the page containing the source url of the image. If there's any
|
|
||||||
* error, it will return null instead of throwing an exception.
|
|
||||||
*
|
|
||||||
* @param page the page whose source image has to be fetched.
|
|
||||||
*/
|
|
||||||
open protected fun fetchImageUrl(page: Page): Observable<Page> {
|
|
||||||
page.status = Page.LOAD_PAGE
|
|
||||||
return client
|
|
||||||
.newCall(imageUrlRequest(page))
|
|
||||||
.asObservable()
|
|
||||||
.map { imageUrlParse(it) }
|
|
||||||
.doOnError { page.status = Page.ERROR }
|
|
||||||
.onErrorReturn { null }
|
|
||||||
.doOnNext { page.imageUrl = it }
|
|
||||||
.map { page }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for getting the url to the source image. Override only if it's needed to
|
|
||||||
* override the url, send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param page the chapter whose page list has to be fetched
|
|
||||||
*/
|
|
||||||
open protected fun imageUrlRequest(page: Page): Request {
|
|
||||||
return GET(page.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site. It should return the absolute url to the source image.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
abstract protected fun imageUrlParse(response: Response): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable of the page with the downloaded image.
|
|
||||||
*
|
|
||||||
* @param page the page whose source image has to be downloaded.
|
|
||||||
*/
|
|
||||||
final override fun fetchImage(page: Page): Observable<Page> =
|
|
||||||
if (page.imageUrl.isNullOrEmpty())
|
|
||||||
fetchImageUrl(page).flatMap { getCachedImage(it) }
|
|
||||||
else
|
|
||||||
getCachedImage(page)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the response of the source image.
|
|
||||||
*
|
|
||||||
* @param page the page whose source image has to be downloaded.
|
|
||||||
*/
|
|
||||||
fun imageResponse(page: Page): Observable<Response> = client
|
|
||||||
.newCallWithProgress(imageRequest(page), page)
|
|
||||||
.asObservable()
|
|
||||||
.doOnNext {
|
|
||||||
if (!it.isSuccessful) {
|
|
||||||
it.close()
|
|
||||||
throw RuntimeException("Not a valid response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for getting the source image. Override only if it's needed to override
|
|
||||||
* the url, send different headers or request method like POST.
|
|
||||||
*
|
|
||||||
* @param page the chapter whose page list has to be fetched
|
|
||||||
*/
|
|
||||||
open protected fun imageRequest(page: Page): Request {
|
|
||||||
return GET(page.imageUrl, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable of the page that gets the image from the chapter or fallbacks to
|
|
||||||
* network and copies it to the cache calling [cacheImage].
|
|
||||||
*
|
|
||||||
* @param page the page.
|
|
||||||
*/
|
|
||||||
fun getCachedImage(page: Page): Observable<Page> {
|
|
||||||
val pageObservable = Observable.just(page)
|
|
||||||
if (page.imageUrl.isNullOrEmpty())
|
|
||||||
return pageObservable
|
|
||||||
|
|
||||||
return pageObservable
|
|
||||||
.flatMap {
|
|
||||||
if (!chapterCache.isImageInCache(page.imageUrl)) {
|
|
||||||
cacheImage(page)
|
|
||||||
} else {
|
|
||||||
Observable.just(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.doOnNext {
|
|
||||||
page.imagePath = chapterCache.getImagePath(page.imageUrl)
|
|
||||||
page.status = Page.READY
|
|
||||||
}
|
|
||||||
.doOnError { page.status = Page.ERROR }
|
|
||||||
.onErrorReturn { page }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable of the page that downloads the image to [ChapterCache].
|
|
||||||
*
|
|
||||||
* @param page the page.
|
|
||||||
*/
|
|
||||||
private fun cacheImage(page: Page): Observable<Page> {
|
|
||||||
page.status = Page.DOWNLOAD_IMAGE
|
|
||||||
return imageResponse(page)
|
|
||||||
.doOnNext { chapterCache.putImageToCache(page.imageUrl, it, preferences.reencodeImage()) }
|
|
||||||
.map { page }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
.filter { !it.imageUrl.isNullOrEmpty() }
|
|
||||||
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
|
||||||
|
|
||||||
fun fetchRemainingImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
|
|
||||||
.filter { it.imageUrl.isNullOrEmpty() }
|
|
||||||
.concatMap { fetchImageUrl(it) }
|
|
||||||
|
|
||||||
fun savePageList(chapter: Chapter, pages: List<Page>?) {
|
|
||||||
if (pages != null) {
|
|
||||||
chapterCache.putPageListToCache(getChapterCacheKey(chapter), pages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overridable method to allow custom parsing.
|
|
||||||
open fun parseChapterNumber(chapter: Chapter) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,189 +0,0 @@
|
|||||||
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.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site and fills [page].
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param page the page object to be filled.
|
|
||||||
*/
|
|
||||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
for (element in document.select(popularMangaSelector())) {
|
|
||||||
Manga().apply {
|
|
||||||
source = this@ParsedOnlineSource.id
|
|
||||||
popularMangaFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
popularMangaNextPageSelector()?.let { selector ->
|
|
||||||
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
|
||||||
getAbsoluteUrl(it, response.request().url())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
|
||||||
*/
|
|
||||||
abstract protected fun popularMangaSelector(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills [manga] with the given [element]. Most sites only show the title and the url, it's
|
|
||||||
* totally safe to fill only those two values.
|
|
||||||
*
|
|
||||||
* @param element an element obtained from [popularMangaSelector].
|
|
||||||
* @param manga the manga to fill.
|
|
||||||
*/
|
|
||||||
abstract protected fun popularMangaFromElement(element: Element, manga: Manga)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
|
||||||
* there's no next page.
|
|
||||||
*/
|
|
||||||
abstract protected fun popularMangaNextPageSelector(): String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site and fills [page].
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param page the page object to be filled.
|
|
||||||
* @param query the search query.
|
|
||||||
*/
|
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
for (element in document.select(searchMangaSelector())) {
|
|
||||||
Manga().apply {
|
|
||||||
source = this@ParsedOnlineSource.id
|
|
||||||
searchMangaFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchMangaNextPageSelector()?.let { selector ->
|
|
||||||
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
|
||||||
getAbsoluteUrl(it, response.request().url())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
|
||||||
*/
|
|
||||||
abstract protected fun searchMangaSelector(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills [manga] with the given [element]. Most sites only show the title and the url, it's
|
|
||||||
* totally safe to fill only those two values.
|
|
||||||
*
|
|
||||||
* @param element an element obtained from [searchMangaSelector].
|
|
||||||
* @param manga the manga to fill.
|
|
||||||
*/
|
|
||||||
abstract protected fun searchMangaFromElement(element: Element, manga: Manga)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
|
||||||
* there's no next page.
|
|
||||||
*/
|
|
||||||
abstract protected fun searchMangaNextPageSelector(): String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site and fills the details of [manga].
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param manga the manga to fill.
|
|
||||||
*/
|
|
||||||
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
|
||||||
mangaDetailsParse(Jsoup.parse(response.body().string()), manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills the details of [manga] from the given [document].
|
|
||||||
*
|
|
||||||
* @param document the parsed document.
|
|
||||||
* @param manga the manga to fill.
|
|
||||||
*/
|
|
||||||
abstract protected fun mangaDetailsParse(document: Document, manga: Manga)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site and fills the chapter list.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param chapters the list of chapters to fill.
|
|
||||||
*/
|
|
||||||
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
|
|
||||||
for (element in document.select(chapterListSelector())) {
|
|
||||||
Chapter.create().apply {
|
|
||||||
chapterFromElement(element, this)
|
|
||||||
chapters.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
|
|
||||||
*/
|
|
||||||
abstract protected fun chapterListSelector(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills [chapter] with the given [element].
|
|
||||||
*
|
|
||||||
* @param element an element obtained from [chapterListSelector].
|
|
||||||
* @param chapter the chapter to fill.
|
|
||||||
*/
|
|
||||||
abstract protected fun chapterFromElement(element: Element, chapter: Chapter)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site and fills the page list.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param pages the list of pages to fill.
|
|
||||||
*/
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
|
||||||
pageListParse(Jsoup.parse(response.body().string()), pages)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills [pages] from the given [document].
|
|
||||||
*
|
|
||||||
* @param document the parsed document.
|
|
||||||
* @param pages the list of pages to fill.
|
|
||||||
*/
|
|
||||||
abstract protected fun pageListParse(document: Document, pages: MutableList<Page>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site and returns the absolute url to the source image.
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
*/
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
|
||||||
return imageUrlParse(Jsoup.parse(response.body().string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the absolute url to the source image from the document.
|
|
||||||
*
|
|
||||||
* @param document the parsed document.
|
|
||||||
*/
|
|
||||||
abstract protected fun imageUrlParse(document: Document): String
|
|
||||||
|
|
||||||
}
|
|
@ -1,165 +0,0 @@
|
|||||||
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.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
|
||||||
import eu.kanade.tachiyomi.data.source.getLanguages
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(context) {
|
|
||||||
|
|
||||||
val map = YamlSourceNode(mappings)
|
|
||||||
|
|
||||||
override val name: String
|
|
||||||
get() = map.name
|
|
||||||
|
|
||||||
override val baseUrl = map.host.let {
|
|
||||||
if (it.endsWith("/")) it.dropLast(1) else it
|
|
||||||
}
|
|
||||||
|
|
||||||
override val lang = map.lang.toUpperCase().let { code ->
|
|
||||||
getLanguages().find { code == it.code }!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override val client = when(map.client) {
|
|
||||||
"cloudflare" -> network.cloudflareClient
|
|
||||||
else -> network.client
|
|
||||||
}
|
|
||||||
|
|
||||||
override val id = map.id.let {
|
|
||||||
if (it is Int) it else (lang.code.hashCode() + 31 * it.hashCode()) and 0x7fffffff
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: MangasPage): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = popularMangaInitialUrl()
|
|
||||||
}
|
|
||||||
return when (map.popular.method?.toLowerCase()) {
|
|
||||||
"post" -> POST(page.url, headers, map.popular.createForm())
|
|
||||||
else -> GET(page.url, headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = map.popular.url
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
for (element in document.select(map.popular.manga_css)) {
|
|
||||||
Manga().apply {
|
|
||||||
source = this@YamlOnlineSource.id
|
|
||||||
title = element.text()
|
|
||||||
setUrl(element.attr("href"))
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
map.popular.next_url_css?.let { selector ->
|
|
||||||
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
|
||||||
getAbsoluteUrl(it, response.request().url())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = searchMangaInitialUrl(query)
|
|
||||||
}
|
|
||||||
return when (map.search.method?.toLowerCase()) {
|
|
||||||
"post" -> POST(page.url, headers, map.search.createForm())
|
|
||||||
else -> GET(page.url, headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = map.search.url.replace("\$query", query)
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
for (element in document.select(map.search.manga_css)) {
|
|
||||||
Manga().apply {
|
|
||||||
source = this@YamlOnlineSource.id
|
|
||||||
title = element.text()
|
|
||||||
setUrl(element.attr("href"))
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
map.search.next_url_css?.let { selector ->
|
|
||||||
page.nextPageUrl = document.select(selector).first()?.attr("href")?.let {
|
|
||||||
getAbsoluteUrl(it, response.request().url())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
with(map.manga) {
|
|
||||||
val pool = parts.get(document)
|
|
||||||
|
|
||||||
manga.author = author?.process(document, pool)
|
|
||||||
manga.artist = artist?.process(document, pool)
|
|
||||||
manga.description = summary?.process(document, pool)
|
|
||||||
manga.thumbnail_url = cover?.process(document, pool)
|
|
||||||
manga.genre = genres?.process(document, pool)
|
|
||||||
manga.status = status?.getStatus(document, pool) ?: Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
with(map.chapters) {
|
|
||||||
val pool = emptyMap<String, Element>()
|
|
||||||
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
|
|
||||||
|
|
||||||
for (element in document.select(chapter_css)) {
|
|
||||||
val chapter = Chapter.create()
|
|
||||||
element.select(title).first().let {
|
|
||||||
chapter.name = it.text()
|
|
||||||
chapter.setUrl(it.attr("href"))
|
|
||||||
}
|
|
||||||
val dateElement = element.select(date?.select).first()
|
|
||||||
chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
|
|
||||||
chapters.add(chapter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
with(map.pages) {
|
|
||||||
val url = response.request().url().toString()
|
|
||||||
pages_css?.let {
|
|
||||||
for (element in document.select(it)) {
|
|
||||||
val value = element.attr(pages_attr)
|
|
||||||
val pageUrl = replace?.let { url.replace(it.toRegex(), replacement!!.replace("\$value", value)) } ?: value
|
|
||||||
pages.add(Page(pages.size, pageUrl))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for ((i, element) in document.select(image_css).withIndex()) {
|
|
||||||
pages.getOrNull(i)?.imageUrl = element.attr(image_attr).let {
|
|
||||||
getAbsoluteUrl(it, response.request().url())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
return with(map.pages) {
|
|
||||||
document.select(image_css).first().attr(image_attr).let {
|
|
||||||
getAbsoluteUrl(it, response.request().url())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,214 +0,0 @@
|
|||||||
@file:Suppress("UNCHECKED_CAST")
|
|
||||||
|
|
||||||
package eu.kanade.tachiyomi.data.source.online
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.RequestBody
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private fun toMap(map: Any?) = map as? Map<String, Any?>
|
|
||||||
|
|
||||||
class YamlSourceNode(uncheckedMap: Map<*, *>) {
|
|
||||||
|
|
||||||
val map = toMap(uncheckedMap)!!
|
|
||||||
|
|
||||||
val id: Any by map
|
|
||||||
|
|
||||||
val name: String by map
|
|
||||||
|
|
||||||
val host: String by map
|
|
||||||
|
|
||||||
val lang: String by map
|
|
||||||
|
|
||||||
val client: String?
|
|
||||||
get() = map["client"] as? String
|
|
||||||
|
|
||||||
val popular = PopularNode(toMap(map["popular"])!!)
|
|
||||||
|
|
||||||
val search = SearchNode(toMap(map["search"])!!)
|
|
||||||
|
|
||||||
val manga = MangaNode(toMap(map["manga"])!!)
|
|
||||||
|
|
||||||
val chapters = ChaptersNode(toMap(map["chapters"])!!)
|
|
||||||
|
|
||||||
val pages = PagesNode(toMap(map["pages"])!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RequestableNode {
|
|
||||||
|
|
||||||
val map: Map<String, Any?>
|
|
||||||
|
|
||||||
val url: String
|
|
||||||
get() = map["url"] as String
|
|
||||||
|
|
||||||
val method: String?
|
|
||||||
get() = map["method"] as? String
|
|
||||||
|
|
||||||
val payload: Map<String, String>?
|
|
||||||
get() = map["payload"] as? Map<String, String>
|
|
||||||
|
|
||||||
fun createForm(): RequestBody {
|
|
||||||
return FormBody.Builder().apply {
|
|
||||||
payload?.let {
|
|
||||||
for ((key, value) in it) {
|
|
||||||
add(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class PopularNode(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 {
|
|
||||||
|
|
||||||
val manga_css: String by map
|
|
||||||
|
|
||||||
val next_url_css: String?
|
|
||||||
get() = map["next_url_css"] as? String
|
|
||||||
}
|
|
||||||
|
|
||||||
class MangaNode(private val map: Map<String, Any?>) {
|
|
||||||
|
|
||||||
val parts = CacheNode(toMap(map["parts"]) ?: emptyMap())
|
|
||||||
|
|
||||||
val artist = toMap(map["artist"])?.let { SelectableNode(it) }
|
|
||||||
|
|
||||||
val author = toMap(map["author"])?.let { SelectableNode(it) }
|
|
||||||
|
|
||||||
val summary = toMap(map["summary"])?.let { SelectableNode(it) }
|
|
||||||
|
|
||||||
val status = toMap(map["status"])?.let { StatusNode(it) }
|
|
||||||
|
|
||||||
val genres = toMap(map["genres"])?.let { SelectableNode(it) }
|
|
||||||
|
|
||||||
val cover = toMap(map["cover"])?.let { CoverNode(it) }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChaptersNode(private val map: Map<String, Any?>) {
|
|
||||||
|
|
||||||
val chapter_css: String by map
|
|
||||||
|
|
||||||
val title: String by map
|
|
||||||
|
|
||||||
val date = toMap(toMap(map["date"]))?.let { DateNode(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
class CacheNode(private val map: Map<String, Any?>) {
|
|
||||||
|
|
||||||
fun get(document: Document) = map.mapValues { document.select(it.value as String).first() }
|
|
||||||
}
|
|
||||||
|
|
||||||
open class SelectableNode(private val map: Map<String, Any?>) {
|
|
||||||
|
|
||||||
val select: String by map
|
|
||||||
|
|
||||||
val from: String?
|
|
||||||
get() = map["from"] as? String
|
|
||||||
|
|
||||||
open val attr: String?
|
|
||||||
get() = map["attr"] as? String
|
|
||||||
|
|
||||||
val capture: String?
|
|
||||||
get() = map["capture"] as? String
|
|
||||||
|
|
||||||
fun process(document: Element, cache: Map<String, Element>): String {
|
|
||||||
val parent = from?.let { cache[it] } ?: document
|
|
||||||
val node = parent.select(select).first()
|
|
||||||
var text = attr?.let { node.attr(it) } ?: node.text()
|
|
||||||
capture?.let {
|
|
||||||
text = Regex(it).find(text)?.groupValues?.get(1) ?: text
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
|
||||||
|
|
||||||
val complete: String?
|
|
||||||
get() = map["complete"] as? String
|
|
||||||
|
|
||||||
val ongoing: String?
|
|
||||||
get() = map["ongoing"] as? String
|
|
||||||
|
|
||||||
val licensed: String?
|
|
||||||
get() = map["licensed"] as? String
|
|
||||||
|
|
||||||
fun getStatus(document: Element, cache: Map<String, Element>): Int {
|
|
||||||
val text = process(document, cache)
|
|
||||||
complete?.let {
|
|
||||||
if (text.contains(it)) return Manga.COMPLETED
|
|
||||||
}
|
|
||||||
ongoing?.let {
|
|
||||||
if (text.contains(it)) return Manga.ONGOING
|
|
||||||
}
|
|
||||||
licensed?.let {
|
|
||||||
if (text.contains(it)) return Manga.LICENSED
|
|
||||||
}
|
|
||||||
return Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CoverNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
|
||||||
|
|
||||||
override val attr: String?
|
|
||||||
get() = map["attr"] as? String ?: "src"
|
|
||||||
}
|
|
||||||
|
|
||||||
class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
|
||||||
|
|
||||||
val format: String by map
|
|
||||||
|
|
||||||
fun getDate(document: Element, cache: Map<String, Element>, formatter: SimpleDateFormat): Date {
|
|
||||||
val text = process(document, cache)
|
|
||||||
try {
|
|
||||||
return formatter.parse(text)
|
|
||||||
} catch (exception: ParseException) {}
|
|
||||||
|
|
||||||
for (i in 0..7) {
|
|
||||||
(map["day$i"] as? List<String>)?.let {
|
|
||||||
it.find { it.toRegex().containsMatchIn(text) }?.let {
|
|
||||||
return Calendar.getInstance().apply { add(Calendar.DATE, -i) }.time
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Date(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class PagesNode(private val map: Map<String, Any?>) {
|
|
||||||
|
|
||||||
val pages_css: String?
|
|
||||||
get() = map["pages_css"] as? String
|
|
||||||
|
|
||||||
val pages_attr: String?
|
|
||||||
get() = map["pages_attr"] as? String ?: "value"
|
|
||||||
|
|
||||||
val replace: String?
|
|
||||||
get() = map["url_replace"] as? String
|
|
||||||
|
|
||||||
val replacement: String?
|
|
||||||
get() = map["url_replacement"] as? String
|
|
||||||
|
|
||||||
val image_css: String by map
|
|
||||||
|
|
||||||
val image_attr: String
|
|
||||||
get() = map["image_attr"] as? String ?: "src"
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,393 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.english;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.text.Html;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
|
||||||
|
import java.net.HttpCookie;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
|
import eu.kanade.tachiyomi.data.network.ReqKt;
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language;
|
||||||
|
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.LoginSource;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||||
|
import eu.kanade.tachiyomi.util.Parser;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.Headers;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import rx.Observable;
|
||||||
|
import rx.functions.Func1;
|
||||||
|
|
||||||
|
public class Batoto extends LoginSource {
|
||||||
|
|
||||||
|
public static final String NAME = "Batoto";
|
||||||
|
public static final String BASE_URL = "http://bato.to";
|
||||||
|
public static final String POPULAR_MANGAS_URL = BASE_URL + "/search_ajax?order_cond=views&order=desc&p=%s";
|
||||||
|
public static final String SEARCH_URL = BASE_URL + "/search_ajax?name=%s&p=%s";
|
||||||
|
public static final String CHAPTER_URL = BASE_URL + "/areader?id=%s&p=1";
|
||||||
|
public static final String PAGE_URL = BASE_URL + "/areader?id=%s&p=%s";
|
||||||
|
public static final String MANGA_URL = BASE_URL + "/comic_pop?id=%s";
|
||||||
|
public static final String LOGIN_URL = BASE_URL + "/forums/index.php?app=core&module=global§ion=login";
|
||||||
|
|
||||||
|
public static final Pattern staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private Pattern datePattern;
|
||||||
|
private Map<String, Integer> dateFields;
|
||||||
|
|
||||||
|
public Batoto(Context context) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*");
|
||||||
|
dateFields = new HashMap<String, Integer>() {{
|
||||||
|
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);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Language getLang() {
|
||||||
|
return LanguageKt.getEN();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Headers.Builder headersBuilder() {
|
||||||
|
Headers.Builder builder = super.headersBuilder();
|
||||||
|
builder.add("Cookie", "lang_option=English");
|
||||||
|
builder.add("Referer", "http://bato.to/reader");
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getInitialPopularMangasUrl() {
|
||||||
|
return String.format(POPULAR_MANGAS_URL, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getInitialSearchUrl(String query) {
|
||||||
|
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Request mangaDetailsRequest(String mangaUrl) {
|
||||||
|
String mangaId = mangaUrl.substring(mangaUrl.lastIndexOf("r") + 1);
|
||||||
|
return ReqKt.get(String.format(MANGA_URL, mangaId), getRequestHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Request pageListRequest(String pageUrl) {
|
||||||
|
String id = pageUrl.substring(pageUrl.indexOf("#") + 1);
|
||||||
|
return ReqKt.get(String.format(CHAPTER_URL, id), getRequestHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Request imageUrlRequest(Page page) {
|
||||||
|
String pageUrl = page.getUrl();
|
||||||
|
int start = pageUrl.indexOf("#") + 1;
|
||||||
|
int end = pageUrl.indexOf("_", start);
|
||||||
|
String id = pageUrl.substring(start, end);
|
||||||
|
return ReqKt.get(String.format(PAGE_URL, id, pageUrl.substring(end+1)), getRequestHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Manga> parseMangasFromHtml(Document parsedHtml) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
if (!parsedHtml.text().contains("No (more) comics found!")) {
|
||||||
|
for (Element currentHtmlBlock : parsedHtml.select("tr:not([id]):not([class])")) {
|
||||||
|
Manga manga = constructMangaFromHtmlBlock(currentHtmlBlock);
|
||||||
|
mangaList.add(manga);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||||
|
return parseMangasFromHtml(parsedHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||||
|
Element next = Parser.element(parsedHtml, "#show_more_row");
|
||||||
|
return next != null ? String.format(POPULAR_MANGAS_URL, page.page + 1) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||||
|
return parseMangasFromHtml(parsedHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructMangaFromHtmlBlock(Element htmlBlock) {
|
||||||
|
Manga manga = new Manga();
|
||||||
|
manga.source = getId();
|
||||||
|
|
||||||
|
Element urlElement = Parser.element(htmlBlock, "a[href^=http://bato.to]");
|
||||||
|
if (urlElement != null) {
|
||||||
|
manga.setUrl(urlElement.attr("href"));
|
||||||
|
manga.title = urlElement.text().trim();
|
||||||
|
}
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||||
|
Element next = Parser.element(parsedHtml, "#show_more_row");
|
||||||
|
return next != null ? String.format(SEARCH_URL, query, page.page + 1) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
|
||||||
|
Element tbody = parsedDocument.select("tbody").first();
|
||||||
|
Element artistElement = tbody.select("tr:contains(Author/Artist:)").first();
|
||||||
|
Elements genreElements = tbody.select("tr:contains(Genres:) img");
|
||||||
|
|
||||||
|
Manga manga = Manga.create(mangaUrl);
|
||||||
|
manga.author = Parser.text(artistElement, "td:eq(1)");
|
||||||
|
manga.artist = Parser.text(artistElement, "td:eq(2)", manga.author);
|
||||||
|
manga.description = Parser.text(tbody, "tr:contains(Description:) > td:eq(1)");
|
||||||
|
manga.thumbnail_url = Parser.src(parsedDocument, "img[src^=http://img.bato.to/forums/uploads/]");
|
||||||
|
manga.status = parseStatus(Parser.text(parsedDocument, "tr:contains(Status:) > td:eq(1)"));
|
||||||
|
|
||||||
|
if (!genreElements.isEmpty()) {
|
||||||
|
List<String> genres = new ArrayList<>();
|
||||||
|
for (Element element : genreElements) {
|
||||||
|
genres.add(element.attr("alt"));
|
||||||
|
}
|
||||||
|
manga.genre = TextUtils.join(", ", genres);
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.initialized = true;
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseStatus(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case "Ongoing":
|
||||||
|
return Manga.ONGOING;
|
||||||
|
case "Complete":
|
||||||
|
return Manga.COMPLETED;
|
||||||
|
default:
|
||||||
|
return Manga.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||||
|
Matcher matcher = staffNotice.matcher(unparsedHtml);
|
||||||
|
if (matcher.find()) {
|
||||||
|
String notice = Html.fromHtml(matcher.group(1)).toString().trim();
|
||||||
|
throw new RuntimeException(notice);
|
||||||
|
}
|
||||||
|
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
|
||||||
|
List<Chapter> chapterList = new ArrayList<>();
|
||||||
|
|
||||||
|
Elements chapterElements = parsedDocument.select("tr.row.lang_English.chapter_row");
|
||||||
|
for (Element chapterElement : chapterElements) {
|
||||||
|
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
||||||
|
chapterList.add(chapter);
|
||||||
|
}
|
||||||
|
return chapterList;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||||
|
Chapter chapter = Chapter.create();
|
||||||
|
|
||||||
|
Element urlElement = chapterElement.select("a[href^=http://bato.to/reader").first();
|
||||||
|
Element dateElement = chapterElement.select("td").get(4);
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
String fieldUrl = urlElement.attr("href");
|
||||||
|
chapter.setUrl(fieldUrl);
|
||||||
|
chapter.name = urlElement.text().trim();
|
||||||
|
}
|
||||||
|
if (dateElement != null) {
|
||||||
|
chapter.date_upload = parseDateFromElement(dateElement);
|
||||||
|
}
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("WrongConstant")
|
||||||
|
private long parseDateFromElement(Element dateElement) {
|
||||||
|
String dateAsString = dateElement.text();
|
||||||
|
|
||||||
|
Date date;
|
||||||
|
try {
|
||||||
|
date = new SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Matcher m = datePattern.matcher(dateAsString);
|
||||||
|
|
||||||
|
if (m.matches()) {
|
||||||
|
String number = m.group(1);
|
||||||
|
int amount = number.contains("A") ? 1 : Integer.parseInt(m.group(1));
|
||||||
|
String unit = m.group(2);
|
||||||
|
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.add(dateFields.get(unit), -amount);
|
||||||
|
date = cal.getTime();
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return date.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
|
||||||
|
List<String> pageUrlList = new ArrayList<>();
|
||||||
|
|
||||||
|
Element selectElement = Parser.element(parsedDocument, "#page_select");
|
||||||
|
if (selectElement != null) {
|
||||||
|
for (Element pageUrlElement : selectElement.select("option")) {
|
||||||
|
pageUrlList.add(pageUrlElement.attr("value"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For webtoons in one page
|
||||||
|
for (int i = 0; i < parsedDocument.select("div > img").size(); i++) {
|
||||||
|
pageUrlList.add("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageUrlList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
|
||||||
|
if (!unparsedHtml.contains("Want to see this chapter per page instead?")) {
|
||||||
|
String firstImage = parseHtmlToImageUrl(unparsedHtml);
|
||||||
|
pages.get(0).setImageUrl(firstImage);
|
||||||
|
} else {
|
||||||
|
// For webtoons in one page
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
Elements imageUrls = parsedDocument.select("div > img");
|
||||||
|
for (int i = 0; i < pages.size(); i++) {
|
||||||
|
pages.get(i).setImageUrl(imageUrls.get(i).attr("src"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (List<Page>) pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("<img id=\"comic_page\"");
|
||||||
|
int endIndex = unparsedHtml.indexOf("</a>", beginIndex);
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||||
|
|
||||||
|
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||||
|
Element imageElement = parsedDocument.getElementById("comic_page");
|
||||||
|
return imageElement.attr("src");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Observable<Boolean> login(final String username, final String password) {
|
||||||
|
return getNetworkService().requestBody(ReqKt.get(LOGIN_URL, getRequestHeaders()))
|
||||||
|
.flatMap(new Func1<String, Observable<Response>>() {
|
||||||
|
@Override
|
||||||
|
public Observable<Response> call(String response) {return doLogin(response, username, password);}
|
||||||
|
})
|
||||||
|
.map(new Func1<Response, Boolean>() {
|
||||||
|
@Override
|
||||||
|
public Boolean call(Response resp) {return isAuthenticationSuccessful(resp);}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Observable<Response> doLogin(String response, String username, String password) {
|
||||||
|
Document doc = Jsoup.parse(response);
|
||||||
|
Element form = doc.select("#login").first();
|
||||||
|
String postUrl = form.attr("action");
|
||||||
|
|
||||||
|
FormBody.Builder formBody = new FormBody.Builder();
|
||||||
|
Element authKey = form.select("input[name=auth_key]").first();
|
||||||
|
|
||||||
|
formBody.add(authKey.attr("name"), authKey.attr("value"));
|
||||||
|
formBody.add("ips_username", username);
|
||||||
|
formBody.add("ips_password", password);
|
||||||
|
formBody.add("invisible", "1");
|
||||||
|
formBody.add("rememberMe", "1");
|
||||||
|
|
||||||
|
return getNetworkService().request(ReqKt.post(postUrl, getRequestHeaders(), formBody.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isAuthenticationSuccessful(Response response) {
|
||||||
|
return response.priorResponse() != null && response.priorResponse().code() == 302;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLogged() {
|
||||||
|
try {
|
||||||
|
for ( HttpCookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL)) ) {
|
||||||
|
if (cookie.getName().equals("pass_hash"))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
|
||||||
|
Observable<List<Chapter>> observable;
|
||||||
|
String username = getPrefs().sourceUsername(this);
|
||||||
|
String password = getPrefs().sourcePassword(this);
|
||||||
|
if (username.isEmpty() && password.isEmpty()) {
|
||||||
|
observable = Observable.error(new Exception("User not logged"));
|
||||||
|
}
|
||||||
|
else if (!isLogged()) {
|
||||||
|
observable = login(username, password)
|
||||||
|
.flatMap(new Func1<Boolean, Observable<? extends List<Chapter>>>() {
|
||||||
|
@Override
|
||||||
|
public Observable<? extends List<Chapter>> call(Boolean result) {return Batoto.super.pullChaptersFromNetwork(mangaUrl);}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
observable = super.pullChaptersFromNetwork(mangaUrl);
|
||||||
|
}
|
||||||
|
return observable;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,269 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.text.Html
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
|
||||||
import eu.kanade.tachiyomi.data.network.asObservable
|
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import rx.Observable
|
|
||||||
import java.net.URI
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(context), LoginSource {
|
|
||||||
|
|
||||||
override val name = "Batoto"
|
|
||||||
|
|
||||||
override val baseUrl = "http://bato.to"
|
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
|
||||||
|
|
||||||
private val datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? 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 staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE)
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.add("Cookie", "lang_option=English")
|
|
||||||
|
|
||||||
private val pageHeaders = super.headersBuilder()
|
|
||||||
.add("Referer", "http://bato.to/reader")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/search_ajax?order_cond=views&order=desc&p=1"
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
for (element in document.select(popularMangaSelector())) {
|
|
||||||
Manga().apply {
|
|
||||||
source = this@Batoto.id
|
|
||||||
popularMangaFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.nextPageUrl = document.select(popularMangaNextPageSelector()).first()?.let {
|
|
||||||
"$baseUrl/search_ajax?order_cond=views&order=desc&p=${page.page + 1}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "tr:not([id]):not([class])"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a[href^=http://bato.to]").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.text().trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "#show_more_row"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=1"
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
for (element in document.select(searchMangaSelector())) {
|
|
||||||
Manga().apply {
|
|
||||||
source = this@Batoto.id
|
|
||||||
searchMangaFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
|
||||||
"$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: Manga): Request {
|
|
||||||
val mangaId = manga.url.substringAfterLast("r")
|
|
||||||
return GET("$baseUrl/comic_pop?id=$mangaId", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
|
||||||
val tbody = document.select("tbody").first()
|
|
||||||
val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
|
|
||||||
|
|
||||||
manga.author = artistElement.selectText("td:eq(1)")
|
|
||||||
manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author
|
|
||||||
manga.description = tbody.selectText("tr:contains(Description:) > td:eq(1)")
|
|
||||||
manga.thumbnail_url = document.select("img[src^=http://img.bato.to/forums/uploads/]").first()?.attr("src")
|
|
||||||
manga.status = parseStatus(document.selectText("tr:contains(Status:) > td:eq(1)"))
|
|
||||||
manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String?) = when (status) {
|
|
||||||
"Ongoing" -> Manga.ONGOING
|
|
||||||
"Complete" -> Manga.COMPLETED
|
|
||||||
else -> Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
|
||||||
val body = response.body().string()
|
|
||||||
val matcher = staffNotice.matcher(body)
|
|
||||||
if (matcher.find()) {
|
|
||||||
val notice = Html.fromHtml(matcher.group(1)).toString().trim()
|
|
||||||
throw Exception(notice)
|
|
||||||
}
|
|
||||||
|
|
||||||
val document = Jsoup.parse(body)
|
|
||||||
|
|
||||||
for (element in document.select(chapterListSelector())) {
|
|
||||||
Chapter.create().apply {
|
|
||||||
chapterFromElement(element, this)
|
|
||||||
chapters.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
|
||||||
val urlElement = element.select("a[href^=http://bato.to/reader").first()
|
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text()
|
|
||||||
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
|
||||||
parseDateFromElement(it)
|
|
||||||
} ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseDateFromElement(dateElement: Element): Long {
|
|
||||||
val dateAsString = dateElement.text()
|
|
||||||
|
|
||||||
val date: Date
|
|
||||||
try {
|
|
||||||
date = SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString)
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
val m = datePattern.matcher(dateAsString)
|
|
||||||
|
|
||||||
if (m.matches()) {
|
|
||||||
val number = m.group(1)
|
|
||||||
val amount = if (number.contains("A")) 1 else Integer.parseInt(m.group(1))
|
|
||||||
val unit = m.group(2)
|
|
||||||
|
|
||||||
date = Calendar.getInstance().apply {
|
|
||||||
add(dateFields[unit]!!, -amount)
|
|
||||||
}.time
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.time
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: Chapter): Request {
|
|
||||||
val id = chapter.url.substringAfterLast("#")
|
|
||||||
return GET("$baseUrl/areader?id=$id&p=1", pageHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
|
||||||
val selectElement = document.select("#page_select").first()
|
|
||||||
if (selectElement != null) {
|
|
||||||
for ((i, element) in selectElement.select("option").withIndex()) {
|
|
||||||
pages.add(Page(i, element.attr("value")))
|
|
||||||
}
|
|
||||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
|
||||||
} else {
|
|
||||||
// For webtoons in one page
|
|
||||||
for ((i, element) in document.select("div > img").withIndex()) {
|
|
||||||
pages.add(Page(i, "", element.attr("src")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlRequest(page: Page): Request {
|
|
||||||
val pageUrl = page.url
|
|
||||||
val start = pageUrl.indexOf("#") + 1
|
|
||||||
val end = pageUrl.indexOf("_", start)
|
|
||||||
val id = pageUrl.substring(start, end)
|
|
||||||
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end+1)}", pageHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String {
|
|
||||||
return document.select("#comic_page").first().attr("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun login(username: String, password: String) =
|
|
||||||
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers))
|
|
||||||
.asObservable()
|
|
||||||
.flatMap { doLogin(it.body().string(), username, password) }
|
|
||||||
.map { isAuthenticationSuccessful(it) }
|
|
||||||
|
|
||||||
private fun doLogin(response: String, username: String, password: String): Observable<Response> {
|
|
||||||
val doc = Jsoup.parse(response)
|
|
||||||
val form = doc.select("#login").first()
|
|
||||||
val url = form.attr("action")
|
|
||||||
val authKey = form.select("input[name=auth_key]").first()
|
|
||||||
|
|
||||||
val payload = FormBody.Builder().apply {
|
|
||||||
add(authKey.attr("name"), authKey.attr("value"))
|
|
||||||
add("ips_username", username)
|
|
||||||
add("ips_password", password)
|
|
||||||
add("invisible", "1")
|
|
||||||
add("rememberMe", "1")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return client.newCall(POST(url, headers, payload)).asObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isAuthenticationSuccessful(response: Response) =
|
|
||||||
response.priorResponse() != null && response.priorResponse().code() == 302
|
|
||||||
|
|
||||||
override fun isLogged(): Boolean {
|
|
||||||
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> {
|
|
||||||
if (!isLogged()) {
|
|
||||||
val username = preferences.sourceUsername(this)
|
|
||||||
val password = preferences.sourcePassword(this)
|
|
||||||
|
|
||||||
if (username.isNullOrEmpty() || password.isNullOrEmpty()) {
|
|
||||||
return Observable.error(Exception("User not logged"))
|
|
||||||
} else {
|
|
||||||
return login(username, password).flatMap { super.fetchChapterList(manga) }
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return super.fetchChapterList(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,234 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.english;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
|
import eu.kanade.tachiyomi.data.network.ReqKt;
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language;
|
||||||
|
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||||
|
import eu.kanade.tachiyomi.util.Parser;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.Headers;
|
||||||
|
import okhttp3.Request;
|
||||||
|
|
||||||
|
public class Kissmanga extends Source {
|
||||||
|
|
||||||
|
public static final String NAME = "Kissmanga";
|
||||||
|
public static final String HOST = "kissmanga.com";
|
||||||
|
public static final String IP = "93.174.95.110";
|
||||||
|
public static final String BASE_URL = "http://" + IP;
|
||||||
|
public static final String POPULAR_MANGAS_URL = BASE_URL + "/MangaList/MostPopular?page=%s";
|
||||||
|
public static final String SEARCH_URL = BASE_URL + "/AdvanceSearch";
|
||||||
|
|
||||||
|
public Kissmanga(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Headers.Builder headersBuilder() {
|
||||||
|
Headers.Builder builder = super.headersBuilder();
|
||||||
|
builder.add("Host", HOST);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Language getLang() {
|
||||||
|
return LanguageKt.getEN();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialPopularMangasUrl() {
|
||||||
|
return String.format(POPULAR_MANGAS_URL, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialSearchUrl(String query) {
|
||||||
|
return SEARCH_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Request searchMangaRequest(MangasPage page, String query) {
|
||||||
|
if (page.page == 1) {
|
||||||
|
page.url = getInitialSearchUrl(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
FormBody.Builder form = new FormBody.Builder();
|
||||||
|
form.add("authorArtist", "");
|
||||||
|
form.add("mangaName", query);
|
||||||
|
form.add("status", "");
|
||||||
|
form.add("genres", "");
|
||||||
|
|
||||||
|
return ReqKt.post(page.url, getRequestHeaders(), form.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Request pageListRequest(String chapterUrl) {
|
||||||
|
return ReqKt.post(getBaseUrl() + chapterUrl, getRequestHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Request imageRequest(Page page) {
|
||||||
|
return ReqKt.get(page.getImageUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element currentHtmlBlock : parsedHtml.select("table.listing tr:gt(1)")) {
|
||||||
|
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
|
||||||
|
mangaList.add(manga);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructPopularMangaFromHtml(Element htmlBlock) {
|
||||||
|
Manga manga = new Manga();
|
||||||
|
manga.source = getId();
|
||||||
|
|
||||||
|
Element urlElement = Parser.element(htmlBlock, "td a:eq(0)");
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
manga.setUrl(urlElement.attr("href"));
|
||||||
|
manga.title = urlElement.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||||
|
String path = Parser.href(parsedHtml, "li > a:contains(› Next)");
|
||||||
|
return path != null ? BASE_URL + path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||||
|
return parsePopularMangasFromHtml(parsedHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
Element infoElement = parsedDocument.select("div.barContent").first();
|
||||||
|
|
||||||
|
Manga manga = Manga.create(mangaUrl);
|
||||||
|
manga.title = Parser.text(infoElement, "a.bigChar");
|
||||||
|
manga.author = Parser.text(infoElement, "p:has(span:contains(Author:)) > a");
|
||||||
|
manga.genre = Parser.allText(infoElement, "p:has(span:contains(Genres:)) > *:gt(0)");
|
||||||
|
manga.description = Parser.allText(infoElement, "p:has(span:contains(Summary:)) ~ p");
|
||||||
|
manga.status = parseStatus(Parser.text(infoElement, "p:has(span:contains(Status:))"));
|
||||||
|
|
||||||
|
String thumbnail = Parser.src(parsedDocument, ".rightBox:eq(0) img");
|
||||||
|
if (thumbnail != null) {
|
||||||
|
manga.thumbnail_url = Uri.parse(thumbnail).buildUpon().authority(IP).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.initialized = true;
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseStatus(String status) {
|
||||||
|
if (status.contains("Ongoing")) {
|
||||||
|
return Manga.ONGOING;
|
||||||
|
}
|
||||||
|
if (status.contains("Completed")) {
|
||||||
|
return Manga.COMPLETED;
|
||||||
|
}
|
||||||
|
return Manga.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
List<Chapter> chapterList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element chapterElement : parsedDocument.select("table.listing tr:gt(1)")) {
|
||||||
|
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
||||||
|
chapterList.add(chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapterList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||||
|
Chapter chapter = Chapter.create();
|
||||||
|
|
||||||
|
Element urlElement = Parser.element(chapterElement, "a");
|
||||||
|
String date = Parser.text(chapterElement, "td:eq(1)");
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
chapter.setUrl(urlElement.attr("href"));
|
||||||
|
chapter.name = urlElement.text();
|
||||||
|
}
|
||||||
|
if (date != null) {
|
||||||
|
try {
|
||||||
|
chapter.date_upload = new SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH).parse(date).getTime();
|
||||||
|
} catch (ParseException e) { /* Ignore */ }
|
||||||
|
}
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
List<String> pageUrlList = new ArrayList<>();
|
||||||
|
|
||||||
|
int numImages = parsedDocument.select("#divImage img").size();
|
||||||
|
|
||||||
|
for (int i = 0; i < numImages; i++) {
|
||||||
|
pageUrlList.add("");
|
||||||
|
}
|
||||||
|
return pageUrlList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
|
||||||
|
Pattern p = Pattern.compile("lstImages.push\\(\"(.+?)\"");
|
||||||
|
Matcher m = p.matcher(unparsedHtml);
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
while (m.find()) {
|
||||||
|
pages.get(i++).setImageUrl(m.group(1));
|
||||||
|
}
|
||||||
|
return (List<Page>) pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,118 +0,0 @@
|
|||||||
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.network.GET
|
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
|
||||||
|
|
||||||
override val name = "Kissmanga"
|
|
||||||
|
|
||||||
override val baseUrl = "http://kissmanga.com"
|
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
|
||||||
|
|
||||||
override val client: OkHttpClient get() = network.cloudflareClient
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/MangaList/MostPopular"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "table.listing tr:gt(1)"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("td a:eq(0)").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = searchMangaInitialUrl(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
val form = FormBody.Builder().apply {
|
|
||||||
add("authorArtist", "")
|
|
||||||
add("mangaName", query)
|
|
||||||
add("status", "")
|
|
||||||
add("genres", "")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return POST(page.url, headers, form)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/AdvanceSearch"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = null
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
|
||||||
val infoElement = document.select("div.barContent").first()
|
|
||||||
|
|
||||||
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.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.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parseStatus(status: String) = when {
|
|
||||||
status.contains("Ongoing") -> Manga.ONGOING
|
|
||||||
status.contains("Completed") -> Manga.COMPLETED
|
|
||||||
else -> Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "table.listing tr:gt(1)"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
|
||||||
val urlElement = element.select("a").first()
|
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text()
|
|
||||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
|
||||||
SimpleDateFormat("MM/dd/yyyy").parse(it).time
|
|
||||||
} ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: Chapter) = POST(baseUrl + chapter.url, headers)
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
|
||||||
//language=RegExp
|
|
||||||
val p = Pattern.compile("""lstImages.push\("(.+?)"""")
|
|
||||||
val m = p.matcher(response.body().string())
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
while (m.find()) {
|
|
||||||
pages.add(Page(i++, "", m.group(1)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not used
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
|
|
||||||
|
|
||||||
override fun imageUrlRequest(page: Page) = GET(page.url)
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,245 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.english;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language;
|
||||||
|
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||||
|
import eu.kanade.tachiyomi.util.Parser;
|
||||||
|
|
||||||
|
public class Mangafox extends Source {
|
||||||
|
|
||||||
|
public static final String NAME = "Mangafox";
|
||||||
|
public static final String BASE_URL = "http://mangafox.me";
|
||||||
|
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
|
||||||
|
public static final String SEARCH_URL =
|
||||||
|
BASE_URL + "/search.php?name_method=cw&advopts=1&order=za&sort=views&name=%s&page=%s";
|
||||||
|
|
||||||
|
public Mangafox(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Language getLang() {
|
||||||
|
return LanguageKt.getEN();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialPopularMangasUrl() {
|
||||||
|
return String.format(POPULAR_MANGAS_URL, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialSearchUrl(String query) {
|
||||||
|
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element currentHtmlBlock : parsedHtml.select("div#mangalist > ul.list > li")) {
|
||||||
|
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
|
||||||
|
mangaList.add(currentManga);
|
||||||
|
}
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
|
||||||
|
Manga manga = new Manga();
|
||||||
|
manga.source = getId();
|
||||||
|
|
||||||
|
Element urlElement = Parser.element(htmlBlock, "a.title");
|
||||||
|
if (urlElement != null) {
|
||||||
|
manga.setUrl(urlElement.attr("href"));
|
||||||
|
manga.title = urlElement.text();
|
||||||
|
}
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||||
|
Element next = Parser.element(parsedHtml, "a:has(span.next)");
|
||||||
|
return next != null ? String.format(POPULAR_MANGAS_URL, next.attr("href")) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element currentHtmlBlock : parsedHtml.select("table#listing > tbody > tr:gt(0)")) {
|
||||||
|
Manga currentManga = constructSearchMangaFromHtmlBlock(currentHtmlBlock);
|
||||||
|
mangaList.add(currentManga);
|
||||||
|
}
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructSearchMangaFromHtmlBlock(Element htmlBlock) {
|
||||||
|
Manga mangaFromHtmlBlock = new Manga();
|
||||||
|
mangaFromHtmlBlock.source = getId();
|
||||||
|
|
||||||
|
Element urlElement = Parser.element(htmlBlock, "a.series_preview");
|
||||||
|
if (urlElement != null) {
|
||||||
|
mangaFromHtmlBlock.setUrl(urlElement.attr("href"));
|
||||||
|
mangaFromHtmlBlock.title = urlElement.text();
|
||||||
|
}
|
||||||
|
return mangaFromHtmlBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||||
|
Element next = Parser.element(parsedHtml, "a:has(span.next)");
|
||||||
|
return next != null ? BASE_URL + next.attr("href") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
|
||||||
|
Element infoElement = parsedDocument.select("div#title").first();
|
||||||
|
Element rowElement = infoElement.select("table > tbody > tr:eq(1)").first();
|
||||||
|
Element sideInfoElement = parsedDocument.select("#series_info").first();
|
||||||
|
|
||||||
|
Manga manga = Manga.create(mangaUrl);
|
||||||
|
manga.author = Parser.text(rowElement, "td:eq(1)");
|
||||||
|
manga.artist = Parser.text(rowElement, "td:eq(2)");
|
||||||
|
manga.description = Parser.text(infoElement, "p.summary");
|
||||||
|
manga.genre = Parser.text(rowElement, "td:eq(3)");
|
||||||
|
manga.thumbnail_url = Parser.src(sideInfoElement, "div.cover > img");
|
||||||
|
manga.status = parseStatus(Parser.text(sideInfoElement, ".data"));
|
||||||
|
|
||||||
|
manga.initialized = true;
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseStatus(String status) {
|
||||||
|
if (status.contains("Ongoing")) {
|
||||||
|
return Manga.ONGOING;
|
||||||
|
}
|
||||||
|
if (status.contains("Completed")) {
|
||||||
|
return Manga.COMPLETED;
|
||||||
|
}
|
||||||
|
return Manga.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
|
||||||
|
List<Chapter> chapterList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element chapterElement : parsedDocument.select("div#chapters li div")) {
|
||||||
|
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
|
||||||
|
chapterList.add(currentChapter);
|
||||||
|
}
|
||||||
|
return chapterList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||||
|
Chapter chapter = Chapter.create();
|
||||||
|
|
||||||
|
Element urlElement = chapterElement.select("a.tips").first();
|
||||||
|
Element dateElement = chapterElement.select("span.date").first();
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
chapter.setUrl(urlElement.attr("href"));
|
||||||
|
chapter.name = urlElement.text();
|
||||||
|
}
|
||||||
|
if (dateElement != null) {
|
||||||
|
chapter.date_upload = parseUpdateFromElement(dateElement);
|
||||||
|
}
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseUpdateFromElement(Element updateElement) {
|
||||||
|
String updatedDateAsString = updateElement.text();
|
||||||
|
|
||||||
|
if (updatedDateAsString.contains("Today")) {
|
||||||
|
Calendar today = Calendar.getInstance();
|
||||||
|
today.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
today.set(Calendar.MINUTE, 0);
|
||||||
|
today.set(Calendar.SECOND, 0);
|
||||||
|
today.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Date withoutDay = new SimpleDateFormat("h:mm a", Locale.ENGLISH).parse(updatedDateAsString.replace("Today", ""));
|
||||||
|
return today.getTimeInMillis() + withoutDay.getTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return today.getTimeInMillis();
|
||||||
|
}
|
||||||
|
} else if (updatedDateAsString.contains("Yesterday")) {
|
||||||
|
Calendar yesterday = Calendar.getInstance();
|
||||||
|
yesterday.add(Calendar.DATE, -1);
|
||||||
|
yesterday.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
yesterday.set(Calendar.MINUTE, 0);
|
||||||
|
yesterday.set(Calendar.SECOND, 0);
|
||||||
|
yesterday.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Date withoutDay = new SimpleDateFormat("h:mm a", Locale.ENGLISH).parse(updatedDateAsString.replace("Yesterday", ""));
|
||||||
|
return yesterday.getTimeInMillis() + withoutDay.getTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return yesterday.getTimeInMillis();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Date specificDate = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(updatedDateAsString);
|
||||||
|
|
||||||
|
return specificDate.getTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
// Do Nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
|
||||||
|
List<String> pageUrlList = new ArrayList<>();
|
||||||
|
|
||||||
|
Elements pageUrlElements = parsedDocument.select("select.m").first().select("option:not([value=0])");
|
||||||
|
String baseUrl = parsedDocument.select("div#series a").first().attr("href").replace("1.html", "");
|
||||||
|
for (Element pageUrlElement : pageUrlElements) {
|
||||||
|
pageUrlList.add(baseUrl + pageUrlElement.attr("value") + ".html");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageUrlList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
|
||||||
|
Element imageElement = parsedDocument.getElementById("image");
|
||||||
|
return imageElement.attr("src");
|
||||||
|
}
|
||||||
|
}
|
@ -1,121 +0,0 @@
|
|||||||
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 okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
|
||||||
|
|
||||||
override val name = "Mangafox"
|
|
||||||
|
|
||||||
override val baseUrl = "http://mangafox.me"
|
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a.title").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) =
|
|
||||||
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a.series_preview").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "a:has(span.next)"
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
|
||||||
val infoElement = document.select("div#title").first()
|
|
||||||
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
|
|
||||||
val sideInfoElement = document.select("#series_info").first()
|
|
||||||
|
|
||||||
manga.author = rowElement.select("td:eq(1)").first()?.text()
|
|
||||||
manga.artist = rowElement.select("td:eq(2)").first()?.text()
|
|
||||||
manga.genre = rowElement.select("td:eq(3)").first()?.text()
|
|
||||||
manga.description = infoElement.select("p.summary").first()?.text()
|
|
||||||
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
|
|
||||||
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String) = when {
|
|
||||||
status.contains("Ongoing") -> Manga.ONGOING
|
|
||||||
status.contains("Completed") -> Manga.COMPLETED
|
|
||||||
else -> Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "div#chapters li div"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
|
||||||
val urlElement = element.select("a.tips").first()
|
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text()
|
|
||||||
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterDate(date: String): Long {
|
|
||||||
return if ("Today" in date || " ago" in date) {
|
|
||||||
Calendar.getInstance().apply {
|
|
||||||
set(Calendar.HOUR_OF_DAY, 0)
|
|
||||||
set(Calendar.MINUTE, 0)
|
|
||||||
set(Calendar.SECOND, 0)
|
|
||||||
set(Calendar.MILLISECOND, 0)
|
|
||||||
}.timeInMillis
|
|
||||||
} else if ("Yesterday" in date) {
|
|
||||||
Calendar.getInstance().apply {
|
|
||||||
add(Calendar.DATE, -1)
|
|
||||||
set(Calendar.HOUR_OF_DAY, 0)
|
|
||||||
set(Calendar.MINUTE, 0)
|
|
||||||
set(Calendar.SECOND, 0)
|
|
||||||
set(Calendar.MILLISECOND, 0)
|
|
||||||
}.timeInMillis
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
|
||||||
val document = Jsoup.parse(response.body().string())
|
|
||||||
|
|
||||||
val url = response.request().url().toString().substringBeforeLast('/')
|
|
||||||
document.select("select.m").first().select("option:not([value=0])").forEach {
|
|
||||||
pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not used, overrides parent.
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,313 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.english;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language;
|
||||||
|
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||||
|
import eu.kanade.tachiyomi.util.Parser;
|
||||||
|
|
||||||
|
public class Mangahere extends Source {
|
||||||
|
|
||||||
|
public static final String NAME = "Mangahere";
|
||||||
|
public static final String BASE_URL = "http://www.mangahere.co";
|
||||||
|
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
|
||||||
|
public static final String SEARCH_URL = BASE_URL + "/search.php?name=%s&page=%s&sort=views&order=za";
|
||||||
|
|
||||||
|
public Mangahere(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Language getLang() {
|
||||||
|
return LanguageKt.getEN();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialPopularMangasUrl() {
|
||||||
|
return String.format(POPULAR_MANGAS_URL, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialSearchUrl(String query) {
|
||||||
|
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element currentHtmlBlock : parsedHtml.select("div.directory_list > ul > li")) {
|
||||||
|
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
|
||||||
|
mangaList.add(currentManga);
|
||||||
|
}
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
|
||||||
|
Manga manga = new Manga();
|
||||||
|
manga.source = getId();
|
||||||
|
|
||||||
|
Element urlElement = Parser.element(htmlBlock, "div.title > a");
|
||||||
|
if (urlElement != null) {
|
||||||
|
manga.setUrl(urlElement.attr("href"));
|
||||||
|
manga.title = urlElement.attr("title");
|
||||||
|
}
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||||
|
Element next = Parser.element(parsedHtml, "div.next-page > a.next");
|
||||||
|
return next != null ? String.format(POPULAR_MANGAS_URL, next.attr("href")) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
Elements mangaHtmlBlocks = parsedHtml.select("div.result_search > dl");
|
||||||
|
for (Element currentHtmlBlock : mangaHtmlBlocks) {
|
||||||
|
Manga currentManga = constructSearchMangaFromHtmlBlock(currentHtmlBlock);
|
||||||
|
mangaList.add(currentManga);
|
||||||
|
}
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructSearchMangaFromHtmlBlock(Element htmlBlock) {
|
||||||
|
Manga manga = new Manga();
|
||||||
|
manga.source = getId();
|
||||||
|
|
||||||
|
Element urlElement = Parser.element(htmlBlock, "a.manga_info");
|
||||||
|
if (urlElement != null) {
|
||||||
|
manga.setUrl(urlElement.attr("href"));
|
||||||
|
manga.title = urlElement.text();
|
||||||
|
}
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||||
|
Element next = Parser.element(parsedHtml, "div.next-page > a.next");
|
||||||
|
return next != null ? BASE_URL + next.attr("href") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseUpdateFromElement(Element updateElement) {
|
||||||
|
String updatedDateAsString = updateElement.text();
|
||||||
|
|
||||||
|
if (updatedDateAsString.contains("Today")) {
|
||||||
|
Calendar today = Calendar.getInstance();
|
||||||
|
today.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
today.set(Calendar.MINUTE, 0);
|
||||||
|
today.set(Calendar.SECOND, 0);
|
||||||
|
today.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Date withoutDay = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString.replace("Today", ""));
|
||||||
|
return today.getTimeInMillis() + withoutDay.getTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return today.getTimeInMillis();
|
||||||
|
}
|
||||||
|
} else if (updatedDateAsString.contains("Yesterday")) {
|
||||||
|
Calendar yesterday = Calendar.getInstance();
|
||||||
|
yesterday.add(Calendar.DATE, -1);
|
||||||
|
yesterday.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
yesterday.set(Calendar.MINUTE, 0);
|
||||||
|
yesterday.set(Calendar.SECOND, 0);
|
||||||
|
yesterday.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Date withoutDay = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString.replace("Yesterday", ""));
|
||||||
|
return yesterday.getTimeInMillis() + withoutDay.getTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return yesterday.getTimeInMillis();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Date specificDate = new SimpleDateFormat("MMM d, yyyy h:mma", Locale.ENGLISH).parse(updatedDateAsString);
|
||||||
|
|
||||||
|
return specificDate.getTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
// Do Nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("<ul class=\"detail_topText\">");
|
||||||
|
int endIndex = unparsedHtml.indexOf("</ul>", beginIndex);
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||||
|
|
||||||
|
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||||
|
Element detailElement = parsedDocument.select("ul.detail_topText").first();
|
||||||
|
|
||||||
|
Manga manga = Manga.create(mangaUrl);
|
||||||
|
manga.author = Parser.text(parsedDocument, "a[href^=http://www.mangahere.co/author/]");
|
||||||
|
manga.artist = Parser.text(parsedDocument, "a[href^=http://www.mangahere.co/artist/]");
|
||||||
|
|
||||||
|
String description = Parser.text(detailElement, "#show");
|
||||||
|
if (description != null) {
|
||||||
|
manga.description = description.substring(0, description.length() - "Show less".length());
|
||||||
|
}
|
||||||
|
String genres = Parser.text(detailElement, "li:eq(3)");
|
||||||
|
if (genres != null) {
|
||||||
|
manga.genre = genres.substring("Genre(s):".length());
|
||||||
|
}
|
||||||
|
manga.status = parseStatus(Parser.text(detailElement, "li:eq(6)"));
|
||||||
|
|
||||||
|
beginIndex = unparsedHtml.indexOf("<img");
|
||||||
|
endIndex = unparsedHtml.indexOf("/>", beginIndex);
|
||||||
|
trimmedHtml = unparsedHtml.substring(beginIndex, endIndex + 2);
|
||||||
|
|
||||||
|
parsedDocument = Jsoup.parse(trimmedHtml);
|
||||||
|
manga.thumbnail_url = Parser.src(parsedDocument, "img");
|
||||||
|
|
||||||
|
manga.initialized = true;
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseStatus(String status) {
|
||||||
|
if (status.contains("Ongoing")) {
|
||||||
|
return Manga.ONGOING;
|
||||||
|
}
|
||||||
|
if (status.contains("Completed")) {
|
||||||
|
return Manga.COMPLETED;
|
||||||
|
}
|
||||||
|
return Manga.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("<ul>");
|
||||||
|
int endIndex = unparsedHtml.indexOf("</ul>", beginIndex);
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||||
|
|
||||||
|
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||||
|
|
||||||
|
List<Chapter> chapterList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element chapterElement : parsedDocument.getElementsByTag("li")) {
|
||||||
|
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
|
||||||
|
chapterList.add(currentChapter);
|
||||||
|
}
|
||||||
|
return chapterList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||||
|
Chapter chapter = Chapter.create();
|
||||||
|
|
||||||
|
Element urlElement = chapterElement.select("a").first();
|
||||||
|
Element dateElement = chapterElement.select("span.right").first();
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
chapter.setUrl(urlElement.attr("href"));
|
||||||
|
chapter.name = urlElement.text();
|
||||||
|
}
|
||||||
|
if (dateElement != null) {
|
||||||
|
chapter.date_upload = parseDateFromElement(dateElement);
|
||||||
|
}
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseDateFromElement(Element dateElement) {
|
||||||
|
String dateAsString = dateElement.text();
|
||||||
|
|
||||||
|
if (dateAsString.contains("Today")) {
|
||||||
|
Calendar today = Calendar.getInstance();
|
||||||
|
today.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
today.set(Calendar.MINUTE, 0);
|
||||||
|
today.set(Calendar.SECOND, 0);
|
||||||
|
today.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Date withoutDay = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString.replace("Today", ""));
|
||||||
|
return today.getTimeInMillis() + withoutDay.getTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return today.getTimeInMillis();
|
||||||
|
}
|
||||||
|
} else if (dateAsString.contains("Yesterday")) {
|
||||||
|
Calendar yesterday = Calendar.getInstance();
|
||||||
|
yesterday.add(Calendar.DATE, -1);
|
||||||
|
yesterday.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
yesterday.set(Calendar.MINUTE, 0);
|
||||||
|
yesterday.set(Calendar.SECOND, 0);
|
||||||
|
yesterday.set(Calendar.MILLISECOND, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Date withoutDay = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString.replace("Yesterday", ""));
|
||||||
|
return yesterday.getTimeInMillis() + withoutDay.getTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return yesterday.getTimeInMillis();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Date date = new SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(dateAsString);
|
||||||
|
|
||||||
|
return date.getTime();
|
||||||
|
} catch (ParseException e) {
|
||||||
|
// Do Nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("<div class=\"go_page clearfix\">");
|
||||||
|
int endIndex = unparsedHtml.indexOf("</div>", beginIndex);
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||||
|
|
||||||
|
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||||
|
|
||||||
|
List<String> pageUrlList = new ArrayList<>();
|
||||||
|
|
||||||
|
Elements pageUrlElements = parsedDocument.select("select.wid60").first().getElementsByTag("option");
|
||||||
|
for (Element pageUrlElement : pageUrlElements) {
|
||||||
|
pageUrlList.add(pageUrlElement.attr("value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageUrlList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String parseHtmlToImageUrl(String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("<section class=\"read_img\" id=\"viewer\">");
|
||||||
|
int endIndex = unparsedHtml.indexOf("</section>", beginIndex);
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||||
|
|
||||||
|
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||||
|
|
||||||
|
Element imageElement = parsedDocument.getElementById("image");
|
||||||
|
|
||||||
|
return imageElement.attr("src");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,113 +0,0 @@
|
|||||||
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 org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
|
||||||
|
|
||||||
override val name = "Mangahere"
|
|
||||||
|
|
||||||
override val baseUrl = "http://www.mangahere.co"
|
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("div.title > a").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) =
|
|
||||||
"$baseUrl/search.php?name=$query&page=1&sort=views&order=za"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a.manga_info").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
|
||||||
val detailElement = document.select(".manga_detail_top").first()
|
|
||||||
val infoElement = detailElement.select(".detail_topText").first()
|
|
||||||
|
|
||||||
manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text()
|
|
||||||
manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text()
|
|
||||||
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
|
|
||||||
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
|
|
||||||
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
|
|
||||||
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String) = when {
|
|
||||||
status.contains("Ongoing") -> Manga.ONGOING
|
|
||||||
status.contains("Completed") -> Manga.COMPLETED
|
|
||||||
else -> Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
|
||||||
val urlElement = element.select("a").first()
|
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text()
|
|
||||||
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterDate(date: String): Long {
|
|
||||||
return if ("Today" in date) {
|
|
||||||
Calendar.getInstance().apply {
|
|
||||||
set(Calendar.HOUR_OF_DAY, 0)
|
|
||||||
set(Calendar.MINUTE, 0)
|
|
||||||
set(Calendar.SECOND, 0)
|
|
||||||
set(Calendar.MILLISECOND, 0)
|
|
||||||
}.timeInMillis
|
|
||||||
} else if ("Yesterday" in date) {
|
|
||||||
Calendar.getInstance().apply {
|
|
||||||
add(Calendar.DATE, -1)
|
|
||||||
set(Calendar.HOUR_OF_DAY, 0)
|
|
||||||
set(Calendar.MINUTE, 0)
|
|
||||||
set(Calendar.SECOND, 0)
|
|
||||||
set(Calendar.MILLISECOND, 0)
|
|
||||||
}.timeInMillis
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
|
||||||
document.select("select.wid60").first().getElementsByTag("option").forEach {
|
|
||||||
pages.add(Page(pages.size, it.attr("value")))
|
|
||||||
}
|
|
||||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,290 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.english;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
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.Language;
|
||||||
|
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||||
|
import eu.kanade.tachiyomi.util.Parser;
|
||||||
|
import okhttp3.Headers;
|
||||||
|
import rx.Observable;
|
||||||
|
import rx.functions.Action1;
|
||||||
|
import rx.functions.Func1;
|
||||||
|
|
||||||
|
public class ReadMangaToday extends Source {
|
||||||
|
public static final String NAME = "ReadMangaToday";
|
||||||
|
public static final String BASE_URL = "http://www.readmanga.today";
|
||||||
|
public static final String POPULAR_MANGAS_URL = BASE_URL + "/hot-manga/%s";
|
||||||
|
public static final String SEARCH_URL = BASE_URL + "/service/search?q=%s";
|
||||||
|
|
||||||
|
private static JsonParser parser = new JsonParser();
|
||||||
|
private static Gson gson = new Gson();
|
||||||
|
|
||||||
|
public ReadMangaToday(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialPopularMangasUrl() {
|
||||||
|
return String.format(POPULAR_MANGAS_URL, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialSearchUrl(String query) {
|
||||||
|
return String.format(SEARCH_URL, Uri.encode(query), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Language getLang() {
|
||||||
|
return LanguageKt.getEN();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element currentHtmlBlock : parsedHtml.select("div.hot-manga > div.style-list > div.box")) {
|
||||||
|
Manga currentManga = constructPopularMangaFromHtmlBlock(currentHtmlBlock);
|
||||||
|
mangaList.add(currentManga);
|
||||||
|
}
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructPopularMangaFromHtmlBlock(Element htmlBlock) {
|
||||||
|
Manga manga = new Manga();
|
||||||
|
manga.source = getId();
|
||||||
|
|
||||||
|
Element urlElement = Parser.element(htmlBlock, "div.title > h2 > a");
|
||||||
|
if (urlElement != null) {
|
||||||
|
manga.setUrl(urlElement.attr("href"));
|
||||||
|
manga.title = urlElement.attr("title");
|
||||||
|
}
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||||
|
Element next = Parser.element(parsedHtml, "div.hot-manga > ul.pagination > li > a:contains(»)");
|
||||||
|
return next != null ? next.attr("href") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Observable<MangasPage> searchMangasFromNetwork(final MangasPage page, String query) {
|
||||||
|
return networkService
|
||||||
|
.requestBody(searchMangaRequest(page, query), true)
|
||||||
|
.doOnNext(new Action1<String>() {
|
||||||
|
@Override
|
||||||
|
public void call(String doc) {
|
||||||
|
page.mangas = ReadMangaToday.this.parseSearchFromJson(doc);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(new Func1<String, MangasPage>() {
|
||||||
|
@Override
|
||||||
|
public MangasPage call(String response) {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Headers.Builder headersBuilder() {
|
||||||
|
return super.headersBuilder().add("X-Requested-With", "XMLHttpRequest");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<Manga> parseSearchFromJson(String unparsedJson) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
JsonArray mangasArray = parser.parse(unparsedJson).getAsJsonArray();
|
||||||
|
|
||||||
|
for (JsonElement mangaElement : mangasArray) {
|
||||||
|
Manga currentManga = constructSearchMangaFromJsonObject(mangaElement.getAsJsonObject());
|
||||||
|
mangaList.add(currentManga);
|
||||||
|
}
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructSearchMangaFromJsonObject(JsonObject jsonObject) {
|
||||||
|
Manga manga = new Manga();
|
||||||
|
manga.source = getId();
|
||||||
|
|
||||||
|
manga.setUrl(gson.fromJson(jsonObject.get("url"), String.class));
|
||||||
|
manga.title = gson.fromJson(jsonObject.get("title"), String.class);
|
||||||
|
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
|
||||||
|
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||||
|
|
||||||
|
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||||
|
Element detailElement = parsedDocument.select("div.movie-meta").first();
|
||||||
|
|
||||||
|
Manga manga = Manga.create(mangaUrl);
|
||||||
|
for (Element castHtmlBlock : parsedDocument.select("div.cast ul.cast-list > li")) {
|
||||||
|
String name = Parser.text(castHtmlBlock, "ul > li > a");
|
||||||
|
String role = Parser.text(castHtmlBlock, "ul > li:eq(1)");
|
||||||
|
if (role.equals("Author")) {
|
||||||
|
manga.author = name;
|
||||||
|
} else if (role.equals("Artist")) {
|
||||||
|
manga.artist = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String description = Parser.text(detailElement, "li.movie-detail");
|
||||||
|
if (description != null) {
|
||||||
|
manga.description = description;
|
||||||
|
}
|
||||||
|
String genres = Parser.text(detailElement, "dl.dl-horizontal > dd:eq(5)");
|
||||||
|
if (genres != null) {
|
||||||
|
manga.genre = genres;
|
||||||
|
}
|
||||||
|
manga.status = parseStatus(Parser.text(detailElement, "dl.dl-horizontal > dd:eq(3)"));
|
||||||
|
manga.thumbnail_url = Parser.src(detailElement, "img.img-responsive");
|
||||||
|
|
||||||
|
manga.initialized = true;
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseStatus(String status) {
|
||||||
|
if (status.contains("Ongoing")) {
|
||||||
|
return Manga.ONGOING;
|
||||||
|
} else if (status.contains("Completed")) {
|
||||||
|
return Manga.COMPLETED;
|
||||||
|
}
|
||||||
|
return Manga.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
|
||||||
|
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||||
|
|
||||||
|
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||||
|
|
||||||
|
List<Chapter> chapterList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element chapterElement : parsedDocument.select("ul.chp_lst > li")) {
|
||||||
|
Chapter currentChapter = constructChapterFromHtmlBlock(chapterElement);
|
||||||
|
chapterList.add(currentChapter);
|
||||||
|
}
|
||||||
|
return chapterList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||||
|
Chapter chapter = Chapter.create();
|
||||||
|
|
||||||
|
Element urlElement = chapterElement.select("a").first();
|
||||||
|
Element dateElement = chapterElement.select("span.dte").first();
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
chapter.setUrl(urlElement.attr("href"));
|
||||||
|
chapter.name = urlElement.select("span.val").text();
|
||||||
|
}
|
||||||
|
if (dateElement != null) {
|
||||||
|
chapter.date_upload = parseDateFromElement(dateElement);
|
||||||
|
}
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseDateFromElement(Element dateElement) {
|
||||||
|
String dateAsString = dateElement.text();
|
||||||
|
String[] dateWords = dateAsString.split(" ");
|
||||||
|
|
||||||
|
if (dateWords.length == 3) {
|
||||||
|
int timeAgo = Integer.parseInt(dateWords[0]);
|
||||||
|
Calendar date = Calendar.getInstance();
|
||||||
|
|
||||||
|
if (dateWords[1].contains("Minute")) {
|
||||||
|
date.add(Calendar.MINUTE, - timeAgo);
|
||||||
|
} else if (dateWords[1].contains("Hour")) {
|
||||||
|
date.add(Calendar.HOUR_OF_DAY, - timeAgo);
|
||||||
|
} else if (dateWords[1].contains("Day")) {
|
||||||
|
date.add(Calendar.DAY_OF_YEAR, -timeAgo);
|
||||||
|
} else if (dateWords[1].contains("Week")) {
|
||||||
|
date.add(Calendar.WEEK_OF_YEAR, -timeAgo);
|
||||||
|
} else if (dateWords[1].contains("Month")) {
|
||||||
|
date.add(Calendar.MONTH, -timeAgo);
|
||||||
|
} else if (dateWords[1].contains("Year")) {
|
||||||
|
date.add(Calendar.YEAR, -timeAgo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.getTimeInMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
|
||||||
|
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||||
|
|
||||||
|
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||||
|
|
||||||
|
List<String> pageUrlList = new ArrayList<>();
|
||||||
|
|
||||||
|
Elements pageUrlElements = parsedDocument.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option");
|
||||||
|
for (Element pageUrlElement : pageUrlElements) {
|
||||||
|
pageUrlList.add(pageUrlElement.attr("value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageUrlList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String parseHtmlToImageUrl(String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("<!-- content start -->");
|
||||||
|
int endIndex = unparsedHtml.indexOf("<!-- /content-end -->", beginIndex);
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex, endIndex);
|
||||||
|
|
||||||
|
Document parsedDocument = Jsoup.parse(trimmedHtml);
|
||||||
|
|
||||||
|
Element imageElement = Parser.element(parsedDocument, "img.img-responsive-2");
|
||||||
|
|
||||||
|
return imageElement.attr("src");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,127 +0,0 @@
|
|||||||
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.network.POST
|
|
||||||
import eu.kanade.tachiyomi.data.source.EN
|
|
||||||
import eu.kanade.tachiyomi.data.source.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
|
||||||
|
|
||||||
override val name = "ReadMangaToday"
|
|
||||||
|
|
||||||
override val baseUrl = "http://www.readmanga.today"
|
|
||||||
|
|
||||||
override val lang: Language get() = EN
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("div.title > h2 > a").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.attr("title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) =
|
|
||||||
"$baseUrl/search"
|
|
||||||
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = searchMangaInitialUrl(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder = okhttp3.FormBody.Builder()
|
|
||||||
builder.add("query", query)
|
|
||||||
|
|
||||||
return POST(page.url, headers, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.content-list > div.style-list > div.box"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("div.title > h2 > a").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.attr("title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
|
||||||
val detailElement = document.select("div.movie-meta").first()
|
|
||||||
|
|
||||||
manga.author = document.select("ul.cast-list li.director > ul a").first()?.text()
|
|
||||||
manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text()
|
|
||||||
manga.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text()
|
|
||||||
manga.description = detailElement.select("li.movie-detail").first()?.text()
|
|
||||||
manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) }
|
|
||||||
manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String) = when {
|
|
||||||
status.contains("Ongoing") -> Manga.ONGOING
|
|
||||||
status.contains("Completed") -> Manga.COMPLETED
|
|
||||||
else -> Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "ul.chp_lst > li"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
|
||||||
val urlElement = element.select("a").first()
|
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.select("span.val").text()
|
|
||||||
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterDate(date: String): Long {
|
|
||||||
val dateWords : List<String> = date.split(" ")
|
|
||||||
|
|
||||||
if (dateWords.size == 3) {
|
|
||||||
val timeAgo = Integer.parseInt(dateWords[0])
|
|
||||||
var date : Calendar = Calendar.getInstance()
|
|
||||||
|
|
||||||
if (dateWords[1].contains("Minute")) {
|
|
||||||
date.add(Calendar.MINUTE, - timeAgo)
|
|
||||||
} else if (dateWords[1].contains("Hour")) {
|
|
||||||
date.add(Calendar.HOUR_OF_DAY, - timeAgo)
|
|
||||||
} else if (dateWords[1].contains("Day")) {
|
|
||||||
date.add(Calendar.DAY_OF_YEAR, -timeAgo)
|
|
||||||
} else if (dateWords[1].contains("Week")) {
|
|
||||||
date.add(Calendar.WEEK_OF_YEAR, -timeAgo)
|
|
||||||
} else if (dateWords[1].contains("Month")) {
|
|
||||||
date.add(Calendar.MONTH, -timeAgo)
|
|
||||||
} else if (dateWords[1].contains("Year")) {
|
|
||||||
date.add(Calendar.YEAR, -timeAgo)
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.getTimeInMillis()
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
|
||||||
document.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option").forEach {
|
|
||||||
pages.add(Page(pages.size, it.attr("value")))
|
|
||||||
}
|
|
||||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,240 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.russian;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language;
|
||||||
|
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||||
|
import eu.kanade.tachiyomi.util.Parser;
|
||||||
|
|
||||||
|
public class Mangachan extends Source {
|
||||||
|
|
||||||
|
public static final String NAME = "Mangachan";
|
||||||
|
public static final String BASE_URL = "http://mangachan.ru";
|
||||||
|
public static final String POPULAR_MANGAS_URL = BASE_URL + "/mostfavorites";
|
||||||
|
public static final String SEARCH_URL = BASE_URL + "/?do=search&subaction=search&story=%s";
|
||||||
|
|
||||||
|
public Mangachan(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Language getLang() {
|
||||||
|
return LanguageKt.getRU();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialPopularMangasUrl() {
|
||||||
|
return POPULAR_MANGAS_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialSearchUrl(String query) {
|
||||||
|
return String.format(SEARCH_URL, Uri.encode(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element currentHtmlBlock : parsedHtml.select("div.content_row")) {
|
||||||
|
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
|
||||||
|
mangaList.add(manga);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
|
||||||
|
Manga manga = new Manga();
|
||||||
|
manga.source = getId();
|
||||||
|
|
||||||
|
Element urlElement = currentHtmlBlock.getElementsByTag("h2").select("a").first();
|
||||||
|
Element imgElement = currentHtmlBlock.getElementsByClass("manga_images").select("img").first();
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
manga.setUrl(urlElement.attr("href"));
|
||||||
|
manga.title = urlElement.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imgElement != null) {
|
||||||
|
manga.thumbnail_url = BASE_URL + imgElement.attr("src");
|
||||||
|
}
|
||||||
|
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||||
|
String path = Parser.href(parsedHtml, "a:contains(Вперед)");
|
||||||
|
return path != null ? POPULAR_MANGAS_URL + path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||||
|
return parsePopularMangasFromHtml(parsedHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
|
||||||
|
Element infoElement = parsedDocument.getElementsByClass("mangatitle").first();
|
||||||
|
String description = parsedDocument.getElementById("description").text();
|
||||||
|
|
||||||
|
Manga manga = Manga.create(mangaUrl);
|
||||||
|
|
||||||
|
manga.author = infoElement.select("tr:eq(2) 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.description = description.replaceAll("Прислать описание", "");
|
||||||
|
|
||||||
|
manga.initialized = true;
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseStatus(String status) {
|
||||||
|
if (status.contains("перевод продолжается")) {
|
||||||
|
return Manga.ONGOING;
|
||||||
|
} else if (status.contains("перевод завершен")) {
|
||||||
|
return Manga.COMPLETED;
|
||||||
|
} else return Manga.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
List<Chapter> chapterList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element chapterElement : parsedDocument.select("table.table_cha tr:gt(1)")) {
|
||||||
|
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
||||||
|
chapterList.add(chapter);
|
||||||
|
}
|
||||||
|
return chapterList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||||
|
Chapter chapter = Chapter.create();
|
||||||
|
|
||||||
|
Element urlElement = chapterElement.select("a").first();
|
||||||
|
String date = Parser.text(chapterElement, "div.date");
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
chapter.name = urlElement.text();
|
||||||
|
chapter.url = urlElement.attr("href");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date != null) {
|
||||||
|
try {
|
||||||
|
chapter.date_upload = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse(date).getTime();
|
||||||
|
} catch (ParseException e) { /* Ignore */ }
|
||||||
|
}
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without this extra chapters are in the wrong place in the list
|
||||||
|
@Override
|
||||||
|
public void parseChapterNumber(Chapter chapter) {
|
||||||
|
// For chapters with url like /online/254903-fairy-tail_v56_ch474.html
|
||||||
|
String url = chapter.url.replace(".html", "");
|
||||||
|
Pattern pattern = Pattern.compile("\\d+_ch[\\d.]+");
|
||||||
|
Matcher matcher = pattern.matcher(url);
|
||||||
|
|
||||||
|
if (matcher.find()) {
|
||||||
|
String[] parts = matcher.group().split("_ch");
|
||||||
|
chapter.chapter_number = Float.parseFloat(parts[0] + "." + AddZero(parts[1]));
|
||||||
|
} else { // For chapters with url like /online/61216-3298.html
|
||||||
|
String name = chapter.name;
|
||||||
|
name = name.replaceAll("[\\s\\d\\w\\W]+v", "");
|
||||||
|
String volume = name.substring(0, name.indexOf(" - "));
|
||||||
|
String[] parts = name.replaceFirst("\\d+ - ", "").split(" ");
|
||||||
|
|
||||||
|
chapter.chapter_number = Float.parseFloat(volume + "." + AddZero(parts[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String AddZero(String num) {
|
||||||
|
if (Float.parseFloat(num) < 1000f) {
|
||||||
|
num = "0" + num.replace(".", "");
|
||||||
|
}
|
||||||
|
if (Float.parseFloat(num) < 100f) {
|
||||||
|
num = "0" + num.replace(".", "");
|
||||||
|
}
|
||||||
|
if (Float.parseFloat(num) < 10f) {
|
||||||
|
num = "0" + num.replace(".", "");
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||||
|
ArrayList<String> pages = new ArrayList<>();
|
||||||
|
|
||||||
|
int beginIndex = unparsedHtml.indexOf("fullimg\":[");
|
||||||
|
int endIndex = unparsedHtml.indexOf("]", beginIndex);
|
||||||
|
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex + 10, endIndex);
|
||||||
|
trimmedHtml = trimmedHtml.replaceAll("\"", "");
|
||||||
|
|
||||||
|
String[] pageUrls = trimmedHtml.split(",");
|
||||||
|
for (int i = 0; i < pageUrls.length; i++) {
|
||||||
|
pages.add("");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("fullimg\":[");
|
||||||
|
int endIndex = unparsedHtml.indexOf("]", beginIndex);
|
||||||
|
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex + 10, endIndex);
|
||||||
|
trimmedHtml = trimmedHtml.replaceAll("\"", "");
|
||||||
|
|
||||||
|
String[] pageUrls = trimmedHtml.split(",");
|
||||||
|
for (int i = 0; i < pageUrls.length; i++) {
|
||||||
|
pages.get(i).setImageUrl(pageUrls[i].replaceAll("im.?\\.", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (List<Page>) pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,95 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.russian
|
|
||||||
|
|
||||||
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.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.RU
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
|
||||||
|
|
||||||
override val name = "Mangachan"
|
|
||||||
|
|
||||||
override val baseUrl = "http://mangachan.ru"
|
|
||||||
|
|
||||||
override val lang: Language get() = RU
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/?do=search&subaction=search&story=$query"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.content_row"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("h2 > a").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
element.select("img").first().let {
|
|
||||||
manga.thumbnail_url = baseUrl + it.attr("src")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
|
||||||
val infoElement = document.select("table.mangatitle").first()
|
|
||||||
val descElement = document.select("div#description").first()
|
|
||||||
|
|
||||||
manga.author = infoElement.select("tr:eq(2) > 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.description = descElement.textNodes().first().text()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(element: String): Int {
|
|
||||||
when {
|
|
||||||
element.contains("перевод завершен") -> return Manga.COMPLETED
|
|
||||||
element.contains("перевод продолжается") -> return Manga.ONGOING
|
|
||||||
else -> return Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
|
||||||
val urlElement = element.select("a").first()
|
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text()
|
|
||||||
chapter.date_upload = element.select("div.date").first()?.text()?.let {
|
|
||||||
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
|
|
||||||
} ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
|
||||||
val html = response.body().string()
|
|
||||||
val beginIndex = html.indexOf("fullimg\":[") + 10
|
|
||||||
val endIndex = html.indexOf(",]", beginIndex)
|
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
|
||||||
val pageUrls = trimmedHtml.split(',')
|
|
||||||
|
|
||||||
for ((i, url) in pageUrls.withIndex()) {
|
|
||||||
pages.add(Page(i, "", url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
}
|
|
@ -0,0 +1,225 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.russian;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language;
|
||||||
|
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||||
|
import eu.kanade.tachiyomi.util.Parser;
|
||||||
|
|
||||||
|
public class Mintmanga extends Source {
|
||||||
|
|
||||||
|
public static final String NAME = "Mintmanga";
|
||||||
|
public static final String BASE_URL = "http://mintmanga.com";
|
||||||
|
public static final String POPULAR_MANGAS_URL = BASE_URL + "/list?sortType=rate";
|
||||||
|
public static final String SEARCH_URL = BASE_URL + "/search?q=%s";
|
||||||
|
|
||||||
|
public Mintmanga(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Language getLang() {
|
||||||
|
return LanguageKt.getRU();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialPopularMangasUrl() {
|
||||||
|
return POPULAR_MANGAS_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialSearchUrl(String query) {
|
||||||
|
return String.format(SEARCH_URL, Uri.encode(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element currentHtmlBlock : parsedHtml.select("div.desc")) {
|
||||||
|
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
|
||||||
|
mangaList.add(manga);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
|
||||||
|
Manga manga = new Manga();
|
||||||
|
manga.source = getId();
|
||||||
|
|
||||||
|
Element urlElement = currentHtmlBlock.getElementsByTag("h3").select("a").first();
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
manga.setUrl(urlElement.attr("href"));
|
||||||
|
manga.title = urlElement.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||||
|
String path = Parser.href(parsedHtml, "a:contains(→)");
|
||||||
|
return path != null ? BASE_URL + path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||||
|
return parsePopularMangasFromHtml(parsedHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
Element infoElement = parsedDocument.select("div.leftContent").first();
|
||||||
|
|
||||||
|
Manga manga = Manga.create(mangaUrl);
|
||||||
|
manga.title = Parser.text(infoElement, "span.eng-name");
|
||||||
|
manga.author = Parser.text(infoElement, "span.elem_author ");
|
||||||
|
manga.genre = Parser.allText(infoElement, "span.elem_genre ").replaceAll(" ,", ",");
|
||||||
|
manga.description = Parser.allText(infoElement, "div.manga-description");
|
||||||
|
if (Parser.text(infoElement, "h1.names:contains(Сингл)") != null) {
|
||||||
|
manga.status = Manga.COMPLETED;
|
||||||
|
} else {
|
||||||
|
manga.status = parseStatus(Parser.text(infoElement, "p:has(b:contains(Перевод:))"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String thumbnail = Parser.element(infoElement, "img").attr("data-full");
|
||||||
|
if (thumbnail != null) {
|
||||||
|
manga.thumbnail_url = thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.initialized = true;
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseStatus(String status) {
|
||||||
|
if (status.contains("продолжается")) {
|
||||||
|
return Manga.ONGOING;
|
||||||
|
}
|
||||||
|
if (status.contains("завершен")) {
|
||||||
|
return Manga.COMPLETED;
|
||||||
|
}
|
||||||
|
return Manga.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
List<Chapter> chapterList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element chapterElement : parsedDocument.select("div.chapters-link tbody tr")) {
|
||||||
|
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
||||||
|
chapterList.add(chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapterList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||||
|
Chapter chapter = Chapter.create();
|
||||||
|
|
||||||
|
Element urlElement = Parser.element(chapterElement, "a");
|
||||||
|
String date = Parser.text(chapterElement, "td:eq(1)");
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
chapter.setUrl(urlElement.attr("href") + "?mature=1");
|
||||||
|
chapter.name = urlElement.text().replaceAll(" новое", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date != null) {
|
||||||
|
try {
|
||||||
|
chapter.date_upload = new SimpleDateFormat("dd/MM/yy", Locale.ENGLISH).parse(date).getTime();
|
||||||
|
} catch (ParseException e) { /* Ignore */ }
|
||||||
|
}
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without this extra chapters are in the wrong place in the list
|
||||||
|
@Override
|
||||||
|
public void parseChapterNumber(Chapter chapter) {
|
||||||
|
String url = chapter.url.replace("?mature=1", "");
|
||||||
|
|
||||||
|
String[] urlParts = url.replaceAll("/[\\w\\d]+/vol", "").split("/");
|
||||||
|
if (Float.parseFloat(urlParts[1]) < 1000f) {
|
||||||
|
urlParts[1] = "0" + urlParts[1];
|
||||||
|
}
|
||||||
|
if (Float.parseFloat(urlParts[1]) < 100f) {
|
||||||
|
urlParts[1] = "0" + urlParts[1];
|
||||||
|
}
|
||||||
|
if (Float.parseFloat(urlParts[1]) < 10f) {
|
||||||
|
urlParts[1] = "0" + urlParts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.chapter_number = Float.parseFloat(urlParts[0] + "." + urlParts[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||||
|
ArrayList<String> pages = new ArrayList<>();
|
||||||
|
|
||||||
|
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
|
||||||
|
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
|
||||||
|
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
|
||||||
|
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
|
||||||
|
String[] pageUrls = trimmedHtml.split("],\\[");
|
||||||
|
for (int i = 0; i < pageUrls.length; i++) {
|
||||||
|
pages.add("");
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
|
||||||
|
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
|
||||||
|
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
|
||||||
|
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
|
||||||
|
String[] pageUrls = trimmedHtml.split("],\\[");
|
||||||
|
for (int i = 0; i < pageUrls.length; i++) {
|
||||||
|
String[] urlParts = pageUrls[i].split(","); // auto/06/35,http://e4.adultmanga.me/,/55/01.png
|
||||||
|
String page = urlParts[1] + urlParts[0] + urlParts[2];
|
||||||
|
pages.get(i).setImageUrl(page);
|
||||||
|
}
|
||||||
|
return (List<Page>) pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,102 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.russian
|
|
||||||
|
|
||||||
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.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.RU
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
|
||||||
|
|
||||||
override val name = "Mintmanga"
|
|
||||||
|
|
||||||
override val baseUrl = "http://mintmanga.com"
|
|
||||||
|
|
||||||
override val lang: Language get() = RU
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.desc"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("h3 > a").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.attr("title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
|
||||||
val infoElement = document.select("div.leftContent").first()
|
|
||||||
|
|
||||||
manga.author = infoElement.select("span.elem_author").first()?.text()
|
|
||||||
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
|
|
||||||
manga.description = infoElement.select("div.manga-description").text()
|
|
||||||
manga.status = parseStatus(infoElement.html())
|
|
||||||
manga.thumbnail_url = infoElement.select("img").attr("data-full")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(element: String): Int {
|
|
||||||
when {
|
|
||||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED
|
|
||||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED
|
|
||||||
element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING
|
|
||||||
else -> return Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
|
||||||
val urlElement = element.select("a").first()
|
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href") + "?mature=1")
|
|
||||||
chapter.name = urlElement.text().replace(" новое", "")
|
|
||||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
|
||||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
|
||||||
} ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parseChapterNumber(chapter: Chapter) {
|
|
||||||
chapter.chapter_number = -2f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
|
||||||
val html = response.body().string()
|
|
||||||
val beginIndex = html.indexOf("rm_h.init( [")
|
|
||||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("[\"\']+".toRegex(), "")
|
|
||||||
|
|
||||||
val p = Pattern.compile("auto/[\\w/]+,http://[\\w.]+/,/[\\w./]+.(png|jpg)+")
|
|
||||||
val m = p.matcher(trimmedHtml)
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
while (m.find()) {
|
|
||||||
val urlParts = m.group().split(',')
|
|
||||||
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
}
|
|
@ -0,0 +1,225 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.source.online.russian;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
|
import eu.kanade.tachiyomi.data.source.Language;
|
||||||
|
import eu.kanade.tachiyomi.data.source.LanguageKt;
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||||
|
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||||
|
import eu.kanade.tachiyomi.util.Parser;
|
||||||
|
|
||||||
|
public class Readmanga extends Source {
|
||||||
|
|
||||||
|
public static final String NAME = "Readmanga";
|
||||||
|
public static final String BASE_URL = "http://readmanga.me";
|
||||||
|
public static final String POPULAR_MANGAS_URL = BASE_URL + "/list?sortType=rate";
|
||||||
|
public static final String SEARCH_URL = BASE_URL + "/search?q=%s";
|
||||||
|
|
||||||
|
public Readmanga(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Language getLang() {
|
||||||
|
return LanguageKt.getRU();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialPopularMangasUrl() {
|
||||||
|
return POPULAR_MANGAS_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getInitialSearchUrl(String query) {
|
||||||
|
return String.format(SEARCH_URL, Uri.encode(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||||
|
List<Manga> mangaList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element currentHtmlBlock : parsedHtml.select("div.desc")) {
|
||||||
|
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
|
||||||
|
mangaList.add(manga);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mangaList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Manga constructPopularMangaFromHtml(Element currentHtmlBlock) {
|
||||||
|
Manga manga = new Manga();
|
||||||
|
manga.source = getId();
|
||||||
|
|
||||||
|
Element urlElement = currentHtmlBlock.getElementsByTag("h3").select("a").first();
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
manga.setUrl(urlElement.attr("href"));
|
||||||
|
manga.title = urlElement.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||||
|
String path = Parser.href(parsedHtml, "a:contains(→)");
|
||||||
|
return path != null ? BASE_URL + path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||||
|
return parsePopularMangasFromHtml(parsedHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
Element infoElement = parsedDocument.select("div.leftContent").first();
|
||||||
|
|
||||||
|
Manga manga = Manga.create(mangaUrl);
|
||||||
|
manga.title = Parser.text(infoElement, "span.eng-name");
|
||||||
|
manga.author = Parser.text(infoElement, "span.elem_author ");
|
||||||
|
manga.genre = Parser.allText(infoElement, "span.elem_genre ").replaceAll(" ,", ",");
|
||||||
|
manga.description = Parser.allText(infoElement, "div.manga-description");
|
||||||
|
if (Parser.text(infoElement, "h1.names:contains(Сингл)") != null) {
|
||||||
|
manga.status = Manga.COMPLETED;
|
||||||
|
} else {
|
||||||
|
manga.status = parseStatus(Parser.text(infoElement, "p:has(b:contains(Перевод:))"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String thumbnail = Parser.element(infoElement, "img").attr("data-full");
|
||||||
|
if (thumbnail != null) {
|
||||||
|
manga.thumbnail_url = thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.initialized = true;
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseStatus(String status) {
|
||||||
|
if (status.contains("продолжается")) {
|
||||||
|
return Manga.ONGOING;
|
||||||
|
}
|
||||||
|
if (status.contains("завершен")) {
|
||||||
|
return Manga.COMPLETED;
|
||||||
|
}
|
||||||
|
return Manga.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||||
|
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||||
|
List<Chapter> chapterList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Element chapterElement : parsedDocument.select("div.chapters-link tbody tr")) {
|
||||||
|
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
|
||||||
|
chapterList.add(chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapterList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
|
||||||
|
Chapter chapter = Chapter.create();
|
||||||
|
|
||||||
|
Element urlElement = Parser.element(chapterElement, "a");
|
||||||
|
String date = Parser.text(chapterElement, "td:eq(1)");
|
||||||
|
|
||||||
|
if (urlElement != null) {
|
||||||
|
chapter.setUrl(urlElement.attr("href") + "?mature=1");
|
||||||
|
chapter.name = urlElement.text().replaceAll(" новое", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date != null) {
|
||||||
|
try {
|
||||||
|
chapter.date_upload = new SimpleDateFormat("dd/MM/yy", Locale.ENGLISH).parse(date).getTime();
|
||||||
|
} catch (ParseException e) { /* Ignore */ }
|
||||||
|
}
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without this extra chapters are in the wrong place in the list
|
||||||
|
@Override
|
||||||
|
public void parseChapterNumber(Chapter chapter) {
|
||||||
|
String url = chapter.url.replace("?mature=1", "");
|
||||||
|
|
||||||
|
String[] urlParts = url.replaceAll("/[\\w\\d]+/vol", "").split("/");
|
||||||
|
if (Float.parseFloat(urlParts[1]) < 1000f) {
|
||||||
|
urlParts[1] = "0" + urlParts[1];
|
||||||
|
}
|
||||||
|
if (Float.parseFloat(urlParts[1]) < 100f) {
|
||||||
|
urlParts[1] = "0" + urlParts[1];
|
||||||
|
}
|
||||||
|
if (Float.parseFloat(urlParts[1]) < 10f) {
|
||||||
|
urlParts[1] = "0" + urlParts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.chapter_number = Float.parseFloat(urlParts[0] + "." + urlParts[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||||
|
ArrayList<String> pages = new ArrayList<>();
|
||||||
|
|
||||||
|
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
|
||||||
|
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
|
||||||
|
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
|
||||||
|
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
|
||||||
|
String[] pageUrls = trimmedHtml.split("],\\[");
|
||||||
|
for (int i = 0; i < pageUrls.length; i++) {
|
||||||
|
pages.add("");
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
|
||||||
|
int beginIndex = unparsedHtml.indexOf("rm_h.init( [");
|
||||||
|
int endIndex = unparsedHtml.indexOf("], 0, false);", beginIndex);
|
||||||
|
|
||||||
|
String trimmedHtml = unparsedHtml.substring(beginIndex + 13, endIndex);
|
||||||
|
trimmedHtml = trimmedHtml.replaceAll("[\"']", "");
|
||||||
|
String[] pageUrls = trimmedHtml.split("],\\[");
|
||||||
|
for (int i = 0; i < pageUrls.length; i++) {
|
||||||
|
String[] urlParts = pageUrls[i].split(","); // auto/12/56,http://e7.postfact.ru/,/51/01.jpg_res.jpg
|
||||||
|
String page = urlParts[1] + urlParts[0] + urlParts[2];
|
||||||
|
pages.get(i).setImageUrl(page);
|
||||||
|
}
|
||||||
|
return (List<Page>) pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,102 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.russian
|
|
||||||
|
|
||||||
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.Language
|
|
||||||
import eu.kanade.tachiyomi.data.source.RU
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(context) {
|
|
||||||
|
|
||||||
override val name = "Readmanga"
|
|
||||||
|
|
||||||
override val baseUrl = "http://readmanga.me"
|
|
||||||
|
|
||||||
override val lang: Language get() = RU
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.desc"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("h3 > a").first().let {
|
|
||||||
manga.setUrl(it.attr("href"))
|
|
||||||
manga.title = it.attr("title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
|
||||||
val infoElement = document.select("div.leftContent").first()
|
|
||||||
|
|
||||||
manga.author = infoElement.select("span.elem_author").first()?.text()
|
|
||||||
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
|
|
||||||
manga.description = infoElement.select("div.manga-description").text()
|
|
||||||
manga.status = parseStatus(infoElement.html())
|
|
||||||
manga.thumbnail_url = infoElement.select("img").attr("data-full")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(element: String): Int {
|
|
||||||
when {
|
|
||||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED
|
|
||||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED
|
|
||||||
element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING
|
|
||||||
else -> return Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
|
||||||
val urlElement = element.select("a").first()
|
|
||||||
|
|
||||||
chapter.setUrl(urlElement.attr("href") + "?mature=1")
|
|
||||||
chapter.name = urlElement.text().replace(" новое", "")
|
|
||||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
|
||||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
|
||||||
} ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun parseChapterNumber(chapter: Chapter) {
|
|
||||||
chapter.chapter_number = -2f
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
|
||||||
val html = response.body().string()
|
|
||||||
val beginIndex = html.indexOf("rm_h.init( [")
|
|
||||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
|
||||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("[\"\']+".toRegex(), "")
|
|
||||||
|
|
||||||
val p = Pattern.compile("auto/[\\w/]+,http://[\\w.]+/,/[\\w./]+.(png|jpg)+")
|
|
||||||
val m = p.matcher(trimmedHtml)
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
while (m.find()) {
|
|
||||||
val urlParts = m.group().split(',')
|
|
||||||
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
}
|
|
@ -9,12 +9,10 @@ import android.net.Uri
|
|||||||
import android.os.AsyncTask
|
import android.os.AsyncTask
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import eu.kanade.tachiyomi.Constants
|
|
||||||
import eu.kanade.tachiyomi.R
|
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.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.data.network.ProgressListener
|
import eu.kanade.tachiyomi.data.network.ProgressListener
|
||||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
import eu.kanade.tachiyomi.data.network.get
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.saveTo
|
import eu.kanade.tachiyomi.util.saveTo
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -45,19 +43,13 @@ class UpdateDownloader(private val context: Context) :
|
|||||||
/**
|
/**
|
||||||
* Default download dir
|
* Default download dir
|
||||||
*/
|
*/
|
||||||
private val apkFile = File(context.externalCacheDir, "update.apk")
|
val apkFile = File(context.externalCacheDir, "update.apk")
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification builder
|
* Notification builder
|
||||||
*/
|
*/
|
||||||
private val notificationBuilder = NotificationCompat.Builder(context)
|
val notificationBuilder = NotificationCompat.Builder(context)
|
||||||
|
|
||||||
/**
|
|
||||||
* Id of the notification
|
|
||||||
*/
|
|
||||||
private val notificationId: Int
|
|
||||||
get() = Constants.NOTIFICATION_UPDATER_ID
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
App.get(context).component.inject(this)
|
App.get(context).component.inject(this)
|
||||||
@ -101,14 +93,12 @@ class UpdateDownloader(private val context: Context) :
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Make the request and download the file
|
// Make the request and download the file
|
||||||
val response = network.client.newCallWithProgress(GET(result.url), progressListener).execute()
|
val response = network.requestBodyProgressBlocking(get(result.url), progressListener)
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
response.body().source().saveTo(apkFile)
|
response.body().source().saveTo(apkFile)
|
||||||
// Set download successful
|
// Set download successful
|
||||||
result.successful = true
|
result.successful = true
|
||||||
} else {
|
|
||||||
response.close()
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, e.message)
|
Timber.e(e, e.message)
|
||||||
@ -126,7 +116,7 @@ class UpdateDownloader(private val context: Context) :
|
|||||||
values.getOrNull(0)?.let {
|
values.getOrNull(0)?.let {
|
||||||
notificationBuilder.setProgress(100, it, false)
|
notificationBuilder.setProgress(100, it, false)
|
||||||
// Displays the progress bar on notification
|
// Displays the progress bar on notification
|
||||||
context.notificationManager.notify(notificationId, notificationBuilder.build())
|
context.notificationManager.notify(InstallOnReceived.notificationId, notificationBuilder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +145,7 @@ class UpdateDownloader(private val context: Context) :
|
|||||||
}
|
}
|
||||||
val notification = notificationBuilder.build()
|
val notification = notificationBuilder.build()
|
||||||
notification.flags = Notification.FLAG_NO_CLEAR
|
notification.flags = Notification.FLAG_NO_CLEAR
|
||||||
context.notificationManager.notify(notificationId, notification)
|
context.notificationManager.notify(InstallOnReceived.notificationId, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,16 +169,19 @@ class UpdateDownloader(private val context: Context) :
|
|||||||
class InstallOnReceived : BroadcastReceiver() {
|
class InstallOnReceived : BroadcastReceiver() {
|
||||||
companion object {
|
companion object {
|
||||||
// Install apk action
|
// Install apk action
|
||||||
const val INSTALL_APK = "eu.kanade.INSTALL_APK"
|
val INSTALL_APK = "eu.kanade.INSTALL_APK"
|
||||||
|
|
||||||
// Retry download action
|
// Retry download action
|
||||||
const val RETRY_DOWNLOAD = "eu.kanade.RETRY_DOWNLOAD"
|
val RETRY_DOWNLOAD = "eu.kanade.RETRY_DOWNLOAD"
|
||||||
|
|
||||||
// Retry download action
|
// Retry download action
|
||||||
const val CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
|
val CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
|
||||||
|
|
||||||
// Absolute path of file || URL of file
|
// Absolute path of file || URL of file
|
||||||
const val FILE_LOCATION = "file_location"
|
val FILE_LOCATION = "file_location"
|
||||||
|
|
||||||
|
// Id of the notification
|
||||||
|
val notificationId = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
@ -198,7 +191,7 @@ class UpdateDownloader(private val context: Context) :
|
|||||||
// Retry download.
|
// Retry download.
|
||||||
RETRY_DOWNLOAD -> UpdateDownloader(context).execute(intent.getStringExtra(FILE_LOCATION))
|
RETRY_DOWNLOAD -> UpdateDownloader(context).execute(intent.getStringExtra(FILE_LOCATION))
|
||||||
|
|
||||||
CANCEL_NOTIFICATION -> context.notificationManager.cancel(Constants.NOTIFICATION_UPDATER_ID)
|
CANCEL_NOTIFICATION -> context.notificationManager.cancel(notificationId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.info
|
package eu.kanade.tachiyomi.event
|
||||||
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.BehaviorSubject
|
import rx.subjects.BehaviorSubject
|
@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.event
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
|
||||||
|
class DownloadChaptersEvent(val manga: Manga, val chapters: List<Chapter>)
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.event
|
||||||
|
|
||||||
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
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga
|
package eu.kanade.tachiyomi.event
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader
|
package eu.kanade.tachiyomi.event
|
||||||
|
|
||||||
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
|
@ -3,13 +3,10 @@ package eu.kanade.tachiyomi.injection.component
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.glide.AppGlideModule
|
|
||||||
import eu.kanade.tachiyomi.data.glide.MangaModelLoader
|
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.data.source.base.Source
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdateDownloader
|
import eu.kanade.tachiyomi.data.updater.UpdateDownloader
|
||||||
import eu.kanade.tachiyomi.injection.module.AppModule
|
import eu.kanade.tachiyomi.injection.module.AppModule
|
||||||
import eu.kanade.tachiyomi.injection.module.DataModule
|
import eu.kanade.tachiyomi.injection.module.DataModule
|
||||||
@ -18,7 +15,7 @@ import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
|||||||
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
|
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadPresenter
|
import eu.kanade.tachiyomi.ui.download.DownloadPresenter
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaPresenter
|
import eu.kanade.tachiyomi.ui.manga.MangaPresenter
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter
|
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter
|
||||||
@ -44,21 +41,16 @@ interface AppComponent {
|
|||||||
fun inject(recentChaptersPresenter: RecentChaptersPresenter)
|
fun inject(recentChaptersPresenter: RecentChaptersPresenter)
|
||||||
fun inject(backupPresenter: BackupPresenter)
|
fun inject(backupPresenter: BackupPresenter)
|
||||||
|
|
||||||
fun inject(mainActivity: MainActivity)
|
fun inject(mangaActivity: MangaActivity)
|
||||||
fun inject(settingsActivity: SettingsActivity)
|
fun inject(settingsActivity: SettingsActivity)
|
||||||
|
|
||||||
fun inject(source: Source)
|
fun inject(source: Source)
|
||||||
fun inject(mangaSyncService: MangaSyncService)
|
fun inject(mangaSyncService: MangaSyncService)
|
||||||
|
|
||||||
fun inject(onlineSource: OnlineSource)
|
|
||||||
|
|
||||||
fun inject(libraryUpdateService: LibraryUpdateService)
|
fun inject(libraryUpdateService: LibraryUpdateService)
|
||||||
fun inject(downloadService: DownloadService)
|
fun inject(downloadService: DownloadService)
|
||||||
fun inject(updateMangaSyncService: UpdateMangaSyncService)
|
fun inject(updateMangaSyncService: UpdateMangaSyncService)
|
||||||
|
|
||||||
fun inject(mangaModelLoader: MangaModelLoader)
|
|
||||||
fun inject(appGlideModule: AppGlideModule)
|
|
||||||
|
|
||||||
fun inject(updateDownloader: UpdateDownloader)
|
fun inject(updateDownloader: UpdateDownloader)
|
||||||
fun application(): Application
|
fun application(): Application
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.activity.ActivityMixin
|
|
||||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||||
import eu.kanade.tachiyomi.util.toast
|
import eu.kanade.tachiyomi.util.toast
|
||||||
import kotlinx.android.synthetic.main.fragment_backup.*
|
import kotlinx.android.synthetic.main.fragment_backup.*
|
||||||
@ -41,7 +40,7 @@ class BackupFragment : BaseRxFragment<BackupPresenter>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
(activity as ActivityMixin).requestPermissionsOnMarshmallow()
|
baseActivity.requestPermissionsOnMarshmallow()
|
||||||
subscriptions = SubscriptionList()
|
subscriptions = SubscriptionList()
|
||||||
|
|
||||||
backup_button.setOnClickListener {
|
backup_button.setOnClickListener {
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.support.v4.app.ActivityCompat
|
|
||||||
import android.support.v4.content.ContextCompat
|
|
||||||
import android.support.v7.app.ActionBar
|
|
||||||
import android.support.v7.app.AppCompatActivity
|
|
||||||
import android.support.v7.widget.Toolbar
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
|
|
||||||
interface ActivityMixin {
|
|
||||||
|
|
||||||
fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
if (backNavigation) {
|
|
||||||
toolbar.setNavigationOnClickListener { onBackPressed() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAppTheme() {
|
|
||||||
setTheme(when (App.get(getActivity()).appTheme) {
|
|
||||||
2 -> R.style.Theme_Tachiyomi_Dark
|
|
||||||
else -> R.style.Theme_Tachiyomi
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarTitle(title: String) {
|
|
||||||
getSupportActionBar()?.title = title
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarTitle(titleResource: Int) {
|
|
||||||
getSupportActionBar()?.title = getString(titleResource)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarSubtitle(title: String) {
|
|
||||||
getSupportActionBar()?.subtitle = title
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToolbarSubtitle(titleResource: Int) {
|
|
||||||
getSupportActionBar()?.subtitle = getString(titleResource)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests read and write permissions on Android M and higher.
|
|
||||||
*/
|
|
||||||
fun requestPermissionsOnMarshmallow() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
if (ContextCompat.checkSelfPermission(getActivity(),
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
|
|
||||||
ActivityCompat.requestPermissions(getActivity(),
|
|
||||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
|
|
||||||
1)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getActivity(): AppCompatActivity
|
|
||||||
|
|
||||||
fun onBackPressed()
|
|
||||||
|
|
||||||
fun getSupportActionBar(): ActionBar?
|
|
||||||
|
|
||||||
fun setSupportActionBar(toolbar: Toolbar?)
|
|
||||||
|
|
||||||
fun setTheme(resource: Int)
|
|
||||||
|
|
||||||
fun getString(resource: Int): String
|
|
||||||
|
|
||||||
}
|
|
@ -1,9 +1,76 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
package eu.kanade.tachiyomi.ui.base.activity
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import android.support.design.widget.Snackbar
|
||||||
|
import android.support.v4.app.ActivityCompat
|
||||||
|
import android.support.v4.content.ContextCompat
|
||||||
import android.support.v7.app.AppCompatActivity
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import android.support.v7.widget.Toolbar
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import eu.kanade.tachiyomi.App
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
open class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun getActivity() = this
|
protected fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
if (backNavigation) {
|
||||||
|
toolbar.setNavigationOnClickListener { onBackPressed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAppTheme() {
|
||||||
|
when (app.appTheme) {
|
||||||
|
2 -> setTheme(R.style.Theme_Tachiyomi_Dark)
|
||||||
|
else -> setTheme(R.style.Theme_Tachiyomi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setToolbarTitle(title: String) {
|
||||||
|
supportActionBar?.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setToolbarTitle(titleResource: Int) {
|
||||||
|
supportActionBar?.title = getString(titleResource)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setToolbarSubtitle(title: String) {
|
||||||
|
supportActionBar?.subtitle = title
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setToolbarSubtitle(titleResource: Int) {
|
||||||
|
supportActionBar?.subtitle = getString(titleResource)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests read and write permissions on Android M and higher.
|
||||||
|
*/
|
||||||
|
fun requestPermissionsOnMarshmallow() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
|
||||||
|
ActivityCompat.requestPermissions(this,
|
||||||
|
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
|
||||||
|
1)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val app: App
|
||||||
|
get() = App.get(this)
|
||||||
|
|
||||||
|
inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit) {
|
||||||
|
val snack = Snackbar.make(this, message, length)
|
||||||
|
val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView
|
||||||
|
textView.setTextColor(Color.WHITE)
|
||||||
|
snack.f()
|
||||||
|
snack.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.activity;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.App;
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||||
|
import nucleus.factory.PresenterFactory;
|
||||||
|
import nucleus.factory.ReflectionPresenterFactory;
|
||||||
|
import nucleus.presenter.Presenter;
|
||||||
|
import nucleus.view.PresenterLifecycleDelegate;
|
||||||
|
import nucleus.view.ViewWithPresenter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is an example of how an activity could controls it's presenter.
|
||||||
|
* You can inherit from this class or copy/paste this class's code to
|
||||||
|
* create your own view implementation.
|
||||||
|
*
|
||||||
|
* @param <P> a type of presenter to return with {@link #getPresenter}.
|
||||||
|
*/
|
||||||
|
public abstract class BaseRxActivity<P extends Presenter> extends BaseActivity implements ViewWithPresenter<P> {
|
||||||
|
|
||||||
|
private static final String PRESENTER_STATE_KEY = "presenter_state";
|
||||||
|
|
||||||
|
private PresenterLifecycleDelegate<P> presenterDelegate =
|
||||||
|
new PresenterLifecycleDelegate<>(ReflectionPresenterFactory.<P>fromViewClass(getClass()));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a current presenter factory.
|
||||||
|
*/
|
||||||
|
public PresenterFactory<P> getPresenterFactory() {
|
||||||
|
return presenterDelegate.getPresenterFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a presenter factory.
|
||||||
|
* Call this method before onCreate/onFinishInflate to override default {@link ReflectionPresenterFactory} presenter factory.
|
||||||
|
* Use this method for presenter dependency injection.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setPresenterFactory(PresenterFactory<P> presenterFactory) {
|
||||||
|
presenterDelegate.setPresenterFactory(presenterFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a current attached presenter.
|
||||||
|
* This method is guaranteed to return a non-null value between
|
||||||
|
* onResume/onPause and onAttachedToWindow/onDetachedFromWindow calls
|
||||||
|
* if the presenter factory returns a non-null value.
|
||||||
|
*
|
||||||
|
* @return a currently attached presenter or null.
|
||||||
|
*/
|
||||||
|
public P getPresenter() {
|
||||||
|
return presenterDelegate.getPresenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
final PresenterFactory<P> superFactory = getPresenterFactory();
|
||||||
|
setPresenterFactory(new PresenterFactory<P>() {
|
||||||
|
@Override
|
||||||
|
public P createPresenter() {
|
||||||
|
P presenter = superFactory.createPresenter();
|
||||||
|
App app = (App) getApplication();
|
||||||
|
app.getComponentReflection().inject(presenter);
|
||||||
|
((BasePresenter) presenter).setContext(app.getApplicationContext());
|
||||||
|
return presenter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
if (savedInstanceState != null)
|
||||||
|
presenterDelegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
outState.putBundle(PRESENTER_STATE_KEY, presenterDelegate.onSaveInstanceState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
presenterDelegate.onResume(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
presenterDelegate.onDropView();
|
||||||
|
presenterDelegate.onDestroy(!isChangingConfigurations());
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.activity
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.App
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import nucleus.view.NucleusAppCompatActivity
|
|
||||||
|
|
||||||
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
val superFactory = presenterFactory
|
|
||||||
setPresenterFactory {
|
|
||||||
superFactory.createPresenter().apply {
|
|
||||||
val app = application as App
|
|
||||||
app.componentReflection.inject(this)
|
|
||||||
context = app.applicationContext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onCreate(savedState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getActivity() = this
|
|
||||||
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user