Merge branch 'master' of https://github.com/inorichi/tachiyomi
# Conflicts: # README.md # app/build.gradle # app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt # app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt # app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt # app/src/main/java/eu/kanade/tachiyomi/ui/backup/BackupPresenter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/main/ChangelogDialogFragment.kt # app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt # app/src/main/res/raw/changelog_release.xml # app/src/main/res/values/arrays.xml # app/src/main/res/values/strings.xml
This commit is contained in:
commit
03cb7062f2
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@ -1,3 +1,7 @@
|
|||||||
|
# Catalogue requests
|
||||||
|
|
||||||
|
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions/issues, not here
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -28,10 +28,9 @@ ext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 25
|
compileSdkVersion 25
|
||||||
buildToolsVersion "25.0.1"
|
buildToolsVersion "25.0.2"
|
||||||
publishNonDefault true
|
publishNonDefault true
|
||||||
|
|
||||||
dexOptions {
|
dexOptions {
|
||||||
@ -53,7 +52,7 @@ android {
|
|||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi", "armeabi-v7a", "x86"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,10 +102,11 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Modified dependencies
|
// Modified dependencies
|
||||||
compile 'com.github.inorichi:subsampling-scale-image-view:c4db85c'
|
compile 'com.github.inorichi:subsampling-scale-image-view:4255750'
|
||||||
|
compile 'com.github.inorichi:junrar-android:634c1f5'
|
||||||
|
|
||||||
// Android support library
|
// Android support library
|
||||||
final support_library_version = '25.0.1'
|
final support_library_version = '25.2.0'
|
||||||
compile "com.android.support:support-v4:$support_library_version"
|
compile "com.android.support:support-v4:$support_library_version"
|
||||||
compile "com.android.support:appcompat-v7:$support_library_version"
|
compile "com.android.support:appcompat-v7:$support_library_version"
|
||||||
compile "com.android.support:cardview-v7:$support_library_version"
|
compile "com.android.support:cardview-v7:$support_library_version"
|
||||||
@ -115,30 +115,30 @@ dependencies {
|
|||||||
compile "com.android.support:support-annotations:$support_library_version"
|
compile "com.android.support:support-annotations:$support_library_version"
|
||||||
compile "com.android.support:customtabs:$support_library_version"
|
compile "com.android.support:customtabs:$support_library_version"
|
||||||
|
|
||||||
compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4'
|
compile 'com.android.support.constraint:constraint-layout:1.0.0'
|
||||||
|
|
||||||
compile 'com.android.support:multidex:1.0.1'
|
compile 'com.android.support:multidex:1.0.1'
|
||||||
|
|
||||||
// ReactiveX
|
// ReactiveX
|
||||||
compile 'io.reactivex:rxandroid:1.2.1'
|
compile 'io.reactivex:rxandroid:1.2.1'
|
||||||
compile 'io.reactivex:rxjava:1.2.4'
|
compile 'io.reactivex:rxjava:1.2.6'
|
||||||
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
|
||||||
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
compile "com.squareup.okhttp3:okhttp:3.5.0"
|
compile "com.squareup.okhttp3:okhttp:3.6.0"
|
||||||
compile 'com.squareup.okio:okio:1.11.0'
|
compile 'com.squareup.okio:okio:1.11.0'
|
||||||
|
|
||||||
// REST
|
// REST
|
||||||
final retrofit_version = '2.1.0'
|
final retrofit_version = '2.2.0'
|
||||||
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||||
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
|
||||||
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
compile 'com.google.code.gson:gson:2.8.0'
|
compile 'com.google.code.gson:gson:2.8.0'
|
||||||
compile 'com.github.salomonbrys.kotson:kotson:2.4.0'
|
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||||
|
|
||||||
// YAML
|
// YAML
|
||||||
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
|
||||||
@ -151,17 +151,17 @@ dependencies {
|
|||||||
compile 'com.github.seven332:unifile:1.0.0'
|
compile 'com.github.seven332:unifile:1.0.0'
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
compile 'org.jsoup:jsoup:1.10.1'
|
compile 'org.jsoup:jsoup:1.10.2'
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
compile 'com.evernote:android-job:1.1.3'
|
compile 'com.evernote:android-job:1.1.6'
|
||||||
compile 'com.google.android.gms:play-services-gcm:10.0.1'
|
compile 'com.google.android.gms:play-services-gcm:10.2.0'
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
compile "com.pushtorefresh.storio:sqlite:1.11.0"
|
compile "com.pushtorefresh.storio:sqlite:1.12.3"
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
final nucleus_version = '3.0.0'
|
final nucleus_version = '3.0.0'
|
||||||
@ -179,16 +179,23 @@ dependencies {
|
|||||||
compile 'jp.wasabeef:glide-transformations:2.0.1'
|
compile 'jp.wasabeef:glide-transformations:2.0.1'
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
compile 'com.jakewharton.timber:timber:4.4.0'
|
compile 'com.jakewharton.timber:timber:4.5.1'
|
||||||
|
|
||||||
|
// Crash reports
|
||||||
|
compile 'ch.acra:acra:4.9.2'
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
compile 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
|
||||||
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||||
compile 'eu.davidea:flexible-adapter:4.2.0'
|
compile 'eu.davidea:flexible-adapter:5.0.0-rc1'
|
||||||
|
compile 'com.github.inorichi:FlexibleAdapter:93985fe' // v4.2.0 to be removed
|
||||||
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.9.1.0'
|
compile 'com.afollestad.material-dialogs:core:0.9.3.0'
|
||||||
compile 'net.xpece.android:support-preference:1.2.0'
|
compile 'net.xpece.android:support-preference:1.2.5'
|
||||||
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||||
compile 'de.hdodenhof:circleimageview:2.1.0'
|
compile 'de.hdodenhof:circleimageview:2.1.0'
|
||||||
|
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="eu.kanade.tachiyomi">
|
package="eu.kanade.tachiyomi">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
|
<uses-permission
|
||||||
|
android:name="android.permission.READ_PHONE_STATE"
|
||||||
|
tools:node="remove" />
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@ -20,9 +21,8 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:theme="@style/Theme.Tachiyomi" >
|
android:theme="@style/Theme.Tachiyomi">
|
||||||
<activity
|
<activity android:name=".ui.main.MainActivity">
|
||||||
android:name=".ui.main.MainActivity">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@ -31,40 +31,40 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.manga.MangaActivity"
|
android:name=".ui.manga.MangaActivity"
|
||||||
android:parentActivityName=".ui.main.MainActivity"
|
android:exported="true"
|
||||||
android:exported="true">
|
android:parentActivityName=".ui.main.MainActivity" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:theme="@style/Theme.Reader">
|
android:theme="@style/Theme.Reader" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.SettingsActivity"
|
android:name=".ui.setting.SettingsActivity"
|
||||||
android:label="@string/label_settings"
|
android:label="@string/label_settings"
|
||||||
android:parentActivityName=".ui.main.MainActivity" >
|
android:parentActivityName=".ui.main.MainActivity" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.category.CategoryActivity"
|
android:name=".ui.category.CategoryActivity"
|
||||||
android:label="@string/label_categories"
|
android:label="@string/label_categories"
|
||||||
android:parentActivityName=".ui.main.MainActivity">
|
android:parentActivityName=".ui.main.MainActivity" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
|
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/FilePickerTheme">
|
android:theme="@style/FilePickerTheme" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.AnilistLoginActivity"
|
android:name=".ui.setting.AnilistLoginActivity"
|
||||||
android:label="Anilist">
|
android:label="Anilist">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="anilist-auth"
|
android:host="anilist-auth"
|
||||||
android:scheme="tachiyomi" />
|
android:scheme="tachiyomi" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.download.DownloadActivity"
|
||||||
|
android:launchMode="singleTop" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
@ -73,26 +73,34 @@
|
|||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths"/>
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<service android:name=".data.library.LibraryUpdateService"
|
<provider
|
||||||
android:exported="false"/>
|
android:name="eu.kanade.tachiyomi.util.ZipContentProvider"
|
||||||
|
android:authorities="${applicationId}.zip-provider"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".data.download.DownloadService"
|
<provider
|
||||||
android:exported="false"/>
|
android:name="eu.kanade.tachiyomi.util.RarContentProvider"
|
||||||
|
android:authorities="${applicationId}.rar-provider"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".data.track.TrackUpdateService"
|
<receiver
|
||||||
android:exported="false"/>
|
android:name=".data.notification.NotificationReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".data.updater.UpdateDownloaderService"
|
<service
|
||||||
android:exported="false"/>
|
android:name=".data.library.LibraryUpdateService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
|
<service
|
||||||
|
android:name=".data.download.DownloadService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver" />
|
<service
|
||||||
|
android:name=".data.updater.UpdateDownloaderService"
|
||||||
<receiver android:name=".ui.reader.notification.ImageNotificationReceiver" />
|
android:exported="false" />
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||||
|
@ -6,10 +6,10 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
|
|||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
|
||||||
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.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
|
@ -5,7 +5,8 @@ import android.text.format.Formatter
|
|||||||
import com.github.salomonbrys.kotson.fromJson
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.jakewharton.disklrucache.DiskLruCache
|
import com.jakewharton.disklrucache.DiskLruCache
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.saveTo
|
import eu.kanade.tachiyomi.util.saveTo
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@ -92,13 +93,13 @@ class ChapterCache(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Get page list from cache.
|
* Get page list from cache.
|
||||||
*
|
*
|
||||||
* @param chapterUrl the url of the chapter.
|
* @param chapter the chapter.
|
||||||
* @return an observable of the list of pages.
|
* @return an observable of the list of pages.
|
||||||
*/
|
*/
|
||||||
fun getPageListFromCache(chapterUrl: String): Observable<List<Page>> {
|
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
|
||||||
return Observable.fromCallable<List<Page>> {
|
return Observable.fromCallable {
|
||||||
// Get the key for the chapter.
|
// Get the key for the chapter.
|
||||||
val key = DiskUtil.hashKeyForDisk(chapterUrl)
|
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||||
|
|
||||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||||
diskCache.get(key).use {
|
diskCache.get(key).use {
|
||||||
@ -110,10 +111,10 @@ class ChapterCache(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Add page list to disk cache.
|
* Add page list to disk cache.
|
||||||
*
|
*
|
||||||
* @param chapterUrl the url of the chapter.
|
* @param chapter the chapter.
|
||||||
* @param pages list of pages.
|
* @param pages list of pages.
|
||||||
*/
|
*/
|
||||||
fun putPageListToCache(chapterUrl: String, pages: List<Page>) {
|
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
||||||
// Convert list of pages to json string.
|
// Convert list of pages to json string.
|
||||||
val cachedValue = gson.toJson(pages)
|
val cachedValue = gson.toJson(pages)
|
||||||
|
|
||||||
@ -122,7 +123,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get editor from md5 key.
|
// Get editor from md5 key.
|
||||||
val key = DiskUtil.hashKeyForDisk(chapterUrl)
|
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||||
editor = diskCache.edit(key) ?: return
|
editor = diskCache.edit(key) ?: return
|
||||||
|
|
||||||
// Write chapter urls to cache.
|
// Write chapter urls to cache.
|
||||||
@ -196,5 +197,8 @@ class ChapterCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getKey(chapter: Chapter): String {
|
||||||
|
return "${chapter.manga_id}${chapter.url}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class CoverCache(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Cache directory used for cache management.
|
* Cache directory used for cache management.
|
||||||
*/
|
*/
|
||||||
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
|
private val cacheDir = context.getExternalFilesDir("covers")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the cover from cache.
|
* Returns the cover from cache.
|
||||||
|
@ -69,7 +69,7 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() {
|
|||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||||
source = cursor.getInt(cursor.getColumnIndex(COL_SOURCE))
|
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||||
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
||||||
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface Chapter : Serializable {
|
interface Chapter : SChapter, Serializable {
|
||||||
|
|
||||||
var id: Long?
|
var id: Long?
|
||||||
|
|
||||||
var manga_id: Long?
|
var manga_id: Long?
|
||||||
|
|
||||||
var url: String
|
|
||||||
|
|
||||||
var name: String
|
|
||||||
|
|
||||||
var read: Boolean
|
var read: Boolean
|
||||||
|
|
||||||
var bookmark: Boolean
|
var bookmark: Boolean
|
||||||
@ -20,10 +17,6 @@ interface Chapter : Serializable {
|
|||||||
|
|
||||||
var date_fetch: Long
|
var date_fetch: Long
|
||||||
|
|
||||||
var date_upload: Long
|
|
||||||
|
|
||||||
var chapter_number: Float
|
|
||||||
|
|
||||||
var source_order: Int
|
var source_order: Int
|
||||||
|
|
||||||
val isRecognizedNumber: Boolean
|
val isRecognizedNumber: Boolean
|
||||||
|
@ -1,35 +1,17 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
import java.io.Serializable
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
|
||||||
interface Manga : Serializable {
|
interface Manga : SManga {
|
||||||
|
|
||||||
var id: Long?
|
var id: Long?
|
||||||
|
|
||||||
var source: Int
|
var source: Long
|
||||||
|
|
||||||
var url: String
|
|
||||||
|
|
||||||
var title: String
|
|
||||||
|
|
||||||
var artist: String?
|
|
||||||
|
|
||||||
var author: String?
|
|
||||||
|
|
||||||
var description: String?
|
|
||||||
|
|
||||||
var genre: String?
|
|
||||||
|
|
||||||
var status: Int
|
|
||||||
|
|
||||||
var thumbnail_url: String?
|
|
||||||
|
|
||||||
var favorite: Boolean
|
var favorite: Boolean
|
||||||
|
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
var initialized: Boolean
|
|
||||||
|
|
||||||
var viewer: Int
|
var viewer: Int
|
||||||
|
|
||||||
var chapter_flags: Int
|
var chapter_flags: Int
|
||||||
@ -38,27 +20,6 @@ interface Manga : Serializable {
|
|||||||
|
|
||||||
var category: Int
|
var category: Int
|
||||||
|
|
||||||
fun copyFrom(other: Manga) {
|
|
||||||
if (other.author != null)
|
|
||||||
author = other.author
|
|
||||||
|
|
||||||
if (other.artist != null)
|
|
||||||
artist = other.artist
|
|
||||||
|
|
||||||
if (other.description != null)
|
|
||||||
description = other.description
|
|
||||||
|
|
||||||
if (other.genre != null)
|
|
||||||
genre = other.genre
|
|
||||||
|
|
||||||
if (other.thumbnail_url != null)
|
|
||||||
thumbnail_url = other.thumbnail_url
|
|
||||||
|
|
||||||
status = other.status
|
|
||||||
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
fun setChapterOrder(order: Int) {
|
||||||
setFlags(order, SORT_MASK)
|
setFlags(order, SORT_MASK)
|
||||||
}
|
}
|
||||||
@ -94,11 +55,6 @@ interface Manga : Serializable {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val UNKNOWN = 0
|
|
||||||
const val ONGOING = 1
|
|
||||||
const val COMPLETED = 2
|
|
||||||
const val LICENSED = 3
|
|
||||||
|
|
||||||
const val SORT_DESC = 0x00000000
|
const val SORT_DESC = 0x00000000
|
||||||
const val SORT_ASC = 0x00000001
|
const val SORT_ASC = 0x00000001
|
||||||
const val SORT_MASK = 0x00000001
|
const val SORT_MASK = 0x00000001
|
||||||
@ -126,12 +82,13 @@ interface Manga : Serializable {
|
|||||||
const val DISPLAY_NUMBER = 0x00100000
|
const val DISPLAY_NUMBER = 0x00100000
|
||||||
const val DISPLAY_MASK = 0x00100000
|
const val DISPLAY_MASK = 0x00100000
|
||||||
|
|
||||||
fun create(source: Int): Manga = MangaImpl().apply {
|
fun create(source: Long): Manga = MangaImpl().apply {
|
||||||
this.source = source
|
this.source = source
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(pathUrl: String, source: Int = 0): Manga = MangaImpl().apply {
|
fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
|
||||||
url = pathUrl
|
url = pathUrl
|
||||||
|
this.title = title
|
||||||
this.source = source
|
this.source = source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var id: Long? = null
|
override var id: Long? = null
|
||||||
|
|
||||||
override var source: Int = 0
|
override var source: Long = -1
|
||||||
|
|
||||||
override lateinit var url: String
|
override lateinit var url: String
|
||||||
|
|
||||||
|
@ -22,8 +22,6 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var status: Int
|
var status: Int
|
||||||
|
|
||||||
var update: Boolean
|
|
||||||
|
|
||||||
fun copyPersonalFrom(other: Track) {
|
fun copyPersonalFrom(other: Track) {
|
||||||
last_chapter_read = other.last_chapter_read
|
last_chapter_read = other.last_chapter_read
|
||||||
score = other.score
|
score = other.score
|
||||||
|
@ -20,8 +20,6 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override var status: Int = 0
|
override var status: Int = 0
|
||||||
|
|
||||||
override var update: Boolean = false
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
@ -40,7 +40,7 @@ interface MangaQueries : DbProvider {
|
|||||||
.build())
|
.build())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getManga(url: String, sourceId: Int) = db.get()
|
fun getManga(url: String, sourceId: Long) = db.get()
|
||||||
.`object`(Manga::class.java)
|
.`object`(Manga::class.java)
|
||||||
.withQuery(Query.builder()
|
.withQuery(Query.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
|
@ -6,8 +6,8 @@ import com.jakewharton.rxrelay.BehaviorRelay
|
|||||||
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.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,10 +60,19 @@ class DownloadManager(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Empties the download queue.
|
* Tells the downloader to pause downloads.
|
||||||
*/
|
*/
|
||||||
fun clearQueue() {
|
fun pauseDownloads() {
|
||||||
downloader.clearQueue()
|
downloader.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empties the download queue.
|
||||||
|
*
|
||||||
|
* @param isNotification value that determines if status is set (needed for view updates)
|
||||||
|
*/
|
||||||
|
fun clearQueue(isNotification: Boolean = false) {
|
||||||
|
downloader.clearQueue(isNotification)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -168,5 +177,4 @@ class DownloadManager(context: Context) {
|
|||||||
fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) {
|
fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) {
|
||||||
provider.findChapterDir(source, manga, chapter)?.delete()
|
provider.findChapterDir(source, manga, chapter)?.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.Constants
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
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.notification.NotificationHandler
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.util.chop
|
import eu.kanade.tachiyomi.util.chop
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
|
||||||
@ -33,12 +35,34 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* The size of queue on start download.
|
* The size of queue on start download.
|
||||||
*/
|
*/
|
||||||
var initialQueueSize = 0
|
var initialQueueSize = 0
|
||||||
|
get() = field
|
||||||
|
set(value) {
|
||||||
|
if (value != 0){
|
||||||
|
isSingleChapter = (value == 1)
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simultaneous download setting > 1.
|
* Simultaneous download setting > 1.
|
||||||
*/
|
*/
|
||||||
var multipleDownloadThreads = false
|
var multipleDownloadThreads = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updated when error is thrown
|
||||||
|
*/
|
||||||
|
var errorThrown = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updated when only single page is downloaded
|
||||||
|
*/
|
||||||
|
var isSingleChapter = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updated when paused
|
||||||
|
*/
|
||||||
|
var paused = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a notification from this builder.
|
* Shows a notification from this builder.
|
||||||
*
|
*
|
||||||
@ -48,6 +72,14 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
context.notificationManager.notify(id, build())
|
context.notificationManager.notify(id, build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear old actions if they exist.
|
||||||
|
*/
|
||||||
|
private fun clearActions() = with(notification) {
|
||||||
|
if (!mActions.isEmpty())
|
||||||
|
mActions.clear()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
|
||||||
* those can only be dismissed by the user.
|
* those can only be dismissed by the user.
|
||||||
@ -88,24 +120,15 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param queue the queue containing downloads.
|
* @param queue the queue containing downloads.
|
||||||
*/
|
*/
|
||||||
private fun doOnProgressChange(download: Download?, queue: DownloadQueue) {
|
private fun doOnProgressChange(download: Download?, queue: DownloadQueue) {
|
||||||
// Check if download is completed
|
|
||||||
if (multipleDownloadThreads) {
|
|
||||||
if (queue.isEmpty()) {
|
|
||||||
onChapterCompleted(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (download != null && download.pages!!.size == download.downloadedImages) {
|
|
||||||
onChapterCompleted(download)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create notification
|
// Create notification
|
||||||
with(notification) {
|
with(notification) {
|
||||||
// Check if icon needs refresh
|
// Check if first call.
|
||||||
if (!isDownloading) {
|
if (!isDownloading) {
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
|
setAutoCancel(false)
|
||||||
|
clearActions()
|
||||||
|
// Open download manager when clicked
|
||||||
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
isDownloading = true
|
isDownloading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +144,9 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
|
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
|
||||||
} else {
|
} else {
|
||||||
download?.let {
|
download?.let {
|
||||||
setContentTitle(it.chapter.name.chop(30))
|
val title = it.manga.title.chop(15)
|
||||||
|
val chapter = download.chapter.name.replaceFirst("$title[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||||
|
setContentTitle("$title - $chapter".chop(30))
|
||||||
setContentText(context.getString(R.string.chapter_downloading_progress)
|
setContentText(context.getString(R.string.chapter_downloading_progress)
|
||||||
.format(it.downloadedImages, it.pages!!.size))
|
.format(it.downloadedImages, it.pages!!.size))
|
||||||
setProgress(it.pages!!.size, it.downloadedImages, false)
|
setProgress(it.pages!!.size, it.downloadedImages, false)
|
||||||
@ -133,17 +158,57 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
notification.show()
|
notification.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notification when download is paused.
|
||||||
|
*/
|
||||||
|
fun onDownloadPaused() {
|
||||||
|
with(notification) {
|
||||||
|
setContentTitle(context.getString(R.string.chapter_paused))
|
||||||
|
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||||
|
setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img)
|
||||||
|
setAutoCancel(false)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
clearActions()
|
||||||
|
// Open download manager when clicked
|
||||||
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
|
// Resume action
|
||||||
|
addAction(R.drawable.ic_av_play_arrow_grey_img,
|
||||||
|
context.getString(R.string.action_resume),
|
||||||
|
NotificationReceiver.resumeDownloadsPendingBroadcast(context))
|
||||||
|
//Clear action
|
||||||
|
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||||
|
context.getString(R.string.action_clear),
|
||||||
|
NotificationReceiver.clearDownloadsPendingBroadcast(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show notification.
|
||||||
|
notification.show()
|
||||||
|
|
||||||
|
// Reset initial values
|
||||||
|
isDownloading = false
|
||||||
|
initialQueueSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when chapter is downloaded.
|
* Called when chapter is downloaded.
|
||||||
*
|
*
|
||||||
* @param download download object containing download information.
|
* @param download download object containing download information.
|
||||||
*/
|
*/
|
||||||
private fun onChapterCompleted(download: Download?) {
|
fun onDownloadCompleted(download: Download, queue: DownloadQueue) {
|
||||||
|
// Check if last download
|
||||||
|
if (!queue.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// Create notification.
|
// Create notification.
|
||||||
with(notification) {
|
with(notification) {
|
||||||
setContentTitle(download?.chapter?.name ?: context.getString(R.string.app_name))
|
val title = download.manga.title.chop(15)
|
||||||
|
val chapter = download.chapter.name.replaceFirst("$title[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||||
|
setContentTitle("$title - $chapter".chop(30))
|
||||||
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
setAutoCancel(true)
|
||||||
|
clearActions()
|
||||||
|
setContentIntent(NotificationReceiver.openChapterPendingBroadcast(context, download.manga, download.chapter))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,9 +230,15 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setContentText(reason)
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
|
setAutoCancel(true)
|
||||||
|
clearActions()
|
||||||
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
notification.show()
|
notification.show()
|
||||||
|
|
||||||
|
// Reset download information
|
||||||
|
isDownloading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,11 +254,15 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
|
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
|
clearActions()
|
||||||
|
setAutoCancel(false)
|
||||||
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
}
|
}
|
||||||
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID)
|
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID)
|
||||||
|
|
||||||
// Reset download information
|
// Reset download information
|
||||||
|
errorThrown = true
|
||||||
isDownloading = false
|
isDownloading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
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.source.Source
|
||||||
import eu.kanade.tachiyomi.util.DiskUtil
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
@ -5,8 +5,8 @@ import com.google.gson.Gson
|
|||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
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.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,7 +93,7 @@ class DownloadStore(context: Context) {
|
|||||||
val manga = cachedManga.getOrPut(mangaId) {
|
val manga = cachedManga.getOrPut(mangaId) {
|
||||||
db.getManga(mangaId).executeAsBlocking()
|
db.getManga(mangaId).executeAsBlocking()
|
||||||
} ?: continue
|
} ?: continue
|
||||||
val source = sourceManager.get(manga.source) as? OnlineSource ?: continue
|
val source = sourceManager.get(manga.source) as? HttpSource ?: continue
|
||||||
val chapter = db.getChapter(chapterId).executeAsBlocking() ?: continue
|
val chapter = db.getChapter(chapterId).executeAsBlocking() ?: continue
|
||||||
downloads.add(Download(source, manga, chapter))
|
downloads.add(Download(source, manga, chapter))
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,12 @@ 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.source.SourceManager
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.RetryWithDelay
|
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||||
import eu.kanade.tachiyomi.util.plusAssign
|
import eu.kanade.tachiyomi.util.*
|
||||||
import eu.kanade.tachiyomi.util.saveTo
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -25,7 +24,6 @@ import rx.subjects.BehaviorSubject
|
|||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLConnection
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is the one in charge of downloading chapters.
|
* This class is the one in charge of downloading chapters.
|
||||||
@ -132,15 +130,42 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
if (reason != null) {
|
if (reason != null) {
|
||||||
notifier.onWarning(reason)
|
notifier.onWarning(reason)
|
||||||
} else {
|
} else {
|
||||||
notifier.dismiss()
|
if (notifier.paused) {
|
||||||
|
notifier.paused = false
|
||||||
|
notifier.onDownloadPaused()
|
||||||
|
} else if (notifier.isSingleChapter && !notifier.errorThrown) {
|
||||||
|
notifier.isSingleChapter = false
|
||||||
|
} else {
|
||||||
|
notifier.dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes everything from the queue.
|
* Pauses the downloader
|
||||||
*/
|
*/
|
||||||
fun clearQueue() {
|
fun pause() {
|
||||||
destroySubscriptions()
|
destroySubscriptions()
|
||||||
|
queue
|
||||||
|
.filter { it.status == Download.DOWNLOADING }
|
||||||
|
.forEach { it.status = Download.QUEUE }
|
||||||
|
notifier.paused = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes everything from the queue.
|
||||||
|
*
|
||||||
|
* @param isNotification value that determines if status is set (needed for view updates)
|
||||||
|
*/
|
||||||
|
fun clearQueue(isNotification: Boolean = false) {
|
||||||
|
destroySubscriptions()
|
||||||
|
|
||||||
|
//Needed to update the chapter view
|
||||||
|
if (isNotification) {
|
||||||
|
queue
|
||||||
|
.filter { it.status == Download.QUEUE }
|
||||||
|
.forEach { it.status = Download.NOT_DOWNLOADED }
|
||||||
|
}
|
||||||
queue.clear()
|
queue.clear()
|
||||||
notifier.dismiss()
|
notifier.dismiss()
|
||||||
}
|
}
|
||||||
@ -192,7 +217,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
* @param chapters the list of chapters to download.
|
* @param chapters the list of chapters to download.
|
||||||
*/
|
*/
|
||||||
fun queueChapters(manga: Manga, chapters: List<Chapter>) {
|
fun queueChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
val source = sourceManager.get(manga.source) as? OnlineSource ?: return
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return
|
||||||
|
|
||||||
val chaptersToQueue = chapters
|
val chaptersToQueue = chapters
|
||||||
// Avoid downloading chapters with the same name.
|
// Avoid downloading chapters with the same name.
|
||||||
@ -213,6 +238,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
// Initialize queue size.
|
// Initialize queue size.
|
||||||
notifier.initialQueueSize = queue.size
|
notifier.initialQueueSize = queue.size
|
||||||
|
|
||||||
|
// Initial multi-thread
|
||||||
|
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
// Send the list of downloads to the downloader.
|
// Send the list of downloads to the downloader.
|
||||||
downloadsRelay.call(chaptersToQueue)
|
downloadsRelay.call(chaptersToQueue)
|
||||||
@ -251,8 +279,11 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
|
|
||||||
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.fetchPageList(download.chapter)
|
||||||
.doOnNext { pages ->
|
.doOnNext { pages ->
|
||||||
|
if (pages.isEmpty()) {
|
||||||
|
throw Exception("Page list is empty")
|
||||||
|
}
|
||||||
download.pages = pages
|
download.pages = pages
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -309,7 +340,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
tmpFile?.delete()
|
tmpFile?.delete()
|
||||||
|
|
||||||
// Try to find the image file.
|
// Try to find the image file.
|
||||||
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.")}
|
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
|
||||||
|
|
||||||
// If the image is already downloaded, do nothing. Otherwise download from network
|
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||||
val pageObservable = if (imageFile != null)
|
val pageObservable = if (imageFile != null)
|
||||||
@ -342,10 +373,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
* @param tmpDir the temporary directory of the download.
|
* @param tmpDir the temporary directory of the download.
|
||||||
* @param filename the filename of the image.
|
* @param filename the filename of the image.
|
||||||
*/
|
*/
|
||||||
private fun downloadImage(page: Page, source: OnlineSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
|
private fun downloadImage(page: Page, source: HttpSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
|
||||||
page.status = Page.DOWNLOAD_IMAGE
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
page.progress = 0
|
page.progress = 0
|
||||||
return source.imageResponse(page)
|
return source.fetchImage(page)
|
||||||
.map { response ->
|
.map { response ->
|
||||||
val file = tmpDir.createFile("$filename.tmp")
|
val file = tmpDir.createFile("$filename.tmp")
|
||||||
try {
|
try {
|
||||||
@ -373,12 +404,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
private fun getImageExtension(response: Response, file: UniFile): String {
|
private fun getImageExtension(response: Response, file: UniFile): String {
|
||||||
// Read content type if available.
|
// Read content type if available.
|
||||||
val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
|
||||||
// Else guess from the uri.
|
// Else guess from the uri.
|
||||||
?: context.contentResolver.getType(file.uri)
|
?: context.contentResolver.getType(file.uri)
|
||||||
// Else read magic numbers.
|
// Else read magic numbers.
|
||||||
?: file.openInputStream().buffered().use {
|
?: DiskUtil.findImageMime { file.openInputStream() }
|
||||||
URLConnection.guessContentTypeFromStream(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
|
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
|
||||||
}
|
}
|
||||||
@ -417,6 +446,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
|||||||
notifier.onProgressChange(queue)
|
notifier.onProgressChange(queue)
|
||||||
}
|
}
|
||||||
if (areAllDownloadsFinished()) {
|
if (areAllDownloadsFinished()) {
|
||||||
|
if (notifier.isSingleChapter && !notifier.errorThrown) {
|
||||||
|
notifier.onDownloadCompleted(download, queue)
|
||||||
|
}
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.data.download.model
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
|
|
||||||
class Download(val source: OnlineSource, val manga: Manga, val chapter: Chapter) {
|
class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
|
||||||
|
|
||||||
var pages: List<Page>? = null
|
var pages: List<Page>? = null
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.download.model
|
|||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.subjects.PublishSubject
|
import rx.subjects.PublishSubject
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
@ -8,7 +8,7 @@ import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
|||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
import com.bumptech.glide.module.GlideModule
|
import com.bumptech.glide.module.GlideModule
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import com.bumptech.glide.Priority
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
|
||||||
|
|
||||||
|
private var data: InputStream? = null
|
||||||
|
|
||||||
|
override fun loadData(priority: Priority): InputStream {
|
||||||
|
data = file.inputStream()
|
||||||
|
return data!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cleanup() {
|
||||||
|
data?.let { data ->
|
||||||
|
try {
|
||||||
|
data.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getId(): String {
|
||||||
|
return file.toString()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.glide
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
open class MangaFileFetcher(private val file: File, private val manga: Manga) : FileFetcher(file) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
}
|
@ -3,20 +3,21 @@ package eu.kanade.tachiyomi.data.glide
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
import com.bumptech.glide.load.model.*
|
import com.bumptech.glide.load.model.*
|
||||||
import com.bumptech.glide.load.model.stream.StreamModelLoader
|
import com.bumptech.glide.load.model.stream.StreamModelLoader
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
||||||
* Coupled with [MangaDataFetcher], this class allows to implement the following flow:
|
* Coupled with [MangaUrlFetcher], this class allows to implement the following flow:
|
||||||
*
|
*
|
||||||
* - Check in RAM LRU.
|
* - Check in RAM LRU.
|
||||||
* - Check in disk LRU.
|
* - Check in disk LRU.
|
||||||
@ -30,17 +31,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
/**
|
/**
|
||||||
* Cover cache where persistent covers are stored.
|
* Cover cache where persistent covers are stored.
|
||||||
*/
|
*/
|
||||||
val coverCache: CoverCache by injectLazy()
|
private val coverCache: CoverCache by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source manager.
|
* Source manager.
|
||||||
*/
|
*/
|
||||||
val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base network loader.
|
* Base network loader.
|
||||||
*/
|
*/
|
||||||
private val baseLoader = Glide.buildModelLoader(GlideUrl::class.java,
|
private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java,
|
||||||
InputStream::class.java, context)
|
InputStream::class.java, context)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,7 +53,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
/**
|
/**
|
||||||
* Map where request headers are stored for a source.
|
* Map where request headers are stored for a source.
|
||||||
*/
|
*/
|
||||||
private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
|
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory class for creating [MangaModelLoader] instances.
|
* Factory class for creating [MangaModelLoader] instances.
|
||||||
@ -66,7 +67,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [MangaDataFetcher] for the given manga or null if the url is empty.
|
* Returns a fetcher for the given manga or null if the url is empty.
|
||||||
*
|
*
|
||||||
* @param manga the model.
|
* @param manga the model.
|
||||||
* @param width the width of the view where the resource will be loaded.
|
* @param width the width of the view where the resource will be loaded.
|
||||||
@ -78,22 +79,33 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
|
|
||||||
// Check thumbnail is not null or empty
|
// Check thumbnail is not null or empty
|
||||||
val url = manga.thumbnail_url
|
val url = manga.thumbnail_url
|
||||||
if (url.isNullOrEmpty()) {
|
if (url == null || url.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain the request url and the file for this url from the LRU cache, or calculate it
|
if (url.startsWith("http")) {
|
||||||
// and add them to the cache.
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
val (glideUrl, file) = lruCache.get(url) ?:
|
|
||||||
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url!!)).apply {
|
|
||||||
lruCache.put(url, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the network fetcher for this request url.
|
// Obtain the request url and the file for this url from the LRU cache, or calculate it
|
||||||
val networkFetcher = baseLoader.getResourceFetcher(glideUrl, width, height)
|
// and add them to the cache.
|
||||||
|
val (glideUrl, file) = lruCache.get(url) ?:
|
||||||
|
Pair(GlideUrl(url, getHeaders(manga, source)), coverCache.getCoverFile(url)).apply {
|
||||||
|
lruCache.put(url, this)
|
||||||
|
}
|
||||||
|
|
||||||
// Return an instance of our fetcher providing the needed elements.
|
// Get the resource fetcher for this request url.
|
||||||
return MangaDataFetcher(networkFetcher, file, manga)
|
val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) }
|
||||||
|
?: baseUrlLoader.getResourceFetcher(glideUrl, width, height)
|
||||||
|
|
||||||
|
// Return an instance of the fetcher providing the needed elements.
|
||||||
|
return MangaUrlFetcher(networkFetcher, file, manga)
|
||||||
|
} else {
|
||||||
|
// Get the file from the url, removing the scheme if present.
|
||||||
|
val file = File(url.substringAfter("file://"))
|
||||||
|
|
||||||
|
// Return an instance of the fetcher providing the needed elements.
|
||||||
|
return MangaFileFetcher(file, manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,8 +113,9 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
|||||||
*
|
*
|
||||||
* @param manga the model.
|
* @param manga the model.
|
||||||
*/
|
*/
|
||||||
fun getHeaders(manga: Manga): Headers {
|
fun getHeaders(manga: Manga, source: HttpSource?): Headers {
|
||||||
val source = sourceManager.get(manga.source) as? OnlineSource ?: return LazyHeaders.DEFAULT
|
if (source == null) return LazyHeaders.DEFAULT
|
||||||
|
|
||||||
return cachedHeaders.getOrPut(manga.source) {
|
return cachedHeaders.getOrPut(manga.source) {
|
||||||
LazyHeaders.Builder().apply {
|
LazyHeaders.Builder().apply {
|
||||||
val nullStr: String? = null
|
val nullStr: String? = null
|
||||||
|
@ -18,13 +18,12 @@ import java.io.InputStream
|
|||||||
* @param file the file where this cover should be. It may exists or not.
|
* @param file the file where this cover should be. It may exists or not.
|
||||||
* @param manga the manga of the cover to load.
|
* @param manga the manga of the cover to load.
|
||||||
*/
|
*/
|
||||||
class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
class MangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
||||||
private val file: File,
|
private val file: File,
|
||||||
private val manga: Manga)
|
private val manga: Manga)
|
||||||
: DataFetcher<InputStream> {
|
: MangaFileFetcher(file, manga) {
|
||||||
|
|
||||||
@Throws(Exception::class)
|
override fun loadData(priority: Priority): InputStream {
|
||||||
override fun loadData(priority: Priority): InputStream? {
|
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
synchronized(file) {
|
synchronized(file) {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
@ -51,7 +50,7 @@ class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return file.inputStream()
|
return super.loadData(priority)
|
||||||
} else {
|
} else {
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
file.delete()
|
file.delete()
|
||||||
@ -60,22 +59,12 @@ class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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() {
|
override fun cancel() {
|
||||||
networkFetcher.cancel()
|
networkFetcher.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cleanup() {
|
override fun cleanup() {
|
||||||
|
super.cleanup()
|
||||||
networkFetcher.cleanup()
|
networkFetcher.cleanup()
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.library
|
|||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
@ -18,10 +17,12 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
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.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
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
|
||||||
@ -68,6 +69,11 @@ class LibraryUpdateService : Service() {
|
|||||||
*/
|
*/
|
||||||
private var subscription: Subscription? = null
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending intent of action that cancels the library update
|
||||||
|
*/
|
||||||
|
private val cancelPendingIntent by lazy {NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of the library update notification.
|
* Id of the library update notification.
|
||||||
*/
|
*/
|
||||||
@ -214,7 +220,7 @@ class LibraryUpdateService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
|
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
|
||||||
listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED }
|
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||||
}
|
}
|
||||||
|
|
||||||
return listToUpdate
|
return listToUpdate
|
||||||
@ -235,13 +241,10 @@ class LibraryUpdateService : Service() {
|
|||||||
val newUpdates = ArrayList<Manga>()
|
val newUpdates = ArrayList<Manga>()
|
||||||
val failedUpdates = ArrayList<Manga>()
|
val failedUpdates = ArrayList<Manga>()
|
||||||
|
|
||||||
val cancelIntent = PendingIntent.getBroadcast(this, 0,
|
|
||||||
Intent(this, CancelUpdateReceiver::class.java), 0)
|
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) }
|
||||||
// Update the chapters of the manga.
|
// Update the chapters of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
updateManga(manga)
|
updateManga(manga)
|
||||||
@ -297,7 +300,7 @@ class LibraryUpdateService : Service() {
|
|||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
val source = sourceManager.get(manga.source) as? OnlineSource ?: return Observable.empty()
|
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
|
||||||
return source.fetchChapterList(manga)
|
return source.fetchChapterList(manga)
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
}
|
}
|
||||||
@ -315,22 +318,20 @@ class LibraryUpdateService : Service() {
|
|||||||
// 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 cancelIntent = PendingIntent.getBroadcast(this, 0,
|
|
||||||
Intent(this, CancelUpdateReceiver::class.java), 0)
|
|
||||||
|
|
||||||
// Emit each manga and update it sequentially.
|
// Emit each manga and update it sequentially.
|
||||||
return Observable.from(mangaToUpdate)
|
return Observable.from(mangaToUpdate)
|
||||||
// Notify manga that will update.
|
// Notify manga that will update.
|
||||||
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
|
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) }
|
||||||
// Update the details of the manga.
|
// Update the details of the manga.
|
||||||
.concatMap { manga ->
|
.concatMap { manga ->
|
||||||
val source = sourceManager.get(manga.source) as? OnlineSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
?: return@concatMap Observable.empty<Manga>()
|
?: return@concatMap Observable.empty<Manga>()
|
||||||
|
|
||||||
source.fetchMangaDetails(manga)
|
source.fetchMangaDetails(manga)
|
||||||
.doOnNext { networkManga ->
|
.map { networkManga ->
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
manga
|
||||||
}
|
}
|
||||||
.onErrorReturn { manga }
|
.onErrorReturn { manga }
|
||||||
}
|
}
|
||||||
@ -457,19 +458,4 @@ class LibraryUpdateService : Service() {
|
|||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that stops updating the library.
|
|
||||||
*/
|
|
||||||
class CancelUpdateReceiver : BroadcastReceiver() {
|
|
||||||
/**
|
|
||||||
* Method called when user wants a library update.
|
|
||||||
* @param context the application context.
|
|
||||||
* @param intent the intent received.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
LibraryUpdateService.stop(context)
|
|
||||||
context.notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.notification
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.support.v4.content.FileProvider
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
import eu.kanade.tachiyomi.ui.download.DownloadActivity
|
||||||
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that manages [PendingIntent] of activity's
|
||||||
|
*/
|
||||||
|
object NotificationHandler {
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a download activity.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
*/
|
||||||
|
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
||||||
|
val intent = Intent(context, DownloadActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a gallery activity
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param file file containing image
|
||||||
|
*/
|
||||||
|
internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
|
||||||
|
setDataAndType(uri, "image/*")
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that prompts user with apk install intent
|
||||||
|
*
|
||||||
|
* @param context context
|
||||||
|
* @param file file of apk that is installed
|
||||||
|
*/
|
||||||
|
fun installApkPendingActivity(context: Context, file: File): PendingIntent {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
val uri = file.getUriCompat(context)
|
||||||
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,277 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.notification
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Handler
|
||||||
|
import eu.kanade.tachiyomi.Constants
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
|
import eu.kanade.tachiyomi.util.deleteIfExists
|
||||||
|
import eu.kanade.tachiyomi.util.getUriCompat
|
||||||
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
import eu.kanade.tachiyomi.util.toast
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global [BroadcastReceiver] that runs on UI thread
|
||||||
|
* Pending Broadcasts should be made from here.
|
||||||
|
* NOTE: Use local broadcasts if possible.
|
||||||
|
*/
|
||||||
|
class NotificationReceiver : BroadcastReceiver() {
|
||||||
|
/**
|
||||||
|
* Download manager.
|
||||||
|
*/
|
||||||
|
private val downloadManager: DownloadManager by injectLazy()
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
// Dismiss notification
|
||||||
|
ACTION_DISMISS_NOTIFICATION -> dismissNotification(context, intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||||
|
// Resume the download service
|
||||||
|
ACTION_RESUME_DOWNLOADS -> DownloadService.start(context)
|
||||||
|
// Clear the download queue
|
||||||
|
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
|
||||||
|
// Launch share activity and dismiss notification
|
||||||
|
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||||
|
// Delete image from path and dismiss notification
|
||||||
|
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
|
||||||
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||||
|
// Cancel library update and dismiss notification
|
||||||
|
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_ID)
|
||||||
|
// Open reader activity
|
||||||
|
ACTION_OPEN_CHAPTER -> {
|
||||||
|
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||||
|
intent.getLongExtra(EXTRA_CHAPTER_ID, -1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the notification
|
||||||
|
*
|
||||||
|
* @param notificationId the id of the notification
|
||||||
|
*/
|
||||||
|
private fun dismissNotification(context: Context, notificationId: Int) {
|
||||||
|
context.notificationManager.cancel(notificationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to start share intent to share image
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param path path of file
|
||||||
|
* @param notificationId id of notification
|
||||||
|
*/
|
||||||
|
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
||||||
|
// Create intent
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
val uri = File(path).getUriCompat(context)
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
type = "image/*"
|
||||||
|
}
|
||||||
|
// Dismiss notification
|
||||||
|
dismissNotification(context, notificationId)
|
||||||
|
// Launch share activity
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts reader activity
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param mangaId id of manga
|
||||||
|
* @param chapterId id of chapter
|
||||||
|
*/
|
||||||
|
internal fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
||||||
|
val db = DatabaseHelper(context)
|
||||||
|
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||||
|
val chapter = db.getChapter(chapterId).executeAsBlocking()
|
||||||
|
|
||||||
|
if (manga != null && chapter != null) {
|
||||||
|
val intent = ReaderActivity.newIntent(context, manga, chapter).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
} else {
|
||||||
|
context.toast(context.getString(R.string.chapter_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to delete image
|
||||||
|
*
|
||||||
|
* @param path path of file
|
||||||
|
* @param notificationId id of notification
|
||||||
|
*/
|
||||||
|
private fun deleteImage(context: Context, path: String, notificationId: Int) {
|
||||||
|
// Dismiss notification
|
||||||
|
dismissNotification(context, notificationId)
|
||||||
|
|
||||||
|
// Delete file
|
||||||
|
File(path).deleteIfExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when user wants to stop a library update
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param notificationId id of notification
|
||||||
|
*/
|
||||||
|
private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
|
||||||
|
LibraryUpdateService.stop(context)
|
||||||
|
Handler().post { dismissNotification(context, notificationId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NAME = "NotificationReceiver"
|
||||||
|
|
||||||
|
// Called to launch share intent.
|
||||||
|
private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
|
||||||
|
|
||||||
|
// Called to delete image.
|
||||||
|
private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
|
||||||
|
|
||||||
|
// Called to cancel library update.
|
||||||
|
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||||
|
|
||||||
|
// Called to open chapter
|
||||||
|
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||||
|
|
||||||
|
// Value containing file location.
|
||||||
|
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
|
||||||
|
|
||||||
|
// Called to resume downloads.
|
||||||
|
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
||||||
|
|
||||||
|
// Called to clear downloads.
|
||||||
|
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
|
||||||
|
|
||||||
|
// Called to dismiss notification.
|
||||||
|
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
|
||||||
|
|
||||||
|
// Value containing notification id.
|
||||||
|
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
|
||||||
|
|
||||||
|
// Value containing manga id.
|
||||||
|
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
||||||
|
|
||||||
|
// Value containing chapter id.
|
||||||
|
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [PendingIntent] that resumes the download of a chapter
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun resumeDownloadsPendingBroadcast(context: Context): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_RESUME_DOWNLOADS
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [PendingIntent] that clears the download queue
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun clearDownloadsPendingBroadcast(context: Context): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_CLEAR_DOWNLOADS
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a service which dismissed the notification
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param notificationId id of notification
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun dismissNotificationPendingBroadcast(context: Context, notificationId: Int): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_DISMISS_NOTIFICATION
|
||||||
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a service which cancels the notification and starts a share activity
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param path location path of file
|
||||||
|
* @param notificationId id of notification
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun shareImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_SHARE_IMAGE
|
||||||
|
putExtra(EXTRA_FILE_LOCATION, path)
|
||||||
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a service which removes an image from disk
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param path location path of file
|
||||||
|
* @param notificationId id of notification
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun deleteImagePendingBroadcast(context: Context, path: String, notificationId: Int): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_DELETE_IMAGE
|
||||||
|
putExtra(EXTRA_FILE_LOCATION, path)
|
||||||
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that start a reader activity containing chapter.
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param manga manga of chapter
|
||||||
|
* @param chapter chapter that needs to be opened
|
||||||
|
*/
|
||||||
|
internal fun openChapterPendingBroadcast(context: Context, manga: Manga, chapter: Chapter): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_OPEN_CHAPTER
|
||||||
|
putExtra(EXTRA_MANGA_ID, manga.id)
|
||||||
|
putExtra(EXTRA_CHAPTER_ID, chapter.id)
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that starts a service which stops the library update
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @return [PendingIntent]
|
||||||
|
*/
|
||||||
|
internal fun cancelLibraryUpdatePendingBroadcast(context: Context): PendingIntent {
|
||||||
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_CANCEL_LIBRARY_UPDATE
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -41,6 +41,8 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val readerTheme = context.getString(R.string.pref_reader_theme_key)
|
val readerTheme = context.getString(R.string.pref_reader_theme_key)
|
||||||
|
|
||||||
|
val cropBorders = context.getString(R.string.pref_crop_borders_key)
|
||||||
|
|
||||||
val readWithTapping = context.getString(R.string.pref_read_with_tapping_key)
|
val readWithTapping = context.getString(R.string.pref_read_with_tapping_key)
|
||||||
|
|
||||||
val readWithVolumeKeys = context.getString(R.string.pref_read_with_volume_keys_key)
|
val readWithVolumeKeys = context.getString(R.string.pref_read_with_volume_keys_key)
|
||||||
@ -91,9 +93,9 @@ class PreferenceKeys(context: Context) {
|
|||||||
|
|
||||||
val downloadNew = context.getString(R.string.pref_download_new_key)
|
val downloadNew = context.getString(R.string.pref_download_new_key)
|
||||||
|
|
||||||
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
|
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
||||||
|
|
||||||
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
|
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
|
@ -7,8 +7,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.source.Source
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import exh.ui.migration.MigrationStatus
|
import exh.ui.migration.MigrationStatus
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
|
fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
|
||||||
|
|
||||||
fun enableTransitions() = rxPrefs.getBoolean(keys.enableTransitions, true)
|
fun pageTransitions() = rxPrefs.getBoolean(keys.enableTransitions, true)
|
||||||
|
|
||||||
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
|
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
|
||||||
|
|
||||||
@ -61,6 +61,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun readerTheme() = rxPrefs.getInteger(keys.readerTheme, 0)
|
fun readerTheme() = rxPrefs.getInteger(keys.readerTheme, 0)
|
||||||
|
|
||||||
|
fun cropBorders() = rxPrefs.getBoolean(keys.cropBorders, false)
|
||||||
|
|
||||||
fun readWithTapping() = rxPrefs.getBoolean(keys.readWithTapping, true)
|
fun readWithTapping() = rxPrefs.getBoolean(keys.readWithTapping, true)
|
||||||
|
|
||||||
fun readWithVolumeKeys() = rxPrefs.getBoolean(keys.readWithVolumeKeys, false)
|
fun readWithVolumeKeys() = rxPrefs.getBoolean(keys.readWithVolumeKeys, false)
|
||||||
@ -75,7 +77,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
|
fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
|
||||||
|
|
||||||
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
|
fun lastUsedCatalogueSource() = rxPrefs.getLong(keys.lastUsedCatalogueSource, -1)
|
||||||
|
|
||||||
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
|
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
|
||||||
|
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source
|
|
||||||
|
|
||||||
import android.Manifest.permission.READ_EXTERNAL_STORAGE
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Environment
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.all.EHentai
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.all.EHentaiMetadata
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.english.*
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.german.WieManga
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.russian.Mangachan
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.russian.Readmanga
|
|
||||||
import eu.kanade.tachiyomi.util.hasPermission
|
|
||||||
import org.yaml.snakeyaml.Yaml
|
|
||||||
import rx.functions.Action1
|
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
open class SourceManager(private val context: Context) {
|
|
||||||
|
|
||||||
private val prefs: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
private var sourcesMap = createSources()
|
|
||||||
|
|
||||||
open fun get(sourceKey: Int): Source? {
|
|
||||||
return sourcesMap[sourceKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
|
|
||||||
|
|
||||||
private fun createOnlineSourceList(): List<Source> = listOf(
|
|
||||||
Batoto(101),
|
|
||||||
Mangahere(102),
|
|
||||||
Mangafox(103),
|
|
||||||
Kissmanga(104),
|
|
||||||
Readmanga(105),
|
|
||||||
Mintmanga(106),
|
|
||||||
Mangachan(107),
|
|
||||||
Readmangatoday(108),
|
|
||||||
Mangasee(109),
|
|
||||||
WieManga(110)
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun createEHSources(): List<Source> {
|
|
||||||
val exSrcs = mutableListOf(
|
|
||||||
EHentai(1, false, context),
|
|
||||||
EHentaiMetadata(3, false, context)
|
|
||||||
)
|
|
||||||
if(prefs.enableExhentai().getOrDefault()) {
|
|
||||||
exSrcs += EHentai(2, true, context)
|
|
||||||
exSrcs += EHentaiMetadata(4, true, context)
|
|
||||||
}
|
|
||||||
return exSrcs
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
//Rebuild EH when settings change
|
|
||||||
val action: Action1<Any> = Action1 { sourcesMap = createSources() }
|
|
||||||
|
|
||||||
prefs.enableExhentai().asObservable().subscribe(action)
|
|
||||||
prefs.imageQuality().asObservable().subscribe (action)
|
|
||||||
prefs.useHentaiAtHome().asObservable().subscribe(action)
|
|
||||||
prefs.useJapaneseTitle().asObservable().subscribe {
|
|
||||||
action.call(null)
|
|
||||||
}
|
|
||||||
prefs.ehSearchSize().asObservable().subscribe (action)
|
|
||||||
prefs.thumbnailRows().asObservable().subscribe(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
|
|
||||||
createEHSources().forEach { put(it.id, it) }
|
|
||||||
createOnlineSourceList().forEach { put(it.id, it) }
|
|
||||||
|
|
||||||
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
|
||||||
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(map).let { put(it.id, it) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e("Error loading source from file. Bad format?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.model
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
|
|
||||||
class MangasPage(val page: Int) {
|
|
||||||
|
|
||||||
val mangas: MutableList<Manga> = mutableListOf()
|
|
||||||
|
|
||||||
lateinit var url: String
|
|
||||||
|
|
||||||
var nextPageUrl: String? = null
|
|
||||||
|
|
||||||
}
|
|
@ -1,469 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
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.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.util.UrlUtil
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple implementation for sources from a website.
|
|
||||||
*/
|
|
||||||
abstract class OnlineSource() : Source {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Network service.
|
|
||||||
*/
|
|
||||||
val network: NetworkHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter cache.
|
|
||||||
*/
|
|
||||||
val chapterCache: ChapterCache by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences helper.
|
|
||||||
*/
|
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
|
||||||
*/
|
|
||||||
abstract val baseUrl: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ISO 639-1 compliant language code (two characters in lower case).
|
|
||||||
*/
|
|
||||||
abstract val lang: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the source has support for latest updates.
|
|
||||||
*/
|
|
||||||
abstract val supportsLatest : Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Headers used for requests.
|
|
||||||
*/
|
|
||||||
val headers by lazy { headersBuilder().build() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Genre filters.
|
|
||||||
*/
|
|
||||||
val filters by lazy { getFilterList() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default network client for doing requests.
|
|
||||||
*/
|
|
||||||
open val client: OkHttpClient
|
|
||||||
get() = network.client
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.toUpperCase()})"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
popularMangaParse(response, page)
|
|
||||||
page
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, filters: List<Filter>): Observable<MangasPage> = client
|
|
||||||
.newCall(searchMangaRequest(page, query, filters))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
searchMangaParse(response, page, query, filters)
|
|
||||||
page
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, filters: List<Filter>): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = searchMangaInitialUrl(query, filters)
|
|
||||||
}
|
|
||||||
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, filters: List<Filter>): 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, filters: List<Filter>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable containing a page with a list of latest manga.
|
|
||||||
*/
|
|
||||||
open fun fetchLatestUpdates(page: MangasPage): Observable<MangasPage> = client
|
|
||||||
.newCall(latestUpdatesRequest(page))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
latestUpdatesParse(response, page)
|
|
||||||
page
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request for latest manga given the page.
|
|
||||||
*/
|
|
||||||
open protected fun latestUpdatesRequest(page: MangasPage): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = latestUpdatesInitialUrl()
|
|
||||||
}
|
|
||||||
return GET(page.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the absolute url of the first page to latest manga.
|
|
||||||
*/
|
|
||||||
abstract protected fun latestUpdatesInitialUrl(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as [popularMangaParse], but for latest manga.
|
|
||||||
*/
|
|
||||||
abstract protected fun latestUpdatesParse(response: Response, page: MangasPage)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
|
||||||
* override this method.
|
|
||||||
*
|
|
||||||
* @param manga the manga to be updated.
|
|
||||||
*/
|
|
||||||
override fun fetchMangaDetails(manga: Manga): Observable<Manga> = client
|
|
||||||
.newCall(mangaDetailsRequest(manga))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.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 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))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.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))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
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))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.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)
|
|
||||||
.asObservableSuccess()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 imageUrl = page.imageUrl ?: return Observable.just(page)
|
|
||||||
|
|
||||||
return Observable.just(page)
|
|
||||||
.flatMap {
|
|
||||||
if (!chapterCache.isImageInCache(imageUrl)) {
|
|
||||||
cacheImage(page)
|
|
||||||
} else {
|
|
||||||
Observable.just(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.doOnNext {
|
|
||||||
page.uri = Uri.fromFile(chapterCache.getImageFile(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) }
|
|
||||||
.map { page }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Utility methods
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Chapter.setUrlWithoutDomain(url: String) {
|
|
||||||
this.url = UrlUtil.getPath(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Manga.setUrlWithoutDomain(url: String) {
|
|
||||||
this.url = UrlUtil.getPath(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before inserting a new chapter into database. Use it if you need to override chapter
|
|
||||||
* fields, like the title or the chapter number. Do not change anything to [manga].
|
|
||||||
*
|
|
||||||
* @param chapter the chapter to be added.
|
|
||||||
* @param manga the manga of the chapter.
|
|
||||||
*/
|
|
||||||
open fun prepareNewChapter(chapter: Chapter, manga: Manga) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Filter(val id: String, val name: String)
|
|
||||||
|
|
||||||
open fun getFilterList(): List<Filter> = emptyList()
|
|
||||||
}
|
|
@ -1,211 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
|
||||||
|
|
||||||
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 eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
|
||||||
*/
|
|
||||||
abstract class ParsedOnlineSource() : OnlineSource() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = response.asJsoup()
|
|
||||||
for (element in document.select(popularMangaSelector())) {
|
|
||||||
Manga.create(id).apply {
|
|
||||||
popularMangaFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
popularMangaNextPageSelector()?.let { selector ->
|
|
||||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, filters: List<Filter>) {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
for (element in document.select(searchMangaSelector())) {
|
|
||||||
Manga.create(id).apply {
|
|
||||||
searchMangaFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchMangaNextPageSelector()?.let { selector ->
|
|
||||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 for latest updates and fills [page].
|
|
||||||
*/
|
|
||||||
override fun latestUpdatesParse(response: Response, page: MangasPage) {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
for (element in document.select(latestUpdatesSelector())) {
|
|
||||||
Manga.create(id).apply {
|
|
||||||
latestUpdatesFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
latestUpdatesNextPageSelector()?.let { selector ->
|
|
||||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector similar to [popularMangaSelector], but for latest updates.
|
|
||||||
*/
|
|
||||||
abstract protected fun latestUpdatesSelector(): String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fills [manga] with the given [element]. For latest updates.
|
|
||||||
*/
|
|
||||||
abstract protected fun latestUpdatesFromElement(element: Element, manga: Manga)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Jsoup selector that returns the <a> tag, like [popularMangaNextPageSelector].
|
|
||||||
*/
|
|
||||||
abstract protected fun latestUpdatesNextPageSelector(): String?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the response from the site and fills the details of [manga].
|
|
||||||
*
|
|
||||||
* @param response the response from the site.
|
|
||||||
* @param manga the manga to fill.
|
|
||||||
*/
|
|
||||||
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
|
||||||
mangaDetailsParse(response.asJsoup(), 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 = response.asJsoup()
|
|
||||||
|
|
||||||
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(response.asJsoup(), 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(response.asJsoup())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the absolute url to the source image from the document.
|
|
||||||
*
|
|
||||||
* @param document the parsed document.
|
|
||||||
*/
|
|
||||||
abstract protected fun imageUrlParse(document: Document): String
|
|
||||||
}
|
|
@ -1,354 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
|
||||||
|
|
||||||
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.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.asJsoup
|
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
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(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
|
||||||
|
|
||||||
override val name = "Batoto"
|
|
||||||
|
|
||||||
override val baseUrl = "http://bato.to"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
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 latestUpdatesInitialUrl() = "$baseUrl/search_ajax?order_cond=update&order=desc&p=1"
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
for (element in document.select(popularMangaSelector())) {
|
|
||||||
Manga.create(id).apply {
|
|
||||||
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 latestUpdatesParse(response: Response, page: MangasPage) {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
for (element in document.select(latestUpdatesSelector())) {
|
|
||||||
Manga.create(id).apply {
|
|
||||||
latestUpdatesFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.nextPageUrl = document.select(latestUpdatesNextPageSelector()).first()?.let {
|
|
||||||
"$baseUrl/search_ajax?order_cond=update&order=desc&p=${page.page + 1}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "tr:has(a)"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "tr:has(a)"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a[href^=http://bato.to]").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.text().trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "#show_more_row"
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1${getFilterParams(filters)}"
|
|
||||||
|
|
||||||
private fun getFilterParams(filters: List<Filter>): String {
|
|
||||||
var genres = ""
|
|
||||||
var completed = ""
|
|
||||||
for (filter in filters) {
|
|
||||||
if (filter.equals(completedFilter)) completed = "&completed=c"
|
|
||||||
else genres += ";i" + filter.id
|
|
||||||
}
|
|
||||||
return if (genres.isEmpty()) completed else "&genres=$genres&genre_cond=and$completed"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = searchMangaInitialUrl(query, filters)
|
|
||||||
}
|
|
||||||
return GET(page.url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
for (element in document.select(searchMangaSelector())) {
|
|
||||||
Manga.create(id).apply {
|
|
||||||
searchMangaFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let {
|
|
||||||
"$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=${page.page + 1}${getFilterParams(filters)}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = response.asJsoup(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.setUrlWithoutDomain(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()
|
|
||||||
|
|
||||||
var 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, username, password) }
|
|
||||||
.map { isAuthenticationSuccessful(it) }
|
|
||||||
|
|
||||||
private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
|
|
||||||
val doc = response.asJsoup()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val completedFilter = Filter("completed", "Completed")
|
|
||||||
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
|
|
||||||
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Filter("${id}", "${el.textContent.trim()}")`
|
|
||||||
// }).join(',\n')
|
|
||||||
// on https://bato.to/search
|
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
|
||||||
completedFilter,
|
|
||||||
Filter("40", "4-Koma"),
|
|
||||||
Filter("1", "Action"),
|
|
||||||
Filter("2", "Adventure"),
|
|
||||||
Filter("39", "Award Winning"),
|
|
||||||
Filter("3", "Comedy"),
|
|
||||||
Filter("41", "Cooking"),
|
|
||||||
Filter("9", "Doujinshi"),
|
|
||||||
Filter("10", "Drama"),
|
|
||||||
Filter("12", "Ecchi"),
|
|
||||||
Filter("13", "Fantasy"),
|
|
||||||
Filter("15", "Gender Bender"),
|
|
||||||
Filter("17", "Harem"),
|
|
||||||
Filter("20", "Historical"),
|
|
||||||
Filter("22", "Horror"),
|
|
||||||
Filter("34", "Josei"),
|
|
||||||
Filter("27", "Martial Arts"),
|
|
||||||
Filter("30", "Mecha"),
|
|
||||||
Filter("42", "Medical"),
|
|
||||||
Filter("37", "Music"),
|
|
||||||
Filter("4", "Mystery"),
|
|
||||||
Filter("38", "Oneshot"),
|
|
||||||
Filter("5", "Psychological"),
|
|
||||||
Filter("6", "Romance"),
|
|
||||||
Filter("7", "School Life"),
|
|
||||||
Filter("8", "Sci-fi"),
|
|
||||||
Filter("32", "Seinen"),
|
|
||||||
Filter("35", "Shoujo"),
|
|
||||||
Filter("16", "Shoujo Ai"),
|
|
||||||
Filter("33", "Shounen"),
|
|
||||||
Filter("19", "Shounen Ai"),
|
|
||||||
Filter("21", "Slice of Life"),
|
|
||||||
Filter("23", "Smut"),
|
|
||||||
Filter("25", "Sports"),
|
|
||||||
Filter("26", "Supernatural"),
|
|
||||||
Filter("28", "Tragedy"),
|
|
||||||
Filter("36", "Webtoon"),
|
|
||||||
Filter("29", "Yaoi"),
|
|
||||||
Filter("31", "Yuri")
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
@ -1,181 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
|
||||||
|
|
||||||
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.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(override val id: Int) : ParsedOnlineSource() {
|
|
||||||
|
|
||||||
override val name = "Kissmanga"
|
|
||||||
|
|
||||||
override val baseUrl = "http://kissmanga.com"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/MangaList/MostPopular"
|
|
||||||
|
|
||||||
override fun latestUpdatesInitialUrl() = "http://kissmanga.com/MangaList/LatestUpdate"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "table.listing tr:gt(1)"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "table.listing tr:gt(1)"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("td a:eq(0)").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String = "ul.pager > li > a:contains(Next)"
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = searchMangaInitialUrl(query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
val form = FormBody.Builder().apply {
|
|
||||||
add("authorArtist", "")
|
|
||||||
add("mangaName", query)
|
|
||||||
|
|
||||||
this@Kissmanga.filters.forEach { filter ->
|
|
||||||
if (filter.equals(completedFilter)) add("status", if (filter in filters) filter.id else "")
|
|
||||||
else add("genres", if (filter in filters) "1" else "0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return POST(page.url, headers, form.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$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.setUrlWithoutDomain(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) = ""
|
|
||||||
|
|
||||||
private val completedFilter = Filter("Completed", "Completed")
|
|
||||||
// $("select[name=\"genres\"]").map((i,el) => `Filter("${i}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
|
||||||
// on http://kissmanga.com/AdvanceSearch
|
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
|
||||||
completedFilter,
|
|
||||||
Filter("0", "Action"),
|
|
||||||
Filter("1", "Adult"),
|
|
||||||
Filter("2", "Adventure"),
|
|
||||||
Filter("3", "Comedy"),
|
|
||||||
Filter("4", "Comic"),
|
|
||||||
Filter("5", "Cooking"),
|
|
||||||
Filter("6", "Doujinshi"),
|
|
||||||
Filter("7", "Drama"),
|
|
||||||
Filter("8", "Ecchi"),
|
|
||||||
Filter("9", "Fantasy"),
|
|
||||||
Filter("10", "Gender Bender"),
|
|
||||||
Filter("11", "Harem"),
|
|
||||||
Filter("12", "Historical"),
|
|
||||||
Filter("13", "Horror"),
|
|
||||||
Filter("14", "Josei"),
|
|
||||||
Filter("15", "Lolicon"),
|
|
||||||
Filter("16", "Manga"),
|
|
||||||
Filter("17", "Manhua"),
|
|
||||||
Filter("18", "Manhwa"),
|
|
||||||
Filter("19", "Martial Arts"),
|
|
||||||
Filter("20", "Mature"),
|
|
||||||
Filter("21", "Mecha"),
|
|
||||||
Filter("22", "Medical"),
|
|
||||||
Filter("23", "Music"),
|
|
||||||
Filter("24", "Mystery"),
|
|
||||||
Filter("25", "One shot"),
|
|
||||||
Filter("26", "Psychological"),
|
|
||||||
Filter("27", "Romance"),
|
|
||||||
Filter("28", "School Life"),
|
|
||||||
Filter("29", "Sci-fi"),
|
|
||||||
Filter("30", "Seinen"),
|
|
||||||
Filter("31", "Shotacon"),
|
|
||||||
Filter("32", "Shoujo"),
|
|
||||||
Filter("33", "Shoujo Ai"),
|
|
||||||
Filter("34", "Shounen"),
|
|
||||||
Filter("35", "Shounen Ai"),
|
|
||||||
Filter("36", "Slice of Life"),
|
|
||||||
Filter("37", "Smut"),
|
|
||||||
Filter("38", "Sports"),
|
|
||||||
Filter("39", "Supernatural"),
|
|
||||||
Filter("40", "Tragedy"),
|
|
||||||
Filter("41", "Webtoon"),
|
|
||||||
Filter("42", "Yaoi"),
|
|
||||||
Filter("43", "Yuri")
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,171 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
|
||||||
|
|
||||||
override val name = "Mangafox"
|
|
||||||
|
|
||||||
override val baseUrl = "http://mangafox.me"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
|
|
||||||
|
|
||||||
override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?latest"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "div#mangalist > ul.list > li"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a.title").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
|
||||||
"$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1&${filters.map { it.id + "=1" }.joinToString("&")}"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div#mangalist > ul.list > li"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a.title").first().let {
|
|
||||||
manga.setUrlWithoutDomain(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.setUrlWithoutDomain(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 = response.asJsoup()
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
// $('select.genres').map((i,el)=>`Filter("${$(el).attr('name')}", "${$(el).next().text().trim()}")`).get().join(',\n')
|
|
||||||
// on http://kissmanga.com/AdvanceSearch
|
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
|
||||||
Filter("is_completed", "Completed"),
|
|
||||||
Filter("genres[Action]", "Action"),
|
|
||||||
Filter("genres[Adult]", "Adult"),
|
|
||||||
Filter("genres[Adventure]", "Adventure"),
|
|
||||||
Filter("genres[Comedy]", "Comedy"),
|
|
||||||
Filter("genres[Doujinshi]", "Doujinshi"),
|
|
||||||
Filter("genres[Drama]", "Drama"),
|
|
||||||
Filter("genres[Ecchi]", "Ecchi"),
|
|
||||||
Filter("genres[Fantasy]", "Fantasy"),
|
|
||||||
Filter("genres[Gender Bender]", "Gender Bender"),
|
|
||||||
Filter("genres[Harem]", "Harem"),
|
|
||||||
Filter("genres[Historical]", "Historical"),
|
|
||||||
Filter("genres[Horror]", "Horror"),
|
|
||||||
Filter("genres[Josei]", "Josei"),
|
|
||||||
Filter("genres[Martial Arts]", "Martial Arts"),
|
|
||||||
Filter("genres[Mature]", "Mature"),
|
|
||||||
Filter("genres[Mecha]", "Mecha"),
|
|
||||||
Filter("genres[Mystery]", "Mystery"),
|
|
||||||
Filter("genres[One Shot]", "One Shot"),
|
|
||||||
Filter("genres[Psychological]", "Psychological"),
|
|
||||||
Filter("genres[Romance]", "Romance"),
|
|
||||||
Filter("genres[School Life]", "School Life"),
|
|
||||||
Filter("genres[Sci-fi]", "Sci-fi"),
|
|
||||||
Filter("genres[Seinen]", "Seinen"),
|
|
||||||
Filter("genres[Shoujo]", "Shoujo"),
|
|
||||||
Filter("genres[Shoujo Ai]", "Shoujo Ai"),
|
|
||||||
Filter("genres[Shounen]", "Shounen"),
|
|
||||||
Filter("genres[Shounen Ai]", "Shounen Ai"),
|
|
||||||
Filter("genres[Slice of Life]", "Slice of Life"),
|
|
||||||
Filter("genres[Smut]", "Smut"),
|
|
||||||
Filter("genres[Sports]", "Sports"),
|
|
||||||
Filter("genres[Supernatural]", "Supernatural"),
|
|
||||||
Filter("genres[Tragedy]", "Tragedy"),
|
|
||||||
Filter("genres[Webtoons]", "Webtoons"),
|
|
||||||
Filter("genres[Yaoi]", "Yaoi"),
|
|
||||||
Filter("genres[Yuri]", "Yuri")
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
@ -1,172 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
|
||||||
|
|
||||||
override val name = "Mangahere"
|
|
||||||
|
|
||||||
override val baseUrl = "http://www.mangahere.co"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/directory/?views.za"
|
|
||||||
|
|
||||||
override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?last_chapter_time.za"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
|
||||||
|
|
||||||
private fun mangaFromElement(query: String, element: Element, manga: Manga) {
|
|
||||||
element.select(query).first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
mangaFromElement("div.title > a", element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search.php?name=$query&page=1&sort=views&order=za&${filters.map { it.id + "=1" }.joinToString("&")}&advopts=1"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
mangaFromElement("a.manga_info", element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 parentEl = element.select("span.left").first()
|
|
||||||
|
|
||||||
val urlElement = parentEl.select("a").first()
|
|
||||||
|
|
||||||
var volume = parentEl.select("span.mr6")?.first()?.text()?.trim()?:""
|
|
||||||
if (volume.length > 0) {
|
|
||||||
volume = " - " + volume
|
|
||||||
}
|
|
||||||
|
|
||||||
var title = parentEl?.textNodes()?.last()?.text()?.trim()?:""
|
|
||||||
if (title.length > 0) {
|
|
||||||
title = " - " + title
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text() + volume + title
|
|
||||||
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")
|
|
||||||
|
|
||||||
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Filter("${el.getAttribute('name')}", "${el.nextSibling.nextSibling.textContent.trim()}")`).join(',\n')
|
|
||||||
// http://www.mangahere.co/advsearch.htm
|
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
|
||||||
Filter("is_completed", "Completed"),
|
|
||||||
Filter("genres[Action]", "Action"),
|
|
||||||
Filter("genres[Adventure]", "Adventure"),
|
|
||||||
Filter("genres[Comedy]", "Comedy"),
|
|
||||||
Filter("genres[Doujinshi]", "Doujinshi"),
|
|
||||||
Filter("genres[Drama]", "Drama"),
|
|
||||||
Filter("genres[Ecchi]", "Ecchi"),
|
|
||||||
Filter("genres[Fantasy]", "Fantasy"),
|
|
||||||
Filter("genres[Gender Bender]", "Gender Bender"),
|
|
||||||
Filter("genres[Harem]", "Harem"),
|
|
||||||
Filter("genres[Historical]", "Historical"),
|
|
||||||
Filter("genres[Horror]", "Horror"),
|
|
||||||
Filter("genres[Josei]", "Josei"),
|
|
||||||
Filter("genres[Martial Arts]", "Martial Arts"),
|
|
||||||
Filter("genres[Mature]", "Mature"),
|
|
||||||
Filter("genres[Mecha]", "Mecha"),
|
|
||||||
Filter("genres[Mystery]", "Mystery"),
|
|
||||||
Filter("genres[One Shot]", "One Shot"),
|
|
||||||
Filter("genres[Psychological]", "Psychological"),
|
|
||||||
Filter("genres[Romance]", "Romance"),
|
|
||||||
Filter("genres[School Life]", "School Life"),
|
|
||||||
Filter("genres[Sci-fi]", "Sci-fi"),
|
|
||||||
Filter("genres[Seinen]", "Seinen"),
|
|
||||||
Filter("genres[Shoujo]", "Shoujo"),
|
|
||||||
Filter("genres[Shoujo Ai]", "Shoujo Ai"),
|
|
||||||
Filter("genres[Shounen]", "Shounen"),
|
|
||||||
Filter("genres[Shounen Ai]", "Shounen Ai"),
|
|
||||||
Filter("genres[Slice of Life]", "Slice of Life"),
|
|
||||||
Filter("genres[Sports]", "Sports"),
|
|
||||||
Filter("genres[Supernatural]", "Supernatural"),
|
|
||||||
Filter("genres[Tragedy]", "Tragedy"),
|
|
||||||
Filter("genres[Yaoi]", "Yaoi"),
|
|
||||||
Filter("genres[Yuri]", "Yuri")
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
@ -1,261 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
|
||||||
|
|
||||||
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.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
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 Mangasee(override val id: Int) : ParsedOnlineSource() {
|
|
||||||
|
|
||||||
override val name = "Mangasee"
|
|
||||||
|
|
||||||
override val baseUrl = "http://mangaseeonline.net"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val recentUpdatesPattern = Pattern.compile("(.*?)\\s(\\d+\\.?\\d*)\\s?(Completed)?")
|
|
||||||
|
|
||||||
private val indexPattern = Pattern.compile("-index-(.*?)-")
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.requested > div.row"
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: MangasPage): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = popularMangaInitialUrl()
|
|
||||||
}
|
|
||||||
val (body, requestUrl) = convertQueryToPost(page)
|
|
||||||
return POST(requestUrl, headers, body.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
for (element in document.select(popularMangaSelector())) {
|
|
||||||
Manga.create(id).apply {
|
|
||||||
popularMangaFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.nextPageUrl = page.url
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a.resultLink").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not used, overrides parent.
|
|
||||||
override fun popularMangaNextPageSelector() = ""
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>): String {
|
|
||||||
var url = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&keyword=$query"
|
|
||||||
var genres: String? = null
|
|
||||||
for (filter in filters) {
|
|
||||||
if (filter.equals(completedFilter)) url += "&status=Complete"
|
|
||||||
else if (genres == null) genres = filter.id
|
|
||||||
else genres += "," + filter.id
|
|
||||||
}
|
|
||||||
return if (genres == null) url else url + "&genre=$genres"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.searchResults > div.requested > div.row"
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = searchMangaInitialUrl(query, filters)
|
|
||||||
}
|
|
||||||
val (body, requestUrl) = convertQueryToPost(page)
|
|
||||||
return POST(requestUrl, headers, body.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun convertQueryToPost(page: MangasPage): Pair<FormBody.Builder, String> {
|
|
||||||
val url = HttpUrl.parse(page.url)
|
|
||||||
val body = FormBody.Builder().add("page", page.page.toString())
|
|
||||||
for (i in 0..url.querySize() - 1) {
|
|
||||||
body.add(url.queryParameterName(i), url.queryParameterValue(i))
|
|
||||||
}
|
|
||||||
val requestUrl = url.scheme() + "://" + url.host() + url.encodedPath()
|
|
||||||
return Pair(body, requestUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
for (element in document.select(popularMangaSelector())) {
|
|
||||||
Manga.create(id).apply {
|
|
||||||
popularMangaFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.nextPageUrl = page.url
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a.resultLink").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not used, overrides parent.
|
|
||||||
override fun searchMangaNextPageSelector() = ""
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
|
||||||
val detailElement = document.select("div.well > div.row").first()
|
|
||||||
|
|
||||||
manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text()
|
|
||||||
manga.genre = detailElement.select("span.details > div.row > div:has(b:contains(Genre(s))) > a").map { it.text() }.joinToString()
|
|
||||||
manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
|
|
||||||
manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) }
|
|
||||||
manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseStatus(status: String) = when {
|
|
||||||
status.contains("Ongoing (Scan)") -> Manga.ONGOING
|
|
||||||
status.contains("Complete (Scan)") -> Manga.COMPLETED
|
|
||||||
else -> Manga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = "div.chapter-list > a"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
|
||||||
val urlElement = element.select("a").first()
|
|
||||||
|
|
||||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
|
||||||
chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: ""
|
|
||||||
chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterDate(dateAsString: String): Long {
|
|
||||||
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
val fullUrl = response.request().url().toString()
|
|
||||||
val url = fullUrl.substringBeforeLast('/')
|
|
||||||
|
|
||||||
val series = document.select("input.IndexName").first().attr("value")
|
|
||||||
val chapter = document.select("span.CurChapter").first().text()
|
|
||||||
var index = ""
|
|
||||||
|
|
||||||
val m = indexPattern.matcher(fullUrl)
|
|
||||||
if (m.find()) {
|
|
||||||
val indexNumber = m.group(1)
|
|
||||||
index = "-index-$indexNumber"
|
|
||||||
}
|
|
||||||
|
|
||||||
document.select("div.ContainerNav").first().select("select.PageSelect > option").forEach {
|
|
||||||
pages.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html"))
|
|
||||||
}
|
|
||||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not used, overrides parent.
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
|
|
||||||
|
|
||||||
private val completedFilter = Filter("Complete", "Completed")
|
|
||||||
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
|
||||||
// http://mangasee.co/advanced-search/
|
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
|
||||||
completedFilter,
|
|
||||||
Filter("Action", "Action"),
|
|
||||||
Filter("Adult", "Adult"),
|
|
||||||
Filter("Adventure", "Adventure"),
|
|
||||||
Filter("Comedy", "Comedy"),
|
|
||||||
Filter("Doujinshi", "Doujinshi"),
|
|
||||||
Filter("Drama", "Drama"),
|
|
||||||
Filter("Ecchi", "Ecchi"),
|
|
||||||
Filter("Fantasy", "Fantasy"),
|
|
||||||
Filter("Gender_Bender", "Gender Bender"),
|
|
||||||
Filter("Harem", "Harem"),
|
|
||||||
Filter("Hentai", "Hentai"),
|
|
||||||
Filter("Historical", "Historical"),
|
|
||||||
Filter("Horror", "Horror"),
|
|
||||||
Filter("Josei", "Josei"),
|
|
||||||
Filter("Lolicon", "Lolicon"),
|
|
||||||
Filter("Martial_Arts", "Martial Arts"),
|
|
||||||
Filter("Mature", "Mature"),
|
|
||||||
Filter("Mecha", "Mecha"),
|
|
||||||
Filter("Mystery", "Mystery"),
|
|
||||||
Filter("Psychological", "Psychological"),
|
|
||||||
Filter("Romance", "Romance"),
|
|
||||||
Filter("School_Life", "School Life"),
|
|
||||||
Filter("Sci-fi", "Sci-fi"),
|
|
||||||
Filter("Seinen", "Seinen"),
|
|
||||||
Filter("Shotacon", "Shotacon"),
|
|
||||||
Filter("Shoujo", "Shoujo"),
|
|
||||||
Filter("Shoujo_Ai", "Shoujo Ai"),
|
|
||||||
Filter("Shounen", "Shounen"),
|
|
||||||
Filter("Shounen_Ai", "Shounen Ai"),
|
|
||||||
Filter("Slice_of_Life", "Slice of Life"),
|
|
||||||
Filter("Smut", "Smut"),
|
|
||||||
Filter("Sports", "Sports"),
|
|
||||||
Filter("Supernatural", "Supernatural"),
|
|
||||||
Filter("Tragedy", "Tragedy"),
|
|
||||||
Filter("Yaoi", "Yaoi"),
|
|
||||||
Filter("Yuri", "Yuri")
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun latestUpdatesInitialUrl(): String = "http://mangaseeonline.net/home/latest.request.php"
|
|
||||||
|
|
||||||
// Not used, overrides parent.
|
|
||||||
override fun latestUpdatesNextPageSelector(): String = ""
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String = "a.latestSeries"
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: MangasPage): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = latestUpdatesInitialUrl()
|
|
||||||
}
|
|
||||||
val (body, requestUrl) = convertQueryToPost(page)
|
|
||||||
return POST(requestUrl, headers, body.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response, page: MangasPage) {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
for (element in document.select(latestUpdatesSelector())) {
|
|
||||||
Manga.create(id).apply {
|
|
||||||
latestUpdatesFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.nextPageUrl = page.url
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a.latestSeries").first().let {
|
|
||||||
val chapterUrl = it.attr("href")
|
|
||||||
val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
|
|
||||||
val indexOfLastPath = chapterUrl.lastIndexOf("/")
|
|
||||||
val mangaUrl = chapterUrl.substring(indexOfLastPath, indexOfMangaUrl)
|
|
||||||
val defaultText = it.select("p.clamp2").text()
|
|
||||||
val m = recentUpdatesPattern.matcher(defaultText)
|
|
||||||
val title = if (m.matches()) m.group(1) else defaultText
|
|
||||||
manga.setUrlWithoutDomain("/manga" + mangaUrl)
|
|
||||||
manga.title = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,197 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.english
|
|
||||||
|
|
||||||
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.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
|
||||||
|
|
||||||
override val name = "ReadMangaToday"
|
|
||||||
|
|
||||||
override val baseUrl = "http://www.readmanga.today"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient get() = network.cloudflareClient
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search only returns data with this set
|
|
||||||
*/
|
|
||||||
override fun headersBuilder() = Headers.Builder().apply {
|
|
||||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
|
||||||
add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/"
|
|
||||||
|
|
||||||
override fun latestUpdatesInitialUrl() = "$baseUrl/latest-releases/"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "div.hot-manga > div.style-grid > div.box"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("div.title > h2 > a").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.attr("title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
|
||||||
"$baseUrl/service/advanced_search"
|
|
||||||
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<OnlineSource.Filter>): Request {
|
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = searchMangaInitialUrl(query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = okhttp3.FormBody.Builder()
|
|
||||||
builder.add("manga-name", query)
|
|
||||||
builder.add("type", "all")
|
|
||||||
var status = "both"
|
|
||||||
for (filter in filters) {
|
|
||||||
if (filter.equals(completedFilter)) status = filter.id
|
|
||||||
else builder.add("include[]", filter.id)
|
|
||||||
}
|
|
||||||
builder.add("status", status)
|
|
||||||
|
|
||||||
return POST(page.url, headers, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "div.style-list > div.box"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("div.title > h2 > a").first().let {
|
|
||||||
manga.setUrlWithoutDomain(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.setUrlWithoutDomain(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")
|
|
||||||
|
|
||||||
private val completedFilter = Filter("completed", "Completed")
|
|
||||||
// [...document.querySelectorAll("ul.manga-cat span")].map(el => `Filter("${el.getAttribute('data-id')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
|
||||||
// http://www.readmanga.today/advanced-search
|
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
|
||||||
completedFilter,
|
|
||||||
Filter("2", "Action"),
|
|
||||||
Filter("4", "Adventure"),
|
|
||||||
Filter("5", "Comedy"),
|
|
||||||
Filter("6", "Doujinshi"),
|
|
||||||
Filter("7", "Drama"),
|
|
||||||
Filter("8", "Ecchi"),
|
|
||||||
Filter("9", "Fantasy"),
|
|
||||||
Filter("10", "Gender Bender"),
|
|
||||||
Filter("11", "Harem"),
|
|
||||||
Filter("12", "Historical"),
|
|
||||||
Filter("13", "Horror"),
|
|
||||||
Filter("14", "Josei"),
|
|
||||||
Filter("15", "Lolicon"),
|
|
||||||
Filter("16", "Martial Arts"),
|
|
||||||
Filter("17", "Mature"),
|
|
||||||
Filter("18", "Mecha"),
|
|
||||||
Filter("19", "Mystery"),
|
|
||||||
Filter("20", "One shot"),
|
|
||||||
Filter("21", "Psychological"),
|
|
||||||
Filter("22", "Romance"),
|
|
||||||
Filter("23", "School Life"),
|
|
||||||
Filter("24", "Sci-fi"),
|
|
||||||
Filter("25", "Seinen"),
|
|
||||||
Filter("26", "Shotacon"),
|
|
||||||
Filter("27", "Shoujo"),
|
|
||||||
Filter("28", "Shoujo Ai"),
|
|
||||||
Filter("29", "Shounen"),
|
|
||||||
Filter("30", "Shounen Ai"),
|
|
||||||
Filter("31", "Slice of Life"),
|
|
||||||
Filter("32", "Smut"),
|
|
||||||
Filter("33", "Sports"),
|
|
||||||
Filter("34", "Supernatural"),
|
|
||||||
Filter("35", "Tragedy"),
|
|
||||||
Filter("36", "Yaoi"),
|
|
||||||
Filter("37", "Yuri")
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,207 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.russian
|
|
||||||
|
|
||||||
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 eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
|
||||||
|
|
||||||
override val name = "Mangachan"
|
|
||||||
|
|
||||||
override val baseUrl = "http://mangachan.me"
|
|
||||||
|
|
||||||
override val lang = "ru"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
|
|
||||||
|
|
||||||
override fun latestUpdatesInitialUrl() = "$baseUrl/newestch"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>): String {
|
|
||||||
if (query.isNotEmpty()) {
|
|
||||||
return "$baseUrl/?do=search&subaction=search&story=$query"
|
|
||||||
} else if (filters.isNotEmpty()) {
|
|
||||||
var genres = ""
|
|
||||||
filters.forEach { genres = genres + it.name + '+' }
|
|
||||||
return "$baseUrl/tags/${genres.dropLast(1)}"
|
|
||||||
} else {
|
|
||||||
return "$baseUrl/?do=search&subaction=search&story=$query"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.content_row"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "ul.area_rightNews li"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("h2 > a").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("a:nth-child(1)").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "a:contains(Далее)"
|
|
||||||
|
|
||||||
private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
for (element in document.select(searchMangaSelector())) {
|
|
||||||
Manga.create(id).apply {
|
|
||||||
searchMangaFromElement(element, this)
|
|
||||||
page.mangas.add(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchMangaNextPageSelector().let { selector ->
|
|
||||||
if (page.nextPageUrl.isNullOrEmpty() && filters.isEmpty()) {
|
|
||||||
val onClick = document.select(selector).first()?.attr("onclick")
|
|
||||||
val pageNum = onClick?.substring(23, onClick.indexOf("); return(false)"))
|
|
||||||
page.nextPageUrl = searchMangaInitialUrl(query, emptyList()) + "&search_start=" + pageNum
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchGenresNextPageSelector().let { selector ->
|
|
||||||
if (page.nextPageUrl.isNullOrEmpty() && filters.isNotEmpty()) {
|
|
||||||
val url = document.select(selector).first()?.attr("href")
|
|
||||||
page.nextPageUrl = searchMangaInitialUrl(query, filters) + url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
|
||||||
val infoElement = document.select("table.mangatitle").first()
|
|
||||||
val descElement = document.select("div#description").first()
|
|
||||||
val imgElement = document.select("img#cover").first()
|
|
||||||
|
|
||||||
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
|
|
||||||
manga.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()
|
|
||||||
manga.thumbnail_url = baseUrl + imgElement.attr("src")
|
|
||||||
}
|
|
||||||
|
|
||||||
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.setUrlWithoutDomain(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(',')
|
|
||||||
|
|
||||||
pageUrls.mapIndexedTo(pages) { i, url -> Page(i, "", url) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
|
|
||||||
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
|
|
||||||
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
|
|
||||||
* return `Filter("${id}", "${id}")` }).join(',\n')
|
|
||||||
* on http://mangachan.me/
|
|
||||||
*/
|
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
|
||||||
Filter("18_плюс", "18_плюс"),
|
|
||||||
Filter("bdsm", "bdsm"),
|
|
||||||
Filter("арт", "арт"),
|
|
||||||
Filter("биография", "биография"),
|
|
||||||
Filter("боевик", "боевик"),
|
|
||||||
Filter("боевые_искусства", "боевые_искусства"),
|
|
||||||
Filter("вампиры", "вампиры"),
|
|
||||||
Filter("веб", "веб"),
|
|
||||||
Filter("гарем", "гарем"),
|
|
||||||
Filter("гендерная_интрига", "гендерная_интрига"),
|
|
||||||
Filter("героическое_фэнтези", "героическое_фэнтези"),
|
|
||||||
Filter("детектив", "детектив"),
|
|
||||||
Filter("дзёсэй", "дзёсэй"),
|
|
||||||
Filter("додзинси", "додзинси"),
|
|
||||||
Filter("драма", "драма"),
|
|
||||||
Filter("игра", "игра"),
|
|
||||||
Filter("инцест", "инцест"),
|
|
||||||
Filter("искусство", "искусство"),
|
|
||||||
Filter("история", "история"),
|
|
||||||
Filter("киберпанк", "киберпанк"),
|
|
||||||
Filter("кодомо", "кодомо"),
|
|
||||||
Filter("комедия", "комедия"),
|
|
||||||
Filter("литРПГ", "литРПГ"),
|
|
||||||
Filter("махо-сёдзё", "махо-сёдзё"),
|
|
||||||
Filter("меха", "меха"),
|
|
||||||
Filter("мистика", "мистика"),
|
|
||||||
Filter("музыка", "музыка"),
|
|
||||||
Filter("научная_фантастика", "научная_фантастика"),
|
|
||||||
Filter("повседневность", "повседневность"),
|
|
||||||
Filter("постапокалиптика", "постапокалиптика"),
|
|
||||||
Filter("приключения", "приключения"),
|
|
||||||
Filter("психология", "психология"),
|
|
||||||
Filter("романтика", "романтика"),
|
|
||||||
Filter("самурайский_боевик", "самурайский_боевик"),
|
|
||||||
Filter("сборник", "сборник"),
|
|
||||||
Filter("сверхъестественное", "сверхъестественное"),
|
|
||||||
Filter("сказка", "сказка"),
|
|
||||||
Filter("спорт", "спорт"),
|
|
||||||
Filter("супергерои", "супергерои"),
|
|
||||||
Filter("сэйнэн", "сэйнэн"),
|
|
||||||
Filter("сёдзё", "сёдзё"),
|
|
||||||
Filter("сёдзё-ай", "сёдзё-ай"),
|
|
||||||
Filter("сёнэн", "сёнэн"),
|
|
||||||
Filter("сёнэн-ай", "сёнэн-ай"),
|
|
||||||
Filter("тентакли", "тентакли"),
|
|
||||||
Filter("трагедия", "трагедия"),
|
|
||||||
Filter("триллер", "триллер"),
|
|
||||||
Filter("ужасы", "ужасы"),
|
|
||||||
Filter("фантастика", "фантастика"),
|
|
||||||
Filter("фурри", "фурри"),
|
|
||||||
Filter("фэнтези", "фэнтези"),
|
|
||||||
Filter("школа", "школа"),
|
|
||||||
Filter("эротика", "эротика"),
|
|
||||||
Filter("юри", "юри"),
|
|
||||||
Filter("яой", "яой"),
|
|
||||||
Filter("ёнкома", "ёнкома")
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,163 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.russian
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.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(override val id: Int) : ParsedOnlineSource() {
|
|
||||||
|
|
||||||
override val name = "Mintmanga"
|
|
||||||
|
|
||||||
override val baseUrl = "http://mintmanga.com"
|
|
||||||
|
|
||||||
override val lang = "ru"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
|
||||||
|
|
||||||
override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
|
||||||
"$baseUrl/search?q=$query&${filters.map { it.id + "=in" }.joinToString("&")}"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.desc"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "div.desc"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("h3 > a").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.attr("title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
// max 200 results
|
|
||||||
override fun searchMangaNextPageSelector() = null
|
|
||||||
|
|
||||||
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.setUrlWithoutDomain(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 prepareNewChapter(chapter: Chapter, manga: Manga) {
|
|
||||||
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)
|
|
||||||
|
|
||||||
val p = Pattern.compile("'.+?','.+?',\".+?\"")
|
|
||||||
val m = p.matcher(trimmedHtml)
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
while (m.find()) {
|
|
||||||
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
|
||||||
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
|
|
||||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
|
||||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
|
||||||
* return `Filter("${id}", "${el.textContent.trim()}")` }).join(',\n')
|
|
||||||
* on http://mintmanga.com/search
|
|
||||||
*/
|
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
|
||||||
Filter("el_2220", "арт"),
|
|
||||||
Filter("el_1353", "бара"),
|
|
||||||
Filter("el_1346", "боевик"),
|
|
||||||
Filter("el_1334", "боевые искусства"),
|
|
||||||
Filter("el_1339", "вампиры"),
|
|
||||||
Filter("el_1333", "гарем"),
|
|
||||||
Filter("el_1347", "гендерная интрига"),
|
|
||||||
Filter("el_1337", "героическое фэнтези"),
|
|
||||||
Filter("el_1343", "детектив"),
|
|
||||||
Filter("el_1349", "дзёсэй"),
|
|
||||||
Filter("el_1332", "додзинси"),
|
|
||||||
Filter("el_1310", "драма"),
|
|
||||||
Filter("el_5229", "игра"),
|
|
||||||
Filter("el_1311", "история"),
|
|
||||||
Filter("el_1351", "киберпанк"),
|
|
||||||
Filter("el_1328", "комедия"),
|
|
||||||
Filter("el_1318", "меха"),
|
|
||||||
Filter("el_1324", "мистика"),
|
|
||||||
Filter("el_1325", "научная фантастика"),
|
|
||||||
Filter("el_1327", "повседневность"),
|
|
||||||
Filter("el_1342", "постапокалиптика"),
|
|
||||||
Filter("el_1322", "приключения"),
|
|
||||||
Filter("el_1335", "психология"),
|
|
||||||
Filter("el_1313", "романтика"),
|
|
||||||
Filter("el_1316", "самурайский боевик"),
|
|
||||||
Filter("el_1350", "сверхъестественное"),
|
|
||||||
Filter("el_1314", "сёдзё"),
|
|
||||||
Filter("el_1320", "сёдзё-ай"),
|
|
||||||
Filter("el_1326", "сёнэн"),
|
|
||||||
Filter("el_1330", "сёнэн-ай"),
|
|
||||||
Filter("el_1321", "спорт"),
|
|
||||||
Filter("el_1329", "сэйнэн"),
|
|
||||||
Filter("el_1344", "трагедия"),
|
|
||||||
Filter("el_1341", "триллер"),
|
|
||||||
Filter("el_1317", "ужасы"),
|
|
||||||
Filter("el_1331", "фантастика"),
|
|
||||||
Filter("el_1323", "фэнтези"),
|
|
||||||
Filter("el_1319", "школа"),
|
|
||||||
Filter("el_1340", "эротика"),
|
|
||||||
Filter("el_1354", "этти"),
|
|
||||||
Filter("el_1315", "юри"),
|
|
||||||
Filter("el_1336", "яой")
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,162 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.russian
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.data.source.online.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(override val id: Int) : ParsedOnlineSource() {
|
|
||||||
|
|
||||||
override val name = "Readmanga"
|
|
||||||
|
|
||||||
override val baseUrl = "http://readmanga.me"
|
|
||||||
|
|
||||||
override val lang = "ru"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
|
||||||
|
|
||||||
override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
|
|
||||||
|
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) =
|
|
||||||
"$baseUrl/search?q=$query&${filters.map { it.id + "=in" }.joinToString("&")}"
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "div.desc"
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "div.desc"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
element.select("h3 > a").first().let {
|
|
||||||
manga.setUrlWithoutDomain(it.attr("href"))
|
|
||||||
manga.title = it.attr("title")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
// max 200 results
|
|
||||||
override fun searchMangaNextPageSelector() = null
|
|
||||||
|
|
||||||
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.setUrlWithoutDomain(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 prepareNewChapter(chapter: Chapter, manga: Manga) {
|
|
||||||
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)
|
|
||||||
|
|
||||||
val p = Pattern.compile("'.+?','.+?',\".+?\"")
|
|
||||||
val m = p.matcher(trimmedHtml)
|
|
||||||
|
|
||||||
var i = 0
|
|
||||||
while (m.find()) {
|
|
||||||
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
|
||||||
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) { }
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) = ""
|
|
||||||
|
|
||||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
|
||||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
|
||||||
* return `Filter("${id}", "${el.textContent.trim()}")` }).join(',\n')
|
|
||||||
* on http://readmanga.me/search
|
|
||||||
*/
|
|
||||||
override fun getFilterList(): List<Filter> = listOf(
|
|
||||||
Filter("el_5685", "арт"),
|
|
||||||
Filter("el_2155", "боевик"),
|
|
||||||
Filter("el_2143", "боевые искусства"),
|
|
||||||
Filter("el_2148", "вампиры"),
|
|
||||||
Filter("el_2142", "гарем"),
|
|
||||||
Filter("el_2156", "гендерная интрига"),
|
|
||||||
Filter("el_2146", "героическое фэнтези"),
|
|
||||||
Filter("el_2152", "детектив"),
|
|
||||||
Filter("el_2158", "дзёсэй"),
|
|
||||||
Filter("el_2141", "додзинси"),
|
|
||||||
Filter("el_2118", "драма"),
|
|
||||||
Filter("el_2154", "игра"),
|
|
||||||
Filter("el_2119", "история"),
|
|
||||||
Filter("el_8032", "киберпанк"),
|
|
||||||
Filter("el_2137", "кодомо"),
|
|
||||||
Filter("el_2136", "комедия"),
|
|
||||||
Filter("el_2147", "махо-сёдзё"),
|
|
||||||
Filter("el_2126", "меха"),
|
|
||||||
Filter("el_2132", "мистика"),
|
|
||||||
Filter("el_2133", "научная фантастика"),
|
|
||||||
Filter("el_2135", "повседневность"),
|
|
||||||
Filter("el_2151", "постапокалиптика"),
|
|
||||||
Filter("el_2130", "приключения"),
|
|
||||||
Filter("el_2144", "психология"),
|
|
||||||
Filter("el_2121", "романтика"),
|
|
||||||
Filter("el_2124", "самурайский боевик"),
|
|
||||||
Filter("el_2159", "сверхъестественное"),
|
|
||||||
Filter("el_2122", "сёдзё"),
|
|
||||||
Filter("el_2128", "сёдзё-ай"),
|
|
||||||
Filter("el_2134", "сёнэн"),
|
|
||||||
Filter("el_2139", "сёнэн-ай"),
|
|
||||||
Filter("el_2129", "спорт"),
|
|
||||||
Filter("el_2138", "сэйнэн"),
|
|
||||||
Filter("el_2153", "трагедия"),
|
|
||||||
Filter("el_2150", "триллер"),
|
|
||||||
Filter("el_2125", "ужасы"),
|
|
||||||
Filter("el_2140", "фантастика"),
|
|
||||||
Filter("el_2131", "фэнтези"),
|
|
||||||
Filter("el_2127", "школа"),
|
|
||||||
Filter("el_2149", "этти"),
|
|
||||||
Filter("el_2123", "юри")
|
|
||||||
)
|
|
||||||
}
|
|
@ -3,8 +3,8 @@ package eu.kanade.tachiyomi.data.track
|
|||||||
import android.support.annotation.CallSuper
|
import android.support.annotation.CallSuper
|
||||||
import android.support.annotation.DrawableRes
|
import android.support.annotation.DrawableRes
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.IBinder
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import rx.Observable
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import rx.subscriptions.CompositeSubscription
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class TrackUpdateService : Service() {
|
|
||||||
|
|
||||||
val trackManager: TrackManager by injectLazy()
|
|
||||||
val db: DatabaseHelper by injectLazy()
|
|
||||||
|
|
||||||
private lateinit var subscriptions: CompositeSubscription
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
subscriptions = CompositeSubscription()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
subscriptions.unsubscribe()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
|
||||||
val track = intent.getSerializableExtra(EXTRA_TRACK)
|
|
||||||
if (track != null) {
|
|
||||||
updateLastChapterRead(track as Track, startId)
|
|
||||||
return Service.START_REDELIVER_INTENT
|
|
||||||
} else {
|
|
||||||
stopSelf(startId)
|
|
||||||
return Service.START_NOT_STICKY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateLastChapterRead(track: Track, startId: Int) {
|
|
||||||
val sync = trackManager.getService(track.sync_id)
|
|
||||||
if (sync == null) {
|
|
||||||
stopSelf(startId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions.add(Observable.defer { sync.update(track) }
|
|
||||||
.flatMap { db.insertTrack(track).asRxObservable() }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({ stopSelf(startId) },
|
|
||||||
{ stopSelf(startId) }))
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val EXTRA_TRACK = "extra_track"
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun start(context: Context, track: Track) {
|
|
||||||
val intent = Intent(context, TrackUpdateService::class.java)
|
|
||||||
intent.putExtra(EXTRA_TRACK, track)
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ import com.github.salomonbrys.kotson.int
|
|||||||
import com.github.salomonbrys.kotson.string
|
import com.github.salomonbrys.kotson.string
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track.kitsu
|
|||||||
import com.github.salomonbrys.kotson.*
|
import com.github.salomonbrys.kotson.*
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
@ -90,7 +90,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
.map { json ->
|
.map { json ->
|
||||||
val data = json["data"].array
|
val data = json["data"].array
|
||||||
if (data.size() > 0) {
|
if (data.size() > 0) {
|
||||||
KitsuLibManga(data[0].obj, json["included"].array[0].obj).toTrack()
|
val manga = json["included"].array[0].obj
|
||||||
|
KitsuLibManga(data[0].obj, manga).toTrack()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@ -102,8 +103,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
.map { json ->
|
.map { json ->
|
||||||
val data = json["data"].array
|
val data = json["data"].array
|
||||||
if (data.size() > 0) {
|
if (data.size() > 0) {
|
||||||
val include = json["included"].array[0].obj
|
val manga = json["included"].array[0].obj
|
||||||
KitsuLibManga(data[0].obj, include).toTrack()
|
KitsuLibManga(data[0].obj, manga).toTrack()
|
||||||
} else {
|
} else {
|
||||||
throw Exception("Could not find manga")
|
throw Exception("Could not find manga")
|
||||||
}
|
}
|
||||||
@ -150,13 +151,13 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
@Query("filter[media_id]", encoded = true) remoteId: Int,
|
@Query("filter[media_id]", encoded = true) remoteId: Int,
|
||||||
@Query("filter[user_id]", encoded = true) userId: String,
|
@Query("filter[user_id]", encoded = true) userId: String,
|
||||||
@Query("page[limit]", encoded = true) limit: Int = 10000,
|
@Query("page[limit]", encoded = true) limit: Int = 10000,
|
||||||
@Query("include") includes: String = "media"
|
@Query("include") includes: String = "manga"
|
||||||
): Observable<JsonObject>
|
): Observable<JsonObject>
|
||||||
|
|
||||||
@GET("library-entries")
|
@GET("library-entries")
|
||||||
fun getLibManga(
|
fun getLibManga(
|
||||||
@Query("filter[id]", encoded = true) remoteId: Int,
|
@Query("filter[id]", encoded = true) remoteId: Int,
|
||||||
@Query("include") includes: String = "media"
|
@Query("include") includes: String = "manga"
|
||||||
): Observable<JsonObject>
|
): Observable<JsonObject>
|
||||||
|
|
||||||
@GET("users")
|
@GET("users")
|
||||||
|
@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.data.track.myanimelist
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Xml
|
import android.util.Xml
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
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.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.asObservable
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.util.selectInt
|
import eu.kanade.tachiyomi.util.selectInt
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.support.v4.app.NotificationCompat
|
||||||
import com.evernote.android.job.Job
|
import com.evernote.android.job.Job
|
||||||
import com.evernote.android.job.JobManager
|
import com.evernote.android.job.JobManager
|
||||||
@ -17,6 +19,10 @@ class UpdateCheckerJob : Job() {
|
|||||||
if (result is GithubUpdateResult.NewUpdate) {
|
if (result is GithubUpdateResult.NewUpdate) {
|
||||||
val url = result.release.downloadLink
|
val url = result.release.downloadLink
|
||||||
|
|
||||||
|
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
|
||||||
|
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
|
||||||
|
}
|
||||||
|
|
||||||
NotificationCompat.Builder(context).update {
|
NotificationCompat.Builder(context).update {
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
setContentText(context.getString(R.string.update_check_notification_update_available))
|
setContentText(context.getString(R.string.update_check_notification_update_available))
|
||||||
@ -24,7 +30,7 @@ class UpdateCheckerJob : Job() {
|
|||||||
// Download action
|
// Download action
|
||||||
addAction(android.R.drawable.stat_sys_download_done,
|
addAction(android.R.drawable.stat_sys_download_done,
|
||||||
context.getString(R.string.action_download),
|
context.getString(R.string.action_download),
|
||||||
UpdateNotificationReceiver.downloadApkIntent(context, url))
|
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Job.Result.SUCCESS
|
Job.Result.SUCCESS
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.support.v4.app.NotificationCompat
|
||||||
|
import eu.kanade.tachiyomi.Constants
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||||
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
import eu.kanade.tachiyomi.util.notificationManager
|
||||||
|
import java.io.File
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local [BroadcastReceiver] that runs on UI thread
|
||||||
|
* Notification calls from [UpdateDownloaderService] should be made from here.
|
||||||
|
*/
|
||||||
|
internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NAME = "UpdateDownloaderReceiver"
|
||||||
|
|
||||||
|
// Called to show initial notification.
|
||||||
|
internal const val NOTIFICATION_UPDATER_INITIAL = "$ID.$NAME.UPDATER_INITIAL"
|
||||||
|
|
||||||
|
// Called to show progress notification.
|
||||||
|
internal const val NOTIFICATION_UPDATER_PROGRESS = "$ID.$NAME.UPDATER_PROGRESS"
|
||||||
|
|
||||||
|
// Called to show install notification.
|
||||||
|
internal const val NOTIFICATION_UPDATER_INSTALL = "$ID.$NAME.UPDATER_INSTALL"
|
||||||
|
|
||||||
|
// Called to show error notification
|
||||||
|
internal const val NOTIFICATION_UPDATER_ERROR = "$ID.$NAME.UPDATER_ERROR"
|
||||||
|
|
||||||
|
// Value containing action of BroadcastReceiver
|
||||||
|
internal const val EXTRA_ACTION = "$ID.$NAME.ACTION"
|
||||||
|
|
||||||
|
// Value containing progress
|
||||||
|
internal const val EXTRA_PROGRESS = "$ID.$NAME.PROGRESS"
|
||||||
|
|
||||||
|
// Value containing apk path
|
||||||
|
internal const val EXTRA_APK_PATH = "$ID.$NAME.APK_PATH"
|
||||||
|
|
||||||
|
// Value containing apk url
|
||||||
|
internal const val EXTRA_APK_URL = "$ID.$NAME.APK_URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification shown to user
|
||||||
|
*/
|
||||||
|
private val notification = NotificationCompat.Builder(context)
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.getStringExtra(EXTRA_ACTION)) {
|
||||||
|
NOTIFICATION_UPDATER_INITIAL -> basicNotification()
|
||||||
|
NOTIFICATION_UPDATER_PROGRESS -> updateProgress(intent.getIntExtra(EXTRA_PROGRESS, 0))
|
||||||
|
NOTIFICATION_UPDATER_INSTALL -> installNotification(intent.getStringExtra(EXTRA_APK_PATH))
|
||||||
|
NOTIFICATION_UPDATER_ERROR -> errorNotification(intent.getStringExtra(EXTRA_APK_URL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to show basic notification
|
||||||
|
*/
|
||||||
|
private fun basicNotification() {
|
||||||
|
// Create notification
|
||||||
|
with(notification) {
|
||||||
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
|
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
|
setOngoing(true)
|
||||||
|
}
|
||||||
|
notification.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to show progress notification
|
||||||
|
*
|
||||||
|
* @param progress progress of download
|
||||||
|
*/
|
||||||
|
private fun updateProgress(progress: Int) {
|
||||||
|
with(notification) {
|
||||||
|
setProgress(100, progress, false)
|
||||||
|
}
|
||||||
|
notification.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to show install notification
|
||||||
|
*
|
||||||
|
* @param path path of file
|
||||||
|
*/
|
||||||
|
private fun installNotification(path: String) {
|
||||||
|
// Prompt the user to install the new update.
|
||||||
|
with(notification) {
|
||||||
|
setContentText(context.getString(R.string.update_check_notification_download_complete))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
// Install action
|
||||||
|
setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
|
||||||
|
addAction(R.drawable.ic_system_update_grey_24dp_img,
|
||||||
|
context.getString(R.string.action_install),
|
||||||
|
NotificationHandler.installApkPendingActivity(context, File(path)))
|
||||||
|
// Cancel action
|
||||||
|
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||||
|
context.getString(R.string.action_cancel),
|
||||||
|
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
|
||||||
|
}
|
||||||
|
notification.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to show error notification
|
||||||
|
*
|
||||||
|
* @param url url of apk
|
||||||
|
*/
|
||||||
|
private fun errorNotification(url: String) {
|
||||||
|
// Prompt the user to retry the download.
|
||||||
|
with(notification) {
|
||||||
|
setContentText(context.getString(R.string.update_check_notification_download_error))
|
||||||
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
|
setProgress(0, 0, false)
|
||||||
|
// Retry action
|
||||||
|
addAction(R.drawable.ic_refresh_grey_24dp_img,
|
||||||
|
context.getString(R.string.action_retry),
|
||||||
|
UpdateDownloaderService.downloadApkPendingService(context, url))
|
||||||
|
// Cancel action
|
||||||
|
addAction(R.drawable.ic_clear_grey_24dp_img,
|
||||||
|
context.getString(R.string.action_cancel),
|
||||||
|
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
|
||||||
|
}
|
||||||
|
notification.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a notification from this builder.
|
||||||
|
*
|
||||||
|
* @param id the id of the notification.
|
||||||
|
*/
|
||||||
|
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_UPDATER_ID) {
|
||||||
|
context.notificationManager.notify(id, build())
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +1,160 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import android.app.IntentService
|
import android.app.IntentService
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.support.v4.app.NotificationCompat
|
import android.content.IntentFilter
|
||||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
import android.os.Build
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.data.network.ProgressListener
|
import eu.kanade.tachiyomi.network.ProgressListener
|
||||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
import eu.kanade.tachiyomi.util.registerLocalReceiver
|
||||||
import eu.kanade.tachiyomi.util.saveTo
|
import eu.kanade.tachiyomi.util.saveTo
|
||||||
|
import eu.kanade.tachiyomi.util.sendLocalBroadcastSync
|
||||||
|
import eu.kanade.tachiyomi.util.unregisterLocalReceiver
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) {
|
class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) {
|
||||||
|
/**
|
||||||
|
* Network helper
|
||||||
|
*/
|
||||||
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local [BroadcastReceiver] that runs on UI thread
|
||||||
|
*/
|
||||||
|
private val updaterNotificationReceiver = UpdateDownloaderReceiver(this)
|
||||||
|
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
// Register receiver
|
||||||
|
registerLocalReceiver(updaterNotificationReceiver, IntentFilter(INTENT_FILTER_NAME))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
// Unregister receiver
|
||||||
|
unregisterLocalReceiver(updaterNotificationReceiver)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHandleIntent(intent: Intent?) {
|
||||||
|
if (intent == null) return
|
||||||
|
|
||||||
|
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
|
||||||
|
downloadApk(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to start downloading apk of new update
|
||||||
|
*
|
||||||
|
* @param url url location of file
|
||||||
|
*/
|
||||||
|
fun downloadApk(url: String) {
|
||||||
|
// Show notification download starting.
|
||||||
|
sendInitialBroadcast()
|
||||||
|
// Progress of the download
|
||||||
|
var savedProgress = 0
|
||||||
|
|
||||||
|
val progressListener = object : ProgressListener {
|
||||||
|
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||||
|
val progress = (100 * bytesRead / contentLength).toInt()
|
||||||
|
if (progress > savedProgress) {
|
||||||
|
savedProgress = progress
|
||||||
|
sendProgressBroadcast(progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Download the new update.
|
||||||
|
val response = network.client.newCallWithProgress(GET(url), progressListener).execute()
|
||||||
|
|
||||||
|
// File where the apk will be saved.
|
||||||
|
val apkFile = File(externalCacheDir, "update.apk")
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
response.body().source().saveTo(apkFile)
|
||||||
|
} else {
|
||||||
|
response.close()
|
||||||
|
throw Exception("Unsuccessful response")
|
||||||
|
}
|
||||||
|
sendInstallBroadcast(apkFile.absolutePath)
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Timber.e(error)
|
||||||
|
sendErrorBroadcast(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notification download starting.
|
||||||
|
*/
|
||||||
|
private fun sendInitialBroadcast() {
|
||||||
|
val intent = Intent(INTENT_FILTER_NAME).apply {
|
||||||
|
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INITIAL)
|
||||||
|
}
|
||||||
|
sendLocalBroadcastSync(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notification progress changed
|
||||||
|
*
|
||||||
|
* @param progress progress of download
|
||||||
|
*/
|
||||||
|
private fun sendProgressBroadcast(progress: Int) {
|
||||||
|
val intent = Intent(INTENT_FILTER_NAME).apply {
|
||||||
|
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_PROGRESS)
|
||||||
|
putExtra(UpdateDownloaderReceiver.EXTRA_PROGRESS, progress)
|
||||||
|
}
|
||||||
|
// Prevents not showing of install notification TODO weird Android N bug. Find out what goes wrong
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || progress <= 95) {
|
||||||
|
// Show download progress notification.
|
||||||
|
sendLocalBroadcastSync(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show install notification.
|
||||||
|
*
|
||||||
|
* @param path location of file
|
||||||
|
*/
|
||||||
|
private fun sendInstallBroadcast(path: String){
|
||||||
|
val intent = Intent(INTENT_FILTER_NAME).apply {
|
||||||
|
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INSTALL)
|
||||||
|
putExtra(UpdateDownloaderReceiver.EXTRA_APK_PATH, path)
|
||||||
|
}
|
||||||
|
sendLocalBroadcastSync(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show error notification.
|
||||||
|
*
|
||||||
|
* @param url url of file
|
||||||
|
*/
|
||||||
|
private fun sendErrorBroadcast(url: String){
|
||||||
|
val intent = Intent(INTENT_FILTER_NAME).apply {
|
||||||
|
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_ERROR)
|
||||||
|
putExtra(UpdateDownloaderReceiver.EXTRA_APK_URL, url)
|
||||||
|
}
|
||||||
|
sendLocalBroadcastSync(intent)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Name of Local BroadCastReceiver.
|
||||||
|
*/
|
||||||
|
private val INTENT_FILTER_NAME = UpdateDownloaderService::class.java.name
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download url.
|
* Download url.
|
||||||
*/
|
*/
|
||||||
const val EXTRA_DOWNLOAD_URL = "eu.kanade.APP_DOWNLOAD_URL"
|
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdateDownloaderService.DOWNLOAD_URL"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads a new update and let the user install the new version from a notification.
|
* Downloads a new update and let the user install the new version from a notification.
|
||||||
@ -35,102 +167,20 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
|
|||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Network helper
|
* Returns [PendingIntent] that starts a service which downloads the apk specified in url.
|
||||||
*/
|
*
|
||||||
private val network: NetworkHelper by injectLazy()
|
* @param url the url to the new update.
|
||||||
|
* @return [PendingIntent]
|
||||||
override fun onHandleIntent(intent: Intent?) {
|
*/
|
||||||
if (intent == null) return
|
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
|
||||||
|
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
|
||||||
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
|
putExtra(EXTRA_DOWNLOAD_URL, url)
|
||||||
downloadApk(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun downloadApk(url: String) {
|
|
||||||
val progressNotification = NotificationCompat.Builder(this)
|
|
||||||
|
|
||||||
progressNotification.update {
|
|
||||||
setContentTitle(getString(R.string.app_name))
|
|
||||||
setContentText(getString(R.string.update_check_notification_download_in_progress))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
|
||||||
setOngoing(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progress of the download
|
|
||||||
var savedProgress = 0
|
|
||||||
|
|
||||||
val progressListener = object : ProgressListener {
|
|
||||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
|
||||||
val progress = (100 * bytesRead / contentLength).toInt()
|
|
||||||
if (progress > savedProgress) {
|
|
||||||
savedProgress = progress
|
|
||||||
|
|
||||||
progressNotification.update { setProgress(100, progress, false) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reference the context for later usage inside apply blocks.
|
|
||||||
val ctx = this
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Download the new update.
|
|
||||||
val response = network.client.newCallWithProgress(GET(url), progressListener).execute()
|
|
||||||
|
|
||||||
// File where the apk will be saved
|
|
||||||
val apkFile = File(externalCacheDir, "update.apk")
|
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
response.body().source().saveTo(apkFile)
|
|
||||||
} else {
|
|
||||||
response.close()
|
|
||||||
throw Exception("Unsuccessful response")
|
|
||||||
}
|
|
||||||
|
|
||||||
val installIntent = UpdateNotificationReceiver.installApkIntent(ctx, apkFile)
|
|
||||||
|
|
||||||
// Prompt the user to install the new update.
|
|
||||||
NotificationCompat.Builder(this).update {
|
|
||||||
setContentTitle(getString(R.string.app_name))
|
|
||||||
setContentText(getString(R.string.update_check_notification_download_complete))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
// Install action
|
|
||||||
setContentIntent(installIntent)
|
|
||||||
addAction(R.drawable.ic_system_update_grey_24dp_img,
|
|
||||||
getString(R.string.action_install),
|
|
||||||
installIntent)
|
|
||||||
// Cancel action
|
|
||||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
|
||||||
getString(R.string.action_cancel),
|
|
||||||
UpdateNotificationReceiver.cancelNotificationIntent(ctx))
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: Exception) {
|
|
||||||
Timber.e(error)
|
|
||||||
|
|
||||||
// Prompt the user to retry the download.
|
|
||||||
NotificationCompat.Builder(this).update {
|
|
||||||
setContentTitle(getString(R.string.app_name))
|
|
||||||
setContentText(getString(R.string.update_check_notification_download_error))
|
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
// Retry action
|
|
||||||
addAction(R.drawable.ic_refresh_grey_24dp_img,
|
|
||||||
getString(R.string.action_retry),
|
|
||||||
UpdateNotificationReceiver.downloadApkIntent(ctx, url))
|
|
||||||
// Cancel action
|
|
||||||
addAction(R.drawable.ic_clear_grey_24dp_img,
|
|
||||||
getString(R.string.action_cancel),
|
|
||||||
UpdateNotificationReceiver.cancelNotificationIntent(ctx))
|
|
||||||
}
|
}
|
||||||
|
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
|
|
||||||
block()
|
|
||||||
notificationManager.notify(NOTIFICATION_UPDATER_ID, build())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.support.v4.content.FileProvider
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
|
|
||||||
import eu.kanade.tachiyomi.util.notificationManager
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class UpdateNotificationReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
when (intent.action) {
|
|
||||||
ACTION_CANCEL_NOTIFICATION -> cancelNotification(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// Cancel notification action
|
|
||||||
const val ACTION_CANCEL_NOTIFICATION = "eu.kanade.CANCEL_NOTIFICATION"
|
|
||||||
|
|
||||||
fun cancelNotificationIntent(context: Context): PendingIntent {
|
|
||||||
val intent = Intent(context, UpdateNotificationReceiver::class.java).apply {
|
|
||||||
action = ACTION_CANCEL_NOTIFICATION
|
|
||||||
}
|
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt user with apk install intent
|
|
||||||
*
|
|
||||||
* @param context context
|
|
||||||
* @param file file of apk that is installed
|
|
||||||
*/
|
|
||||||
fun installApkIntent(context: Context, file: File): PendingIntent {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
|
||||||
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
|
|
||||||
else Uri.fromFile(file)
|
|
||||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
|
||||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
}
|
|
||||||
cancelNotification(context)
|
|
||||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads a new update and let the user install the new version from a notification.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
* @param url the url to the new update.
|
|
||||||
*/
|
|
||||||
fun downloadApkIntent(context: Context, url: String): PendingIntent {
|
|
||||||
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
|
|
||||||
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
|
|
||||||
}
|
|
||||||
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelNotification(context: Context) {
|
|
||||||
context.notificationManager.cancel(NOTIFICATION_UPDATER_ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,80 +1,77 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import com.squareup.duktape.Duktape
|
import com.squareup.duktape.Duktape
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interceptor {
|
class CloudflareInterceptor : Interceptor {
|
||||||
|
|
||||||
//language=RegExp
|
//language=RegExp
|
||||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
||||||
|
|
||||||
//language=RegExp
|
//language=RegExp
|
||||||
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
||||||
|
|
||||||
//language=RegExp
|
//language=RegExp
|
||||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
@Synchronized
|
||||||
val response = chain.proceed(chain.request())
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val response = chain.proceed(chain.request())
|
||||||
// Check if we already solved a challenge
|
|
||||||
if (response.code() != 503 &&
|
// Check if Cloudflare anti-bot is on
|
||||||
cookies.get(response.request().url()).any { it.name() == "cf_clearance" }) {
|
if (response.code() == 503 && "cloudflare-nginx" == response.header("Server")) {
|
||||||
return response
|
return chain.proceed(resolveChallenge(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Cloudflare anti-bot is on
|
return response
|
||||||
if ("URL=/cdn-cgi/" in response.header("Refresh", "")
|
}
|
||||||
&& response.header("Server", "") == "cloudflare-nginx") {
|
|
||||||
return chain.proceed(resolveChallenge(response))
|
private fun resolveChallenge(response: Response): Request {
|
||||||
}
|
Duktape.create().use { duktape ->
|
||||||
|
val originalRequest = response.request()
|
||||||
return response
|
val url = originalRequest.url()
|
||||||
}
|
val domain = url.host()
|
||||||
|
val content = response.body().string()
|
||||||
private fun resolveChallenge(response: Response): Request {
|
|
||||||
val duktape = Duktape.create()
|
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
||||||
try {
|
Thread.sleep(4000)
|
||||||
val originalRequest = response.request()
|
|
||||||
val domain = originalRequest.url().host()
|
val operation = operationPattern.find(content)?.groups?.get(1)?.value
|
||||||
val content = response.body().string()
|
val challenge = challengePattern.find(content)?.groups?.get(1)?.value
|
||||||
|
val pass = passPattern.find(content)?.groups?.get(1)?.value
|
||||||
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
|
||||||
Thread.sleep(4000)
|
if (operation == null || challenge == null || pass == null) {
|
||||||
|
throw RuntimeException("Failed resolving Cloudflare challenge")
|
||||||
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
|
val js = operation
|
||||||
|
//language=RegExp
|
||||||
if (operation == null || challenge == null || pass == null) {
|
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
||||||
throw RuntimeException("Failed resolving Cloudflare challenge")
|
//language=RegExp
|
||||||
}
|
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||||
|
.replace("\n", "")
|
||||||
val js = operation
|
|
||||||
//language=RegExp
|
val result = (duktape.evaluate(js) as Double).toInt()
|
||||||
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
|
||||||
//language=RegExp
|
val answer = "${result + domain.length}"
|
||||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
|
||||||
.replace("\n", "")
|
val cloudflareUrl = HttpUrl.parse("${url.scheme()}://$domain/cdn-cgi/l/chk_jschl")
|
||||||
|
.newBuilder()
|
||||||
val result = (duktape.evaluate(js) as Double).toInt()
|
.addQueryParameter("jschl_vc", challenge)
|
||||||
|
.addQueryParameter("pass", pass)
|
||||||
val answer = "${result + domain.length}"
|
.addQueryParameter("jschl_answer", answer)
|
||||||
|
.toString()
|
||||||
val url = HttpUrl.parse("http://$domain/cdn-cgi/l/chk_jschl").newBuilder()
|
|
||||||
.addQueryParameter("jschl_vc", challenge)
|
val cloudflareHeaders = originalRequest.headers()
|
||||||
.addQueryParameter("pass", pass)
|
.newBuilder()
|
||||||
.addQueryParameter("jschl_answer", answer)
|
.add("Referer", url.toString())
|
||||||
.toString()
|
.build()
|
||||||
|
|
||||||
val referer = originalRequest.url().toString()
|
return GET(cloudflareUrl, cloudflareHeaders)
|
||||||
return GET(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
|
}
|
||||||
} finally {
|
}
|
||||||
duktape.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,38 +1,38 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class NetworkHelper(context: Context) {
|
class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
private val cacheDir = File(context.cacheDir, "network_cache")
|
private val cacheDir = File(context.cacheDir, "network_cache")
|
||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
private val cookieManager = PersistentCookieJar(context)
|
private val cookieManager = PersistentCookieJar(context)
|
||||||
|
|
||||||
val client = OkHttpClient.Builder()
|
val client = OkHttpClient.Builder()
|
||||||
.cookieJar(cookieManager)
|
.cookieJar(cookieManager)
|
||||||
.cache(Cache(cacheDir, cacheSize))
|
.cache(Cache(cacheDir, cacheSize))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val forceCacheClient = client.newBuilder()
|
val forceCacheClient = client.newBuilder()
|
||||||
.addNetworkInterceptor { chain ->
|
.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()
|
.build()
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val cloudflareClient = client.newBuilder()
|
val cloudflareClient = client.newBuilder()
|
||||||
.addInterceptor(CloudflareInterceptor(cookies))
|
.addInterceptor(CloudflareInterceptor())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val cookies: PersistentCookieStore
|
val cookies: PersistentCookieStore
|
||||||
get() = cookieManager.store
|
get() = cookieManager.store
|
||||||
|
|
||||||
}
|
}
|
@ -1,70 +1,70 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
fun Call.asObservable(): Observable<Response> {
|
fun Call.asObservable(): Observable<Response> {
|
||||||
return Observable.create { subscriber ->
|
return Observable.create { subscriber ->
|
||||||
// Since Call is a one-shot type, clone it for each new subscriber.
|
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||||
val call = clone()
|
val call = clone()
|
||||||
|
|
||||||
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
||||||
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
|
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
|
||||||
override fun request(n: Long) {
|
override fun request(n: Long) {
|
||||||
if (n == 0L || !compareAndSet(false, true)) return
|
if (n == 0L || !compareAndSet(false, true)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val response = call.execute()
|
val response = call.execute()
|
||||||
if (!subscriber.isUnsubscribed) {
|
if (!subscriber.isUnsubscribed) {
|
||||||
subscriber.onNext(response)
|
subscriber.onNext(response)
|
||||||
subscriber.onCompleted()
|
subscriber.onCompleted()
|
||||||
}
|
}
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
if (!subscriber.isUnsubscribed) {
|
if (!subscriber.isUnsubscribed) {
|
||||||
subscriber.onError(error)
|
subscriber.onError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unsubscribe() {
|
override fun unsubscribe() {
|
||||||
call.cancel()
|
call.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isUnsubscribed(): Boolean {
|
override fun isUnsubscribed(): Boolean {
|
||||||
return call.isCanceled
|
return call.isCanceled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriber.add(requestArbiter)
|
subscriber.add(requestArbiter)
|
||||||
subscriber.setProducer(requestArbiter)
|
subscriber.setProducer(requestArbiter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Call.asObservableSuccess(): Observable<Response> {
|
fun Call.asObservableSuccess(): Observable<Response> {
|
||||||
return asObservable().doOnNext { response ->
|
return asObservable().doOnNext { response ->
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
response.close()
|
response.close()
|
||||||
throw Exception("HTTP error ${response.code()}")
|
throw Exception("HTTP error ${response.code()}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
val progressClient = newBuilder()
|
val progressClient = newBuilder()
|
||||||
.cache(null)
|
.cache(null)
|
||||||
.addNetworkInterceptor { chain ->
|
.addNetworkInterceptor { chain ->
|
||||||
val originalResponse = chain.proceed(chain.request())
|
val originalResponse = chain.proceed(chain.request())
|
||||||
originalResponse.newBuilder()
|
originalResponse.newBuilder()
|
||||||
.body(ProgressResponseBody(originalResponse.body(), listener))
|
.body(ProgressResponseBody(originalResponse.body(), listener))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return progressClient.newCall(request)
|
return progressClient.newCall(request)
|
||||||
}
|
}
|
@ -1,19 +1,19 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
class PersistentCookieJar(context: Context) : CookieJar {
|
class PersistentCookieJar(context: Context) : CookieJar {
|
||||||
|
|
||||||
val store = PersistentCookieStore(context)
|
val store = PersistentCookieStore(context)
|
||||||
|
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
store.addAll(url, cookies)
|
store.addAll(url, cookies)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
return store.get(url)
|
return store.get(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,75 +1,73 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class PersistentCookieStore(context: Context) {
|
class PersistentCookieStore(context: Context) {
|
||||||
|
|
||||||
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
||||||
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
for ((key, value) in prefs.all) {
|
for ((key, value) in prefs.all) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val cookies = value as? Set<String>
|
val cookies = value as? Set<String>
|
||||||
if (cookies != null) {
|
if (cookies != null) {
|
||||||
try {
|
try {
|
||||||
val url = HttpUrl.parse("http://$key")
|
val url = HttpUrl.parse("http://$key")
|
||||||
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
|
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
|
||||||
.filter { !it.hasExpired() }
|
.filter { !it.hasExpired() }
|
||||||
cookieMap.put(key, nonExpiredCookies)
|
cookieMap.put(key, nonExpiredCookies)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
@Synchronized
|
||||||
synchronized(this) {
|
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
val key = url.uri().host
|
val key = url.uri().host
|
||||||
|
|
||||||
// Append or replace the cookies for this domain.
|
// Append or replace the cookies for this domain.
|
||||||
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
|
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
|
||||||
for (cookie in cookies) {
|
for (cookie in cookies) {
|
||||||
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
||||||
val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
|
val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
|
||||||
if (pos == -1) {
|
if (pos == -1) {
|
||||||
cookiesForDomain.add(cookie)
|
cookiesForDomain.add(cookie)
|
||||||
} else {
|
} else {
|
||||||
cookiesForDomain[pos] = cookie
|
cookiesForDomain[pos] = cookie
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cookieMap.put(key, cookiesForDomain)
|
cookieMap.put(key, cookiesForDomain)
|
||||||
|
|
||||||
// Get cookies to be stored in disk
|
// Get cookies to be stored in disk
|
||||||
val newValues = cookiesForDomain.asSequence()
|
val newValues = cookiesForDomain.asSequence()
|
||||||
.filter { it.persistent() && !it.hasExpired() }
|
.filter { it.persistent() && !it.hasExpired() }
|
||||||
.map { it.toString() }
|
.map(Cookie::toString)
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
prefs.edit().putStringSet(key, newValues).apply()
|
prefs.edit().putStringSet(key, newValues).apply()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Synchronized
|
||||||
fun removeAll() {
|
fun removeAll() {
|
||||||
synchronized(this) {
|
prefs.edit().clear().apply()
|
||||||
prefs.edit().clear().apply()
|
cookieMap.clear()
|
||||||
cookieMap.clear()
|
}
|
||||||
}
|
|
||||||
}
|
fun get(url: HttpUrl) = get(url.uri().host)
|
||||||
|
|
||||||
fun get(url: HttpUrl) = get(url.uri().host)
|
fun get(uri: URI) = get(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 get(url: String): List<Cookie> {
|
}
|
||||||
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
|
||||||
}
|
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
|
||||||
|
|
||||||
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
interface ProgressListener {
|
interface ProgressListener {
|
||||||
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
|
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
|
||||||
}
|
}
|
@ -1,40 +1,40 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import okio.*
|
import okio.*
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
||||||
|
|
||||||
private val bufferedSource: BufferedSource by lazy {
|
private val bufferedSource: BufferedSource by lazy {
|
||||||
Okio.buffer(source(responseBody.source()))
|
Okio.buffer(source(responseBody.source()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun contentType(): MediaType {
|
override fun contentType(): MediaType {
|
||||||
return responseBody.contentType()
|
return responseBody.contentType()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun contentLength(): Long {
|
override fun contentLength(): Long {
|
||||||
return responseBody.contentLength()
|
return responseBody.contentLength()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun source(): BufferedSource {
|
override fun source(): BufferedSource {
|
||||||
return bufferedSource
|
return bufferedSource
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun source(source: Source): Source {
|
private fun source(source: Source): Source {
|
||||||
return object : ForwardingSource(source) {
|
return object : ForwardingSource(source) {
|
||||||
internal var totalBytesRead = 0L
|
internal var totalBytesRead = 0L
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||||
val bytesRead = super.read(sink, byteCount)
|
val bytesRead = super.read(sink, byteCount)
|
||||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||||
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||||
return bytesRead
|
return bytesRead
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,32 +1,32 @@
|
|||||||
package eu.kanade.tachiyomi.data.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import java.util.concurrent.TimeUnit.MINUTES
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
|
|
||||||
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, 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,
|
fun GET(url: String,
|
||||||
headers: Headers = DEFAULT_HEADERS,
|
headers: Headers = DEFAULT_HEADERS,
|
||||||
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
||||||
|
|
||||||
return Request.Builder()
|
return Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.headers(headers)
|
.headers(headers)
|
||||||
.cacheControl(cache)
|
.cacheControl(cache)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun POST(url: String,
|
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 {
|
||||||
|
|
||||||
return Request.Builder()
|
return Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.post(body)
|
.post(body)
|
||||||
.headers(headers)
|
.headers(headers)
|
||||||
.cacheControl(cache)
|
.cacheControl(cache)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
interface CatalogueSource : Source {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||||
|
*/
|
||||||
|
val lang: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the source has support for latest updates.
|
||||||
|
*/
|
||||||
|
val supportsLatest: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of manga.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
fun fetchPopularManga(page: Int): Observable<MangasPage>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of manga.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
* @param query the search query.
|
||||||
|
* @param filters the list of filters to apply.
|
||||||
|
*/
|
||||||
|
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of latest manga updates.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
fun fetchLatestUpdates(page: Int): Observable<MangasPage>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of filters for the source.
|
||||||
|
*/
|
||||||
|
fun getFilterList(): FilterList
|
||||||
|
}
|
318
app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
Normal file
318
app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.util.ChapterRecognition
|
||||||
|
import eu.kanade.tachiyomi.util.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.RarContentProvider
|
||||||
|
import eu.kanade.tachiyomi.util.ZipContentProvider
|
||||||
|
import junrar.Archive
|
||||||
|
import junrar.rarfile.FileHeader
|
||||||
|
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import rx.Observable
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
|
class LocalSource(private val context: Context) : CatalogueSource {
|
||||||
|
companion object {
|
||||||
|
private val COVER_NAME = "cover.jpg"
|
||||||
|
private val POPULAR_FILTERS = FilterList(OrderBy())
|
||||||
|
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
||||||
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
|
val ID = 0L
|
||||||
|
|
||||||
|
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||||
|
val dir = getBaseDirectories(context).firstOrNull()
|
||||||
|
if (dir == null) {
|
||||||
|
input.close()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
||||||
|
|
||||||
|
// It might not exist if using the external SD card
|
||||||
|
cover.parentFile.mkdirs()
|
||||||
|
input.use {
|
||||||
|
cover.outputStream().use {
|
||||||
|
input.copyTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cover
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBaseDirectories(context: Context): List<File> {
|
||||||
|
val c = context.getString(R.string.app_name) + File.separator + "local"
|
||||||
|
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val id = ID
|
||||||
|
override val name = "LocalSource"
|
||||||
|
override val lang = "en"
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun toString() = context.getString(R.string.local_source)
|
||||||
|
|
||||||
|
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
val baseDirs = getBaseDirectories(context)
|
||||||
|
|
||||||
|
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||||
|
var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() }
|
||||||
|
.flatten()
|
||||||
|
.filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||||
|
.distinctBy { it.name }
|
||||||
|
|
||||||
|
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
||||||
|
when (state?.index) {
|
||||||
|
0 -> {
|
||||||
|
if (state!!.ascending)
|
||||||
|
mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
|
||||||
|
else
|
||||||
|
mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
if (state!!.ascending)
|
||||||
|
mangaDirs = mangaDirs.sortedBy(File::lastModified)
|
||||||
|
else
|
||||||
|
mangaDirs = mangaDirs.sortedByDescending(File::lastModified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangas = mangaDirs.map { mangaDir ->
|
||||||
|
SManga.create().apply {
|
||||||
|
title = mangaDir.name
|
||||||
|
url = mangaDir.name
|
||||||
|
|
||||||
|
// Try to find the cover
|
||||||
|
for (dir in baseDirs) {
|
||||||
|
val cover = File("${dir.absolutePath}/$url", COVER_NAME)
|
||||||
|
if (cover.exists()) {
|
||||||
|
thumbnail_url = cover.absolutePath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the cover from the first chapter found.
|
||||||
|
if (thumbnail_url == null) {
|
||||||
|
val chapters = fetchChapterList(this).toBlocking().first()
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
|
val uri = fetchPageList(chapters.last()).toBlocking().first().firstOrNull()?.uri
|
||||||
|
if (uri != null) {
|
||||||
|
val input = context.contentResolver.openInputStream(uri)
|
||||||
|
try {
|
||||||
|
val dest = updateCover(context, this, input)
|
||||||
|
thumbnail_url = dest?.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Observable.just(MangasPage(mangas, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
|
override fun fetchMangaDetails(manga: SManga) = Observable.just(manga)
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
||||||
|
val chapters = getBaseDirectories(context)
|
||||||
|
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||||
|
.flatten()
|
||||||
|
.filter { it.isDirectory || isSupportedFormat(it.extension) }
|
||||||
|
.map { chapterFile ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
url = "${manga.url}/${chapterFile.name}"
|
||||||
|
val chapName = if (chapterFile.isDirectory) {
|
||||||
|
chapterFile.name
|
||||||
|
} else {
|
||||||
|
chapterFile.nameWithoutExtension
|
||||||
|
}
|
||||||
|
val chapNameCut = chapName.replace(manga.title, "", true).trim()
|
||||||
|
name = if (chapNameCut.isEmpty()) chapName else chapNameCut
|
||||||
|
date_upload = chapterFile.lastModified()
|
||||||
|
ChapterRecognition.parseChapterNumber(this, manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sortedWith(Comparator<SChapter> { c1, c2 ->
|
||||||
|
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||||
|
if (c == 0) comparator.compare(c2.name, c1.name) else c
|
||||||
|
})
|
||||||
|
|
||||||
|
return Observable.just(chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
|
val baseDirs = getBaseDirectories(context)
|
||||||
|
|
||||||
|
for (dir in baseDirs) {
|
||||||
|
val chapFile = File(dir, chapter.url)
|
||||||
|
if (!chapFile.exists()) continue
|
||||||
|
|
||||||
|
return Observable.just(getLoader(chapFile).load())
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.error(Exception("Chapter not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSupportedFormat(extension: String): Boolean {
|
||||||
|
return extension.equals("zip", true) || extension.equals("cbz", true)
|
||||||
|
|| extension.equals("rar", true) || extension.equals("cbr", true)
|
||||||
|
|| extension.equals("epub", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLoader(file: File): Loader {
|
||||||
|
val extension = file.extension
|
||||||
|
return if (file.isDirectory) {
|
||||||
|
DirectoryLoader(file)
|
||||||
|
} else if (extension.equals("zip", true) || extension.equals("cbz", true)) {
|
||||||
|
ZipLoader(file)
|
||||||
|
} else if (extension.equals("epub", true)) {
|
||||||
|
EpubLoader(file)
|
||||||
|
} else if (extension.equals("rar", true) || extension.equals("cbr", true)) {
|
||||||
|
RarLoader(file)
|
||||||
|
} else {
|
||||||
|
throw Exception("Invalid chapter format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Filter.Sort.Selection(0, true))
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(OrderBy())
|
||||||
|
|
||||||
|
interface Loader {
|
||||||
|
fun load(): List<Page>
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectoryLoader(val file: File) : Loader {
|
||||||
|
override fun load(): List<Page> {
|
||||||
|
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
||||||
|
return file.listFiles()
|
||||||
|
.filter { !it.isDirectory && DiskUtil.isImage(it.name, { FileInputStream(it) }) }
|
||||||
|
.sortedWith(Comparator<File> { f1, f2 -> comparator.compare(f1.name, f2.name) })
|
||||||
|
.map { Uri.fromFile(it) }
|
||||||
|
.mapIndexed { i, uri -> Page(i, uri = uri).apply { status = Page.READY } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZipLoader(val file: File) : Loader {
|
||||||
|
override fun load(): List<Page> {
|
||||||
|
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
||||||
|
return ZipFile(file).use { zip ->
|
||||||
|
zip.entries().toList()
|
||||||
|
.filter { !it.isDirectory && DiskUtil.isImage(it.name, { zip.getInputStream(it) }) }
|
||||||
|
.sortedWith(Comparator<ZipEntry> { f1, f2 -> comparator.compare(f1.name, f2.name) })
|
||||||
|
.map { Uri.parse("content://${ZipContentProvider.PROVIDER}${file.absolutePath}!/${it.name}") }
|
||||||
|
.mapIndexed { i, uri -> Page(i, uri = uri).apply { status = Page.READY } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RarLoader(val file: File) : Loader {
|
||||||
|
override fun load(): List<Page> {
|
||||||
|
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
||||||
|
return Archive(file).use { archive ->
|
||||||
|
archive.fileHeaders
|
||||||
|
.filter { !it.isDirectory && DiskUtil.isImage(it.fileNameString, { archive.getInputStream(it) }) }
|
||||||
|
.sortedWith(Comparator<FileHeader> { f1, f2 -> comparator.compare(f1.fileNameString, f2.fileNameString) })
|
||||||
|
.map { Uri.parse("content://${RarContentProvider.PROVIDER}${file.absolutePath}!-/${it.fileNameString}") }
|
||||||
|
.mapIndexed { i, uri -> Page(i, uri = uri).apply { status = Page.READY } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EpubLoader(val file: File) : Loader {
|
||||||
|
|
||||||
|
override fun load(): List<Page> {
|
||||||
|
ZipFile(file).use { zip ->
|
||||||
|
val allEntries = zip.entries().toList()
|
||||||
|
val ref = getPackageHref(zip)
|
||||||
|
val doc = getPackageDocument(zip, ref)
|
||||||
|
val pages = getPagesFromDocument(doc)
|
||||||
|
val hrefs = getHrefMap(ref, allEntries.map { it.name })
|
||||||
|
return getImagesFromPages(zip, pages, hrefs)
|
||||||
|
.map { Uri.parse("content://${ZipContentProvider.PROVIDER}${file.absolutePath}!/$it") }
|
||||||
|
.mapIndexed { i, uri -> Page(i, uri = uri).apply { status = Page.READY } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the package document.
|
||||||
|
*/
|
||||||
|
private fun getPackageHref(zip: ZipFile): String {
|
||||||
|
val meta = zip.getEntry("META-INF/container.xml")
|
||||||
|
if (meta != null) {
|
||||||
|
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") }
|
||||||
|
val path = metaDoc.getElementsByTag("rootfile").first()?.attr("full-path")
|
||||||
|
if (path != null) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "OEBPS/content.opf"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the package document where all the files are listed.
|
||||||
|
*/
|
||||||
|
private fun getPackageDocument(zip: ZipFile, ref: String): Document {
|
||||||
|
val entry = zip.getEntry(ref)
|
||||||
|
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the pages from the epub.
|
||||||
|
*/
|
||||||
|
private fun getPagesFromDocument(document: Document): List<String> {
|
||||||
|
val pages = document.select("manifest > item")
|
||||||
|
.filter { "application/xhtml+xml" == it.attr("media-type") }
|
||||||
|
.associateBy { it.attr("id") }
|
||||||
|
|
||||||
|
val spine = document.select("spine > itemref").map { it.attr("idref") }
|
||||||
|
return spine.mapNotNull { pages[it] }.map { it.attr("href") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the images contained in every page from the epub.
|
||||||
|
*/
|
||||||
|
private fun getImagesFromPages(zip: ZipFile, pages: List<String>, hrefs: Map<String, String>): List<String> {
|
||||||
|
return pages.map { page ->
|
||||||
|
val entry = zip.getEntry(hrefs[page])
|
||||||
|
val document = zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
|
||||||
|
document.getElementsByTag("img").mapNotNull { hrefs[it.attr("src")] }
|
||||||
|
}.flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a map with a relative url as key and abolute url as path.
|
||||||
|
*/
|
||||||
|
private fun getHrefMap(packageHref: String, entries: List<String>): Map<String, String> {
|
||||||
|
val lastSlashPos = packageHref.lastIndexOf('/')
|
||||||
|
if (lastSlashPos < 0) {
|
||||||
|
return entries.associateBy { it }
|
||||||
|
}
|
||||||
|
return entries.associateBy { entry ->
|
||||||
|
if (entry.isNotBlank() && entry.length > lastSlashPos) {
|
||||||
|
entry.substring(lastSlashPos + 1)
|
||||||
|
} else {
|
||||||
|
entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,50 +1,44 @@
|
|||||||
package eu.kanade.tachiyomi.data.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
||||||
*/
|
*/
|
||||||
interface Source {
|
interface Source {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id for the source. Must be unique.
|
* Id for the source. Must be unique.
|
||||||
*/
|
*/
|
||||||
val id: Int
|
val id: Long
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the source.
|
* Name of the source.
|
||||||
*/
|
*/
|
||||||
val name: String
|
val name: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated details for a manga.
|
* Returns an observable with the updated details for a manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
*/
|
*/
|
||||||
fun fetchMangaDetails(manga: Manga): Observable<Manga>
|
fun fetchMangaDetails(manga: SManga): Observable<SManga>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with all the available chapters for a manga.
|
* Returns an observable with all the available chapters for a manga.
|
||||||
*
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
*/
|
*/
|
||||||
fun fetchChapterList(manga: Manga): Observable<List<Chapter>>
|
fun fetchChapterList(manga: SManga): Observable<List<SChapter>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the list of pages a chapter has.
|
* Returns an observable with the list of pages a chapter has.
|
||||||
*
|
*
|
||||||
* @param chapter the chapter.
|
* @param chapter the chapter.
|
||||||
*/
|
*/
|
||||||
fun fetchPageList(chapter: Chapter): Observable<List<Page>>
|
fun fetchPageList(chapter: SChapter): Observable<List<Page>>
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the path of the image.
|
|
||||||
*
|
|
||||||
* @param page the page.
|
|
||||||
*/
|
|
||||||
fun fetchImage(page: Page): Observable<Page>
|
|
||||||
}
|
}
|
140
app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
Normal file
140
app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
|
import android.Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Environment
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.YamlHttpSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.english.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.german.WieManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.russian.Mangachan
|
||||||
|
import eu.kanade.tachiyomi.source.online.russian.Mintmanga
|
||||||
|
import eu.kanade.tachiyomi.source.online.russian.Readmanga
|
||||||
|
import eu.kanade.tachiyomi.util.hasPermission
|
||||||
|
import org.yaml.snakeyaml.Yaml
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
open class SourceManager(private val context: Context) {
|
||||||
|
|
||||||
|
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
createSources()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun get(sourceKey: Long): Source? {
|
||||||
|
return sourcesMap[sourceKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
|
||||||
|
|
||||||
|
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
|
||||||
|
|
||||||
|
private fun createSources() {
|
||||||
|
createExtensionSources().forEach { registerSource(it) }
|
||||||
|
createYamlSources().forEach { registerSource(it) }
|
||||||
|
createInternalSources().forEach { registerSource(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerSource(source: Source, overwrite: Boolean = false) {
|
||||||
|
if (overwrite || !sourcesMap.containsKey(source.id)) {
|
||||||
|
sourcesMap.put(source.id, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createInternalSources(): List<Source> = listOf(
|
||||||
|
LocalSource(context),
|
||||||
|
Batoto(),
|
||||||
|
Mangahere(),
|
||||||
|
Mangafox(),
|
||||||
|
Kissmanga(),
|
||||||
|
Readmanga(),
|
||||||
|
Mintmanga(),
|
||||||
|
Mangachan(),
|
||||||
|
Readmangatoday(),
|
||||||
|
Mangasee(),
|
||||||
|
WieManga()
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createYamlSources(): List<Source> {
|
||||||
|
val sources = mutableListOf<Source>()
|
||||||
|
|
||||||
|
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||||
|
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) }
|
||||||
|
sources.add(YamlHttpSource(map))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Error loading source from file. Bad format?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createExtensionSources(): List<Source> {
|
||||||
|
val pkgManager = context.packageManager
|
||||||
|
val flags = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
||||||
|
val installedPkgs = pkgManager.getInstalledPackages(flags)
|
||||||
|
val extPkgs = installedPkgs.filter { it.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE } }
|
||||||
|
|
||||||
|
val sources = mutableListOf<Source>()
|
||||||
|
for (pkgInfo in extPkgs) {
|
||||||
|
val appInfo = pkgManager.getApplicationInfo(pkgInfo.packageName,
|
||||||
|
PackageManager.GET_META_DATA) ?: continue
|
||||||
|
|
||||||
|
val extName = pkgManager.getApplicationLabel(appInfo).toString()
|
||||||
|
.substringAfter("Tachiyomi: ")
|
||||||
|
val version = pkgInfo.versionName
|
||||||
|
var sourceClass = appInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||||
|
if (sourceClass.startsWith(".")) {
|
||||||
|
sourceClass = pkgInfo.packageName + sourceClass
|
||||||
|
}
|
||||||
|
|
||||||
|
val extension = Extension(extName, appInfo, version, sourceClass)
|
||||||
|
try {
|
||||||
|
val instance = loadExtension(extension)
|
||||||
|
sources.add(instance)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("Extension load error: $extName. Reason: ${e.message}")
|
||||||
|
} catch (e: LinkageError) {
|
||||||
|
Timber.e("Extension load error: $extName. Reason: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadExtension(ext: Extension): Source {
|
||||||
|
// Validate lib version
|
||||||
|
val majorLibVersion = ext.version.substringBefore('.').toInt()
|
||||||
|
if (majorLibVersion < LIB_VERSION_MIN || majorLibVersion > LIB_VERSION_MAX) {
|
||||||
|
throw Exception("Lib version is $majorLibVersion, while only versions "
|
||||||
|
+ "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
val classLoader = PathClassLoader(ext.appInfo.sourceDir, null, context.classLoader)
|
||||||
|
return Class.forName(ext.sourceClass, false, classLoader).newInstance() as Source
|
||||||
|
}
|
||||||
|
|
||||||
|
class Extension(val name: String,
|
||||||
|
val appInfo: ApplicationInfo,
|
||||||
|
val version: String,
|
||||||
|
val sourceClass: String)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||||
|
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||||
|
const val LIB_VERSION_MIN = 1
|
||||||
|
const val LIB_VERSION_MAX = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt
Normal file
40
app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
sealed class Filter<T>(val name: String, var state: T) {
|
||||||
|
open class Header(name: String) : Filter<Any>(name, 0)
|
||||||
|
open class Separator(name: String = "") : Filter<Any>(name, 0)
|
||||||
|
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
|
||||||
|
abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
|
||||||
|
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
|
||||||
|
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
|
||||||
|
fun isIgnored() = state == STATE_IGNORE
|
||||||
|
fun isIncluded() = state == STATE_INCLUDE
|
||||||
|
fun isExcluded() = state == STATE_EXCLUDE
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val STATE_IGNORE = 0
|
||||||
|
const val STATE_INCLUDE = 1
|
||||||
|
const val STATE_EXCLUDE = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
abstract class Group<V>(name: String, state: List<V>): Filter<List<V>>(name, state)
|
||||||
|
|
||||||
|
abstract class Sort(name: String, val values: Array<String>, state: Selection? = null)
|
||||||
|
: Filter<Sort.Selection?>(name, state) {
|
||||||
|
data class Selection(val index: Int, val ascending: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Filter<*>) return false
|
||||||
|
|
||||||
|
return name == other.name && state == other.state
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = name.hashCode()
|
||||||
|
result = 31 * result + (state?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
|
||||||
|
|
||||||
|
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.data.network.ProgressListener
|
import eu.kanade.tachiyomi.network.ProgressListener
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
|
||||||
import rx.subjects.Subject
|
import rx.subjects.Subject
|
||||||
|
|
@ -0,0 +1,28 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface SChapter : Serializable {
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
var date_upload: Long
|
||||||
|
|
||||||
|
var chapter_number: Float
|
||||||
|
|
||||||
|
fun copyFrom(other: SChapter) {
|
||||||
|
name = other.name
|
||||||
|
url = other.url
|
||||||
|
date_upload = other.date_upload
|
||||||
|
chapter_number = other.chapter_number
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(): SChapter {
|
||||||
|
return SChapterImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
class SChapterImpl : SChapter {
|
||||||
|
|
||||||
|
override lateinit var url: String
|
||||||
|
|
||||||
|
override lateinit var name: String
|
||||||
|
|
||||||
|
override var date_upload: Long = 0
|
||||||
|
|
||||||
|
override var chapter_number: Float = -1f
|
||||||
|
|
||||||
|
}
|
58
app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt
Normal file
58
app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface SManga : Serializable {
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
|
||||||
|
var artist: String?
|
||||||
|
|
||||||
|
var author: String?
|
||||||
|
|
||||||
|
var description: String?
|
||||||
|
|
||||||
|
var genre: String?
|
||||||
|
|
||||||
|
var status: Int
|
||||||
|
|
||||||
|
var thumbnail_url: String?
|
||||||
|
|
||||||
|
var initialized: Boolean
|
||||||
|
|
||||||
|
fun copyFrom(other: SManga) {
|
||||||
|
if (other.author != null)
|
||||||
|
author = other.author
|
||||||
|
|
||||||
|
if (other.artist != null)
|
||||||
|
artist = other.artist
|
||||||
|
|
||||||
|
if (other.description != null)
|
||||||
|
description = other.description
|
||||||
|
|
||||||
|
if (other.genre != null)
|
||||||
|
genre = other.genre
|
||||||
|
|
||||||
|
if (other.thumbnail_url != null)
|
||||||
|
thumbnail_url = other.thumbnail_url
|
||||||
|
|
||||||
|
status = other.status
|
||||||
|
|
||||||
|
if (!initialized)
|
||||||
|
initialized = other.initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val UNKNOWN = 0
|
||||||
|
const val ONGOING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val LICENSED = 3
|
||||||
|
|
||||||
|
fun create(): SManga {
|
||||||
|
return SMangaImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
class SMangaImpl : SManga {
|
||||||
|
|
||||||
|
override lateinit var url: String
|
||||||
|
|
||||||
|
override lateinit var title: String
|
||||||
|
|
||||||
|
override var artist: String? = null
|
||||||
|
|
||||||
|
override var author: String? = null
|
||||||
|
|
||||||
|
override var description: String? = null
|
||||||
|
|
||||||
|
override var genre: String? = null
|
||||||
|
|
||||||
|
override var status: Int = 0
|
||||||
|
|
||||||
|
override var thumbnail_url: String? = null
|
||||||
|
|
||||||
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,361 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple implementation for sources from a website.
|
||||||
|
*/
|
||||||
|
abstract class HttpSource : CatalogueSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network service.
|
||||||
|
*/
|
||||||
|
protected val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences helper.
|
||||||
|
*/
|
||||||
|
protected val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||||
|
*/
|
||||||
|
abstract val baseUrl: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version id used to generate the source id. If the site completely changes and urls are
|
||||||
|
* incompatible, you may increase this value and it'll be considered as a new source.
|
||||||
|
*/
|
||||||
|
open val versionId = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||||
|
* of the MD5 of the string: sourcename/language/versionId
|
||||||
|
* Note the generated id sets the sign bit to 0.
|
||||||
|
*/
|
||||||
|
override val id by lazy {
|
||||||
|
val key = "${name.toLowerCase()}/$lang/$versionId"
|
||||||
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
|
(0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headers used for requests.
|
||||||
|
*/
|
||||||
|
val headers: Headers by lazy { headersBuilder().build() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default network client for doing requests.
|
||||||
|
*/
|
||||||
|
open val client: OkHttpClient
|
||||||
|
get() = network.client
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.toUpperCase()})"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
|
* override this method.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
|
return client.newCall(popularMangaRequest(page))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
popularMangaParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for the popular manga given the page.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
abstract protected fun popularMangaRequest(page: Int): Request
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
abstract protected fun popularMangaParse(response: Response): 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 number to retrieve.
|
||||||
|
* @param query the search query.
|
||||||
|
* @param filters the list of filters to apply.
|
||||||
|
*/
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
return client.newCall(searchMangaRequest(page, query, filters))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
searchMangaParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for the search manga given the page.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
* @param query the search query.
|
||||||
|
* @param filters the list of filters to apply.
|
||||||
|
*/
|
||||||
|
abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
abstract protected fun searchMangaParse(response: Response): MangasPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of latest manga updates.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||||
|
return client.newCall(latestUpdatesRequest(page))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
latestUpdatesParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for latest manga given the page.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
abstract protected fun latestUpdatesRequest(page: Int): Request
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
abstract protected fun latestUpdatesParse(response: Response): MangasPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: SManga): Observable<SManga> {
|
||||||
|
return client.newCall(mangaDetailsRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
mangaDetailsParse(response).apply { initialized = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for the details of a manga. Override only if it's needed to change the
|
||||||
|
* url, send different headers or request method like POST.
|
||||||
|
*
|
||||||
|
* @param manga the manga to be updated.
|
||||||
|
*/
|
||||||
|
open fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
return GET(baseUrl + manga.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns the details of a manga.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
abstract protected fun mangaDetailsParse(response: Response): SManga
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: SManga): Observable<List<SChapter>> {
|
||||||
|
return client.newCall(chapterListRequest(manga))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
chapterListParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: SManga): Request {
|
||||||
|
return GET(baseUrl + manga.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a list of chapters.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
abstract protected fun chapterListParse(response: Response): List<SChapter>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the page list for a chapter.
|
||||||
|
*
|
||||||
|
* @param chapter the chapter whose page list has to be fetched.
|
||||||
|
*/
|
||||||
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
|
return client.newCall(pageListRequest(chapter))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
pageListParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: SChapter): Request {
|
||||||
|
return GET(baseUrl + chapter.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a list of pages.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
abstract protected fun pageListParse(response: Response): List<Page>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 fun fetchImageUrl(page: Page): Observable<String> {
|
||||||
|
return client.newCall(imageUrlRequest(page))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { imageUrlParse(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns the absolute url to the source image.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
abstract protected fun imageUrlParse(response: Response): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the response of the source image.
|
||||||
|
*
|
||||||
|
* @param page the page whose source image has to be downloaded.
|
||||||
|
*/
|
||||||
|
fun fetchImage(page: Page): Observable<Response> {
|
||||||
|
return client.newCallWithProgress(imageRequest(page), page)
|
||||||
|
.asObservableSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
|
||||||
|
* database and the urls could still work after a domain change.
|
||||||
|
*
|
||||||
|
* @param url the full url to the chapter.
|
||||||
|
*/
|
||||||
|
fun SChapter.setUrlWithoutDomain(url: String) {
|
||||||
|
this.url = getUrlWithoutDomain(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from
|
||||||
|
* database and the urls could still work after a domain change.
|
||||||
|
*
|
||||||
|
* @param url the full url to the manga.
|
||||||
|
*/
|
||||||
|
fun SManga.setUrlWithoutDomain(url: String) {
|
||||||
|
this.url = getUrlWithoutDomain(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the url of the given string without the scheme and domain.
|
||||||
|
*
|
||||||
|
* @param orig the full url.
|
||||||
|
*/
|
||||||
|
private fun getUrlWithoutDomain(orig: String): String {
|
||||||
|
try {
|
||||||
|
val uri = URI(orig)
|
||||||
|
var out = uri.path
|
||||||
|
if (uri.query != null)
|
||||||
|
out += "?" + uri.query
|
||||||
|
if (uri.fragment != null)
|
||||||
|
out += "#" + uri.fragment
|
||||||
|
return out
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
return orig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before inserting a new chapter into database. Use it if you need to override chapter
|
||||||
|
* fields, like the title or the chapter number. Do not change anything to [manga].
|
||||||
|
*
|
||||||
|
* @param chapter the chapter to be added.
|
||||||
|
* @param manga the manga of the chapter.
|
||||||
|
*/
|
||||||
|
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of filters for the source.
|
||||||
|
*/
|
||||||
|
override fun getFilterList() = FilterList()
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: this should be handled with a different approach.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter cache.
|
||||||
|
*/
|
||||||
|
private val chapterCache: ChapterCache by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param chapter the chapter whose page list has to be fetched.
|
||||||
|
*/
|
||||||
|
fun HttpSource.fetchPageListFromCacheThenNet(chapter: Chapter): Observable<List<Page>> {
|
||||||
|
return chapterCache
|
||||||
|
.getPageListFromCache(chapter)
|
||||||
|
.onErrorResumeNext { fetchPageList(chapter) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable of the page with the downloaded image.
|
||||||
|
*
|
||||||
|
* @param page the page whose source image has to be downloaded.
|
||||||
|
*/
|
||||||
|
fun HttpSource.fetchImageFromCacheThenNet(page: Page): Observable<Page> {
|
||||||
|
return if (page.imageUrl.isNullOrEmpty())
|
||||||
|
getImageUrl(page).flatMap { getCachedImage(it) }
|
||||||
|
else
|
||||||
|
getCachedImage(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HttpSource.getImageUrl(page: Page): Observable<Page> {
|
||||||
|
page.status = Page.LOAD_PAGE
|
||||||
|
return fetchImageUrl(page)
|
||||||
|
.doOnError { page.status = Page.ERROR }
|
||||||
|
.onErrorReturn { null }
|
||||||
|
.doOnNext { page.imageUrl = it }
|
||||||
|
.map { page }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 HttpSource.getCachedImage(page: Page): Observable<Page> {
|
||||||
|
val imageUrl = page.imageUrl ?: return Observable.just(page)
|
||||||
|
|
||||||
|
return Observable.just(page)
|
||||||
|
.flatMap {
|
||||||
|
if (!chapterCache.isImageInCache(imageUrl)) {
|
||||||
|
cacheImage(page)
|
||||||
|
} else {
|
||||||
|
Observable.just(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
page.uri = Uri.fromFile(chapterCache.getImageFile(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 HttpSource.cacheImage(page: Page): Observable<Page> {
|
||||||
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
|
return fetchImage(page)
|
||||||
|
.doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) }
|
||||||
|
.map { page }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||||
|
return Observable.from(pages)
|
||||||
|
.filter { !it.imageUrl.isNullOrEmpty() }
|
||||||
|
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||||
|
return Observable.from(pages)
|
||||||
|
.filter { it.imageUrl.isNullOrEmpty() }
|
||||||
|
.concatMap { getImageUrl(it) }
|
||||||
|
}
|
@ -1,15 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
interface LoginSource : Source {
|
interface LoginSource : Source {
|
||||||
|
|
||||||
fun isLogged(): Boolean
|
fun isLogged(): Boolean
|
||||||
|
|
||||||
fun login(username: String, password: String): Observable<Boolean>
|
fun login(username: String, password: String): Observable<Boolean>
|
||||||
|
|
||||||
fun isAuthenticationSuccessful(response: Response): Boolean
|
fun isAuthenticationSuccessful(response: Response): Boolean
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,200 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||||
|
*/
|
||||||
|
abstract class ParsedHttpSource : HttpSource() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||||
|
popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
|
||||||
|
document.select(selector).first()
|
||||||
|
} != null
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||||
|
*/
|
||||||
|
abstract protected fun popularMangaSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||||
|
* totally fine to fill only those two values.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [popularMangaSelector].
|
||||||
|
*/
|
||||||
|
abstract protected fun popularMangaFromElement(element: Element): SManga
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||||
|
searchMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
|
||||||
|
document.select(selector).first()
|
||||||
|
} != null
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||||
|
*/
|
||||||
|
abstract protected fun searchMangaSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||||
|
* totally fine to fill only those two values.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [searchMangaSelector].
|
||||||
|
*/
|
||||||
|
abstract protected fun searchMangaFromElement(element: Element): SManga
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val mangas = document.select(latestUpdatesSelector()).map { element ->
|
||||||
|
latestUpdatesFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
|
||||||
|
document.select(selector).first()
|
||||||
|
} != null
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||||
|
*/
|
||||||
|
abstract protected fun latestUpdatesSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||||
|
* totally fine to fill only those two values.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [latestUpdatesSelector].
|
||||||
|
*/
|
||||||
|
abstract protected fun latestUpdatesFromElement(element: Element): SManga
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 latestUpdatesNextPageSelector(): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns the details of a manga.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
return mangaDetailsParse(response.asJsoup())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the details of the manga from the given [document].
|
||||||
|
*
|
||||||
|
* @param document the parsed document.
|
||||||
|
*/
|
||||||
|
abstract protected fun mangaDetailsParse(document: Document): SManga
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a list of chapters.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
|
||||||
|
*/
|
||||||
|
abstract protected fun chapterListSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a chapter from the given element.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [chapterListSelector].
|
||||||
|
*/
|
||||||
|
abstract protected fun chapterFromElement(element: Element): SChapter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns the page list.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
return pageListParse(response.asJsoup())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a page list from the given document.
|
||||||
|
*
|
||||||
|
* @param document the parsed document.
|
||||||
|
*/
|
||||||
|
abstract protected fun pageListParse(document: Document): List<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(response.asJsoup())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the absolute url to the source image from the document.
|
||||||
|
*
|
||||||
|
* @param document the parsed document.
|
||||||
|
*/
|
||||||
|
abstract protected fun imageUrlParse(document: Document): String
|
||||||
|
}
|
@ -1,212 +1,232 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.data.network.GET
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
import eu.kanade.tachiyomi.data.network.POST
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
import eu.kanade.tachiyomi.util.attrOrText
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import okhttp3.Request
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import okhttp3.Response
|
||||||
import eu.kanade.tachiyomi.util.attrOrText
|
import org.jsoup.Jsoup
|
||||||
import okhttp3.Request
|
import org.jsoup.nodes.Element
|
||||||
import okhttp3.Response
|
import java.text.SimpleDateFormat
|
||||||
import org.jsoup.Jsoup
|
import java.util.*
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
class YamlHttpSource(mappings: Map<*, *>) : HttpSource() {
|
||||||
import java.util.*
|
|
||||||
|
val map = YamlSourceNode(mappings)
|
||||||
class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
|
||||||
|
override val name: String
|
||||||
val map = YamlSourceNode(mappings)
|
get() = map.name
|
||||||
|
|
||||||
override val name: String
|
override val baseUrl = map.host.let {
|
||||||
get() = map.name
|
if (it.endsWith("/")) it.dropLast(1) else it
|
||||||
|
}
|
||||||
override val baseUrl = map.host.let {
|
|
||||||
if (it.endsWith("/")) it.dropLast(1) else it
|
override val lang = map.lang.toLowerCase()
|
||||||
}
|
|
||||||
|
override val supportsLatest = map.latestupdates != null
|
||||||
override val lang = map.lang.toLowerCase()
|
|
||||||
|
override val client = when (map.client) {
|
||||||
override val supportsLatest = map.latestupdates != null
|
"cloudflare" -> network.cloudflareClient
|
||||||
|
else -> network.client
|
||||||
override val client = when(map.client) {
|
}
|
||||||
"cloudflare" -> network.cloudflareClient
|
|
||||||
else -> network.client
|
override val id = map.id.let {
|
||||||
}
|
(it as? Int ?: (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff).toLong()
|
||||||
|
}
|
||||||
override val id = map.id.let {
|
|
||||||
if (it is Int) it else (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff
|
// Ugly, but needed after the changes
|
||||||
}
|
var popularNextPage: String? = null
|
||||||
|
var searchNextPage: String? = null
|
||||||
override fun popularMangaRequest(page: MangasPage): Request {
|
var latestNextPage: String? = null
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = popularMangaInitialUrl()
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
}
|
val url = if (page == 1) {
|
||||||
return when (map.popular.method?.toLowerCase()) {
|
popularNextPage = null
|
||||||
"post" -> POST(page.url, headers, map.popular.createForm())
|
map.popular.url
|
||||||
else -> GET(page.url, headers)
|
} else {
|
||||||
}
|
popularNextPage!!
|
||||||
}
|
}
|
||||||
|
return when (map.popular.method?.toLowerCase()) {
|
||||||
override fun popularMangaInitialUrl() = map.popular.url
|
"post" -> POST(url, headers, map.popular.createForm())
|
||||||
|
else -> GET(url, headers)
|
||||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
}
|
||||||
val document = response.asJsoup()
|
}
|
||||||
for (element in document.select(map.popular.manga_css)) {
|
|
||||||
Manga.create(id).apply {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
title = element.text()
|
val document = response.asJsoup()
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
page.mangas.add(this)
|
val mangas = document.select(map.popular.manga_css).map { element ->
|
||||||
}
|
SManga.create().apply {
|
||||||
}
|
title = element.text()
|
||||||
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
map.popular.next_url_css?.let { selector ->
|
}
|
||||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
}
|
||||||
}
|
|
||||||
}
|
popularNextPage = map.popular.next_url_css?.let { selector ->
|
||||||
|
document.select(selector).first()?.absUrl("href")
|
||||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): Request {
|
}
|
||||||
if (page.page == 1) {
|
|
||||||
page.url = searchMangaInitialUrl(query, filters)
|
return MangasPage(mangas, popularNextPage != null)
|
||||||
}
|
}
|
||||||
return when (map.search.method?.toLowerCase()) {
|
|
||||||
"post" -> POST(page.url, headers, map.search.createForm())
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
else -> GET(page.url, headers)
|
val url = if (page == 1) {
|
||||||
}
|
searchNextPage = null
|
||||||
}
|
map.search.url.replace("\$query", query)
|
||||||
|
} else {
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = map.search.url.replace("\$query", query)
|
searchNextPage!!
|
||||||
|
}
|
||||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
return when (map.search.method?.toLowerCase()) {
|
||||||
val document = response.asJsoup()
|
"post" -> POST(url, headers, map.search.createForm())
|
||||||
for (element in document.select(map.search.manga_css)) {
|
else -> GET(url, headers)
|
||||||
Manga.create(id).apply {
|
}
|
||||||
title = element.text()
|
}
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
page.mangas.add(this)
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
}
|
val document = response.asJsoup()
|
||||||
}
|
|
||||||
|
val mangas = document.select(map.search.manga_css).map { element ->
|
||||||
map.search.next_url_css?.let { selector ->
|
SManga.create().apply {
|
||||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
title = element.text()
|
||||||
}
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
override fun latestUpdatesRequest(page: MangasPage): Request {
|
|
||||||
if (page.page == 1) {
|
searchNextPage = map.search.next_url_css?.let { selector ->
|
||||||
page.url = latestUpdatesInitialUrl()
|
document.select(selector).first()?.absUrl("href")
|
||||||
}
|
}
|
||||||
return when (map.latestupdates!!.method?.toLowerCase()) {
|
|
||||||
"post" -> POST(page.url, headers, map.latestupdates.createForm())
|
return MangasPage(mangas, searchNextPage != null)
|
||||||
else -> GET(page.url, headers)
|
}
|
||||||
}
|
|
||||||
}
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val url = if (page == 1) {
|
||||||
override fun latestUpdatesInitialUrl() = map.latestupdates!!.url
|
latestNextPage = null
|
||||||
|
map.latestupdates!!.url
|
||||||
override fun latestUpdatesParse(response: Response, page: MangasPage) {
|
} else {
|
||||||
val document = response.asJsoup()
|
latestNextPage!!
|
||||||
for (element in document.select(map.latestupdates!!.manga_css)) {
|
}
|
||||||
Manga.create(id).apply {
|
return when (map.latestupdates!!.method?.toLowerCase()) {
|
||||||
title = element.text()
|
"post" -> POST(url, headers, map.latestupdates.createForm())
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
else -> GET(url, headers)
|
||||||
page.mangas.add(this)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
map.latestupdates.next_url_css?.let { selector ->
|
val document = response.asJsoup()
|
||||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
|
||||||
}
|
val mangas = document.select(map.latestupdates!!.manga_css).map { element ->
|
||||||
}
|
SManga.create().apply {
|
||||||
|
title = element.text()
|
||||||
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
setUrlWithoutDomain(element.attr("href"))
|
||||||
val document = response.asJsoup()
|
}
|
||||||
with(map.manga) {
|
}
|
||||||
val pool = parts.get(document)
|
|
||||||
|
popularNextPage = map.latestupdates.next_url_css?.let { selector ->
|
||||||
manga.author = author?.process(document, pool)
|
document.select(selector).first()?.absUrl("href")
|
||||||
manga.artist = artist?.process(document, pool)
|
}
|
||||||
manga.description = summary?.process(document, pool)
|
|
||||||
manga.thumbnail_url = cover?.process(document, pool)
|
return MangasPage(mangas, popularNextPage != null)
|
||||||
manga.genre = genres?.process(document, pool)
|
}
|
||||||
manga.status = status?.getStatus(document, pool) ?: Manga.UNKNOWN
|
|
||||||
}
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
}
|
val document = response.asJsoup()
|
||||||
|
|
||||||
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
val manga = SManga.create()
|
||||||
val document = response.asJsoup()
|
with(map.manga) {
|
||||||
with(map.chapters) {
|
val pool = parts.get(document)
|
||||||
val pool = emptyMap<String, Element>()
|
|
||||||
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
|
manga.author = author?.process(document, pool)
|
||||||
|
manga.artist = artist?.process(document, pool)
|
||||||
for (element in document.select(chapter_css)) {
|
manga.description = summary?.process(document, pool)
|
||||||
val chapter = Chapter.create()
|
manga.thumbnail_url = cover?.process(document, pool)
|
||||||
element.select(title).first().let {
|
manga.genre = genres?.process(document, pool)
|
||||||
chapter.name = it.text()
|
manga.status = status?.getStatus(document, pool) ?: SManga.UNKNOWN
|
||||||
chapter.setUrlWithoutDomain(it.attr("href"))
|
}
|
||||||
}
|
return manga
|
||||||
val dateElement = element.select(date?.select).first()
|
}
|
||||||
chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
|
|
||||||
chapters.add(chapter)
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
}
|
val document = response.asJsoup()
|
||||||
}
|
|
||||||
}
|
val chapters = mutableListOf<SChapter>()
|
||||||
|
with(map.chapters) {
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
val pool = emptyMap<String, Element>()
|
||||||
val body = response.body().string()
|
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
|
||||||
val url = response.request().url().toString()
|
|
||||||
|
for (element in document.select(chapter_css)) {
|
||||||
// TODO lazy initialization in Kotlin 1.1
|
val chapter = SChapter.create()
|
||||||
val document = Jsoup.parse(body, url)
|
element.select(title).first().let {
|
||||||
|
chapter.name = it.text()
|
||||||
with(map.pages) {
|
chapter.setUrlWithoutDomain(it.attr("href"))
|
||||||
// Capture a list of values where page urls will be resolved.
|
}
|
||||||
val capturedPages = if (pages_regex != null)
|
val dateElement = element.select(date?.select).first()
|
||||||
pages_regex!!.toRegex().findAll(body).map { it.value }.toList()
|
chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
|
||||||
else if (pages_css != null)
|
chapters.add(chapter)
|
||||||
document.select(pages_css).map { it.attrOrText(pages_attr!!) }
|
}
|
||||||
else
|
}
|
||||||
null
|
return chapters
|
||||||
|
}
|
||||||
// For each captured value, obtain the url and create a new page.
|
|
||||||
capturedPages?.forEach { value ->
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
// If the captured value isn't an url, we have to use replaces with the chapter url.
|
val body = response.body().string()
|
||||||
val pageUrl = if (replace != null && replacement != null)
|
val url = response.request().url().toString()
|
||||||
url.replace(replace!!.toRegex(), replacement!!.replace("\$value", value))
|
|
||||||
else
|
val pages = mutableListOf<Page>()
|
||||||
value
|
|
||||||
|
// TODO lazy initialization in Kotlin 1.1
|
||||||
pages.add(Page(pages.size, pageUrl))
|
val document = Jsoup.parse(body, url)
|
||||||
}
|
|
||||||
|
with(map.pages) {
|
||||||
// Capture a list of images.
|
// Capture a list of values where page urls will be resolved.
|
||||||
val capturedImages = if (image_regex != null)
|
val capturedPages = if (pages_regex != null)
|
||||||
image_regex!!.toRegex().findAll(body).map { it.groups[1]?.value }.toList()
|
pages_regex!!.toRegex().findAll(body).map { it.value }.toList()
|
||||||
else if (image_css != null)
|
else if (pages_css != null)
|
||||||
document.select(image_css).map { it.absUrl(image_attr) }
|
document.select(pages_css).map { it.attrOrText(pages_attr!!) }
|
||||||
else
|
else
|
||||||
null
|
null
|
||||||
|
|
||||||
// Assign the image url to each page
|
// For each captured value, obtain the url and create a new page.
|
||||||
capturedImages?.forEachIndexed { i, url ->
|
capturedPages?.forEach { value ->
|
||||||
val page = pages.getOrElse(i) { Page(i, "").apply { pages.add(this) } }
|
// If the captured value isn't an url, we have to use replaces with the chapter url.
|
||||||
page.imageUrl = url
|
val pageUrl = if (replace != null && replacement != null)
|
||||||
}
|
url.replace(replace!!.toRegex(), replacement!!.replace("\$value", value))
|
||||||
}
|
else
|
||||||
}
|
value
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
pages.add(Page(pages.size, pageUrl))
|
||||||
val body = response.body().string()
|
}
|
||||||
val url = response.request().url().toString()
|
|
||||||
|
// Capture a list of images.
|
||||||
with(map.pages) {
|
val capturedImages = if (image_regex != null)
|
||||||
return if (image_regex != null)
|
image_regex!!.toRegex().findAll(body).map { it.groups[1]?.value }.toList()
|
||||||
image_regex!!.toRegex().find(body)!!.groups[1]!!.value
|
else if (image_css != null)
|
||||||
else if (image_css != null)
|
document.select(image_css).map { it.absUrl(image_attr) }
|
||||||
Jsoup.parse(body, url).select(image_css).first().absUrl(image_attr)
|
else
|
||||||
else
|
null
|
||||||
throw Exception("image_regex and image_css are null")
|
|
||||||
}
|
// Assign the image url to each page
|
||||||
}
|
capturedImages?.forEachIndexed { i, url ->
|
||||||
}
|
val page = pages.getOrElse(i) { Page(i, "").apply { pages.add(this) } }
|
||||||
|
page.imageUrl = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String {
|
||||||
|
val body = response.body().string()
|
||||||
|
val url = response.request().url().toString()
|
||||||
|
|
||||||
|
with(map.pages) {
|
||||||
|
return if (image_regex != null)
|
||||||
|
image_regex!!.toRegex().find(body)!!.groups[1]!!.value
|
||||||
|
else if (image_css != null)
|
||||||
|
Jsoup.parse(body, url).select(image_css).first().absUrl(image_attr)
|
||||||
|
else
|
||||||
|
throw Exception("image_regex and image_css are null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,234 +1,234 @@
|
|||||||
@file:Suppress("UNCHECKED_CAST")
|
@file:Suppress("UNCHECKED_CAST")
|
||||||
|
|
||||||
package eu.kanade.tachiyomi.data.source.online
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
private fun toMap(map: Any?) = map as? Map<String, Any?>
|
private fun toMap(map: Any?) = map as? Map<String, Any?>
|
||||||
|
|
||||||
class YamlSourceNode(uncheckedMap: Map<*, *>) {
|
class YamlSourceNode(uncheckedMap: Map<*, *>) {
|
||||||
|
|
||||||
val map = toMap(uncheckedMap)!!
|
val map = toMap(uncheckedMap)!!
|
||||||
|
|
||||||
val id: Any by map
|
val id: Any by map
|
||||||
|
|
||||||
val name: String by map
|
val name: String by map
|
||||||
|
|
||||||
val host: String by map
|
val host: String by map
|
||||||
|
|
||||||
val lang: String by map
|
val lang: String by map
|
||||||
|
|
||||||
val client: String?
|
val client: String?
|
||||||
get() = map["client"] as? String
|
get() = map["client"] as? String
|
||||||
|
|
||||||
val popular = PopularNode(toMap(map["popular"])!!)
|
val popular = PopularNode(toMap(map["popular"])!!)
|
||||||
|
|
||||||
val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) }
|
val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) }
|
||||||
|
|
||||||
val search = SearchNode(toMap(map["search"])!!)
|
val search = SearchNode(toMap(map["search"])!!)
|
||||||
|
|
||||||
val manga = MangaNode(toMap(map["manga"])!!)
|
val manga = MangaNode(toMap(map["manga"])!!)
|
||||||
|
|
||||||
val chapters = ChaptersNode(toMap(map["chapters"])!!)
|
val chapters = ChaptersNode(toMap(map["chapters"])!!)
|
||||||
|
|
||||||
val pages = PagesNode(toMap(map["pages"])!!)
|
val pages = PagesNode(toMap(map["pages"])!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestableNode {
|
interface RequestableNode {
|
||||||
|
|
||||||
val map: Map<String, Any?>
|
val map: Map<String, Any?>
|
||||||
|
|
||||||
val url: String
|
val url: String
|
||||||
get() = map["url"] as String
|
get() = map["url"] as String
|
||||||
|
|
||||||
val method: String?
|
val method: String?
|
||||||
get() = map["method"] as? String
|
get() = map["method"] as? String
|
||||||
|
|
||||||
val payload: Map<String, String>?
|
val payload: Map<String, String>?
|
||||||
get() = map["payload"] as? Map<String, String>
|
get() = map["payload"] as? Map<String, String>
|
||||||
|
|
||||||
fun createForm(): RequestBody {
|
fun createForm(): RequestBody {
|
||||||
return FormBody.Builder().apply {
|
return FormBody.Builder().apply {
|
||||||
payload?.let {
|
payload?.let {
|
||||||
for ((key, value) in it) {
|
for ((key, value) in it) {
|
||||||
add(key, value)
|
add(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopularNode(override val map: Map<String, Any?>): RequestableNode {
|
class PopularNode(override val map: Map<String, Any?>): RequestableNode {
|
||||||
|
|
||||||
val manga_css: String by map
|
val manga_css: String by map
|
||||||
|
|
||||||
val next_url_css: String?
|
val next_url_css: String?
|
||||||
get() = map["next_url_css"] as? String
|
get() = map["next_url_css"] as? String
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LatestUpdatesNode(override val map: Map<String, Any?>): RequestableNode {
|
class LatestUpdatesNode(override val map: Map<String, Any?>): RequestableNode {
|
||||||
|
|
||||||
val manga_css: String by map
|
val manga_css: String by map
|
||||||
|
|
||||||
val next_url_css: String?
|
val next_url_css: String?
|
||||||
get() = map["next_url_css"] as? String
|
get() = map["next_url_css"] as? String
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SearchNode(override val map: Map<String, Any?>): RequestableNode {
|
class SearchNode(override val map: Map<String, Any?>): RequestableNode {
|
||||||
|
|
||||||
val manga_css: String by map
|
val manga_css: String by map
|
||||||
|
|
||||||
val next_url_css: String?
|
val next_url_css: String?
|
||||||
get() = map["next_url_css"] as? String
|
get() = map["next_url_css"] as? String
|
||||||
}
|
}
|
||||||
|
|
||||||
class MangaNode(private val map: Map<String, Any?>) {
|
class MangaNode(private val map: Map<String, Any?>) {
|
||||||
|
|
||||||
val parts = CacheNode(toMap(map["parts"]) ?: emptyMap())
|
val parts = CacheNode(toMap(map["parts"]) ?: emptyMap())
|
||||||
|
|
||||||
val artist = toMap(map["artist"])?.let { SelectableNode(it) }
|
val artist = toMap(map["artist"])?.let { SelectableNode(it) }
|
||||||
|
|
||||||
val author = toMap(map["author"])?.let { SelectableNode(it) }
|
val author = toMap(map["author"])?.let { SelectableNode(it) }
|
||||||
|
|
||||||
val summary = toMap(map["summary"])?.let { SelectableNode(it) }
|
val summary = toMap(map["summary"])?.let { SelectableNode(it) }
|
||||||
|
|
||||||
val status = toMap(map["status"])?.let { StatusNode(it) }
|
val status = toMap(map["status"])?.let { StatusNode(it) }
|
||||||
|
|
||||||
val genres = toMap(map["genres"])?.let { SelectableNode(it) }
|
val genres = toMap(map["genres"])?.let { SelectableNode(it) }
|
||||||
|
|
||||||
val cover = toMap(map["cover"])?.let { CoverNode(it) }
|
val cover = toMap(map["cover"])?.let { CoverNode(it) }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChaptersNode(private val map: Map<String, Any?>) {
|
class ChaptersNode(private val map: Map<String, Any?>) {
|
||||||
|
|
||||||
val chapter_css: String by map
|
val chapter_css: String by map
|
||||||
|
|
||||||
val title: String by map
|
val title: String by map
|
||||||
|
|
||||||
val date = toMap(toMap(map["date"]))?.let { DateNode(it) }
|
val date = toMap(toMap(map["date"]))?.let { DateNode(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
class CacheNode(private val map: Map<String, Any?>) {
|
class CacheNode(private val map: Map<String, Any?>) {
|
||||||
|
|
||||||
fun get(document: Document) = map.mapValues { document.select(it.value as String).first() }
|
fun get(document: Document) = map.mapValues { document.select(it.value as String).first() }
|
||||||
}
|
}
|
||||||
|
|
||||||
open class SelectableNode(private val map: Map<String, Any?>) {
|
open class SelectableNode(private val map: Map<String, Any?>) {
|
||||||
|
|
||||||
val select: String by map
|
val select: String by map
|
||||||
|
|
||||||
val from: String?
|
val from: String?
|
||||||
get() = map["from"] as? String
|
get() = map["from"] as? String
|
||||||
|
|
||||||
open val attr: String?
|
open val attr: String?
|
||||||
get() = map["attr"] as? String
|
get() = map["attr"] as? String
|
||||||
|
|
||||||
val capture: String?
|
val capture: String?
|
||||||
get() = map["capture"] as? String
|
get() = map["capture"] as? String
|
||||||
|
|
||||||
fun process(document: Element, cache: Map<String, Element>): String {
|
fun process(document: Element, cache: Map<String, Element>): String {
|
||||||
val parent = from?.let { cache[it] } ?: document
|
val parent = from?.let { cache[it] } ?: document
|
||||||
val node = parent.select(select).first()
|
val node = parent.select(select).first()
|
||||||
var text = attr?.let { node.attr(it) } ?: node.text()
|
var text = attr?.let { node.attr(it) } ?: node.text()
|
||||||
capture?.let {
|
capture?.let {
|
||||||
text = Regex(it).find(text)?.groupValues?.get(1) ?: text
|
text = Regex(it).find(text)?.groupValues?.get(1) ?: text
|
||||||
}
|
}
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
||||||
|
|
||||||
val complete: String?
|
val complete: String?
|
||||||
get() = map["complete"] as? String
|
get() = map["complete"] as? String
|
||||||
|
|
||||||
val ongoing: String?
|
val ongoing: String?
|
||||||
get() = map["ongoing"] as? String
|
get() = map["ongoing"] as? String
|
||||||
|
|
||||||
val licensed: String?
|
val licensed: String?
|
||||||
get() = map["licensed"] as? String
|
get() = map["licensed"] as? String
|
||||||
|
|
||||||
fun getStatus(document: Element, cache: Map<String, Element>): Int {
|
fun getStatus(document: Element, cache: Map<String, Element>): Int {
|
||||||
val text = process(document, cache)
|
val text = process(document, cache)
|
||||||
complete?.let {
|
complete?.let {
|
||||||
if (text.contains(it)) return Manga.COMPLETED
|
if (text.contains(it)) return SManga.COMPLETED
|
||||||
}
|
}
|
||||||
ongoing?.let {
|
ongoing?.let {
|
||||||
if (text.contains(it)) return Manga.ONGOING
|
if (text.contains(it)) return SManga.ONGOING
|
||||||
}
|
}
|
||||||
licensed?.let {
|
licensed?.let {
|
||||||
if (text.contains(it)) return Manga.LICENSED
|
if (text.contains(it)) return SManga.LICENSED
|
||||||
}
|
}
|
||||||
return Manga.UNKNOWN
|
return SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CoverNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
class CoverNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
||||||
|
|
||||||
override val attr: String?
|
override val attr: String?
|
||||||
get() = map["attr"] as? String ?: "src"
|
get() = map["attr"] as? String ?: "src"
|
||||||
}
|
}
|
||||||
|
|
||||||
class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
||||||
|
|
||||||
val format: String by map
|
val format: String by map
|
||||||
|
|
||||||
fun getDate(document: Element, cache: Map<String, Element>, formatter: SimpleDateFormat): Date {
|
fun getDate(document: Element, cache: Map<String, Element>, formatter: SimpleDateFormat): Date {
|
||||||
val text = process(document, cache)
|
val text = process(document, cache)
|
||||||
try {
|
try {
|
||||||
return formatter.parse(text)
|
return formatter.parse(text)
|
||||||
} catch (exception: ParseException) {}
|
} catch (exception: ParseException) {}
|
||||||
|
|
||||||
for (i in 0..7) {
|
for (i in 0..7) {
|
||||||
(map["day$i"] as? List<String>)?.let {
|
(map["day$i"] as? List<String>)?.let {
|
||||||
it.find { it.toRegex().containsMatchIn(text) }?.let {
|
it.find { it.toRegex().containsMatchIn(text) }?.let {
|
||||||
return Calendar.getInstance().apply { add(Calendar.DATE, -i) }.time
|
return Calendar.getInstance().apply { add(Calendar.DATE, -i) }.time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Date(0)
|
return Date(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PagesNode(private val map: Map<String, Any?>) {
|
class PagesNode(private val map: Map<String, Any?>) {
|
||||||
|
|
||||||
val pages_regex: String?
|
val pages_regex: String?
|
||||||
get() = map["pages_regex"] as? String
|
get() = map["pages_regex"] as? String
|
||||||
|
|
||||||
val pages_css: String?
|
val pages_css: String?
|
||||||
get() = map["pages_css"] as? String
|
get() = map["pages_css"] as? String
|
||||||
|
|
||||||
val pages_attr: String?
|
val pages_attr: String?
|
||||||
get() = map["pages_attr"] as? String ?: "value"
|
get() = map["pages_attr"] as? String ?: "value"
|
||||||
|
|
||||||
val replace: String?
|
val replace: String?
|
||||||
get() = map["url_replace"] as? String
|
get() = map["url_replace"] as? String
|
||||||
|
|
||||||
val replacement: String?
|
val replacement: String?
|
||||||
get() = map["url_replacement"] as? String
|
get() = map["url_replacement"] as? String
|
||||||
|
|
||||||
val image_regex: String?
|
val image_regex: String?
|
||||||
get() = map["image_regex"] as? String
|
get() = map["image_regex"] as? String
|
||||||
|
|
||||||
val image_css: String?
|
val image_css: String?
|
||||||
get() = map["image_css"] as? String
|
get() = map["image_css"] as? String
|
||||||
|
|
||||||
val image_attr: String
|
val image_attr: String
|
||||||
get() = map["image_attr"] as? String ?: "src"
|
get() = map["image_attr"] as? String ?: "src"
|
||||||
|
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.all
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.all
|
package eu.kanade.tachiyomi.source.online.all
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
@ -0,0 +1,366 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import android.text.Html
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.asObservable
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
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 : ParsedHttpSource(), LoginSource {
|
||||||
|
|
||||||
|
override val id: Long = 1
|
||||||
|
|
||||||
|
override val name = "Batoto"
|
||||||
|
|
||||||
|
override val baseUrl = "http://bato.to"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
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 popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/search_ajax?order_cond=views&order=desc&p=$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/search_ajax?order_cond=update&order=desc&p=$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "tr:has(a)"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "tr:has(a)"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("a[href^=http://bato.to]").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.text().trim()
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "#show_more_row"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
|
||||||
|
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
|
||||||
|
var genres = ""
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is Status -> if (!filter.isIgnored()) {
|
||||||
|
url.addQueryParameter("completed", if (filter.isExcluded()) "i" else "c")
|
||||||
|
}
|
||||||
|
is GenreList -> {
|
||||||
|
filter.state.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is Genre -> if (!filter.isIgnored()) {
|
||||||
|
genres += (if (filter.isExcluded()) ";e" else ";i") + filter.id
|
||||||
|
}
|
||||||
|
is SelectField -> {
|
||||||
|
val sel = filter.values[filter.state].value
|
||||||
|
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TextField -> {
|
||||||
|
if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
|
||||||
|
}
|
||||||
|
is SelectField -> {
|
||||||
|
val sel = filter.values[filter.state].value
|
||||||
|
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||||
|
}
|
||||||
|
is Flag -> {
|
||||||
|
val sel = if (filter.state) filter.valTrue else filter.valFalse
|
||||||
|
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||||
|
}
|
||||||
|
is OrderBy -> {
|
||||||
|
url.addQueryParameter("order_cond", arrayOf("title", "author", "artist", "rating", "views", "update")[filter.state!!.index])
|
||||||
|
url.addQueryParameter("order", if (filter.state?.ascending == true) "asc" else "desc")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
|
||||||
|
url.addQueryParameter("p", page.toString())
|
||||||
|
return GET(url.toString(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val mangaId = manga.url.substringAfterLast("r")
|
||||||
|
return GET("$baseUrl/comic_pop?id=$mangaId", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val tbody = document.select("tbody").first()
|
||||||
|
val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
|
||||||
|
|
||||||
|
val manga = SManga.create()
|
||||||
|
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(", ")
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String?) = when (status) {
|
||||||
|
"Ongoing" -> SManga.ONGOING
|
||||||
|
"Complete" -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val body = response.body().string()
|
||||||
|
val matcher = staffNotice.matcher(body)
|
||||||
|
if (matcher.find()) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val notice = Html.fromHtml(matcher.group(1)).toString().trim()
|
||||||
|
throw Exception(notice)
|
||||||
|
}
|
||||||
|
|
||||||
|
val document = response.asJsoup(body)
|
||||||
|
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val urlElement = element.select("a[href^=http://bato.to/reader").first()
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.text()
|
||||||
|
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
||||||
|
parseDateFromElement(it)
|
||||||
|
} ?: 0
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDateFromElement(dateElement: Element): Long {
|
||||||
|
val dateAsString = dateElement.text()
|
||||||
|
|
||||||
|
var 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: SChapter): Request {
|
||||||
|
val id = chapter.url.substringAfterLast("#")
|
||||||
|
return GET("$baseUrl/areader?id=$id&p=1", pageHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val pages = mutableListOf<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")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
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, username, password) }
|
||||||
|
.map { isAuthenticationSuccessful(it) }
|
||||||
|
|
||||||
|
private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
|
||||||
|
val doc = response.asJsoup()
|
||||||
|
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: SManga): Observable<List<SChapter>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ListValue(val name: String, val value: String) {
|
||||||
|
override fun toString(): String = name
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Status : Filter.TriState("Completed")
|
||||||
|
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||||
|
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||||
|
private class SelectField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.Select<ListValue>(name, values, state)
|
||||||
|
private class Flag(name: String, val key: String, val valTrue: String, val valFalse: String) : Filter.CheckBox(name)
|
||||||
|
private class GenreList(genres: List<Filter<*>>) : Filter.Group<Filter<*>>("Genres", genres)
|
||||||
|
private class OrderBy : Filter.Sort("Order by",
|
||||||
|
arrayOf("Title", "Author", "Artist", "Rating", "Views", "Last Update"),
|
||||||
|
Filter.Sort.Selection(4, false))
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
TextField("Author", "artist_name"),
|
||||||
|
SelectField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
|
||||||
|
Status(),
|
||||||
|
Flag("Exclude mature", "mature", "m", ""),
|
||||||
|
OrderBy(),
|
||||||
|
GenreList(getGenreList())
|
||||||
|
)
|
||||||
|
|
||||||
|
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
|
||||||
|
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Genre("${el.textContent.trim()}", ${id})`
|
||||||
|
// }).join(',\n')
|
||||||
|
// on https://bato.to/search
|
||||||
|
private fun getGenreList() = listOf(
|
||||||
|
SelectField("Inclusion mode", "genre_cond", arrayOf(ListValue("And (all selected genres)", "and"), ListValue("Or (any selected genres) ", "or"))),
|
||||||
|
Genre("4-Koma", 40),
|
||||||
|
Genre("Action", 1),
|
||||||
|
Genre("Adventure", 2),
|
||||||
|
Genre("Award Winning", 39),
|
||||||
|
Genre("Comedy", 3),
|
||||||
|
Genre("Cooking", 41),
|
||||||
|
Genre("Doujinshi", 9),
|
||||||
|
Genre("Drama", 10),
|
||||||
|
Genre("Ecchi", 12),
|
||||||
|
Genre("Fantasy", 13),
|
||||||
|
Genre("Gender Bender", 15),
|
||||||
|
Genre("Harem", 17),
|
||||||
|
Genre("Historical", 20),
|
||||||
|
Genre("Horror", 22),
|
||||||
|
Genre("Josei", 34),
|
||||||
|
Genre("Martial Arts", 27),
|
||||||
|
Genre("Mecha", 30),
|
||||||
|
Genre("Medical", 42),
|
||||||
|
Genre("Music", 37),
|
||||||
|
Genre("Mystery", 4),
|
||||||
|
Genre("Oneshot", 38),
|
||||||
|
Genre("Psychological", 5),
|
||||||
|
Genre("Romance", 6),
|
||||||
|
Genre("School Life", 7),
|
||||||
|
Genre("Sci-fi", 8),
|
||||||
|
Genre("Seinen", 32),
|
||||||
|
Genre("Shoujo", 35),
|
||||||
|
Genre("Shoujo Ai", 16),
|
||||||
|
Genre("Shounen", 33),
|
||||||
|
Genre("Shounen Ai", 19),
|
||||||
|
Genre("Slice of Life", 21),
|
||||||
|
Genre("Smut", 23),
|
||||||
|
Genre("Sports", 25),
|
||||||
|
Genre("Supernatural", 26),
|
||||||
|
Genre("Tragedy", 28),
|
||||||
|
Genre("Webtoon", 36),
|
||||||
|
Genre("Yaoi", 29),
|
||||||
|
Genre("Yuri", 31),
|
||||||
|
Genre("[no chapters]", 44)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,197 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
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 : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val id: Long = 4
|
||||||
|
|
||||||
|
override val name = "Kissmanga"
|
||||||
|
|
||||||
|
override val baseUrl = "http://kissmanga.com"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "table.listing tr:gt(1)"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "table.listing tr:gt(1)"
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/MangaList/MostPopular?page=$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("http://kissmanga.com/MangaList/LatestUpdate?page=$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("td a:eq(0)").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String = "ul.pager > li > a:contains(Next)"
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val form = FormBody.Builder().apply {
|
||||||
|
add("mangaName", query)
|
||||||
|
|
||||||
|
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||||
|
when (filter) {
|
||||||
|
is Author -> add("authorArtist", filter.state)
|
||||||
|
is Status -> add("status", arrayOf("", "Completed", "Ongoing")[filter.state])
|
||||||
|
is GenreList -> filter.state.forEach { genre -> add("genres", genre.state.toString()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return POST("$baseUrl/AdvanceSearch", headers, form.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = null
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val infoElement = document.select("div.barContent").first()
|
||||||
|
|
||||||
|
val manga = SManga.create()
|
||||||
|
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")
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing") -> SManga.ONGOING
|
||||||
|
status.contains("Completed") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "table.listing tr:gt(1)"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(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
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val pages = mutableListOf<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)))
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
throw Exception("Not used")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlRequest(page: Page) = GET(page.url)
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
private class Status : Filter.TriState("Completed")
|
||||||
|
private class Author : Filter.Text("Author")
|
||||||
|
private class Genre(name: String) : Filter.TriState(name)
|
||||||
|
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
Author(),
|
||||||
|
Status(),
|
||||||
|
GenreList(getGenreList())
|
||||||
|
)
|
||||||
|
|
||||||
|
// $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
|
||||||
|
// on http://kissmanga.com/AdvanceSearch
|
||||||
|
private fun getGenreList() = listOf(
|
||||||
|
Genre("4-Koma"),
|
||||||
|
Genre("Action"),
|
||||||
|
Genre("Adult"),
|
||||||
|
Genre("Adventure"),
|
||||||
|
Genre("Comedy"),
|
||||||
|
Genre("Comic"),
|
||||||
|
Genre("Cooking"),
|
||||||
|
Genre("Doujinshi"),
|
||||||
|
Genre("Drama"),
|
||||||
|
Genre("Ecchi"),
|
||||||
|
Genre("Fantasy"),
|
||||||
|
Genre("Gender Bender"),
|
||||||
|
Genre("Harem"),
|
||||||
|
Genre("Historical"),
|
||||||
|
Genre("Horror"),
|
||||||
|
Genre("Josei"),
|
||||||
|
Genre("Lolicon"),
|
||||||
|
Genre("Manga"),
|
||||||
|
Genre("Manhua"),
|
||||||
|
Genre("Manhwa"),
|
||||||
|
Genre("Martial Arts"),
|
||||||
|
Genre("Mature"),
|
||||||
|
Genre("Mecha"),
|
||||||
|
Genre("Medical"),
|
||||||
|
Genre("Music"),
|
||||||
|
Genre("Mystery"),
|
||||||
|
Genre("One shot"),
|
||||||
|
Genre("Psychological"),
|
||||||
|
Genre("Romance"),
|
||||||
|
Genre("School Life"),
|
||||||
|
Genre("Sci-fi"),
|
||||||
|
Genre("Seinen"),
|
||||||
|
Genre("Shotacon"),
|
||||||
|
Genre("Shoujo"),
|
||||||
|
Genre("Shoujo Ai"),
|
||||||
|
Genre("Shounen"),
|
||||||
|
Genre("Shounen Ai"),
|
||||||
|
Genre("Slice of Life"),
|
||||||
|
Genre("Smut"),
|
||||||
|
Genre("Sports"),
|
||||||
|
Genre("Supernatural"),
|
||||||
|
Genre("Tragedy"),
|
||||||
|
Genre("Webtoon"),
|
||||||
|
Genre("Yaoi"),
|
||||||
|
Genre("Yuri")
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,223 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Mangafox : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val id: Long = 3
|
||||||
|
|
||||||
|
override val name = "Mangafox"
|
||||||
|
|
||||||
|
override val baseUrl = "http://mangafox.me"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
val pageStr = if (page != 1) "$page.htm" else ""
|
||||||
|
return GET("$baseUrl/directory/$pageStr", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div#mangalist > ul.list > li"
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val pageStr = if (page != 1) "$page.htm" else ""
|
||||||
|
return GET("$baseUrl/directory/$pageStr?latest")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("a.title").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
||||||
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is Status -> url.addQueryParameter(filter.id, filter.state.toString())
|
||||||
|
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||||
|
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||||
|
is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
|
||||||
|
is OrderBy -> {
|
||||||
|
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||||
|
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url.addQueryParameter("page", page.toString())
|
||||||
|
return GET(url.toString(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = "div#mangalist > ul.list > li"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("a.title").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = "a:has(span.next)"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val infoElement = document.select("div#title").first()
|
||||||
|
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
|
||||||
|
val sideInfoElement = document.select("#series_info").first()
|
||||||
|
|
||||||
|
val manga = SManga.create()
|
||||||
|
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")
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing") -> SManga.ONGOING
|
||||||
|
status.contains("Completed") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "div#chapters li div"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val urlElement = element.select("a.tips").first()
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.text()
|
||||||
|
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
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(document: Document): List<Page> {
|
||||||
|
val url = document.baseUri().substringBeforeLast('/')
|
||||||
|
|
||||||
|
val pages = mutableListOf<Page>()
|
||||||
|
document.select("select.m").first()?.select("option:not([value=0])")?.forEach {
|
||||||
|
pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document): String {
|
||||||
|
val url = document.getElementById("image").attr("src")
|
||||||
|
return if ("compressed?token=" !in url) {
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
"http://mangafox.me/media/logo.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Status(val id: String = "is_completed") : Filter.TriState("Completed")
|
||||||
|
private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
|
||||||
|
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||||
|
private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
|
||||||
|
private class OrderBy : Filter.Sort("Order by",
|
||||||
|
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||||
|
Filter.Sort.Selection(2, false))
|
||||||
|
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
TextField("Author", "author"),
|
||||||
|
TextField("Artist", "artist"),
|
||||||
|
Type(),
|
||||||
|
Status(),
|
||||||
|
OrderBy(),
|
||||||
|
GenreList(getGenreList())
|
||||||
|
)
|
||||||
|
|
||||||
|
// $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
|
||||||
|
// on http://mangafox.me/search.php
|
||||||
|
private fun getGenreList() = listOf(
|
||||||
|
Genre("Action"),
|
||||||
|
Genre("Adult"),
|
||||||
|
Genre("Adventure"),
|
||||||
|
Genre("Comedy"),
|
||||||
|
Genre("Doujinshi"),
|
||||||
|
Genre("Drama"),
|
||||||
|
Genre("Ecchi"),
|
||||||
|
Genre("Fantasy"),
|
||||||
|
Genre("Gender Bender"),
|
||||||
|
Genre("Harem"),
|
||||||
|
Genre("Historical"),
|
||||||
|
Genre("Horror"),
|
||||||
|
Genre("Josei"),
|
||||||
|
Genre("Martial Arts"),
|
||||||
|
Genre("Mature"),
|
||||||
|
Genre("Mecha"),
|
||||||
|
Genre("Mystery"),
|
||||||
|
Genre("One Shot"),
|
||||||
|
Genre("Psychological"),
|
||||||
|
Genre("Romance"),
|
||||||
|
Genre("School Life"),
|
||||||
|
Genre("Sci-fi"),
|
||||||
|
Genre("Seinen"),
|
||||||
|
Genre("Shoujo"),
|
||||||
|
Genre("Shoujo Ai"),
|
||||||
|
Genre("Shounen"),
|
||||||
|
Genre("Shounen Ai"),
|
||||||
|
Genre("Slice of Life"),
|
||||||
|
Genre("Smut"),
|
||||||
|
Genre("Sports"),
|
||||||
|
Genre("Supernatural"),
|
||||||
|
Genre("Tragedy"),
|
||||||
|
Genre("Webtoons"),
|
||||||
|
Genre("Yaoi"),
|
||||||
|
Genre("Yuri")
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,220 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Mangahere : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val id: Long = 2
|
||||||
|
|
||||||
|
override val name = "Mangahere"
|
||||||
|
|
||||||
|
override val baseUrl = "http://www.mangahere.co"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/directory/$page.htm?views.za", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/directory/$page.htm?last_chapter_time.za", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangaFromElement(query: String, element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select(query).first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text()
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
return mangaFromElement("div.title > a", element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
||||||
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
||||||
|
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||||
|
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||||
|
is Type -> url.addQueryParameter("direction", arrayOf("", "rl", "lr")[filter.state])
|
||||||
|
is OrderBy -> {
|
||||||
|
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||||
|
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url.addQueryParameter("page", page.toString())
|
||||||
|
return GET(url.toString(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
return mangaFromElement("a.manga_info", element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val detailElement = document.select(".manga_detail_top").first()
|
||||||
|
val infoElement = detailElement.select(".detail_topText").first()
|
||||||
|
|
||||||
|
val manga = SManga.create()
|
||||||
|
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")
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing") -> SManga.ONGOING
|
||||||
|
status.contains("Completed") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val parentEl = element.select("span.left").first()
|
||||||
|
|
||||||
|
val urlElement = parentEl.select("a").first()
|
||||||
|
|
||||||
|
var volume = parentEl.select("span.mr6")?.first()?.text()?.trim() ?: ""
|
||||||
|
if (volume.length > 0) {
|
||||||
|
volume = " - " + volume
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = parentEl?.textNodes()?.last()?.text()?.trim() ?: ""
|
||||||
|
if (title.length > 0) {
|
||||||
|
title = " - " + title
|
||||||
|
}
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.text() + volume + title
|
||||||
|
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
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): List<Page> {
|
||||||
|
val pages = mutableListOf<Page>()
|
||||||
|
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
||||||
|
pages.add(Page(pages.size, it.attr("value")))
|
||||||
|
}
|
||||||
|
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
||||||
|
|
||||||
|
private class Status : Filter.TriState("Completed")
|
||||||
|
private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
|
||||||
|
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||||
|
private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga (read from right to left)", "Korean Manhwa (read from left to right)"))
|
||||||
|
private class OrderBy : Filter.Sort("Order by",
|
||||||
|
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||||
|
Filter.Sort.Selection(2, false))
|
||||||
|
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
TextField("Author", "author"),
|
||||||
|
TextField("Artist", "artist"),
|
||||||
|
Type(),
|
||||||
|
Status(),
|
||||||
|
OrderBy(),
|
||||||
|
GenreList(getGenreList())
|
||||||
|
)
|
||||||
|
|
||||||
|
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Genre("${el.nextSibling.nextSibling.textContent.trim()}", "${el.getAttribute('name')}")`).join(',\n')
|
||||||
|
// http://www.mangahere.co/advsearch.htm
|
||||||
|
private fun getGenreList() = listOf(
|
||||||
|
Genre("Action"),
|
||||||
|
Genre("Adventure"),
|
||||||
|
Genre("Comedy"),
|
||||||
|
Genre("Doujinshi"),
|
||||||
|
Genre("Drama"),
|
||||||
|
Genre("Ecchi"),
|
||||||
|
Genre("Fantasy"),
|
||||||
|
Genre("Gender Bender"),
|
||||||
|
Genre("Harem"),
|
||||||
|
Genre("Historical"),
|
||||||
|
Genre("Horror"),
|
||||||
|
Genre("Josei"),
|
||||||
|
Genre("Martial Arts"),
|
||||||
|
Genre("Mature"),
|
||||||
|
Genre("Mecha"),
|
||||||
|
Genre("Mystery"),
|
||||||
|
Genre("One Shot"),
|
||||||
|
Genre("Psychological"),
|
||||||
|
Genre("Romance"),
|
||||||
|
Genre("School Life"),
|
||||||
|
Genre("Sci-fi"),
|
||||||
|
Genre("Seinen"),
|
||||||
|
Genre("Shoujo"),
|
||||||
|
Genre("Shoujo Ai"),
|
||||||
|
Genre("Shounen"),
|
||||||
|
Genre("Shounen Ai"),
|
||||||
|
Genre("Slice of Life"),
|
||||||
|
Genre("Sports"),
|
||||||
|
Genre("Supernatural"),
|
||||||
|
Genre("Tragedy"),
|
||||||
|
Genre("Yaoi"),
|
||||||
|
Genre("Yuri")
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,243 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
class Mangasee : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val id: Long = 9
|
||||||
|
|
||||||
|
override val name = "Mangasee"
|
||||||
|
|
||||||
|
override val baseUrl = "http://mangaseeonline.net"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val recentUpdatesPattern = Pattern.compile("(.*?)\\s(\\d+\\.?\\d*)\\s?(Completed)?")
|
||||||
|
|
||||||
|
private val indexPattern = Pattern.compile("-index-(.*?)-")
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.requested > div.row"
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending")
|
||||||
|
return POST(requestUrl, headers, body.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("a.resultLink").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "button.requestMore"
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = "div.requested > div.row"
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
|
||||||
|
if (!query.isEmpty()) url.addQueryParameter("keyword", query)
|
||||||
|
val genres = mutableListOf<String>()
|
||||||
|
val genresNo = mutableListOf<String>()
|
||||||
|
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||||
|
when (filter) {
|
||||||
|
is Sort -> {
|
||||||
|
if (filter.state?.index != 0)
|
||||||
|
url.addQueryParameter("sortBy", if (filter.state?.index == 1) "dateUpdated" else "popularity")
|
||||||
|
if (filter.state?.ascending != true)
|
||||||
|
url.addQueryParameter("sortOrder", "descending")
|
||||||
|
}
|
||||||
|
is SelectField -> if (filter.state != 0) url.addQueryParameter(filter.key, filter.values[filter.state])
|
||||||
|
is TextField -> if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
|
||||||
|
is GenreList -> filter.state.forEach { genre ->
|
||||||
|
when (genre.state) {
|
||||||
|
Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
|
||||||
|
Filter.TriState.STATE_EXCLUDE -> genresNo.add(genre.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (genres.isNotEmpty()) url.addQueryParameter("genre", genres.joinToString(","))
|
||||||
|
if (genresNo.isNotEmpty()) url.addQueryParameter("genreNo", genresNo.joinToString(","))
|
||||||
|
|
||||||
|
val (body, requestUrl) = convertQueryToPost(page, url.toString())
|
||||||
|
return POST(requestUrl, headers, body.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
||||||
|
val url = HttpUrl.parse(url)
|
||||||
|
val body = FormBody.Builder().add("page", page.toString())
|
||||||
|
for (i in 0..url.querySize() - 1) {
|
||||||
|
body.add(url.queryParameterName(i), url.queryParameterValue(i))
|
||||||
|
}
|
||||||
|
val requestUrl = url.scheme() + "://" + url.host() + url.encodedPath()
|
||||||
|
return Pair(body, requestUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("a.resultLink").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = "button.requestMore"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val detailElement = document.select("div.well > div.row").first()
|
||||||
|
|
||||||
|
val manga = SManga.create()
|
||||||
|
manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text()
|
||||||
|
manga.genre = detailElement.select("span.details > div.row > div:has(b:contains(Genre(s))) > a").map { it.text() }.joinToString()
|
||||||
|
manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
|
||||||
|
manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) }
|
||||||
|
manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing (Scan)") -> SManga.ONGOING
|
||||||
|
status.contains("Complete (Scan)") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "div.chapter-list > a"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
|
chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: ""
|
||||||
|
chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterDate(dateAsString: String): Long {
|
||||||
|
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val fullUrl = document.baseUri()
|
||||||
|
val url = fullUrl.substringBeforeLast('/')
|
||||||
|
|
||||||
|
val pages = mutableListOf<Page>()
|
||||||
|
|
||||||
|
val series = document.select("input.IndexName").first().attr("value")
|
||||||
|
val chapter = document.select("span.CurChapter").first().text()
|
||||||
|
var index = ""
|
||||||
|
|
||||||
|
val m = indexPattern.matcher(fullUrl)
|
||||||
|
if (m.find()) {
|
||||||
|
val indexNumber = m.group(1)
|
||||||
|
index = "-index-$indexNumber"
|
||||||
|
}
|
||||||
|
|
||||||
|
document.select("div.ContainerNav").first().select("select.PageSelect > option").forEach {
|
||||||
|
pages.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html"))
|
||||||
|
}
|
||||||
|
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "button.requestMore"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector(): String = "a.latestSeries"
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val url = "http://mangaseeonline.net/home/latest.request.php"
|
||||||
|
val (body, requestUrl) = convertQueryToPost(page, url)
|
||||||
|
return POST(requestUrl, headers, body.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("a.latestSeries").first().let {
|
||||||
|
val chapterUrl = it.attr("href")
|
||||||
|
val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
|
||||||
|
val indexOfLastPath = chapterUrl.lastIndexOf("/")
|
||||||
|
val mangaUrl = chapterUrl.substring(indexOfLastPath, indexOfMangaUrl)
|
||||||
|
val defaultText = it.select("p.clamp2").text()
|
||||||
|
val m = recentUpdatesPattern.matcher(defaultText)
|
||||||
|
val title = if (m.matches()) m.group(1) else defaultText
|
||||||
|
manga.setUrlWithoutDomain("/manga" + mangaUrl)
|
||||||
|
manga.title = title
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Filter.Sort.Selection(2, false))
|
||||||
|
private class Genre(name: String) : Filter.TriState(name)
|
||||||
|
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||||
|
private class SelectField(name: String, val key: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
|
||||||
|
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
TextField("Years", "year"),
|
||||||
|
TextField("Author", "author"),
|
||||||
|
SelectField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
|
||||||
|
SelectField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
|
||||||
|
SelectField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
|
||||||
|
Sort(),
|
||||||
|
GenreList(getGenreList())
|
||||||
|
)
|
||||||
|
|
||||||
|
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||||
|
// http://mangasee.co/advanced-search/
|
||||||
|
private fun getGenreList() = listOf(
|
||||||
|
Genre("Action"),
|
||||||
|
Genre("Adult"),
|
||||||
|
Genre("Adventure"),
|
||||||
|
Genre("Comedy"),
|
||||||
|
Genre("Doujinshi"),
|
||||||
|
Genre("Drama"),
|
||||||
|
Genre("Ecchi"),
|
||||||
|
Genre("Fantasy"),
|
||||||
|
Genre("Gender Bender"),
|
||||||
|
Genre("Harem"),
|
||||||
|
Genre("Hentai"),
|
||||||
|
Genre("Historical"),
|
||||||
|
Genre("Horror"),
|
||||||
|
Genre("Josei"),
|
||||||
|
Genre("Lolicon"),
|
||||||
|
Genre("Martial Arts"),
|
||||||
|
Genre("Mature"),
|
||||||
|
Genre("Mecha"),
|
||||||
|
Genre("Mystery"),
|
||||||
|
Genre("Psychological"),
|
||||||
|
Genre("Romance"),
|
||||||
|
Genre("School Life"),
|
||||||
|
Genre("Sci-fi"),
|
||||||
|
Genre("Seinen"),
|
||||||
|
Genre("Shotacon"),
|
||||||
|
Genre("Shoujo"),
|
||||||
|
Genre("Shoujo Ai"),
|
||||||
|
Genre("Shounen"),
|
||||||
|
Genre("Shounen Ai"),
|
||||||
|
Genre("Slice of Life"),
|
||||||
|
Genre("Smut"),
|
||||||
|
Genre("Sports"),
|
||||||
|
Genre("Supernatural"),
|
||||||
|
Genre("Tragedy"),
|
||||||
|
Genre("Yaoi"),
|
||||||
|
Genre("Yuri")
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,219 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.english
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Readmangatoday : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val id: Long = 8
|
||||||
|
|
||||||
|
override val name = "ReadMangaToday"
|
||||||
|
|
||||||
|
override val baseUrl = "http://www.readmanga.today"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client: OkHttpClient get() = network.cloudflareClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search only returns data with this set
|
||||||
|
*/
|
||||||
|
override fun headersBuilder() = Headers.Builder().apply {
|
||||||
|
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||||
|
add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/hot-manga/$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/latest-releases/$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.hot-manga > div.style-grid > div.box"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("div.title > h2 > a").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.attr("title")
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val builder = okhttp3.FormBody.Builder()
|
||||||
|
builder.add("manga-name", query)
|
||||||
|
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is TextField -> builder.add(filter.key, filter.state)
|
||||||
|
is Type -> builder.add("type", arrayOf("all", "japanese", "korean", "chinese")[filter.state])
|
||||||
|
is Status -> builder.add("status", arrayOf("both", "completed", "ongoing")[filter.state])
|
||||||
|
is GenreList -> filter.state.forEach { genre ->
|
||||||
|
when (genre.state) {
|
||||||
|
Filter.TriState.STATE_INCLUDE -> builder.add("include[]", genre.id.toString())
|
||||||
|
Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", genre.id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return POST("$baseUrl/service/advanced_search", headers, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = "div.style-list > div.box"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("div.title > h2 > a").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.attr("title")
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val detailElement = document.select("div.movie-meta").first()
|
||||||
|
|
||||||
|
val manga = SManga.create()
|
||||||
|
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")
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("Ongoing") -> SManga.ONGOING
|
||||||
|
status.contains("Completed") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "ul.chp_lst > li"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.select("span.val").text()
|
||||||
|
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterDate(date: String): Long {
|
||||||
|
val dateWords: List<String> = date.split(" ")
|
||||||
|
|
||||||
|
if (dateWords.size == 3) {
|
||||||
|
val timeAgo = Integer.parseInt(dateWords[0])
|
||||||
|
val 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.timeInMillis
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val pages = mutableListOf<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)
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
||||||
|
|
||||||
|
private class Status : Filter.TriState("Completed")
|
||||||
|
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||||
|
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||||
|
private class Type : Filter.Select<String>("Type", arrayOf("All", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
|
||||||
|
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
TextField("Author", "author-name"),
|
||||||
|
TextField("Artist", "artist-name"),
|
||||||
|
Type(),
|
||||||
|
Status(),
|
||||||
|
GenreList(getGenreList())
|
||||||
|
)
|
||||||
|
|
||||||
|
// [...document.querySelectorAll("ul.manga-cat span")].map(el => `Genre("${el.nextSibling.textContent.trim()}", ${el.getAttribute('data-id')})`).join(',\n')
|
||||||
|
// http://www.readmanga.today/advanced-search
|
||||||
|
private fun getGenreList() = listOf(
|
||||||
|
Genre("Action", 2),
|
||||||
|
Genre("Adventure", 4),
|
||||||
|
Genre("Comedy", 5),
|
||||||
|
Genre("Doujinshi", 6),
|
||||||
|
Genre("Drama", 7),
|
||||||
|
Genre("Ecchi", 8),
|
||||||
|
Genre("Fantasy", 9),
|
||||||
|
Genre("Gender Bender", 10),
|
||||||
|
Genre("Harem", 11),
|
||||||
|
Genre("Historical", 12),
|
||||||
|
Genre("Horror", 13),
|
||||||
|
Genre("Josei", 14),
|
||||||
|
Genre("Lolicon", 15),
|
||||||
|
Genre("Martial Arts", 16),
|
||||||
|
Genre("Mature", 17),
|
||||||
|
Genre("Mecha", 18),
|
||||||
|
Genre("Mystery", 19),
|
||||||
|
Genre("One shot", 20),
|
||||||
|
Genre("Psychological", 21),
|
||||||
|
Genre("Romance", 22),
|
||||||
|
Genre("School Life", 23),
|
||||||
|
Genre("Sci-fi", 24),
|
||||||
|
Genre("Seinen", 25),
|
||||||
|
Genre("Shotacon", 26),
|
||||||
|
Genre("Shoujo", 27),
|
||||||
|
Genre("Shoujo Ai", 28),
|
||||||
|
Genre("Shounen", 29),
|
||||||
|
Genre("Shounen Ai", 30),
|
||||||
|
Genre("Slice of Life", 31),
|
||||||
|
Genre("Smut", 32),
|
||||||
|
Genre("Sports", 33),
|
||||||
|
Genre("Supernatural", 34),
|
||||||
|
Genre("Tragedy", 35),
|
||||||
|
Genre("Yaoi", 36),
|
||||||
|
Genre("Yuri", 37)
|
||||||
|
)
|
||||||
|
}
|
@ -1,106 +1,122 @@
|
|||||||
package eu.kanade.tachiyomi.data.source.online.german
|
package eu.kanade.tachiyomi.source.online.german
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.data.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import okhttp3.Response
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import org.jsoup.nodes.Document
|
import okhttp3.Request
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Document
|
||||||
import java.text.SimpleDateFormat
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
class WieManga(override val id: Int) : ParsedOnlineSource() {
|
|
||||||
|
class WieManga : ParsedHttpSource() {
|
||||||
override val name = "Wie Manga!"
|
|
||||||
|
override val id: Long = 10
|
||||||
override val baseUrl = "http://www.wiemanga.com"
|
|
||||||
|
override val name = "Wie Manga!"
|
||||||
override val lang = "de"
|
|
||||||
|
override val baseUrl = "http://www.wiemanga.com"
|
||||||
override val supportsLatest = true
|
|
||||||
|
override val lang = "de"
|
||||||
override fun popularMangaInitialUrl() = "$baseUrl/list/Hot-Book/"
|
|
||||||
|
override val supportsLatest = true
|
||||||
override fun latestUpdatesInitialUrl() = "$baseUrl/list/New-Update/"
|
|
||||||
|
override fun popularMangaSelector() = ".booklist td > div"
|
||||||
override fun popularMangaSelector() = ".booklist td > div"
|
|
||||||
|
override fun latestUpdatesSelector() = ".booklist td > div"
|
||||||
override fun latestUpdatesSelector() = ".booklist td > div"
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
return GET("$baseUrl/list/Hot-Book/", headers)
|
||||||
val image = element.select("dt img")
|
}
|
||||||
val title = element.select("dd a:first-child")
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
manga.setUrlWithoutDomain(title.attr("href"))
|
return GET("$baseUrl/list/New-Update/", headers)
|
||||||
manga.title = title.text()
|
}
|
||||||
manga.thumbnail_url = image.attr("src")
|
|
||||||
}
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val image = element.select("dt img")
|
||||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
val title = element.select("dd a:first-child")
|
||||||
popularMangaFromElement(element, manga)
|
|
||||||
}
|
val manga = SManga.create()
|
||||||
|
manga.setUrlWithoutDomain(title.attr("href"))
|
||||||
override fun popularMangaNextPageSelector() = null
|
manga.title = title.text()
|
||||||
|
manga.thumbnail_url = image.attr("src")
|
||||||
override fun latestUpdatesNextPageSelector() = null
|
return manga
|
||||||
|
}
|
||||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter>) = "$baseUrl/search/?wd=$query"
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
override fun searchMangaSelector() = ".searchresult td > div"
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
|
||||||
val image = element.select(".resultimg img")
|
override fun popularMangaNextPageSelector() = null
|
||||||
val title = element.select(".resultbookname")
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = null
|
||||||
manga.setUrlWithoutDomain(title.attr("href"))
|
|
||||||
manga.title = title.text()
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
manga.thumbnail_url = image.attr("src")
|
return GET("$baseUrl/search/?wd=$query", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = ".pagetor a.l"
|
override fun searchMangaSelector() = ".searchresult td > div"
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
val imageElement = document.select(".bookmessgae tr > td:nth-child(1)").first()
|
val image = element.select(".resultimg img")
|
||||||
val infoElement = document.select(".bookmessgae tr > td:nth-child(2)").first()
|
val title = element.select(".resultbookname")
|
||||||
|
|
||||||
manga.author = infoElement.select("dd:nth-of-type(2) a").first()?.text()
|
val manga = SManga.create()
|
||||||
manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text()
|
manga.setUrlWithoutDomain(title.attr("href"))
|
||||||
manga.description = infoElement.select("dl > dt:last-child").first()?.text()?.replaceFirst("Beschreibung", "")
|
manga.title = title.text()
|
||||||
manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
|
manga.thumbnail_url = image.attr("src")
|
||||||
|
return manga
|
||||||
if (manga.author == "RSS")
|
}
|
||||||
manga.author = null
|
|
||||||
|
override fun searchMangaNextPageSelector() = ".pagetor a.l"
|
||||||
if (manga.artist == "RSS")
|
|
||||||
manga.artist = null
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
}
|
val imageElement = document.select(".bookmessgae tr > td:nth-child(1)").first()
|
||||||
|
val infoElement = document.select(".bookmessgae tr > td:nth-child(2)").first()
|
||||||
override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
|
|
||||||
|
val manga = SManga.create()
|
||||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
manga.author = infoElement.select("dd:nth-of-type(2) a").first()?.text()
|
||||||
val urlElement = element.select(".col1 a").first()
|
manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text()
|
||||||
val dateElement = element.select(".col3 a").first()
|
manga.description = infoElement.select("dl > dt:last-child").first()?.text()?.replaceFirst("Beschreibung", "")
|
||||||
|
manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
|
||||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
|
||||||
chapter.name = urlElement.text()
|
if (manga.author == "RSS")
|
||||||
chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0
|
manga.author = null
|
||||||
}
|
|
||||||
|
if (manga.artist == "RSS")
|
||||||
private fun parseChapterDate(date: String): Long {
|
manga.artist = null
|
||||||
return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
|
||||||
val document = response.asJsoup()
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
document.select("select#page").first().select("option").forEach {
|
val urlElement = element.select(".col1 a").first()
|
||||||
pages.add(Page(pages.size, it.attr("value")))
|
val dateElement = element.select(".col3 a").first()
|
||||||
}
|
|
||||||
}
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {}
|
chapter.name = urlElement.text()
|
||||||
|
chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0
|
||||||
override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterDate(date: String): Long {
|
||||||
|
return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val pages = mutableListOf<Page>()
|
||||||
|
|
||||||
|
document.select("select#page").first().select("option").forEach {
|
||||||
|
pages.add(Page(pages.size, it.attr("value")))
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,236 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.russian
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class Mangachan : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val id: Long = 7
|
||||||
|
|
||||||
|
override val name = "Mangachan"
|
||||||
|
|
||||||
|
override val baseUrl = "http://mangachan.me"
|
||||||
|
|
||||||
|
override val lang = "ru"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
var pageNum = 1
|
||||||
|
when {
|
||||||
|
page < 1 -> pageNum = 1
|
||||||
|
page >= 1 -> pageNum = page
|
||||||
|
}
|
||||||
|
val url = if (query.isNotEmpty()) {
|
||||||
|
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
|
||||||
|
} else {
|
||||||
|
val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
|
||||||
|
if (filt.isNotEmpty()) {
|
||||||
|
var genres = ""
|
||||||
|
filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
|
||||||
|
"$baseUrl/tags/${genres.dropLast(1)}?offset=${20 * (pageNum - 1)}"
|
||||||
|
} else {
|
||||||
|
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/newestch?page=$page")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.content_row"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "ul.area_rightNews li"
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("h2 > a").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("a:nth-child(1)").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = "a:contains(Далее)"
|
||||||
|
|
||||||
|
private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
var hasNextPage = false
|
||||||
|
|
||||||
|
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||||
|
searchMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
val nextSearchPage = document.select(searchMangaNextPageSelector())
|
||||||
|
if (nextSearchPage.isNotEmpty()) {
|
||||||
|
val query = document.select("input#searchinput").first().attr("value")
|
||||||
|
val pageNum = nextSearchPage.let { selector ->
|
||||||
|
val onClick = selector.attr("onclick")
|
||||||
|
onClick?.split("""\\d+""")
|
||||||
|
}
|
||||||
|
nextSearchPage.attr("href", "$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum")
|
||||||
|
hasNextPage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val nextGenresPage = document.select(searchGenresNextPageSelector())
|
||||||
|
if (nextGenresPage.isNotEmpty()) {
|
||||||
|
hasNextPage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val infoElement = document.select("table.mangatitle").first()
|
||||||
|
val descElement = document.select("div#description").first()
|
||||||
|
val imgElement = document.select("img#cover").first()
|
||||||
|
|
||||||
|
val manga = SManga.create()
|
||||||
|
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()
|
||||||
|
manga.thumbnail_url = baseUrl + imgElement.attr("src")
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(element: String): Int {
|
||||||
|
when {
|
||||||
|
element.contains("перевод завершен") -> return SManga.COMPLETED
|
||||||
|
element.contains("перевод продолжается") -> return SManga.ONGOING
|
||||||
|
else -> return SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(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
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<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(',')
|
||||||
|
|
||||||
|
return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
throw Exception("Not used")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||||
|
|
||||||
|
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
|
||||||
|
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
|
||||||
|
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
|
||||||
|
* on http://mangachan.me/
|
||||||
|
*/
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
Genre("18 плюс"),
|
||||||
|
Genre("bdsm"),
|
||||||
|
Genre("арт"),
|
||||||
|
Genre("биография"),
|
||||||
|
Genre("боевик"),
|
||||||
|
Genre("боевые искусства"),
|
||||||
|
Genre("вампиры"),
|
||||||
|
Genre("веб"),
|
||||||
|
Genre("гарем"),
|
||||||
|
Genre("гендерная интрига"),
|
||||||
|
Genre("героическое фэнтези"),
|
||||||
|
Genre("детектив"),
|
||||||
|
Genre("дзёсэй"),
|
||||||
|
Genre("додзинси"),
|
||||||
|
Genre("драма"),
|
||||||
|
Genre("игра"),
|
||||||
|
Genre("инцест"),
|
||||||
|
Genre("искусство"),
|
||||||
|
Genre("история"),
|
||||||
|
Genre("киберпанк"),
|
||||||
|
Genre("кодомо"),
|
||||||
|
Genre("комедия"),
|
||||||
|
Genre("литРПГ"),
|
||||||
|
Genre("магия"),
|
||||||
|
Genre("махо-сёдзё"),
|
||||||
|
Genre("меха"),
|
||||||
|
Genre("мистика"),
|
||||||
|
Genre("музыка"),
|
||||||
|
Genre("научная фантастика"),
|
||||||
|
Genre("повседневность"),
|
||||||
|
Genre("постапокалиптика"),
|
||||||
|
Genre("приключения"),
|
||||||
|
Genre("психология"),
|
||||||
|
Genre("романтика"),
|
||||||
|
Genre("самурайский боевик"),
|
||||||
|
Genre("сборник"),
|
||||||
|
Genre("сверхъестественное"),
|
||||||
|
Genre("сказка"),
|
||||||
|
Genre("спорт"),
|
||||||
|
Genre("супергерои"),
|
||||||
|
Genre("сэйнэн"),
|
||||||
|
Genre("сёдзё"),
|
||||||
|
Genre("сёдзё-ай"),
|
||||||
|
Genre("сёнэн"),
|
||||||
|
Genre("сёнэн-ай"),
|
||||||
|
Genre("тентакли"),
|
||||||
|
Genre("трагедия"),
|
||||||
|
Genre("триллер"),
|
||||||
|
Genre("ужасы"),
|
||||||
|
Genre("фантастика"),
|
||||||
|
Genre("фурри"),
|
||||||
|
Genre("фэнтези"),
|
||||||
|
Genre("школа"),
|
||||||
|
Genre("эротика"),
|
||||||
|
Genre("юри"),
|
||||||
|
Genre("яой"),
|
||||||
|
Genre("ёнкома")
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,198 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.russian
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.Request
|
||||||
|
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 : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val id: Long = 6
|
||||||
|
|
||||||
|
override val name = "Mintmanga"
|
||||||
|
|
||||||
|
override val baseUrl = "http://mintmanga.com"
|
||||||
|
|
||||||
|
override val lang = "ru"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.desc"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.desc"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("h3 > a").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.attr("title")
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
|
||||||
|
return GET("$baseUrl/search?q=$query&$genres", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
// max 200 results
|
||||||
|
override fun searchMangaNextPageSelector() = null
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val infoElement = document.select("div.leftContent").first()
|
||||||
|
|
||||||
|
val manga = SManga.create()
|
||||||
|
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")
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(element: String): Int {
|
||||||
|
when {
|
||||||
|
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
|
||||||
|
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
|
||||||
|
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
|
||||||
|
else -> return SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(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
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||||
|
val basic = Regex("""\s([0-9]+)(\s-\s)([0-9]+)\s*""")
|
||||||
|
val extra = Regex("""\s([0-9]+\sЭкстра)\s*""")
|
||||||
|
val single = Regex("""\sСингл\s*""")
|
||||||
|
when {
|
||||||
|
basic.containsMatchIn(chapter.name) -> {
|
||||||
|
basic.find(chapter.name)?.let {
|
||||||
|
val number = it.groups[3]?.value!!
|
||||||
|
chapter.chapter_number = number.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extra.containsMatchIn(chapter.name) -> // Extra chapters doesn't contain chapter number
|
||||||
|
chapter.chapter_number = -2f
|
||||||
|
single.containsMatchIn(chapter.name) -> // Oneshoots, doujinshi and other mangas with one chapter
|
||||||
|
chapter.chapter_number = 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<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)
|
||||||
|
|
||||||
|
val p = Pattern.compile("'.+?','.+?',\".+?\"")
|
||||||
|
val m = p.matcher(trimmedHtml)
|
||||||
|
|
||||||
|
val pages = mutableListOf<Page>()
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (m.find()) {
|
||||||
|
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
||||||
|
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
throw Exception("Not used")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
private class Genre(name: String, val id: String) : Filter.TriState(name)
|
||||||
|
|
||||||
|
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||||
|
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||||
|
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||||
|
* on http://mintmanga.com/search
|
||||||
|
*/
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
Genre("арт", "el_2220"),
|
||||||
|
Genre("бара", "el_1353"),
|
||||||
|
Genre("боевик", "el_1346"),
|
||||||
|
Genre("боевые искусства", "el_1334"),
|
||||||
|
Genre("вампиры", "el_1339"),
|
||||||
|
Genre("гарем", "el_1333"),
|
||||||
|
Genre("гендерная интрига", "el_1347"),
|
||||||
|
Genre("героическое фэнтези", "el_1337"),
|
||||||
|
Genre("детектив", "el_1343"),
|
||||||
|
Genre("дзёсэй", "el_1349"),
|
||||||
|
Genre("додзинси", "el_1332"),
|
||||||
|
Genre("драма", "el_1310"),
|
||||||
|
Genre("игра", "el_5229"),
|
||||||
|
Genre("история", "el_1311"),
|
||||||
|
Genre("киберпанк", "el_1351"),
|
||||||
|
Genre("комедия", "el_1328"),
|
||||||
|
Genre("меха", "el_1318"),
|
||||||
|
Genre("мистика", "el_1324"),
|
||||||
|
Genre("научная фантастика", "el_1325"),
|
||||||
|
Genre("повседневность", "el_1327"),
|
||||||
|
Genre("постапокалиптика", "el_1342"),
|
||||||
|
Genre("приключения", "el_1322"),
|
||||||
|
Genre("психология", "el_1335"),
|
||||||
|
Genre("романтика", "el_1313"),
|
||||||
|
Genre("самурайский боевик", "el_1316"),
|
||||||
|
Genre("сверхъестественное", "el_1350"),
|
||||||
|
Genre("сёдзё", "el_1314"),
|
||||||
|
Genre("сёдзё-ай", "el_1320"),
|
||||||
|
Genre("сёнэн", "el_1326"),
|
||||||
|
Genre("сёнэн-ай", "el_1330"),
|
||||||
|
Genre("спорт", "el_1321"),
|
||||||
|
Genre("сэйнэн", "el_1329"),
|
||||||
|
Genre("трагедия", "el_1344"),
|
||||||
|
Genre("триллер", "el_1341"),
|
||||||
|
Genre("ужасы", "el_1317"),
|
||||||
|
Genre("фантастика", "el_1331"),
|
||||||
|
Genre("фэнтези", "el_1323"),
|
||||||
|
Genre("школа", "el_1319"),
|
||||||
|
Genre("эротика", "el_1340"),
|
||||||
|
Genre("этти", "el_1354"),
|
||||||
|
Genre("юри", "el_1315"),
|
||||||
|
Genre("яой", "el_1336")
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,197 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online.russian
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.Request
|
||||||
|
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 : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val id: Long = 5
|
||||||
|
|
||||||
|
override val name = "Readmanga"
|
||||||
|
|
||||||
|
override val baseUrl = "http://readmanga.me"
|
||||||
|
|
||||||
|
override val lang = "ru"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = "div.desc"
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = "div.desc"
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("h3 > a").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.attr("title")
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
|
||||||
|
return GET("$baseUrl/search?q=$query&$genres", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
|
return popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
// max 200 results
|
||||||
|
override fun searchMangaNextPageSelector() = null
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val infoElement = document.select("div.leftContent").first()
|
||||||
|
|
||||||
|
val manga = SManga.create()
|
||||||
|
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")
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(element: String): Int {
|
||||||
|
when {
|
||||||
|
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
|
||||||
|
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
|
||||||
|
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
|
||||||
|
else -> return SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(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
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||||
|
val basic = Regex("""\s([0-9]+)(\s-\s)([0-9]+)\s*""")
|
||||||
|
val extra = Regex("""\s([0-9]+\sЭкстра)\s*""")
|
||||||
|
val single = Regex("""\sСингл\s*""")
|
||||||
|
when {
|
||||||
|
basic.containsMatchIn(chapter.name) -> {
|
||||||
|
basic.find(chapter.name)?.let {
|
||||||
|
val number = it.groups[3]?.value!!
|
||||||
|
chapter.chapter_number = number.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extra.containsMatchIn(chapter.name) -> // Extra chapters doesn't contain chapter number
|
||||||
|
chapter.chapter_number = -2f
|
||||||
|
single.containsMatchIn(chapter.name) -> // Oneshoots, doujinshi and other mangas with one chapter
|
||||||
|
chapter.chapter_number = 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<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)
|
||||||
|
|
||||||
|
val p = Pattern.compile("'.+?','.+?',\".+?\"")
|
||||||
|
val m = p.matcher(trimmedHtml)
|
||||||
|
|
||||||
|
val pages = mutableListOf<Page>()
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (m.find()) {
|
||||||
|
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
||||||
|
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
throw Exception("Not used")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
private class Genre(name: String, val id: String) : Filter.TriState(name)
|
||||||
|
|
||||||
|
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||||
|
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||||
|
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||||
|
* on http://readmanga.me/search
|
||||||
|
*/
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
Genre("арт", "el_5685"),
|
||||||
|
Genre("боевик", "el_2155"),
|
||||||
|
Genre("боевые искусства", "el_2143"),
|
||||||
|
Genre("вампиры", "el_2148"),
|
||||||
|
Genre("гарем", "el_2142"),
|
||||||
|
Genre("гендерная интрига", "el_2156"),
|
||||||
|
Genre("героическое фэнтези", "el_2146"),
|
||||||
|
Genre("детектив", "el_2152"),
|
||||||
|
Genre("дзёсэй", "el_2158"),
|
||||||
|
Genre("додзинси", "el_2141"),
|
||||||
|
Genre("драма", "el_2118"),
|
||||||
|
Genre("игра", "el_2154"),
|
||||||
|
Genre("история", "el_2119"),
|
||||||
|
Genre("киберпанк", "el_8032"),
|
||||||
|
Genre("кодомо", "el_2137"),
|
||||||
|
Genre("комедия", "el_2136"),
|
||||||
|
Genre("махо-сёдзё", "el_2147"),
|
||||||
|
Genre("меха", "el_2126"),
|
||||||
|
Genre("мистика", "el_2132"),
|
||||||
|
Genre("научная фантастика", "el_2133"),
|
||||||
|
Genre("повседневность", "el_2135"),
|
||||||
|
Genre("постапокалиптика", "el_2151"),
|
||||||
|
Genre("приключения", "el_2130"),
|
||||||
|
Genre("психология", "el_2144"),
|
||||||
|
Genre("романтика", "el_2121"),
|
||||||
|
Genre("самурайский боевик", "el_2124"),
|
||||||
|
Genre("сверхъестественное", "el_2159"),
|
||||||
|
Genre("сёдзё", "el_2122"),
|
||||||
|
Genre("сёдзё-ай", "el_2128"),
|
||||||
|
Genre("сёнэн", "el_2134"),
|
||||||
|
Genre("сёнэн-ай", "el_2139"),
|
||||||
|
Genre("спорт", "el_2129"),
|
||||||
|
Genre("сэйнэн", "el_2138"),
|
||||||
|
Genre("трагедия", "el_2153"),
|
||||||
|
Genre("триллер", "el_2150"),
|
||||||
|
Genre("ужасы", "el_2125"),
|
||||||
|
Genre("фантастика", "el_2140"),
|
||||||
|
Genre("фэнтези", "el_2131"),
|
||||||
|
Genre("школа", "el_2127"),
|
||||||
|
Genre("этти", "el_2149"),
|
||||||
|
Genre("юри", "el_2123")
|
||||||
|
)
|
||||||
|
}
|
@ -4,6 +4,7 @@ import android.os.Bundle
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupManager
|
import eu.kanade.tachiyomi.data.backup.BackupManager
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||||
import exh.ui.migration.UrlMigrator
|
import exh.ui.migration.UrlMigrator
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -49,13 +50,13 @@ class BackupPresenter : BasePresenter<BackupFragment>() {
|
|||||||
* @param file the path where the file will be saved.
|
* @param file the path where the file will be saved.
|
||||||
*/
|
*/
|
||||||
fun createBackup(file: File) {
|
fun createBackup(file: File) {
|
||||||
if (isUnsubscribed(backupSubscription)) {
|
if (backupSubscription.isNullOrUnsubscribed()) {
|
||||||
backupSubscription = getBackupObservable(file)
|
backupSubscription = getBackupObservable(file)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst(
|
.subscribeFirst(
|
||||||
{ view, result -> view.onBackupCompleted(file) },
|
{ view, result -> view.onBackupCompleted(file) },
|
||||||
{ view, error -> view.onBackupError(error) })
|
BackupFragment::onBackupError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,13 +66,13 @@ class BackupPresenter : BasePresenter<BackupFragment>() {
|
|||||||
* @param stream the input stream of the backup file.
|
* @param stream the input stream of the backup file.
|
||||||
*/
|
*/
|
||||||
fun restoreBackup(stream: InputStream) {
|
fun restoreBackup(stream: InputStream) {
|
||||||
if (isUnsubscribed(restoreSubscription)) {
|
if (restoreSubscription.isNullOrUnsubscribed()) {
|
||||||
restoreSubscription = getRestoreObservable(stream)
|
restoreSubscription = getRestoreObservable(stream)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst(
|
.subscribeFirst(
|
||||||
{ view, result -> view.onRestoreCompleted() },
|
{ view, result -> view.onRestoreCompleted() },
|
||||||
{ view, error -> view.onRestoreError(error) })
|
BackupFragment::onRestoreError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,11 +15,17 @@ import uy.kohesive.injekt.api.get
|
|||||||
|
|
||||||
interface ActivityMixin {
|
interface ActivityMixin {
|
||||||
|
|
||||||
|
var resumed: Boolean
|
||||||
|
|
||||||
fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
|
fun setupToolbar(toolbar: Toolbar, backNavigation: Boolean = true) {
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
|
getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
|
||||||
if (backNavigation) {
|
if (backNavigation) {
|
||||||
toolbar.setNavigationOnClickListener { onBackPressed() }
|
toolbar.setNavigationOnClickListener {
|
||||||
|
if (resumed) {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,15 +5,14 @@ import eu.kanade.tachiyomi.util.LocaleHelper
|
|||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
||||||
|
|
||||||
|
override var resumed = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
LocaleHelper.updateConfiguration(this)
|
LocaleHelper.updateConfiguration(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getActivity() = this
|
override fun getActivity() = this
|
||||||
|
|
||||||
var resumed = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
resumed = true
|
resumed = true
|
||||||
|
@ -8,6 +8,8 @@ import nucleus.view.NucleusAppCompatActivity
|
|||||||
|
|
||||||
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
|
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
|
||||||
|
|
||||||
|
override var resumed = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
LocaleHelper.updateConfiguration(this)
|
LocaleHelper.updateConfiguration(this)
|
||||||
}
|
}
|
||||||
@ -25,9 +27,6 @@ abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P
|
|||||||
|
|
||||||
override fun getActivity() = this
|
override fun getActivity() = this
|
||||||
|
|
||||||
var resumed = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
resumed = true
|
resumed = true
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.base.adapter
|
|||||||
import android.support.v7.widget.RecyclerView
|
import android.support.v7.widget.RecyclerView
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
||||||
|
|
||||||
abstract class FlexibleViewHolder(view: View,
|
abstract class FlexibleViewHolder(view: View,
|
||||||
private val adapter: FlexibleAdapter<*, *>,
|
private val adapter: FlexibleAdapter<*, *>,
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.adapter
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback].
|
|
||||||
*
|
|
||||||
* @author Paul Burke (ipaulpro)
|
|
||||||
*/
|
|
||||||
interface ItemTouchHelperAdapter {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when an item has been dragged far enough to trigger a move. This is called every time
|
|
||||||
* an item is shifted, and **not** at the end of a "drop" event.
|
|
||||||
*
|
|
||||||
* Implementations should call [RecyclerView.Adapter.notifyItemMoved] after
|
|
||||||
* adjusting the underlying data to reflect this move.
|
|
||||||
*
|
|
||||||
* @param fromPosition The start position of the moved item.
|
|
||||||
* @param toPosition Then resolved position of the moved item.
|
|
||||||
* @see [RecyclerView.getAdapterPositionFor]
|
|
||||||
* @see [RecyclerView.ViewHolder.getAdapterPosition]
|
|
||||||
*/
|
|
||||||
fun onItemMove(fromPosition: Int, toPosition: Int)
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when an item has been dismissed by a swipe.
|
|
||||||
*
|
|
||||||
* Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after
|
|
||||||
* adjusting the underlying data to reflect this removal.
|
|
||||||
*
|
|
||||||
* @param position The position of the item dismissed.
|
|
||||||
* @see RecyclerView.getAdapterPositionFor
|
|
||||||
* @see RecyclerView.ViewHolder.getAdapterPosition
|
|
||||||
*/
|
|
||||||
fun onItemDismiss(position: Int)
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.adapter
|
|
||||||
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
|
|
||||||
interface OnStartDragListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a view is requesting a start of a drag.
|
|
||||||
*
|
|
||||||
* @param viewHolder The holder of the view to drag.
|
|
||||||
*/
|
|
||||||
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.adapter
|
|
||||||
|
|
||||||
import android.support.v7.widget.RecyclerView
|
|
||||||
import android.support.v7.widget.helper.ItemTouchHelper
|
|
||||||
|
|
||||||
open class SimpleItemTouchHelperCallback(private val adapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
|
|
||||||
|
|
||||||
override fun isLongPressDragEnabled() = true
|
|
||||||
|
|
||||||
override fun isItemViewSwipeEnabled() = true
|
|
||||||
|
|
||||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
|
||||||
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
|
||||||
val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
|
|
||||||
return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
|
|
||||||
target: RecyclerView.ViewHolder): Boolean {
|
|
||||||
adapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
|
||||||
adapter.onItemDismiss(viewHolder.adapterPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.presenter
|
package eu.kanade.tachiyomi.ui.base.presenter
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import nucleus.presenter.RxPresenter
|
||||||
import nucleus.view.ViewWithPresenter
|
import nucleus.view.ViewWithPresenter
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
|
@ -1,492 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.presenter;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.CallSuper;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import nucleus.presenter.Presenter;
|
|
||||||
import nucleus.presenter.delivery.DeliverFirst;
|
|
||||||
import nucleus.presenter.delivery.DeliverLatestCache;
|
|
||||||
import nucleus.presenter.delivery.DeliverReplay;
|
|
||||||
import nucleus.presenter.delivery.Delivery;
|
|
||||||
import rx.Observable;
|
|
||||||
import rx.Subscription;
|
|
||||||
import rx.functions.Action1;
|
|
||||||
import rx.functions.Action2;
|
|
||||||
import rx.functions.Func0;
|
|
||||||
import rx.internal.util.SubscriptionList;
|
|
||||||
import rx.subjects.BehaviorSubject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an extension of {@link Presenter} which provides RxJava functionality.
|
|
||||||
*
|
|
||||||
* @param <View> a type of view.
|
|
||||||
*/
|
|
||||||
public class RxPresenter<View> extends Presenter<View> {
|
|
||||||
|
|
||||||
private static final String REQUESTED_KEY = RxPresenter.class.getName() + "#requested";
|
|
||||||
|
|
||||||
private final BehaviorSubject<View> views = BehaviorSubject.create();
|
|
||||||
private final SubscriptionList subscriptions = new SubscriptionList();
|
|
||||||
|
|
||||||
private final HashMap<Integer, Func0<Subscription>> restartables = new HashMap<>();
|
|
||||||
private final HashMap<Integer, Subscription> restartableSubscriptions = new HashMap<>();
|
|
||||||
private final ArrayList<Integer> requested = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an {@link rx.Observable} that emits the current attached view or null.
|
|
||||||
* See {@link BehaviorSubject} for more information.
|
|
||||||
*
|
|
||||||
* @return an observable that emits the current attached view or null.
|
|
||||||
*/
|
|
||||||
public Observable<View> view() {
|
|
||||||
return views;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a subscription to automatically unsubscribe it during onDestroy.
|
|
||||||
* See {@link SubscriptionList#add(Subscription) for details.}
|
|
||||||
*
|
|
||||||
* @param subscription a subscription to add.
|
|
||||||
*/
|
|
||||||
public void add(Subscription subscription) {
|
|
||||||
subscriptions.add(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes and unsubscribes a subscription that has been registered with {@link #add} previously.
|
|
||||||
* See {@link SubscriptionList#remove(Subscription)} for details.
|
|
||||||
*
|
|
||||||
* @param subscription a subscription to remove.
|
|
||||||
*/
|
|
||||||
public void remove(Subscription subscription) {
|
|
||||||
subscriptions.remove(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A restartable is any RxJava observable that can be started (subscribed) and
|
|
||||||
* should be automatically restarted (re-subscribed) after a process restart if
|
|
||||||
* it was still subscribed at the moment of saving presenter's state.
|
|
||||||
*
|
|
||||||
* Registers a factory. Re-subscribes the restartable after the process restart.
|
|
||||||
*
|
|
||||||
* @param restartableId id of the restartable
|
|
||||||
* @param factory factory of the restartable
|
|
||||||
*/
|
|
||||||
public void restartable(int restartableId, Func0<Subscription> factory) {
|
|
||||||
restartables.put(restartableId, factory);
|
|
||||||
if (requested.contains(restartableId))
|
|
||||||
start(restartableId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the given restartable.
|
|
||||||
*
|
|
||||||
* @param restartableId id of the restartable
|
|
||||||
*/
|
|
||||||
public void start(int restartableId) {
|
|
||||||
stop(restartableId);
|
|
||||||
requested.add(restartableId);
|
|
||||||
restartableSubscriptions.put(restartableId, restartables.get(restartableId).call());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribes a restartable
|
|
||||||
*
|
|
||||||
* @param restartableId id of a restartable.
|
|
||||||
*/
|
|
||||||
public void stop(int restartableId) {
|
|
||||||
requested.remove((Integer) restartableId);
|
|
||||||
Subscription subscription = restartableSubscriptions.get(restartableId);
|
|
||||||
if (subscription != null)
|
|
||||||
subscription.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a restartable is unsubscribed.
|
|
||||||
*
|
|
||||||
* @param restartableId id of the restartable.
|
|
||||||
* @return true if the subscription is null or unsubscribed, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean isUnsubscribed(int restartableId) {
|
|
||||||
return isUnsubscribed(restartableSubscriptions.get(restartableId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a subscription is unsubscribed.
|
|
||||||
*
|
|
||||||
* @param subscription the subscription to check.
|
|
||||||
* @return true if the subscription is null or unsubscribed, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean isUnsubscribed(@Nullable Subscription subscription) {
|
|
||||||
return subscription == null || subscription.isUnsubscribed();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #restartable(int, Func0)},
|
|
||||||
* {@link #deliverFirst()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param restartableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the restartable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void restartableFirst(int restartableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartable(restartableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverFirst())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #restartableFirst(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void restartableFirst(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
restartableFirst(restartableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #restartable(int, Func0)},
|
|
||||||
* {@link #deliverLatestCache()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param restartableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the restartable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void restartableLatestCache(int restartableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartable(restartableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverLatestCache())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #restartableLatestCache(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void restartableLatestCache(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
restartableLatestCache(restartableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #restartable(int, Func0)},
|
|
||||||
* {@link #deliverReplay()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param restartableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the restartable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void restartableReplay(int restartableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartable(restartableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverReplay())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #restartableReplay(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void restartableReplay(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
restartableReplay(restartableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A startable behaves the same as a restartable but it does not resubscribe on process restart
|
|
||||||
*
|
|
||||||
* @param startableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
*/
|
|
||||||
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory) {
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {return observableFactory.call().subscribe();}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A startable behaves the same as a restartable but it does not resubscribe on process restart
|
|
||||||
*
|
|
||||||
* @param startableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
*/
|
|
||||||
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action1<T> onNext, final Action1<Throwable> onError) {
|
|
||||||
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {return observableFactory.call().subscribe(onNext, onError);}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A startable behaves the same as a restartable but it does not resubscribe on process restart
|
|
||||||
*
|
|
||||||
* @param startableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
*/
|
|
||||||
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory, final Action1<T> onNext) {
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {return observableFactory.call().subscribe(onNext);}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #startable(int, Func0)},
|
|
||||||
* {@link #deliverFirst()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param startableId an id of the startable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void startableFirst(int startableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverFirst())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #startableFirst(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void startableFirst(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
startableFirst(startableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #startable(int, Func0)},
|
|
||||||
* {@link #deliverLatestCache()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param startableId an id of the startable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void startableLatestCache(int startableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverLatestCache())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #startableLatestCache(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void startableLatestCache(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
startableLatestCache(startableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #startable(int, Func0)},
|
|
||||||
* {@link #deliverReplay()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param startableId an id of the startable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void startableReplay(int startableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverReplay())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #startableReplay(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void startableReplay(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
startableReplay(startableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
|
|
||||||
* the source {@link rx.Observable}.
|
|
||||||
*
|
|
||||||
* {@link #deliverLatestCache} keeps the latest onNext value and emits it each time a new view gets attached.
|
|
||||||
* If a new onNext value appears while a view is attached, it will be delivered immediately.
|
|
||||||
*
|
|
||||||
* @param <T> the type of source observable emissions
|
|
||||||
*/
|
|
||||||
public <T> DeliverLatestCache<View, T> deliverLatestCache() {
|
|
||||||
return new DeliverLatestCache<>(views);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
|
|
||||||
* the source {@link rx.Observable}.
|
|
||||||
*
|
|
||||||
* {@link #deliverFirst} delivers only the first onNext value that has been emitted by the source observable.
|
|
||||||
*
|
|
||||||
* @param <T> the type of source observable emissions
|
|
||||||
*/
|
|
||||||
public <T> DeliverFirst<View, T> deliverFirst() {
|
|
||||||
return new DeliverFirst<>(views);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
|
|
||||||
* the source {@link rx.Observable}.
|
|
||||||
*
|
|
||||||
* {@link #deliverReplay} keeps all onNext values and emits them each time a new view gets attached.
|
|
||||||
* If a new onNext value appears while a view is attached, it will be delivered immediately.
|
|
||||||
*
|
|
||||||
* @param <T> the type of source observable emissions
|
|
||||||
*/
|
|
||||||
public <T> DeliverReplay<View, T> deliverReplay() {
|
|
||||||
return new DeliverReplay<>(views);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a method that can be used for manual restartable chain build. It returns an Action1 that splits
|
|
||||||
* a received {@link Delivery} into two {@link Action2} onNext and onError calls.
|
|
||||||
*
|
|
||||||
* @param onNext a method that will be called if the delivery contains an emitted onNext value.
|
|
||||||
* @param onError a method that will be called if the delivery contains an onError throwable.
|
|
||||||
* @param <T> a type on onNext value.
|
|
||||||
* @return an Action1 that splits a received {@link Delivery} into two {@link Action2} onNext and onError calls.
|
|
||||||
*/
|
|
||||||
public <T> Action1<Delivery<View, T>> split(final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
return new Action1<Delivery<View, T>>() {
|
|
||||||
@Override
|
|
||||||
public void call(Delivery<View, T> delivery) {
|
|
||||||
delivery.split(onNext, onError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #split(Action2, Action2)} when the second parameter is null.
|
|
||||||
*/
|
|
||||||
public <T> Action1<Delivery<View, T>> split(Action2<View, T> onNext) {
|
|
||||||
return split(onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedState) {
|
|
||||||
if (savedState != null)
|
|
||||||
requested.addAll(savedState.getIntegerArrayList(REQUESTED_KEY));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
views.onCompleted();
|
|
||||||
subscriptions.unsubscribe();
|
|
||||||
for (Map.Entry<Integer, Subscription> entry : restartableSubscriptions.entrySet())
|
|
||||||
entry.getValue().unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
@Override
|
|
||||||
protected void onSave(Bundle state) {
|
|
||||||
for (int i = requested.size() - 1; i >= 0; i--) {
|
|
||||||
int restartableId = requested.get(i);
|
|
||||||
Subscription subscription = restartableSubscriptions.get(restartableId);
|
|
||||||
if (subscription != null && subscription.isUnsubscribed())
|
|
||||||
requested.remove(i);
|
|
||||||
}
|
|
||||||
state.putIntegerArrayList(REQUESTED_KEY, requested);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
@Override
|
|
||||||
protected void onTakeView(View view) {
|
|
||||||
views.onNext(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
@Override
|
|
||||||
protected void onDropView() {
|
|
||||||
views.onNext(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Please, use restartableXX and deliverXX methods for pushing data from RxPresenter into View.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View getView() {
|
|
||||||
return super.getView();
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user