# 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:
NerdNumber9 2017-03-04 22:43:06 -05:00
commit 03cb7062f2
290 changed files with 10387 additions and 6883 deletions

View File

@ -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

View File

@ -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'

View File

@ -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"

View File

@ -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

View File

@ -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}"
}
} }

View File

@ -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.

View File

@ -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))

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()
} }
} }

View File

@ -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
} }
} }

View File

@ -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

View File

@ -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))
} }

View File

@ -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)
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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()
} }

View File

@ -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)
}
}
} }

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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"

View File

@ -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)

View File

@ -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?")
}
}
}
}
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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&section=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")
)
}

View File

@ -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")
)
}

View File

@ -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")
)
}

View File

@ -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")
)
}

View File

@ -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
}
}
}

View File

@ -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")
)
}

View File

@ -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("ёнкома", "ёнкома")
)
}

View File

@ -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", "яой")
)
}

View File

@ -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", "юри")
)
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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")

View File

@ -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.*

View File

@ -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

View File

@ -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())
}
}

View File

@ -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())
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}
} }

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -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)
} }
} }

View File

@ -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()
} }

View File

@ -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)
} }

View File

@ -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
} }
} }
} }
} }

View File

@ -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()
} }

View File

@ -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
}

View 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
}
}
}
}
}

View File

@ -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>
} }

View 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
}
}

View 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
}
}

View File

@ -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())
}

View File

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.source.model
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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
}

View 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()
}
}
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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) }
}

View File

@ -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
} }

View File

@ -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
}

View File

@ -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")
}
}
}

View File

@ -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"
} }

View File

@ -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

View File

@ -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

View File

@ -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&section=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)
)
}

View File

@ -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")
)
}

View File

@ -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")
)
}

View File

@ -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")
)
}

View File

@ -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")
)
}

View File

@ -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)
)
}

View File

@ -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")
} }

View File

@ -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("ёнкома")
)
}

View File

@ -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")
)
}

View File

@ -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")
)
}

View File

@ -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)
} }
} }

View File

@ -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()
}
}
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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<*, *>,

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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