Initial support for external sources
This commit is contained in:
parent
77d986f213
commit
dd56d7c0bb
@ -5,6 +5,7 @@ import android.text.format.Formatter
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.Gson
|
||||
import com.jakewharton.disklrucache.DiskLruCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.saveTo
|
||||
@ -92,13 +93,13 @@ class ChapterCache(private val context: Context) {
|
||||
/**
|
||||
* Get page list from cache.
|
||||
*
|
||||
* @param chapterUrl the url of the chapter.
|
||||
* @param chapter the chapter.
|
||||
* @return an observable of the list of pages.
|
||||
*/
|
||||
fun getPageListFromCache(chapterUrl: String): Observable<List<Page>> {
|
||||
return Observable.fromCallable<List<Page>> {
|
||||
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
|
||||
return Observable.fromCallable {
|
||||
// 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
|
||||
diskCache.get(key).use {
|
||||
@ -110,10 +111,10 @@ class ChapterCache(private val context: Context) {
|
||||
/**
|
||||
* Add page list to disk cache.
|
||||
*
|
||||
* @param chapterUrl the url of the chapter.
|
||||
* @param chapter the chapter.
|
||||
* @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.
|
||||
val cachedValue = gson.toJson(pages)
|
||||
|
||||
@ -122,7 +123,7 @@ class ChapterCache(private val context: Context) {
|
||||
|
||||
try {
|
||||
// Get editor from md5 key.
|
||||
val key = DiskUtil.hashKeyForDisk(chapterUrl)
|
||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||
editor = diskCache.edit(key) ?: return
|
||||
|
||||
// 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}"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() {
|
||||
|
||||
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
|
||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
||||
source = cursor.getInt(cursor.getColumnIndex(COL_SOURCE))
|
||||
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
||||
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
||||
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
||||
|
@ -1,17 +1,14 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.model.SChapter
|
||||
import java.io.Serializable
|
||||
|
||||
interface Chapter : Serializable {
|
||||
interface Chapter : SChapter, Serializable {
|
||||
|
||||
var id: Long?
|
||||
|
||||
var manga_id: Long?
|
||||
|
||||
var url: String
|
||||
|
||||
var name: String
|
||||
|
||||
var read: Boolean
|
||||
|
||||
var bookmark: Boolean
|
||||
@ -20,10 +17,6 @@ interface Chapter : Serializable {
|
||||
|
||||
var date_fetch: Long
|
||||
|
||||
var date_upload: Long
|
||||
|
||||
var chapter_number: Float
|
||||
|
||||
var source_order: Int
|
||||
|
||||
val isRecognizedNumber: Boolean
|
||||
|
@ -1,35 +1,17 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import java.io.Serializable
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
|
||||
interface Manga : Serializable {
|
||||
interface Manga : SManga {
|
||||
|
||||
var id: Long?
|
||||
|
||||
var source: Int
|
||||
|
||||
var url: String
|
||||
|
||||
var title: String
|
||||
|
||||
var artist: String?
|
||||
|
||||
var author: String?
|
||||
|
||||
var description: String?
|
||||
|
||||
var genre: String?
|
||||
|
||||
var status: Int
|
||||
|
||||
var thumbnail_url: String?
|
||||
var source: Long
|
||||
|
||||
var favorite: Boolean
|
||||
|
||||
var last_update: Long
|
||||
|
||||
var initialized: Boolean
|
||||
|
||||
var viewer: Int
|
||||
|
||||
var chapter_flags: Int
|
||||
@ -38,27 +20,6 @@ interface Manga : Serializable {
|
||||
|
||||
var category: Int
|
||||
|
||||
fun copyFrom(other: Manga) {
|
||||
if (other.author != null)
|
||||
author = other.author
|
||||
|
||||
if (other.artist != null)
|
||||
artist = other.artist
|
||||
|
||||
if (other.description != null)
|
||||
description = other.description
|
||||
|
||||
if (other.genre != null)
|
||||
genre = other.genre
|
||||
|
||||
if (other.thumbnail_url != null)
|
||||
thumbnail_url = other.thumbnail_url
|
||||
|
||||
status = other.status
|
||||
|
||||
initialized = true
|
||||
}
|
||||
|
||||
fun setChapterOrder(order: Int) {
|
||||
setFlags(order, SORT_MASK)
|
||||
}
|
||||
@ -94,11 +55,6 @@ interface Manga : Serializable {
|
||||
|
||||
companion object {
|
||||
|
||||
const val UNKNOWN = 0
|
||||
const val ONGOING = 1
|
||||
const val COMPLETED = 2
|
||||
const val LICENSED = 3
|
||||
|
||||
const val SORT_DESC = 0x00000000
|
||||
const val SORT_ASC = 0x00000001
|
||||
const val SORT_MASK = 0x00000001
|
||||
@ -126,12 +82,13 @@ interface Manga : Serializable {
|
||||
const val DISPLAY_NUMBER = 0x00100000
|
||||
const val DISPLAY_MASK = 0x00100000
|
||||
|
||||
fun create(source: Int): Manga = MangaImpl().apply {
|
||||
fun create(source: Long): Manga = MangaImpl().apply {
|
||||
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
|
||||
this.title = title
|
||||
this.source = source
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ class MangaImpl : Manga {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var source: Int = 0
|
||||
override var source: Long = 0
|
||||
|
||||
override lateinit var url: String
|
||||
|
||||
|
@ -40,7 +40,7 @@ interface MangaQueries : DbProvider {
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getManga(url: String, sourceId: Int) = db.get()
|
||||
fun getManga(url: String, sourceId: Long) = db.get()
|
||||
.`object`(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.data.source.online.fetchAllImageUrlsFromPageList
|
||||
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
|
||||
import eu.kanade.tachiyomi.util.RetryWithDelay
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
@ -251,8 +252,11 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
|
||||
val pageListObservable = if (download.pages == null) {
|
||||
// Pull page list from network and add them to download object
|
||||
download.source.fetchPageListFromNetwork(download.chapter)
|
||||
download.source.fetchPageList(download.chapter)
|
||||
.doOnNext { pages ->
|
||||
if (pages.isEmpty()) {
|
||||
throw Exception("Page list is empty")
|
||||
}
|
||||
download.pages = pages
|
||||
}
|
||||
} else {
|
||||
@ -345,7 +349,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
|
||||
private fun downloadImage(page: Page, source: OnlineSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
|
||||
page.status = Page.DOWNLOAD_IMAGE
|
||||
page.progress = 0
|
||||
return source.imageResponse(page)
|
||||
return source.fetchImage(page)
|
||||
.map { response ->
|
||||
val file = tmpDir.createFile("$filename.tmp")
|
||||
try {
|
||||
|
@ -52,7 +52,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
|
||||
/**
|
||||
* 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.
|
||||
|
@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
@ -214,7 +215,7 @@ class LibraryUpdateService : Service() {
|
||||
}
|
||||
|
||||
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
|
||||
listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED }
|
||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
||||
}
|
||||
|
||||
return listToUpdate
|
||||
@ -328,9 +329,10 @@ class LibraryUpdateService : Service() {
|
||||
?: return@concatMap Observable.empty<Manga>()
|
||||
|
||||
source.fetchMangaDetails(manga)
|
||||
.doOnNext { networkManga ->
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
manga
|
||||
}
|
||||
.onErrorReturn { manga }
|
||||
}
|
||||
|
@ -91,9 +91,9 @@ class PreferenceKeys(context: Context) {
|
||||
|
||||
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"
|
||||
|
||||
|
@ -74,7 +74,7 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
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)
|
||||
|
||||
|
@ -0,0 +1,46 @@
|
||||
package eu.kanade.tachiyomi.data.source
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.data.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
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.source
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
@ -13,7 +13,7 @@ interface Source {
|
||||
/**
|
||||
* Id for the source. Must be unique.
|
||||
*/
|
||||
val id: Int
|
||||
val id: Long
|
||||
|
||||
/**
|
||||
* Name of the source.
|
||||
@ -25,26 +25,20 @@ interface Source {
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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>
|
||||
}
|
@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.data.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.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
|
||||
@ -18,29 +21,47 @@ import java.io.File
|
||||
|
||||
open class SourceManager(private val context: Context) {
|
||||
|
||||
private val sourcesMap = createSources()
|
||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||
|
||||
open fun get(sourceKey: Int): Source? {
|
||||
init {
|
||||
createSources()
|
||||
}
|
||||
|
||||
open fun get(sourceKey: Long): Source? {
|
||||
return sourcesMap[sourceKey]
|
||||
}
|
||||
|
||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
|
||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance<OnlineSource>()
|
||||
|
||||
private fun createOnlineSourceList(): List<Source> = listOf(
|
||||
Batoto(1),
|
||||
Mangahere(2),
|
||||
Mangafox(3),
|
||||
Kissmanga(4),
|
||||
Readmanga(5),
|
||||
Mintmanga(6),
|
||||
Mangachan(7),
|
||||
Readmangatoday(8),
|
||||
Mangasee(9),
|
||||
WieManga(10)
|
||||
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(
|
||||
Batoto(),
|
||||
Mangahere(),
|
||||
Mangafox(),
|
||||
Kissmanga(),
|
||||
Readmanga(),
|
||||
Mintmanga(),
|
||||
Mangachan(),
|
||||
Readmangatoday(),
|
||||
Mangasee(),
|
||||
WieManga()
|
||||
)
|
||||
|
||||
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
|
||||
createOnlineSourceList().forEach { put(it.id, it) }
|
||||
private fun createYamlSources(): List<Source> {
|
||||
val sources = mutableListOf<Source>()
|
||||
|
||||
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||
File.separator + context.getString(R.string.app_name), "parsers")
|
||||
@ -50,12 +71,89 @@ open class SourceManager(private val context: Context) {
|
||||
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) }
|
||||
sources.add(YamlOnlineSource(map))
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Error loading source from file. Bad format?")
|
||||
}
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
private fun createExtensionSources(): List<OnlineSource> {
|
||||
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 == FEATURE } }
|
||||
|
||||
val sources = mutableListOf<OnlineSource>()
|
||||
for (pkgInfo in extPkgs) {
|
||||
val appInfo = pkgManager.getApplicationInfo(pkgInfo.packageName,
|
||||
PackageManager.GET_META_DATA) ?: continue
|
||||
|
||||
|
||||
val data = appInfo.metaData
|
||||
val extName = data.getString(NAME)
|
||||
val version = data.getInt(VERSION)
|
||||
val sourceClass = extendClassName(data.getString(SOURCE), pkgInfo.packageName)
|
||||
|
||||
val ext = Extension(extName, appInfo, version, sourceClass)
|
||||
if (!validateExtension(ext)) {
|
||||
continue
|
||||
}
|
||||
|
||||
val instance = loadExtension(ext, pkgManager)
|
||||
if (instance == null) {
|
||||
Timber.e("Extension error: failed to instance $extName")
|
||||
continue
|
||||
}
|
||||
sources.add(instance)
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
private fun validateExtension(ext: Extension): Boolean {
|
||||
if (ext.version < LIB_VERSION_MIN || ext.version > LIB_VERSION_MAX) {
|
||||
Timber.e("Extension error: ${ext.name} has version ${ext.version}, while only versions "
|
||||
+ "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun loadExtension(ext: Extension, pkgManager: PackageManager): OnlineSource? {
|
||||
return try {
|
||||
val classLoader = PathClassLoader(ext.appInfo.sourceDir, null, context.classLoader)
|
||||
val resources = pkgManager.getResourcesForApplication(ext.appInfo)
|
||||
|
||||
Class.forName(ext.sourceClass, false, classLoader).newInstance() as? OnlineSource
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} catch (e: LinkageError) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun extendClassName(className: String, packageName: String): String {
|
||||
return if (className.startsWith(".")) {
|
||||
packageName + className
|
||||
} else {
|
||||
className
|
||||
}
|
||||
}
|
||||
|
||||
class Extension(val name: String,
|
||||
val appInfo: ApplicationInfo,
|
||||
val version: Int,
|
||||
val sourceClass: String)
|
||||
|
||||
private companion object {
|
||||
const val FEATURE = "tachiyomi.extension"
|
||||
const val NAME = "tachiyomi.extension.name"
|
||||
const val VERSION = "tachiyomi.extension.version"
|
||||
const val SOURCE = "tachiyomi.extension.source"
|
||||
const val LIB_VERSION_MIN = 1
|
||||
const val LIB_VERSION_MAX = 1
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
|
||||
sealed class Filter<T>(val name: String, var state: T) {
|
||||
open class Header(name: String) : Filter<Any>(name, 0)
|
||||
abstract class List<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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
|
||||
class FilterList(list: List<Filter<*>>) : List<Filter<*>> by list {
|
||||
|
||||
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
|
||||
|
||||
fun hasSameState(other: FilterList): Boolean {
|
||||
if (size != other.size) return false
|
||||
|
||||
return (0..lastIndex)
|
||||
.all { get(it).javaClass == other[it].javaClass && get(it).state == other[it].state }
|
||||
}
|
||||
|
||||
}
|
@ -1,13 +1,3 @@
|
||||
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
|
||||
|
||||
}
|
||||
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
@ -0,0 +1,28 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface SChapter : Serializable {
|
||||
|
||||
var url: String
|
||||
|
||||
var name: String
|
||||
|
||||
var date_upload: Long
|
||||
|
||||
var chapter_number: Float
|
||||
|
||||
fun copyFrom(other: SChapter) {
|
||||
name = other.name
|
||||
url = other.url
|
||||
date_upload = other.date_upload
|
||||
chapter_number = other.chapter_number
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(): SChapter {
|
||||
return SChapterImpl()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package eu.kanade.tachiyomi.data.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
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface SManga : Serializable {
|
||||
|
||||
var url: String
|
||||
|
||||
var title: String
|
||||
|
||||
var artist: String?
|
||||
|
||||
var author: String?
|
||||
|
||||
var description: String?
|
||||
|
||||
var genre: String?
|
||||
|
||||
var status: Int
|
||||
|
||||
var thumbnail_url: String?
|
||||
|
||||
var initialized: Boolean
|
||||
|
||||
fun copyFrom(other: SManga) {
|
||||
if (other.author != null)
|
||||
author = other.author
|
||||
|
||||
if (other.artist != null)
|
||||
artist = other.artist
|
||||
|
||||
if (other.description != null)
|
||||
description = other.description
|
||||
|
||||
if (other.genre != null)
|
||||
genre = other.genre
|
||||
|
||||
if (other.thumbnail_url != null)
|
||||
thumbnail_url = other.thumbnail_url
|
||||
|
||||
status = other.status
|
||||
|
||||
if (!initialized)
|
||||
initialized = other.initialized
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val UNKNOWN = 0
|
||||
const val ONGOING = 1
|
||||
const val COMPLETED = 2
|
||||
const val LICENSED = 3
|
||||
|
||||
fun create(): SManga {
|
||||
return SMangaImpl()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package eu.kanade.tachiyomi.data.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
|
||||
|
||||
}
|
@ -1,40 +1,32 @@
|
||||
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 eu.kanade.tachiyomi.data.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.data.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 OnlineSource() : Source {
|
||||
abstract class OnlineSource : CatalogueSource {
|
||||
|
||||
/**
|
||||
* Network service.
|
||||
*/
|
||||
val network: NetworkHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Chapter cache.
|
||||
*/
|
||||
val chapterCache: ChapterCache by injectLazy()
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
@ -46,24 +38,26 @@ abstract class OnlineSource() : Source {
|
||||
abstract val baseUrl: String
|
||||
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two characters in lower case).
|
||||
* 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.
|
||||
*/
|
||||
abstract val lang: String
|
||||
open val versionId = 1
|
||||
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
* 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.
|
||||
*/
|
||||
abstract val supportsLatest: Boolean
|
||||
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 by lazy { headersBuilder().build() }
|
||||
|
||||
/**
|
||||
* Genre filters.
|
||||
*/
|
||||
val filters by lazy { getFilterList() }
|
||||
val headers: Headers by lazy { headersBuilder().build() }
|
||||
|
||||
/**
|
||||
* Default network client for doing requests.
|
||||
@ -87,121 +81,88 @@ abstract class OnlineSource() : Source {
|
||||
* 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 page the page number to retrieve.
|
||||
*/
|
||||
open fun fetchPopularManga(page: MangasPage): Observable<MangasPage> = client
|
||||
.newCall(popularMangaRequest(page))
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
popularMangaParse(response, page)
|
||||
page
|
||||
popularMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page object.
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
open protected fun popularMangaRequest(page: MangasPage): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = popularMangaInitialUrl()
|
||||
}
|
||||
return GET(page.url, headers)
|
||||
}
|
||||
abstract protected fun popularMangaRequest(page: Int): Request
|
||||
|
||||
/**
|
||||
* 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].
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
* @param page the page object to be filled.
|
||||
*/
|
||||
abstract protected fun popularMangaParse(response: Response, page: MangasPage)
|
||||
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 object where the information will be saved, like the list of manga,
|
||||
* the current page and the next page url.
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter<*>>): Observable<MangasPage> = client
|
||||
.newCall(searchMangaRequest(page, query, filters))
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response, page, query, filters)
|
||||
page
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page object.
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
|
||||
|
||||
/**
|
||||
* 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].
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @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<*>>)
|
||||
abstract protected fun searchMangaParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga.
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
open fun fetchLatestUpdates(page: MangasPage): Observable<MangasPage> = client
|
||||
.newCall(latestUpdatesRequest(page))
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
latestUpdatesParse(response, page)
|
||||
page
|
||||
latestUpdatesParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
open protected fun latestUpdatesRequest(page: MangasPage): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = latestUpdatesInitialUrl()
|
||||
}
|
||||
return GET(page.url, headers)
|
||||
}
|
||||
abstract protected fun latestUpdatesRequest(page: Int): Request
|
||||
|
||||
/**
|
||||
* Returns the absolute url of the first page to latest manga.
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun latestUpdatesInitialUrl(): String
|
||||
|
||||
/**
|
||||
* Same as [popularMangaParse], but for latest manga.
|
||||
*/
|
||||
abstract protected fun latestUpdatesParse(response: Response, page: MangasPage)
|
||||
abstract protected fun latestUpdatesParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
@ -209,33 +170,30 @@ abstract class OnlineSource() : Source {
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: Manga): Observable<Manga> = client
|
||||
.newCall(mangaDetailsRequest(manga))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
Manga.create(manga.url, id).apply {
|
||||
mangaDetailsParse(response, this)
|
||||
initialized = true
|
||||
mangaDetailsParse(response).apply { 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.
|
||||
* 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: Manga): Request {
|
||||
open fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the response from the site. It should fill [manga].
|
||||
* Parses the response from the site and returns the details of a 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)
|
||||
abstract protected fun mangaDetailsParse(response: Response): SManga
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||
@ -243,15 +201,11 @@ abstract class OnlineSource() : Source {
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> = client
|
||||
.newCall(chapterListRequest(manga))
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mutableListOf<Chapter>().apply {
|
||||
chapterListParse(response, this)
|
||||
if (isEmpty()) {
|
||||
throw Exception("No chapters found")
|
||||
}
|
||||
chapterListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,43 +215,27 @@ abstract class OnlineSource() : Source {
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
open protected fun chapterListRequest(manga: Manga): Request {
|
||||
open protected fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the response from the site. It should fill [chapters].
|
||||
* Parses the response from the site and returns a list of 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>)
|
||||
abstract protected fun chapterListParse(response: Response): List<SChapter>
|
||||
|
||||
/**
|
||||
* 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].
|
||||
* Returns an observable with the page list for a chapter.
|
||||
*
|
||||
* @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))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mutableListOf<Page>().apply {
|
||||
pageListParse(response, this)
|
||||
if (isEmpty()) {
|
||||
throw Exception("Page list is empty")
|
||||
}
|
||||
pageListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,24 +243,18 @@ abstract class OnlineSource() : Source {
|
||||
* 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
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
open protected fun pageListRequest(chapter: Chapter): Request {
|
||||
open protected fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(baseUrl + chapter.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the response from the site. It should fill [pages].
|
||||
* Parses the response from the site and returns a list of 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}"
|
||||
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
|
||||
@ -330,16 +262,10 @@ abstract class OnlineSource() : Source {
|
||||
*
|
||||
* @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))
|
||||
open fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return client.newCall(imageUrlRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { imageUrlParse(it) }
|
||||
.doOnError { page.status = Page.ERROR }
|
||||
.onErrorReturn { null }
|
||||
.doOnNext { page.imageUrl = it }
|
||||
.map { page }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -353,31 +279,21 @@ abstract class OnlineSource() : Source {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the response from the site. It should return the absolute url to the source image.
|
||||
* 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 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)
|
||||
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
|
||||
@ -390,68 +306,44 @@ abstract class OnlineSource() : Source {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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].
|
||||
* 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 page the page.
|
||||
* @param url the full url to the chapter.
|
||||
*/
|
||||
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 }
|
||||
fun SChapter.setUrlWithoutDomain(url: String) {
|
||||
this.url = getUrlWithoutDomain(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of the page that downloads the image to [ChapterCache].
|
||||
* 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 page the page.
|
||||
* @param url the full url to the manga.
|
||||
*/
|
||||
private fun cacheImage(page: Page): Observable<Page> {
|
||||
page.status = Page.DOWNLOAD_IMAGE
|
||||
return imageResponse(page)
|
||||
.doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) }
|
||||
.map { page }
|
||||
fun SManga.setUrlWithoutDomain(url: String) {
|
||||
this.url = getUrlWithoutDomain(url)
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
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].
|
||||
@ -459,22 +351,11 @@ abstract class OnlineSource() : Source {
|
||||
* @param chapter the chapter to be added.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
open fun prepareNewChapter(chapter: Chapter, manga: Manga) {
|
||||
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
}
|
||||
|
||||
sealed class Filter<T>(val name: String, var state: T) {
|
||||
open class Header(name: String) : Filter<Any>(name, 0)
|
||||
abstract class List<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) {
|
||||
companion object {
|
||||
const val STATE_IGNORE = 0
|
||||
const val STATE_INCLUDE = 1
|
||||
const val STATE_EXCLUDE = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun getFilterList(): List<Filter<*>> = emptyList()
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
override fun getFilterList() = FilterList()
|
||||
}
|
||||
|
@ -0,0 +1,98 @@
|
||||
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.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 OnlineSource.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 OnlineSource.fetchImageFromCacheThenNet(page: Page): Observable<Page> {
|
||||
return if (page.imageUrl.isNullOrEmpty())
|
||||
getImageUrl(page).flatMap { getCachedImage(it) }
|
||||
else
|
||||
getCachedImage(page)
|
||||
}
|
||||
|
||||
fun OnlineSource.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 OnlineSource.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 OnlineSource.cacheImage(page: Page): Observable<Page> {
|
||||
page.status = Page.DOWNLOAD_IMAGE
|
||||
return fetchImage(page)
|
||||
.doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) }
|
||||
.map { page }
|
||||
}
|
||||
|
||||
fun OnlineSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { !it.imageUrl.isNullOrEmpty() }
|
||||
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
||||
}
|
||||
|
||||
fun OnlineSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { it.imageUrl.isNullOrEmpty() }
|
||||
.concatMap { getImageUrl(it) }
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
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.data.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
@ -12,26 +12,25 @@ import org.jsoup.nodes.Element
|
||||
/**
|
||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||
*/
|
||||
abstract class ParsedOnlineSource() : OnlineSource() {
|
||||
abstract class ParsedOnlineSource : OnlineSource() {
|
||||
|
||||
/**
|
||||
* Parse the response from the site and fills [page].
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
* @param page the page object to be filled.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
for (element in document.select(popularMangaSelector())) {
|
||||
Manga.create(id).apply {
|
||||
popularMangaFromElement(element, this)
|
||||
page.mangas.add(this)
|
||||
}
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
popularMangaNextPageSelector()?.let { selector ->
|
||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -40,13 +39,12 @@ abstract class ParsedOnlineSource() : OnlineSource() {
|
||||
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.
|
||||
* 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].
|
||||
* @param manga the manga to fill.
|
||||
*/
|
||||
abstract protected fun popularMangaFromElement(element: Element, manga: Manga)
|
||||
abstract protected fun popularMangaFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
@ -55,24 +53,22 @@ abstract class ParsedOnlineSource() : OnlineSource() {
|
||||
abstract protected fun popularMangaNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parse the response from the site and fills [page].
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @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<*>>) {
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
for (element in document.select(searchMangaSelector())) {
|
||||
Manga.create(id).apply {
|
||||
searchMangaFromElement(element, this)
|
||||
page.mangas.add(this)
|
||||
}
|
||||
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
|
||||
searchMangaNextPageSelector()?.let { selector ->
|
||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,13 +77,12 @@ abstract class ParsedOnlineSource() : OnlineSource() {
|
||||
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.
|
||||
* 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].
|
||||
* @param manga the manga to fill.
|
||||
*/
|
||||
abstract protected fun searchMangaFromElement(element: Element, manga: Manga)
|
||||
abstract protected fun searchMangaFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
@ -96,70 +91,67 @@ abstract class ParsedOnlineSource() : OnlineSource() {
|
||||
abstract protected fun searchMangaNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parse the response from the site for latest updates and fills [page].
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response, page: MangasPage) {
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
for (element in document.select(latestUpdatesSelector())) {
|
||||
Manga.create(id).apply {
|
||||
latestUpdatesFromElement(element, this)
|
||||
page.mangas.add(this)
|
||||
}
|
||||
|
||||
val mangas = document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
|
||||
latestUpdatesNextPageSelector()?.let { selector ->
|
||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector similar to [popularMangaSelector], but for latest updates.
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||
*/
|
||||
abstract protected fun latestUpdatesSelector(): String
|
||||
|
||||
/**
|
||||
* Fills [manga] with the given [element]. For latest updates.
|
||||
* 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, manga: Manga)
|
||||
abstract protected fun latestUpdatesFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag, like [popularMangaNextPageSelector].
|
||||
* 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?
|
||||
|
||||
/**
|
||||
* Parse the response from the site and fills the details of [manga].
|
||||
* Parses the response from the site and returns the details of a 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)
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return mangaDetailsParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the details of [manga] from the given [document].
|
||||
* Returns the details of the manga from the given [document].
|
||||
*
|
||||
* @param document the parsed document.
|
||||
* @param manga the manga to fill.
|
||||
*/
|
||||
abstract protected fun mangaDetailsParse(document: Document, manga: Manga)
|
||||
abstract protected fun mangaDetailsParse(document: Document): SManga
|
||||
|
||||
/**
|
||||
* Parse the response from the site and fills the chapter list.
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
* @param chapters the list of chapters to fill.
|
||||
*/
|
||||
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
for (element in document.select(chapterListSelector())) {
|
||||
Chapter.create().apply {
|
||||
chapterFromElement(element, this)
|
||||
chapters.add(this)
|
||||
}
|
||||
}
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,30 +160,27 @@ abstract class ParsedOnlineSource() : OnlineSource() {
|
||||
abstract protected fun chapterListSelector(): String
|
||||
|
||||
/**
|
||||
* Fills [chapter] with the given [element].
|
||||
* Returns a chapter from the given element.
|
||||
*
|
||||
* @param element an element obtained from [chapterListSelector].
|
||||
* @param chapter the chapter to fill.
|
||||
*/
|
||||
abstract protected fun chapterFromElement(element: Element, chapter: Chapter)
|
||||
abstract protected fun chapterFromElement(element: Element): SChapter
|
||||
|
||||
/**
|
||||
* Parse the response from the site and fills the page list.
|
||||
* Parses the response from the site and returns 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)
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return pageListParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills [pages] from the given [document].
|
||||
* Returns a page list 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>)
|
||||
abstract protected fun pageListParse(document: Document): List<Page>
|
||||
|
||||
/**
|
||||
* Parse the response from the site and returns the absolute url to the source image.
|
||||
|
@ -1,11 +1,8 @@
|
||||
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.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.model.*
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.attrOrText
|
||||
import okhttp3.Request
|
||||
@ -36,92 +33,108 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
||||
}
|
||||
|
||||
override val id = map.id.let {
|
||||
if (it is Int) it else (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff
|
||||
(it as? Int ?: (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff).toLong()
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: MangasPage): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = popularMangaInitialUrl()
|
||||
// Ugly, but needed after the changes
|
||||
var popularNextPage: String? = null
|
||||
var searchNextPage: String? = null
|
||||
var latestNextPage: String? = null
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = if (page == 1) {
|
||||
popularNextPage = null
|
||||
map.popular.url
|
||||
} else {
|
||||
popularNextPage!!
|
||||
}
|
||||
return when (map.popular.method?.toLowerCase()) {
|
||||
"post" -> POST(page.url, headers, map.popular.createForm())
|
||||
else -> GET(page.url, headers)
|
||||
"post" -> POST(url, headers, map.popular.createForm())
|
||||
else -> GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaInitialUrl() = map.popular.url
|
||||
|
||||
override fun popularMangaParse(response: Response, page: MangasPage) {
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
for (element in document.select(map.popular.manga_css)) {
|
||||
Manga.create(id).apply {
|
||||
|
||||
val mangas = document.select(map.popular.manga_css).map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.text()
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
page.mangas.add(this)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if (page == 1) {
|
||||
searchNextPage = null
|
||||
map.search.url.replace("\$query", query)
|
||||
} else {
|
||||
searchNextPage!!
|
||||
}
|
||||
return when (map.search.method?.toLowerCase()) {
|
||||
"post" -> POST(page.url, headers, map.search.createForm())
|
||||
else -> GET(page.url, headers)
|
||||
"post" -> POST(url, headers, map.search.createForm())
|
||||
else -> GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = map.search.url.replace("\$query", query)
|
||||
|
||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
for (element in document.select(map.search.manga_css)) {
|
||||
Manga.create(id).apply {
|
||||
|
||||
val mangas = document.select(map.search.manga_css).map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.text()
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
page.mangas.add(this)
|
||||
}
|
||||
}
|
||||
|
||||
map.search.next_url_css?.let { selector ->
|
||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
searchNextPage = map.search.next_url_css?.let { selector ->
|
||||
document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: MangasPage): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = latestUpdatesInitialUrl()
|
||||
return MangasPage(mangas, searchNextPage != null)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = if (page == 1) {
|
||||
latestNextPage = null
|
||||
map.latestupdates!!.url
|
||||
} else {
|
||||
latestNextPage!!
|
||||
}
|
||||
return when (map.latestupdates!!.method?.toLowerCase()) {
|
||||
"post" -> POST(page.url, headers, map.latestupdates.createForm())
|
||||
else -> GET(page.url, headers)
|
||||
"post" -> POST(url, headers, map.latestupdates.createForm())
|
||||
else -> GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesInitialUrl() = map.latestupdates!!.url
|
||||
|
||||
override fun latestUpdatesParse(response: Response, page: MangasPage) {
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
for (element in document.select(map.latestupdates!!.manga_css)) {
|
||||
Manga.create(id).apply {
|
||||
|
||||
val mangas = document.select(map.latestupdates!!.manga_css).map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.text()
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
page.mangas.add(this)
|
||||
}
|
||||
}
|
||||
|
||||
map.latestupdates.next_url_css?.let { selector ->
|
||||
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
popularNextPage = map.latestupdates.next_url_css?.let { selector ->
|
||||
document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response, manga: Manga) {
|
||||
return MangasPage(mangas, popularNextPage != null)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val manga = SManga.create()
|
||||
with(map.manga) {
|
||||
val pool = parts.get(document)
|
||||
|
||||
@ -130,18 +143,21 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
||||
manga.description = summary?.process(document, pool)
|
||||
manga.thumbnail_url = cover?.process(document, pool)
|
||||
manga.genre = genres?.process(document, pool)
|
||||
manga.status = status?.getStatus(document, pool) ?: Manga.UNKNOWN
|
||||
manga.status = status?.getStatus(document, pool) ?: SManga.UNKNOWN
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
with(map.chapters) {
|
||||
val pool = emptyMap<String, Element>()
|
||||
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
|
||||
|
||||
for (element in document.select(chapter_css)) {
|
||||
val chapter = Chapter.create()
|
||||
val chapter = SChapter.create()
|
||||
element.select(title).first().let {
|
||||
chapter.name = it.text()
|
||||
chapter.setUrlWithoutDomain(it.attr("href"))
|
||||
@ -151,12 +167,15 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
||||
chapters.add(chapter)
|
||||
}
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val body = response.body().string()
|
||||
val url = response.request().url().toString()
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
// TODO lazy initialization in Kotlin 1.1
|
||||
val document = Jsoup.parse(body, url)
|
||||
|
||||
@ -194,6 +213,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
||||
page.imageUrl = url
|
||||
}
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
package eu.kanade.tachiyomi.data.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.RequestBody
|
||||
import org.jsoup.nodes.Document
|
||||
@ -164,15 +164,15 @@ class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
||||
fun getStatus(document: Element, cache: Map<String, Element>): Int {
|
||||
val text = process(document, cache)
|
||||
complete?.let {
|
||||
if (text.contains(it)) return Manga.COMPLETED
|
||||
if (text.contains(it)) return SManga.COMPLETED
|
||||
}
|
||||
ongoing?.let {
|
||||
if (text.contains(it)) return Manga.ONGOING
|
||||
if (text.contains(it)) return SManga.ONGOING
|
||||
}
|
||||
licensed?.let {
|
||||
if (text.contains(it)) return Manga.LICENSED
|
||||
if (text.contains(it)) return SManga.LICENSED
|
||||
}
|
||||
return Manga.UNKNOWN
|
||||
return SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
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.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
@ -25,7 +22,9 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
class Batoto : ParsedOnlineSource(), LoginSource {
|
||||
|
||||
override val id: Long = 1
|
||||
|
||||
override val name = "Batoto"
|
||||
|
||||
@ -56,70 +55,46 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
.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)
|
||||
}
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/search_ajax?order_cond=views&order=desc&p=$page", headers)
|
||||
}
|
||||
|
||||
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 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, manga: Manga) {
|
||||
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, manga: Manga) {
|
||||
popularMangaFromElement(element, manga)
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "#show_more_row"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = searchMangaUrl(query, filters, 1)
|
||||
|
||||
private fun searchMangaUrl(query: String, filterStates: List<Filter<*>>, page: Int): String {
|
||||
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 = ""
|
||||
for (filter in if (filterStates.isEmpty()) filters else filterStates) {
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> if (filter.state != Filter.TriState.STATE_IGNORE) {
|
||||
url.addQueryParameter("completed", if (filter.state == Filter.TriState.STATE_EXCLUDE) "i" else "c")
|
||||
is Status -> if (!filter.isIgnored()) {
|
||||
url.addQueryParameter("completed", if (filter.isExcluded()) "i" else "c")
|
||||
}
|
||||
is Genre -> if (filter.state != Filter.TriState.STATE_IGNORE) {
|
||||
genres += (if (filter.state == Filter.TriState.STATE_EXCLUDE) ";e" else ";i") + filter.id
|
||||
is Genre -> if (!filter.isIgnored()) {
|
||||
genres += (if (filter.isExcluded()) ";e" else ";i") + filter.id
|
||||
}
|
||||
is TextField -> {
|
||||
if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
|
||||
@ -136,89 +111,67 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
}
|
||||
if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
|
||||
url.addQueryParameter("p", page.toString())
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
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 {
|
||||
searchMangaUrl(query, filters, page.page + 1)
|
||||
}
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||
popularMangaFromElement(element, manga)
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun mangaDetailsRequest(manga: Manga): Request {
|
||||
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, manga: Manga) {
|
||||
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" -> Manga.ONGOING
|
||||
"Complete" -> Manga.COMPLETED
|
||||
else -> Manga.UNKNOWN
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Complete" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
|
||||
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)
|
||||
|
||||
for (element in document.select(chapterListSelector())) {
|
||||
Chapter.create().apply {
|
||||
chapterFromElement(element, this)
|
||||
chapters.add(this)
|
||||
}
|
||||
}
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
||||
|
||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||
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 {
|
||||
@ -246,12 +199,13 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
return date.time
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: Chapter): Request {
|
||||
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, pages: MutableList<Page>) {
|
||||
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()) {
|
||||
@ -264,6 +218,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
pages.add(Page(i, "", element.attr("src")))
|
||||
}
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page): Request {
|
||||
@ -308,7 +263,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> {
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
if (!isLogged()) {
|
||||
val username = preferences.sourceUsername(this)
|
||||
val password = preferences.sourcePassword(this)
|
||||
@ -328,7 +283,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
private class Status() : Filter.TriState("Completed")
|
||||
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 ListField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.List<ListValue>(name, values, state)
|
||||
@ -338,7 +293,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
|
||||
// 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
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "artist_name"),
|
||||
ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
|
||||
Status(),
|
||||
|
@ -1,11 +1,8 @@
|
||||
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.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
@ -16,7 +13,9 @@ import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
class Kissmanga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 4
|
||||
|
||||
override val name = "Kissmanga"
|
||||
|
||||
@ -28,38 +27,40 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
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) {
|
||||
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, manga: Manga) {
|
||||
popularMangaFromElement(element, 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: MangasPage, query: String, filters: List<Filter<*>>): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = searchMangaInitialUrl(query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val form = FormBody.Builder().apply {
|
||||
add("mangaName", query)
|
||||
|
||||
for (filter in if (filters.isEmpty()) this@Kissmanga.filters else filters) {
|
||||
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])
|
||||
@ -67,50 +68,53 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
}
|
||||
}
|
||||
}
|
||||
return POST(page.url, headers, form.build())
|
||||
return POST("$baseUrl/AdvanceSearch", 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 searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||
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") -> Manga.ONGOING
|
||||
status.contains("Completed") -> Manga.COMPLETED
|
||||
else -> Manga.UNKNOWN
|
||||
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, chapter: Chapter) {
|
||||
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: Chapter) = POST(baseUrl + chapter.url, headers)
|
||||
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
||||
|
||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||
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())
|
||||
@ -119,10 +123,11 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
while (m.find()) {
|
||||
pages.add(Page(i++, "", m.group(1)))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
// Not used
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page) = GET(page.url)
|
||||
@ -131,57 +136,58 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
private class Status() : Filter.TriState("Completed")
|
||||
private class Author() : Filter.Text("Author")
|
||||
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||
private class Genre(name: String) : Filter.TriState(name)
|
||||
|
||||
// $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
|
||||
// on http://kissmanga.com/AdvanceSearch
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
override fun getFilterList() = FilterList(
|
||||
Author(),
|
||||
Status(),
|
||||
Filter.Header("Genres"),
|
||||
Genre("Action", 0),
|
||||
Genre("Adult", 1),
|
||||
Genre("Adventure", 2),
|
||||
Genre("Comedy", 3),
|
||||
Genre("Comic", 4),
|
||||
Genre("Cooking", 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("Manga", 16),
|
||||
Genre("Manhua", 17),
|
||||
Genre("Manhwa", 18),
|
||||
Genre("Martial Arts", 19),
|
||||
Genre("Mature", 20),
|
||||
Genre("Mecha", 21),
|
||||
Genre("Medical", 22),
|
||||
Genre("Music", 23),
|
||||
Genre("Mystery", 24),
|
||||
Genre("One shot", 25),
|
||||
Genre("Psychological", 26),
|
||||
Genre("Romance", 27),
|
||||
Genre("School Life", 28),
|
||||
Genre("Sci-fi", 29),
|
||||
Genre("Seinen", 30),
|
||||
Genre("Shotacon", 31),
|
||||
Genre("Shoujo", 32),
|
||||
Genre("Shoujo Ai", 33),
|
||||
Genre("Shounen", 34),
|
||||
Genre("Shounen Ai", 35),
|
||||
Genre("Slice of Life", 36),
|
||||
Genre("Smut", 37),
|
||||
Genre("Sports", 38),
|
||||
Genre("Supernatural", 39),
|
||||
Genre("Tragedy", 40),
|
||||
Genre("Webtoon", 41),
|
||||
Genre("Yaoi", 42),
|
||||
Genre("Yuri", 43)
|
||||
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")
|
||||
)
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
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.network.GET
|
||||
import eu.kanade.tachiyomi.data.source.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Response
|
||||
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(override val id: Int) : ParsedOnlineSource() {
|
||||
class Mangafox : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 3
|
||||
|
||||
override val name = "Mangafox"
|
||||
|
||||
@ -23,32 +23,40 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
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 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 popularMangaFromElement(element: Element, manga: Manga) {
|
||||
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, manga: Manga) {
|
||||
popularMangaFromElement(element, 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 searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
|
||||
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)
|
||||
for (filter in if (filters.isEmpty()) this@Mangafox.filters else filters) {
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
@ -56,47 +64,54 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
||||
is Order -> url.addQueryParameter("order", if (filter.state) "az" else "za")
|
||||
}
|
||||
}
|
||||
return url.toString()
|
||||
url.addQueryParameter("page", page.toString())
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||
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, manga: Manga) {
|
||||
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") -> Manga.ONGOING
|
||||
status.contains("Completed") -> Manga.COMPLETED
|
||||
else -> Manga.UNKNOWN
|
||||
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, chapter: Chapter) {
|
||||
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 {
|
||||
@ -124,17 +139,14 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||
val document = response.asJsoup()
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val url = document.baseUri().substringBeforeLast('/')
|
||||
|
||||
val url = response.request().url().toString().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"))
|
||||
}
|
||||
}
|
||||
|
||||
// Not used, overrides parent.
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String {
|
||||
@ -157,7 +169,7 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
// $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
|
||||
// on http://mangafox.me/search.php
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Artist", "artist"),
|
||||
ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga", "1"), ListValue("Korean Manhwa", "2"), ListValue("Chinese Manhua", "3"))),
|
||||
|
@ -1,17 +1,19 @@
|
||||
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.network.GET
|
||||
import eu.kanade.tachiyomi.data.source.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
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(override val id: Int) : ParsedOnlineSource() {
|
||||
class Mangahere : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 2
|
||||
|
||||
override val name = "Mangahere"
|
||||
|
||||
@ -21,36 +23,42 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
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) {
|
||||
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, manga: Manga) {
|
||||
mangaFromElement("div.title > a", element, manga)
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return mangaFromElement("div.title > a", element)
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||
popularMangaFromElement(element, manga)
|
||||
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 searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
|
||||
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)
|
||||
for (filter in if (filters.isEmpty()) this@Mangahere.filters else filters) {
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
||||
is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
|
||||
@ -59,39 +67,41 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
||||
is Order -> url.addQueryParameter("order", if (filter.state) "az" else "za")
|
||||
}
|
||||
}
|
||||
return url.toString()
|
||||
url.addQueryParameter("page", page.toString())
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
|
||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||
|
||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||
mangaFromElement("a.manga_info", element, manga)
|
||||
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, manga: Manga) {
|
||||
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") -> Manga.ONGOING
|
||||
status.contains("Completed") -> Manga.COMPLETED
|
||||
else -> Manga.UNKNOWN
|
||||
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, chapter: Chapter) {
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val parentEl = element.select("span.left").first()
|
||||
|
||||
val urlElement = parentEl.select("a").first()
|
||||
@ -106,9 +116,11 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
||||
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 {
|
||||
@ -136,11 +148,13 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||
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")
|
||||
@ -157,7 +171,7 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
// [...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
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Artist", "artist"),
|
||||
ListField("Type", "direction", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga (read from right to left)", "rl"), ListValue("Korean Manhwa (read from left to right)", "lr"))),
|
||||
|
@ -1,22 +1,19 @@
|
||||
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.model.*
|
||||
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() {
|
||||
class Mangasee : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 9
|
||||
|
||||
override val name = "Mangasee"
|
||||
|
||||
@ -30,46 +27,32 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
private val indexPattern = Pattern.compile("-index-(.*?)-")
|
||||
|
||||
override fun popularMangaInitialUrl() = "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&todo=1"
|
||||
|
||||
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)
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&todo=1")
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
// Not used, overrides parent.
|
||||
override fun popularMangaNextPageSelector() = ""
|
||||
override fun popularMangaNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
|
||||
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)
|
||||
var genres: String? = null
|
||||
var genresNo: String? = null
|
||||
for (filter in if (filters.isEmpty()) this@Mangasee.filters else filters) {
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is Sort -> filter.values[filter.state].keys.forEachIndexed { i, s ->
|
||||
url.addQueryParameter(s, filter.values[filter.state].values[i])
|
||||
@ -84,22 +67,14 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
}
|
||||
if (genres != null) url.addQueryParameter("genre", genres)
|
||||
if (genresNo != null) url.addQueryParameter("genreNo", genresNo)
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
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)
|
||||
val (body, requestUrl) = convertQueryToPost(page, url.toString())
|
||||
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())
|
||||
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))
|
||||
}
|
||||
@ -107,63 +82,57 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
// Not used, overrides parent.
|
||||
override fun searchMangaNextPageSelector() = ""
|
||||
override fun searchMangaNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||
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)") -> Manga.ONGOING
|
||||
status.contains("Complete (Scan)") -> Manga.COMPLETED
|
||||
else -> Manga.UNKNOWN
|
||||
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, chapter: Chapter) {
|
||||
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(response: Response, pages: MutableList<Page>) {
|
||||
val document = response.asJsoup()
|
||||
val fullUrl = response.request().url().toString()
|
||||
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 = ""
|
||||
@ -178,10 +147,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
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>) {
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
|
||||
@ -197,7 +163,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
// [...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(
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Years", "year"),
|
||||
TextField("Author", "author"),
|
||||
Sort("Sort By", arrayOf(SortOption("Alphabetical A-Z", emptyArray(), emptyArray()),
|
||||
@ -249,34 +215,18 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
Genre("Yuri")
|
||||
)
|
||||
|
||||
override fun latestUpdatesInitialUrl(): String = "http://mangaseeonline.net/home/latest.request.php"
|
||||
|
||||
// Not used, overrides parent.
|
||||
override fun latestUpdatesNextPageSelector(): String = ""
|
||||
override fun latestUpdatesNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun latestUpdatesSelector(): String = "a.latestSeries"
|
||||
|
||||
override fun latestUpdatesRequest(page: MangasPage): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = latestUpdatesInitialUrl()
|
||||
}
|
||||
val (body, requestUrl) = convertQueryToPost(page)
|
||||
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 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) {
|
||||
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-")
|
||||
@ -288,6 +238,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
|
||||
manga.setUrlWithoutDomain("/manga" + mangaUrl)
|
||||
manga.title = title
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
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.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
@ -13,7 +11,9 @@ import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.util.*
|
||||
|
||||
class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
||||
class Readmangatoday : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 8
|
||||
|
||||
override val name = "ReadMangaToday"
|
||||
|
||||
@ -33,41 +33,39 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}
|
||||
|
||||
override fun popularMangaInitialUrl() = "$baseUrl/hot-manga/"
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/hot-manga/$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesInitialUrl() = "$baseUrl/latest-releases/"
|
||||
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, manga: Manga) {
|
||||
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, manga: Manga) {
|
||||
popularMangaFromElement(element, manga)
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
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<Filter<*>>): Request {
|
||||
if (page.page == 1) {
|
||||
page.url = searchMangaInitialUrl(query, filters)
|
||||
}
|
||||
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)
|
||||
for (filter in if (filters.isEmpty()) this@Readmangatoday.filters else filters) {
|
||||
(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])
|
||||
@ -75,49 +73,54 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
||||
is Genre -> when (filter.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> builder.add("include[]", filter.id.toString())
|
||||
Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", filter.id.toString())
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return POST(page.url, headers, builder.build())
|
||||
return POST("$baseUrl/service/advanced_search", headers, builder.build())
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.style-list > div.box"
|
||||
|
||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||
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, manga: Manga) {
|
||||
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") -> Manga.ONGOING
|
||||
status.contains("Completed") -> Manga.COMPLETED
|
||||
else -> Manga.UNKNOWN
|
||||
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, chapter: Chapter) {
|
||||
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 {
|
||||
@ -125,7 +128,7 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
if (dateWords.size == 3) {
|
||||
val timeAgo = Integer.parseInt(dateWords[0])
|
||||
var date: Calendar = Calendar.getInstance()
|
||||
val date: Calendar = Calendar.getInstance()
|
||||
|
||||
if (dateWords[1].contains("Minute")) {
|
||||
date.add(Calendar.MINUTE, -timeAgo)
|
||||
@ -141,17 +144,19 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
||||
date.add(Calendar.YEAR, -timeAgo)
|
||||
}
|
||||
|
||||
return date.getTimeInMillis()
|
||||
return date.timeInMillis
|
||||
}
|
||||
|
||||
return 0L
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||
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")
|
||||
@ -163,7 +168,7 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
// [...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
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author-name"),
|
||||
TextField("Artist", "artist-name"),
|
||||
Type(),
|
||||
|
@ -1,16 +1,19 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.german
|
||||
|
||||
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.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Response
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class WieManga(override val id: Int) : ParsedOnlineSource() {
|
||||
class WieManga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 10
|
||||
|
||||
override val name = "Wie Manga!"
|
||||
|
||||
@ -20,50 +23,61 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaInitialUrl() = "$baseUrl/list/Hot-Book/"
|
||||
|
||||
override fun latestUpdatesInitialUrl() = "$baseUrl/list/New-Update/"
|
||||
|
||||
override fun popularMangaSelector() = ".booklist td > div"
|
||||
|
||||
override fun latestUpdatesSelector() = ".booklist td > div"
|
||||
|
||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list/Hot-Book/", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list/New-Update/", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val image = element.select("dt img")
|
||||
val title = element.select("dd a:first-child")
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.setUrlWithoutDomain(title.attr("href"))
|
||||
manga.title = title.text()
|
||||
manga.thumbnail_url = image.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
|
||||
popularMangaFromElement(element, manga)
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = null
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = null
|
||||
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = "$baseUrl/search/?wd=$query"
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/search/?wd=$query", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = ".searchresult td > div"
|
||||
|
||||
override fun searchMangaFromElement(element: Element, manga: Manga) {
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val image = element.select(".resultimg img")
|
||||
val title = element.select(".resultbookname")
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.setUrlWithoutDomain(title.attr("href"))
|
||||
manga.title = title.text()
|
||||
manga.thumbnail_url = image.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = ".pagetor a.l"
|
||||
|
||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||
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()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("dd:nth-of-type(2) a").first()?.text()
|
||||
manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text()
|
||||
manga.description = infoElement.select("dl > dt:last-child").first()?.text()?.replaceFirst("Beschreibung", "")
|
||||
@ -74,32 +88,33 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
if (manga.artist == "RSS")
|
||||
manga.artist = null
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
|
||||
|
||||
override fun chapterFromElement(element: Element, chapter: Chapter) {
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select(".col1 a").first()
|
||||
val dateElement = element.select(".col3 a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||
val document = response.asJsoup()
|
||||
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")))
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
|
||||
|
@ -1,18 +1,19 @@
|
||||
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.network.GET
|
||||
import eu.kanade.tachiyomi.data.source.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
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(override val id: Int) : ParsedOnlineSource() {
|
||||
class Mangachan : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 7
|
||||
|
||||
override val name = "Mangachan"
|
||||
|
||||
@ -22,23 +23,28 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites"
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
||||
}
|
||||
|
||||
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"
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if (query.isNotEmpty()) {
|
||||
"$baseUrl/?do=search&subaction=search&story=$query"
|
||||
} else {
|
||||
val filt = filters.filter { it.state != Filter.TriState.STATE_IGNORE }
|
||||
val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
|
||||
if (filt.isNotEmpty()) {
|
||||
var genres = ""
|
||||
filt.forEach { genres += (if (it.state == Filter.TriState.STATE_EXCLUDE) "-" else "") + (it as Genre).id + '+' }
|
||||
return "$baseUrl/tags/${genres.dropLast(1)}"
|
||||
filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
|
||||
"$baseUrl/tags/${genres.dropLast(1)}"
|
||||
} else {
|
||||
return "$baseUrl/?do=search&subaction=search&story=$query"
|
||||
"$baseUrl/?do=search&subaction=search&story=$query"
|
||||
}
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/newestch?page=$page")
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.content_row"
|
||||
@ -47,22 +53,26 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||
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, manga: 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, manga: Manga) {
|
||||
popularMangaFromElement(element, manga)
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
||||
@ -73,74 +83,80 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
for (element in document.select(searchMangaSelector())) {
|
||||
Manga.create(id).apply {
|
||||
searchMangaFromElement(element, this)
|
||||
page.mangas.add(this)
|
||||
}
|
||||
}
|
||||
val allIgnore = filters.all { it.state == Filter.TriState.STATE_IGNORE }
|
||||
searchMangaNextPageSelector().let { selector ->
|
||||
if (page.nextPageUrl.isNullOrEmpty() && allIgnore) {
|
||||
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
|
||||
}
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
|
||||
searchGenresNextPageSelector().let { selector ->
|
||||
if (page.nextPageUrl.isNullOrEmpty() && !allIgnore) {
|
||||
val url = document.select(selector).first()?.attr("href")
|
||||
page.nextPageUrl = searchMangaInitialUrl(query, filters) + url
|
||||
}
|
||||
}
|
||||
// FIXME
|
||||
// val allIgnore = filters.all { it.state == Filter.TriState.STATE_IGNORE }
|
||||
// searchMangaNextPageSelector().let { selector ->
|
||||
// if (page.nextPageUrl.isNullOrEmpty() && allIgnore) {
|
||||
// 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() && !allIgnore) {
|
||||
// val url = document.select(selector).first()?.attr("href")
|
||||
// page.nextPageUrl = searchMangaInitialUrl(query, filters) + url
|
||||
// }
|
||||
// }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||
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 Manga.COMPLETED
|
||||
element.contains("перевод продолжается") -> return Manga.ONGOING
|
||||
else -> return Manga.UNKNOWN
|
||||
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, chapter: Chapter) {
|
||||
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, pages: MutableList<Page>) {
|
||||
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(',')
|
||||
|
||||
pageUrls.mapIndexedTo(pages) { i, url -> Page(i, "", url) }
|
||||
return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
@ -152,7 +168,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
|
||||
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
|
||||
* on http://mangachan.me/
|
||||
*/
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("18 плюс"),
|
||||
Genre("bdsm"),
|
||||
Genre("арт"),
|
||||
|
@ -1,9 +1,9 @@
|
||||
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.network.GET
|
||||
import eu.kanade.tachiyomi.data.source.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
@ -11,7 +11,9 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Mintmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
class Mintmanga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 6
|
||||
|
||||
override val name = "Mintmanga"
|
||||
|
||||
@ -21,77 +23,89 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate"
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesInitialUrl() = "$baseUrl/list?sortType=updated"
|
||||
|
||||
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) =
|
||||
"$baseUrl/search?q=$query&${filters.map { (it as Genre).id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")}"
|
||||
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, manga: Manga) {
|
||||
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, manga: Manga) {
|
||||
popularMangaFromElement(element, 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, manga: Manga) {
|
||||
popularMangaFromElement(element, manga)
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||
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 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
|
||||
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, chapter: Chapter) {
|
||||
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: Chapter, manga: Manga) {
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
chapter.chapter_number = -2f
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||
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)
|
||||
@ -100,14 +114,18 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
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, pages: MutableList<Page>) {
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
@ -119,7 +137,7 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
* on http://mintmanga.com/search
|
||||
*/
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("арт", "el_2220"),
|
||||
Genre("бара", "el_1353"),
|
||||
Genre("боевик", "el_1346"),
|
||||
|
@ -1,9 +1,9 @@
|
||||
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.network.GET
|
||||
import eu.kanade.tachiyomi.data.source.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
@ -11,7 +11,9 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Readmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
class Readmanga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 5
|
||||
|
||||
override val name = "Readmanga"
|
||||
|
||||
@ -21,77 +23,89 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
|
||||
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 as Genre).id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")}"
|
||||
|
||||
override fun popularMangaSelector() = "div.desc"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.desc"
|
||||
|
||||
override fun popularMangaFromElement(element: Element, manga: Manga) {
|
||||
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, manga: Manga) {
|
||||
popularMangaFromElement(element, 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, manga: Manga) {
|
||||
popularMangaFromElement(element, manga)
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document, manga: Manga) {
|
||||
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 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
|
||||
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, chapter: Chapter) {
|
||||
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: Chapter, manga: Manga) {
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
chapter.chapter_number = -2f
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||
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)
|
||||
@ -100,14 +114,18 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
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, pages: MutableList<Page>) {
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
@ -119,7 +137,7 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
* on http://readmanga.me/search
|
||||
*/
|
||||
override fun getFilterList(): List<Filter<*>> = listOf(
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("арт", "el_5685"),
|
||||
Genre("боевик", "el_2155"),
|
||||
Genre("боевые искусства", "el_2143"),
|
||||
|
@ -14,6 +14,7 @@ import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
@ -32,7 +33,6 @@ import nucleus.factory.RequiresPresenter
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
|
||||
/**
|
||||
@ -104,6 +104,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
|
||||
private val toolbar: Toolbar
|
||||
get() = (activity as MainActivity).toolbar
|
||||
|
||||
/**
|
||||
* Snackbar containing an error message when a request fails.
|
||||
*/
|
||||
private var snack: Snackbar? = null
|
||||
|
||||
/**
|
||||
* Navigation view containing filter items.
|
||||
*/
|
||||
@ -201,8 +206,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
|
||||
} else if (source != presenter.source) {
|
||||
selectedIndex = position
|
||||
showProgressBar()
|
||||
glm.scrollToPositionWithOffset(0, 0)
|
||||
llm.scrollToPositionWithOffset(0, 0)
|
||||
adapter.clear()
|
||||
presenter.setActiveSource(source)
|
||||
navView?.setFilters(presenter.sourceFilters)
|
||||
activity.invalidateOptionsMenu()
|
||||
@ -233,14 +237,14 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
|
||||
}
|
||||
|
||||
navView.onSearchClicked = {
|
||||
val allDefault = (0..navView.adapter.items.lastIndex)
|
||||
.none { navView.adapter.items[it].state != presenter.source.filters[it].state }
|
||||
|
||||
presenter.setSourceFilter(if (allDefault) emptyList() else navView.adapter.items)
|
||||
val allDefault = navView.adapter.items.hasSameState(presenter.source.getFilterList())
|
||||
showProgressBar()
|
||||
adapter.clear()
|
||||
presenter.setSourceFilter(if (allDefault) FilterList() else navView.adapter.items)
|
||||
}
|
||||
|
||||
navView.onResetClicked = {
|
||||
presenter.appliedFilters = emptyList()
|
||||
presenter.appliedFilters = FilterList()
|
||||
val newFilters = presenter.source.getFilterList()
|
||||
presenter.sourceFilters = newFilters
|
||||
navView.setFilters(newFilters)
|
||||
@ -277,7 +281,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
|
||||
// Setup filters button
|
||||
menu.findItem(R.id.action_set_filter).apply {
|
||||
icon.mutate()
|
||||
if (presenter.source.filters.isEmpty()) {
|
||||
if (presenter.sourceFilters.isEmpty()) {
|
||||
isEnabled = false
|
||||
icon.alpha = 128
|
||||
} else {
|
||||
@ -355,8 +359,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
|
||||
return
|
||||
|
||||
showProgressBar()
|
||||
catalogue_grid.layoutManager.scrollToPosition(0)
|
||||
catalogue_list.layoutManager.scrollToPosition(0)
|
||||
adapter.clear()
|
||||
|
||||
presenter.restartPager(newQuery)
|
||||
}
|
||||
@ -394,9 +397,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
|
||||
*/
|
||||
fun onAddPageError(error: Throwable) {
|
||||
hideProgressBar()
|
||||
Timber.e(error)
|
||||
|
||||
catalogue_view.snack(error.message ?: "", Snackbar.LENGTH_INDEFINITE) {
|
||||
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
|
||||
|
||||
snack?.dismiss()
|
||||
snack = catalogue_view.snack(message, Snackbar.LENGTH_INDEFINITE) {
|
||||
setAction(R.string.action_retry) {
|
||||
showProgressBar()
|
||||
presenter.requestNext()
|
||||
@ -456,6 +461,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
|
||||
*/
|
||||
private fun showProgressBar() {
|
||||
progress.visibility = ProgressBar.VISIBLE
|
||||
snack?.dismiss()
|
||||
snack = null
|
||||
}
|
||||
|
||||
/**
|
||||
@ -463,6 +470,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
|
||||
*/
|
||||
private fun showGridProgressBar() {
|
||||
progress_grid.visibility = ProgressBar.VISIBLE
|
||||
snack?.dismiss()
|
||||
snack = null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -9,7 +9,8 @@ import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.data.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.util.dpToPx
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
@ -38,14 +39,14 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
|
||||
reset_btn.setOnClickListener { onResetClicked() }
|
||||
}
|
||||
|
||||
fun setFilters(items: List<Filter<*>>) {
|
||||
fun setFilters(items: FilterList) {
|
||||
adapter.items = items
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
inner class Adapter : RecyclerView.Adapter<Holder>() {
|
||||
|
||||
var items: List<Filter<*>> = emptyList()
|
||||
var items: FilterList = FilterList()
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
|
@ -1,28 +1,32 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.data.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
|
||||
open class CataloguePager(val source: OnlineSource, val query: String, val filters: List<Filter<*>>) : Pager() {
|
||||
open class CataloguePager(val source: CatalogueSource, val query: String, val filters: FilterList) : Pager() {
|
||||
|
||||
override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
|
||||
val lastPage = lastPage
|
||||
|
||||
val page = if (lastPage == null)
|
||||
MangasPage(1)
|
||||
else
|
||||
MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! }
|
||||
override fun requestNext(): Observable<MangasPage> {
|
||||
val page = currentPage
|
||||
|
||||
val observable = if (query.isBlank() && filters.isEmpty())
|
||||
source.fetchPopularManga(page)
|
||||
else
|
||||
source.fetchSearchManga(page, query, filters)
|
||||
|
||||
return transformer(observable)
|
||||
.doOnNext { results.onNext(it) }
|
||||
.doOnNext { this@CataloguePager.lastPage = it }
|
||||
return observable
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext {
|
||||
if (it.mangas.isNotEmpty()) {
|
||||
onPageReceived(it)
|
||||
} else {
|
||||
throw NoResultsException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -6,12 +6,12 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
@ -55,7 +55,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
/**
|
||||
* Active source.
|
||||
*/
|
||||
lateinit var source: OnlineSource
|
||||
lateinit var source: CatalogueSource
|
||||
private set
|
||||
|
||||
/**
|
||||
@ -67,12 +67,12 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
/**
|
||||
* Modifiable list of filters.
|
||||
*/
|
||||
var sourceFilters: List<Filter<*>> = emptyList()
|
||||
var sourceFilters = FilterList()
|
||||
|
||||
/**
|
||||
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
|
||||
*/
|
||||
var appliedFilters: List<Filter<*>> = emptyList()
|
||||
var appliedFilters = FilterList()
|
||||
|
||||
/**
|
||||
* Pager containing a list of manga results.
|
||||
@ -136,7 +136,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
* @param query the query.
|
||||
* @param filters the current state of the filters (for search mode).
|
||||
*/
|
||||
fun restartPager(query: String = this.query, filters: List<Filter<*>> = this.appliedFilters) {
|
||||
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
|
||||
this.query = query
|
||||
this.appliedFilters = filters
|
||||
|
||||
@ -145,11 +145,17 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
// Create a new pager.
|
||||
pager = createPager(query, filters)
|
||||
|
||||
val sourceId = source.id
|
||||
|
||||
// Prepare the pager.
|
||||
pagerSubscription?.let { remove(it) }
|
||||
pagerSubscription = pager.results()
|
||||
.subscribeReplay({ view, page ->
|
||||
view.onAddPage(page.page, page.mangas)
|
||||
.observeOn(Schedulers.io())
|
||||
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
|
||||
.doOnNext { initializeMangas(it.second) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeReplay({ view, pair ->
|
||||
view.onAddPage(pair.first, pair.second)
|
||||
}, { view, error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
@ -165,7 +171,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
if (!hasNextPage()) return
|
||||
|
||||
pageSubscription?.let { remove(it) }
|
||||
pageSubscription = pager.requestNext { getPageTransformer(it) }
|
||||
pageSubscription = Observable.defer { pager.requestNext() }
|
||||
.subscribeFirst({ view, page ->
|
||||
// Nothing to do when onNext is emitted.
|
||||
}, CatalogueFragment::onAddPageError)
|
||||
@ -175,7 +181,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
* Returns true if the last fetched page has a next page.
|
||||
*/
|
||||
fun hasNextPage(): Boolean {
|
||||
return pager.hasNextPage()
|
||||
return pager.hasNextPage
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,12 +189,12 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
*
|
||||
* @param source the new active source.
|
||||
*/
|
||||
fun setActiveSource(source: OnlineSource) {
|
||||
fun setActiveSource(source: CatalogueSource) {
|
||||
prefs.lastUsedCatalogueSource().set(source.id)
|
||||
this.source = source
|
||||
sourceFilters = source.getFilterList()
|
||||
|
||||
restartPager(query = "", filters = emptyList())
|
||||
restartPager(query = "", filters = FilterList())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -208,7 +214,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
initializerSubscription?.let { remove(it) }
|
||||
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { !it.initialized }
|
||||
.filter { it.thumbnail_url == null && !it.initialized }
|
||||
.concatMap { getMangaDetailsObservable(it) }
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@ -221,41 +227,21 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
.apply { add(this) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the function to apply to the observable of the list of manga from the source.
|
||||
*
|
||||
* @param observable the observable from the source.
|
||||
* @return the function to apply.
|
||||
*/
|
||||
fun getPageTransformer(observable: Observable<MangasPage>): Observable<MangasPage> {
|
||||
return observable.subscribeOn(Schedulers.io())
|
||||
.doOnNext { it.mangas.replace { networkToLocalManga(it) } }
|
||||
.doOnNext { initializeMangas(it.mangas) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an object in the list with another.
|
||||
*/
|
||||
fun <T> MutableList<T>.replace(block: (T) -> T) {
|
||||
forEachIndexed { i, obj ->
|
||||
set(i, block(obj))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga from the database for the given manga from network. It creates a new entry
|
||||
* if the manga is not yet in the database.
|
||||
*
|
||||
* @param networkManga the manga from network.
|
||||
* @param sManga the manga from the source.
|
||||
* @return a manga from the database.
|
||||
*/
|
||||
private fun networkToLocalManga(networkManga: Manga): Manga {
|
||||
var localManga = db.getManga(networkManga.url, source.id).executeAsBlocking()
|
||||
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
||||
if (localManga == null) {
|
||||
val result = db.insertManga(networkManga).executeAsBlocking()
|
||||
networkManga.id = result.insertedId()
|
||||
localManga = networkManga
|
||||
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||
newManga.copyFrom(sManga)
|
||||
val result = db.insertManga(newManga).executeAsBlocking()
|
||||
newManga.id = result.insertedId()
|
||||
localManga = newManga
|
||||
}
|
||||
return localManga
|
||||
}
|
||||
@ -279,6 +265,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
return source.fetchMangaDetails(manga)
|
||||
.flatMap { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
Observable.just(manga)
|
||||
}
|
||||
@ -290,13 +277,13 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
*
|
||||
* @return a source.
|
||||
*/
|
||||
fun getLastUsedSource(): OnlineSource {
|
||||
fun getLastUsedSource(): CatalogueSource {
|
||||
val id = prefs.lastUsedCatalogueSource().get() ?: -1
|
||||
val source = sourceManager.get(id)
|
||||
if (!isValidSource(source)) {
|
||||
return findFirstValidSource()
|
||||
}
|
||||
return source as OnlineSource
|
||||
return source as CatalogueSource
|
||||
}
|
||||
|
||||
/**
|
||||
@ -320,14 +307,14 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
*
|
||||
* @return the index of the first valid source.
|
||||
*/
|
||||
fun findFirstValidSource(): OnlineSource {
|
||||
fun findFirstValidSource(): CatalogueSource {
|
||||
return sources.first { isValidSource(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of enabled sources ordered by language and name.
|
||||
*/
|
||||
open protected fun getEnabledSources(): List<OnlineSource> {
|
||||
open protected fun getEnabledSources(): List<CatalogueSource> {
|
||||
val languages = prefs.enabledLanguages().getOrDefault()
|
||||
val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
|
||||
|
||||
@ -336,7 +323,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
languages.add("en")
|
||||
}
|
||||
|
||||
return sourceManager.getOnlineSources()
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in languages }
|
||||
.filterNot { it.id.toString() in hiddenCatalogues }
|
||||
.sortedBy { "(${it.lang}) ${it.name}" }
|
||||
@ -365,13 +352,13 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
/**
|
||||
* Set the filter states for the current source.
|
||||
*
|
||||
* @param filterStates a list of active filters.
|
||||
* @param filters a list of active filters.
|
||||
*/
|
||||
fun setSourceFilter(filters: List<Filter<*>>) {
|
||||
fun setSourceFilter(filters: FilterList) {
|
||||
restartPager(filters = filters)
|
||||
}
|
||||
|
||||
open fun createPager(query: String, filters: List<Filter<*>>): Pager {
|
||||
open fun createPager(query: String, filters: FilterList): Pager {
|
||||
return CataloguePager(source, query, filters)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
class NoResultsException : Exception()
|
@ -1,25 +1,31 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import rx.subjects.PublishSubject
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
* A general pager for source requests (latest updates, popular, search)
|
||||
*/
|
||||
abstract class Pager {
|
||||
abstract class Pager(var currentPage: Int = 1) {
|
||||
|
||||
protected var lastPage: MangasPage? = null
|
||||
var hasNextPage = true
|
||||
private set
|
||||
|
||||
protected val results = PublishSubject.create<MangasPage>()
|
||||
protected val results: PublishRelay<Pair<Int, List<SManga>>> = PublishRelay.create()
|
||||
|
||||
fun results(): Observable<MangasPage> {
|
||||
fun results(): Observable<Pair<Int, List<SManga>>> {
|
||||
return results.asObservable()
|
||||
}
|
||||
|
||||
fun hasNextPage(): Boolean {
|
||||
return lastPage == null || lastPage?.nextPageUrl != null
|
||||
abstract fun requestNext(): Observable<MangasPage>
|
||||
|
||||
fun onPageReceived(mangasPage: MangasPage) {
|
||||
val page = currentPage
|
||||
currentPage++
|
||||
hasNextPage = mangasPage.hasNextPage && !mangasPage.mangas.isEmpty()
|
||||
results.call(Pair(page, mangasPage.mangas))
|
||||
}
|
||||
|
||||
abstract fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage>
|
||||
}
|
@ -1,28 +1,22 @@
|
||||
package eu.kanade.tachiyomi.ui.latest_updates
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
|
||||
/**
|
||||
* LatestUpdatesPager inherited from the general Pager.
|
||||
*/
|
||||
class LatestUpdatesPager(val source: OnlineSource): Pager() {
|
||||
class LatestUpdatesPager(val source: CatalogueSource): Pager() {
|
||||
|
||||
override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
|
||||
val lastPage = lastPage
|
||||
|
||||
val page = if (lastPage == null)
|
||||
MangasPage(1)
|
||||
else
|
||||
MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! }
|
||||
|
||||
val observable = source.fetchLatestUpdates(page)
|
||||
|
||||
return transformer(observable)
|
||||
.doOnNext { results.onNext(it) }
|
||||
.doOnNext { this@LatestUpdatesPager.lastPage = it }
|
||||
override fun requestNext(): Observable<MangasPage> {
|
||||
return source.fetchLatestUpdates(currentPage)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { onPageReceived(it) }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
package eu.kanade.tachiyomi.ui.latest_updates
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.data.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
|
||||
|
||||
/**
|
||||
* Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter.
|
||||
*/
|
||||
class LatestUpdatesPresenter : CataloguePresenter() {
|
||||
|
||||
override fun createPager(query: String, filters: List<Filter<*>>): Pager {
|
||||
override fun createPager(query: String, filters: FilterList): Pager {
|
||||
return LatestUpdatesPager(source)
|
||||
}
|
||||
|
||||
override fun getEnabledSources(): List<OnlineSource> {
|
||||
override fun getEnabledSources(): List<CatalogueSource> {
|
||||
return super.getEnabledSources().filter { it.supportsLatest }
|
||||
}
|
||||
|
||||
override fun isValidSource(source: Source?): Boolean {
|
||||
return super.isValidSource(source) && (source as OnlineSource).supportsLatest
|
||||
return super.isValidSource(source) && (source as CatalogueSource).supportsLatest
|
||||
}
|
||||
|
||||
}
|
@ -125,7 +125,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
*/
|
||||
private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
||||
// Cached list of downloaded manga directories given a source id.
|
||||
val mangaDirectories = mutableMapOf<Int, Array<UniFile>>()
|
||||
val mangaDirectories = mutableMapOf<Long, Array<UniFile>>()
|
||||
|
||||
// Cached list of downloaded chapter directories for a manga.
|
||||
val chapterDirectories = mutableMapOf<Long, Boolean>()
|
||||
|
@ -197,7 +197,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
||||
/**
|
||||
* Returns an observable that updates the chapter list with the latest from the source.
|
||||
*/
|
||||
fun getRemoteChaptersObservable() = source.fetchChapterList(manga)
|
||||
fun getRemoteChaptersObservable() = Observable.defer { source.fetchChapterList(manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -15,6 +15,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||
@ -122,9 +123,9 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
|
||||
|
||||
// Update status TextView.
|
||||
manga_status.setText(when (manga.status) {
|
||||
Manga.ONGOING -> R.string.ongoing
|
||||
Manga.COMPLETED -> R.string.completed
|
||||
Manga.LICENSED -> R.string.licensed
|
||||
SManga.ONGOING -> R.string.ongoing
|
||||
SManga.COMPLETED -> R.string.completed
|
||||
SManga.LICENSED -> R.string.licensed
|
||||
else -> R.string.unknown
|
||||
})
|
||||
|
||||
|
@ -99,9 +99,10 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
||||
* @return manga information.
|
||||
*/
|
||||
private fun fetchMangaObs(): Observable<Manga> {
|
||||
return source.fetchMangaDetails(manga)
|
||||
return Observable.defer { source.fetchMangaDetails(manga) }
|
||||
.flatMap { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
Observable.just<Manga>(manga)
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.data.source.online.fetchImageFromCacheThenNet
|
||||
import eu.kanade.tachiyomi.data.source.online.fetchPageListFromCacheThenNet
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
@ -36,9 +39,11 @@ class ChapterLoader(
|
||||
}
|
||||
|
||||
private fun prepareOnlineReading() {
|
||||
if (source !is OnlineSource) return
|
||||
|
||||
subscriptions += Observable.defer { Observable.just(queue.take().page) }
|
||||
.filter { it.status == Page.QUEUE }
|
||||
.concatMap { source.fetchImage(it) }
|
||||
.concatMap { source.fetchImageFromCacheThenNet(it) }
|
||||
.repeat()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe({
|
||||
@ -57,6 +62,10 @@ class ChapterLoader(
|
||||
Observable.just(chapter.pages!!)
|
||||
}
|
||||
.doOnNext { pages ->
|
||||
if (pages.isEmpty()) {
|
||||
throw Exception("Page list is empty")
|
||||
}
|
||||
|
||||
// Now that the number of pages is known, fix the requested page if the last one
|
||||
// was requested.
|
||||
if (chapter.requestedPage == -1) {
|
||||
@ -76,8 +85,8 @@ class ChapterLoader(
|
||||
// Fetch the page list from disk.
|
||||
downloadManager.buildPageList(source, manga, chapter)
|
||||
} else {
|
||||
// Fetch the page list from cache or fallback to network
|
||||
source.fetchPageList(chapter)
|
||||
(source as? OnlineSource)?.fetchPageListFromCacheThenNet(chapter)
|
||||
?: source.fetchPageList(chapter)
|
||||
}
|
||||
}
|
||||
.doOnNext { pages ->
|
||||
@ -111,6 +120,8 @@ class ChapterLoader(
|
||||
queue.offer(PriorityPage(page, 2))
|
||||
}
|
||||
|
||||
|
||||
|
||||
private data class PriorityPage(val page: Page, val priority: Int): Comparable<PriorityPage> {
|
||||
|
||||
companion object {
|
||||
|
@ -372,7 +372,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
Observable.fromCallable {
|
||||
// Cache current page list progress for online chapters to allow a faster reopen
|
||||
if (!chapter.isDownloaded) {
|
||||
source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
|
||||
source.let {
|
||||
if (it is OnlineSource) chapterCache.putPageListToCache(chapter, pages)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -130,7 +130,7 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
|
||||
*/
|
||||
private fun setDownloadedChapters(chapters: List<RecentChapter>) {
|
||||
// Cached list of downloaded manga directories.
|
||||
val mangaDirectories = mutableMapOf<Int, Array<UniFile>>()
|
||||
val mangaDirectories = mutableMapOf<Long, Array<UniFile>>()
|
||||
|
||||
// Cached list of downloaded chapter directories for a manga.
|
||||
val chapterDirectories = mutableMapOf<Long, Array<UniFile>>()
|
||||
|
@ -123,13 +123,14 @@ class SettingsSourcesFragment : SettingsFragment() {
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == SOURCE_CHANGE_REQUEST) {
|
||||
val pref = findPreference(getSourceKey(resultCode)) as? LoginCheckBoxPreference
|
||||
if (requestCode == SOURCE_CHANGE_REQUEST && data != null) {
|
||||
val sourceId = data.getLongExtra("key", -1L)
|
||||
val pref = findPreference(getSourceKey(sourceId)) as? LoginCheckBoxPreference
|
||||
pref?.notifyChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSourceKey(sourceId: Int): String {
|
||||
private fun getSourceKey(sourceId: Long): String {
|
||||
return "source_$sourceId"
|
||||
}
|
||||
|
||||
|
@ -81,8 +81,9 @@ class SettingsTrackingFragment : SettingsFragment() {
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == SYNC_CHANGE_REQUEST) {
|
||||
updatePreference(resultCode)
|
||||
if (requestCode == SYNC_CHANGE_REQUEST && data != null) {
|
||||
val serviceId = data.getIntExtra("key", -1)
|
||||
updatePreference(serviceId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ 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.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import java.util.*
|
||||
|
||||
@ -11,23 +12,29 @@ import java.util.*
|
||||
* Helper method for syncing the list of chapters from the source with the ones from the database.
|
||||
*
|
||||
* @param db the database.
|
||||
* @param sourceChapters a list of chapters from the source.
|
||||
* @param rawSourceChapters a list of chapters from the source.
|
||||
* @param manga the manga of the chapters.
|
||||
* @param source the source of the chapters.
|
||||
* @return a pair of new insertions and deletions.
|
||||
*/
|
||||
fun syncChaptersWithSource(db: DatabaseHelper,
|
||||
sourceChapters: List<Chapter>,
|
||||
rawSourceChapters: List<SChapter>,
|
||||
manga: Manga,
|
||||
source: Source) : Pair<List<Chapter>, List<Chapter>> {
|
||||
|
||||
if (rawSourceChapters.isEmpty()) {
|
||||
throw Exception("No chapters found")
|
||||
}
|
||||
|
||||
// Chapters from db.
|
||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||
|
||||
// Fix manga id and order in source.
|
||||
sourceChapters.forEachIndexed { i, chapter ->
|
||||
chapter.manga_id = manga.id
|
||||
chapter.source_order = i
|
||||
val sourceChapters = rawSourceChapters.mapIndexed { i, sChapter ->
|
||||
Chapter.create().apply {
|
||||
copyFrom(sChapter)
|
||||
manga_id = manga.id
|
||||
source_order = i
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters from the source not in db.
|
||||
|
@ -1,26 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public final class UrlUtil {
|
||||
|
||||
private UrlUtil() throws InstantiationException {
|
||||
throw new InstantiationException("This class is not for instantiation");
|
||||
}
|
||||
|
||||
public static String getPath(String s) {
|
||||
try {
|
||||
URI uri = new URI(s);
|
||||
String out = uri.getPath();
|
||||
if (uri.getQuery() != null)
|
||||
out += "?" + uri.getQuery();
|
||||
if (uri.getFragment() != null)
|
||||
out += "#" + uri.getFragment();
|
||||
return out;
|
||||
} catch (URISyntaxException e) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -21,10 +21,11 @@ fun View.getCoordinates() = Point((left + right) / 2, (top + bottom) / 2)
|
||||
* @param length the duration of the snack.
|
||||
* @param f a function to execute in the snack, allowing for example to define a custom action.
|
||||
*/
|
||||
inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit) {
|
||||
inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit): Snackbar {
|
||||
val snack = Snackbar.make(this, message, length)
|
||||
val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView
|
||||
textView.setTextColor(Color.WHITE)
|
||||
snack.f()
|
||||
snack.show()
|
||||
return snack
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.widget.preference
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
@ -70,7 +71,8 @@ abstract class LoginDialogPreference : DialogFragment() {
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
targetFragment?.onActivityResult(targetRequestCode, arguments.getInt("key"), Intent())
|
||||
val intent = Intent().putExtras(arguments)
|
||||
targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_OK, intent)
|
||||
}
|
||||
|
||||
protected abstract fun checkLogin()
|
||||
|
@ -19,7 +19,7 @@ class SourceLoginDialog : LoginDialogPreference() {
|
||||
fun newInstance(source: Source): LoginDialogPreference {
|
||||
val fragment = SourceLoginDialog()
|
||||
val bundle = Bundle(1)
|
||||
bundle.putInt("key", source.id)
|
||||
bundle.putLong("key", source.id)
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
@ -32,7 +32,7 @@ class SourceLoginDialog : LoginDialogPreference() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val sourceId = arguments.getInt("key")
|
||||
val sourceId = arguments.getLong("key")
|
||||
source = sourceManager.get(sourceId) as LoginSource
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@
|
||||
<string name="pref_enable_automatic_updates_key">automatic_updates</string>
|
||||
|
||||
<string name="pref_display_catalogue_as_list">pref_display_catalogue_as_list</string>
|
||||
<string name="pref_last_catalogue_source_key">pref_last_catalogue_source_key</string>
|
||||
<string name="pref_last_catalogue_source_key">last_catalogue_source</string>
|
||||
|
||||
<string name="pref_download_new_key">download_new</string>
|
||||
|
||||
|
@ -9,12 +9,13 @@ import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Matchers.anyInt
|
||||
import org.mockito.Matchers.anyLong
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.*
|
||||
import org.robolectric.Robolectric
|
||||
@ -51,7 +52,7 @@ class LibraryUpdateServiceTest {
|
||||
|
||||
service = Robolectric.setupService(LibraryUpdateService::class.java)
|
||||
source = mock(OnlineSource::class.java)
|
||||
`when`(service.sourceManager.get(anyInt())).thenReturn(source)
|
||||
`when`(service.sourceManager.get(anyLong())).thenReturn(source)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -91,7 +92,7 @@ class LibraryUpdateServiceTest {
|
||||
|
||||
// One of the updates will fail
|
||||
`when`(source.fetchChapterList(favManga[0])).thenReturn(Observable.just(chapters))
|
||||
`when`(source.fetchChapterList(favManga[1])).thenReturn(Observable.error<List<Chapter>>(Exception()))
|
||||
`when`(source.fetchChapterList(favManga[1])).thenReturn(Observable.error<List<SChapter>>(Exception()))
|
||||
`when`(source.fetchChapterList(favManga[2])).thenReturn(Observable.just(chapters3))
|
||||
|
||||
val intent = Intent()
|
||||
@ -117,8 +118,7 @@ class LibraryUpdateServiceTest {
|
||||
private fun createManga(vararg urls: String): List<Manga> {
|
||||
val list = ArrayList<Manga>()
|
||||
for (url in urls) {
|
||||
val m = Manga.create(url)
|
||||
m.title = url.substring(1)
|
||||
val m = Manga.create(url, url.substring(1))
|
||||
m.favorite = true
|
||||
list.add(m)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user