Initial support for external sources

This commit is contained in:
inorichi 2017-01-08 18:12:19 +01:00 committed by GitHub
parent 77d986f213
commit dd56d7c0bb
60 changed files with 1371 additions and 1126 deletions

View file

@ -5,6 +5,7 @@ import android.text.format.Formatter
import com.github.salomonbrys.kotson.fromJson import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson import com.google.gson.Gson
import com.jakewharton.disklrucache.DiskLruCache import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.saveTo import eu.kanade.tachiyomi.util.saveTo
@ -92,13 +93,13 @@ class ChapterCache(private val context: Context) {
/** /**
* Get page list from cache. * Get page list from cache.
* *
* @param chapterUrl the url of the chapter. * @param chapter the chapter.
* @return an observable of the list of pages. * @return an observable of the list of pages.
*/ */
fun getPageListFromCache(chapterUrl: String): Observable<List<Page>> { fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
return Observable.fromCallable<List<Page>> { return Observable.fromCallable {
// Get the key for the chapter. // Get the key for the chapter.
val key = DiskUtil.hashKeyForDisk(chapterUrl) val key = DiskUtil.hashKeyForDisk(getKey(chapter))
// Convert JSON string to list of objects. Throws an exception if snapshot is null // Convert JSON string to list of objects. Throws an exception if snapshot is null
diskCache.get(key).use { diskCache.get(key).use {
@ -110,10 +111,10 @@ class ChapterCache(private val context: Context) {
/** /**
* Add page list to disk cache. * Add page list to disk cache.
* *
* @param chapterUrl the url of the chapter. * @param chapter the chapter.
* @param pages list of pages. * @param pages list of pages.
*/ */
fun putPageListToCache(chapterUrl: String, pages: List<Page>) { fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
// Convert list of pages to json string. // Convert list of pages to json string.
val cachedValue = gson.toJson(pages) val cachedValue = gson.toJson(pages)
@ -122,7 +123,7 @@ class ChapterCache(private val context: Context) {
try { try {
// Get editor from md5 key. // Get editor from md5 key.
val key = DiskUtil.hashKeyForDisk(chapterUrl) val key = DiskUtil.hashKeyForDisk(getKey(chapter))
editor = diskCache.edit(key) ?: return editor = diskCache.edit(key) ?: return
// Write chapter urls to cache. // Write chapter urls to cache.
@ -196,5 +197,8 @@ class ChapterCache(private val context: Context) {
} }
} }
private fun getKey(chapter: Chapter): String {
return "${chapter.manga_id}${chapter.url}"
}
} }

View file

@ -69,7 +69,7 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() {
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply { override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)) id = cursor.getLong(cursor.getColumnIndex(COL_ID))
source = cursor.getInt(cursor.getColumnIndex(COL_SOURCE)) source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndex(COL_URL)) url = cursor.getString(cursor.getColumnIndex(COL_URL))
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST)) artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR)) author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))

View file

@ -1,17 +1,14 @@
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.data.source.model.SChapter
import java.io.Serializable import java.io.Serializable
interface Chapter : Serializable { interface Chapter : SChapter, Serializable {
var id: Long? var id: Long?
var manga_id: Long? var manga_id: Long?
var url: String
var name: String
var read: Boolean var read: Boolean
var bookmark: Boolean var bookmark: Boolean
@ -20,10 +17,6 @@ interface Chapter : Serializable {
var date_fetch: Long var date_fetch: Long
var date_upload: Long
var chapter_number: Float
var source_order: Int var source_order: Int
val isRecognizedNumber: Boolean val isRecognizedNumber: Boolean

View file

@ -1,35 +1,17 @@
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable import eu.kanade.tachiyomi.data.source.model.SManga
interface Manga : Serializable { interface Manga : SManga {
var id: Long? var id: Long?
var source: Int var source: Long
var url: String
var title: String
var artist: String?
var author: String?
var description: String?
var genre: String?
var status: Int
var thumbnail_url: String?
var favorite: Boolean var favorite: Boolean
var last_update: Long var last_update: Long
var initialized: Boolean
var viewer: Int var viewer: Int
var chapter_flags: Int var chapter_flags: Int
@ -38,27 +20,6 @@ interface Manga : Serializable {
var category: Int var category: Int
fun copyFrom(other: Manga) {
if (other.author != null)
author = other.author
if (other.artist != null)
artist = other.artist
if (other.description != null)
description = other.description
if (other.genre != null)
genre = other.genre
if (other.thumbnail_url != null)
thumbnail_url = other.thumbnail_url
status = other.status
initialized = true
}
fun setChapterOrder(order: Int) { fun setChapterOrder(order: Int) {
setFlags(order, SORT_MASK) setFlags(order, SORT_MASK)
} }
@ -94,11 +55,6 @@ interface Manga : Serializable {
companion object { companion object {
const val UNKNOWN = 0
const val ONGOING = 1
const val COMPLETED = 2
const val LICENSED = 3
const val SORT_DESC = 0x00000000 const val SORT_DESC = 0x00000000
const val SORT_ASC = 0x00000001 const val SORT_ASC = 0x00000001
const val SORT_MASK = 0x00000001 const val SORT_MASK = 0x00000001
@ -126,12 +82,13 @@ interface Manga : Serializable {
const val DISPLAY_NUMBER = 0x00100000 const val DISPLAY_NUMBER = 0x00100000
const val DISPLAY_MASK = 0x00100000 const val DISPLAY_MASK = 0x00100000
fun create(source: Int): Manga = MangaImpl().apply { fun create(source: Long): Manga = MangaImpl().apply {
this.source = source this.source = source
} }
fun create(pathUrl: String, source: Int = 0): Manga = MangaImpl().apply { fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
url = pathUrl url = pathUrl
this.title = title
this.source = source this.source = source
} }
} }

View file

@ -4,7 +4,7 @@ class MangaImpl : Manga {
override var id: Long? = null override var id: Long? = null
override var source: Int = 0 override var source: Long = 0
override lateinit var url: String override lateinit var url: String

View file

@ -40,7 +40,7 @@ interface MangaQueries : DbProvider {
.build()) .build())
.prepare() .prepare()
fun getManga(url: String, sourceId: Int) = db.get() fun getManga(url: String, sourceId: Long) = db.get()
.`object`(Manga::class.java) .`object`(Manga::class.java)
.withQuery(Query.builder() .withQuery(Query.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)

View file

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.source.online.fetchAllImageUrlsFromPageList
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
import eu.kanade.tachiyomi.util.RetryWithDelay import eu.kanade.tachiyomi.util.RetryWithDelay
import eu.kanade.tachiyomi.util.plusAssign 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) { val pageListObservable = if (download.pages == null) {
// Pull page list from network and add them to download object // Pull page list from network and add them to download object
download.source.fetchPageListFromNetwork(download.chapter) download.source.fetchPageList(download.chapter)
.doOnNext { pages -> .doOnNext { pages ->
if (pages.isEmpty()) {
throw Exception("Page list is empty")
}
download.pages = pages download.pages = pages
} }
} else { } else {
@ -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> { private fun downloadImage(page: Page, source: OnlineSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
page.status = Page.DOWNLOAD_IMAGE page.status = Page.DOWNLOAD_IMAGE
page.progress = 0 page.progress = 0
return source.imageResponse(page) return source.fetchImage(page)
.map { response -> .map { response ->
val file = tmpDir.createFile("$filename.tmp") val file = tmpDir.createFile("$filename.tmp")
try { try {

View file

@ -52,7 +52,7 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
/** /**
* Map where request headers are stored for a source. * Map where request headers are stored for a source.
*/ */
private val cachedHeaders = hashMapOf<Int, LazyHeaders>() private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
/** /**
* Factory class for creating [MangaModelLoader] instances. * Factory class for creating [MangaModelLoader] instances.

View file

@ -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.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.SManga
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.* import eu.kanade.tachiyomi.util.*
@ -214,7 +215,7 @@ class LibraryUpdateService : Service() {
} }
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) { if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED } listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
} }
return listToUpdate return listToUpdate
@ -328,9 +329,10 @@ class LibraryUpdateService : Service() {
?: return@concatMap Observable.empty<Manga>() ?: return@concatMap Observable.empty<Manga>()
source.fetchMangaDetails(manga) source.fetchMangaDetails(manga)
.doOnNext { networkManga -> .map { networkManga ->
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
manga
} }
.onErrorReturn { manga } .onErrorReturn { manga }
} }

View file

@ -91,9 +91,9 @@ class PreferenceKeys(context: Context) {
val downloadNew = context.getString(R.string.pref_download_new_key) val downloadNew = context.getString(R.string.pref_download_new_key)
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId" fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId" fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"

View file

@ -74,7 +74,7 @@ class PreferencesHelper(val context: Context) {
fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false) fun askUpdateTrack() = prefs.getBoolean(keys.askUpdateTrack, false)
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1) fun lastUsedCatalogueSource() = rxPrefs.getLong(keys.lastUsedCatalogueSource, -1)
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0) fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)

View file

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

View file

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.data.source 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.Page
import eu.kanade.tachiyomi.data.source.model.SChapter
import eu.kanade.tachiyomi.data.source.model.SManga
import rx.Observable import rx.Observable
/** /**
@ -13,7 +13,7 @@ interface Source {
/** /**
* Id for the source. Must be unique. * Id for the source. Must be unique.
*/ */
val id: Int val id: Long
/** /**
* Name of the source. * Name of the source.
@ -25,26 +25,20 @@ interface Source {
* *
* @param manga the manga to update. * @param manga the manga to update.
*/ */
fun fetchMangaDetails(manga: Manga): Observable<Manga> fun fetchMangaDetails(manga: SManga): Observable<SManga>
/** /**
* Returns an observable with all the available chapters for a manga. * Returns an observable with all the available chapters for a manga.
* *
* @param manga the manga to update. * @param manga the manga to update.
*/ */
fun fetchChapterList(manga: Manga): Observable<List<Chapter>> fun fetchChapterList(manga: SManga): Observable<List<SChapter>>
/** /**
* Returns an observable with the list of pages a chapter has. * Returns an observable with the list of pages a chapter has.
* *
* @param chapter the chapter. * @param chapter the chapter.
*/ */
fun fetchPageList(chapter: Chapter): Observable<List<Page>> fun fetchPageList(chapter: SChapter): Observable<List<Page>>
/**
* Returns an observable with the path of the image.
*
* @param page the page.
*/
fun fetchImage(page: Page): Observable<Page>
} }

View file

@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.data.source
import android.Manifest.permission.READ_EXTERNAL_STORAGE import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Environment import android.os.Environment
import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
@ -18,29 +21,47 @@ import java.io.File
open class SourceManager(private val context: Context) { 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] return sourcesMap[sourceKey]
} }
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java) fun getOnlineSources() = sourcesMap.values.filterIsInstance<OnlineSource>()
private fun createOnlineSourceList(): List<Source> = listOf( fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
Batoto(1),
Mangahere(2), private fun createSources() {
Mangafox(3), createExtensionSources().forEach { registerSource(it) }
Kissmanga(4), createYamlSources().forEach { registerSource(it) }
Readmanga(5), createInternalSources().forEach { registerSource(it) }
Mintmanga(6), }
Mangachan(7),
Readmangatoday(8), private fun registerSource(source: Source, overwrite: Boolean = false) {
Mangasee(9), if (overwrite || !sourcesMap.containsKey(source.id)) {
WieManga(10) 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 { private fun createYamlSources(): List<Source> {
createOnlineSourceList().forEach { put(it.id, it) } val sources = mutableListOf<Source>()
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath + val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
File.separator + context.getString(R.string.app_name), "parsers") File.separator + context.getString(R.string.app_name), "parsers")
@ -50,12 +71,89 @@ open class SourceManager(private val context: Context) {
for (file in parsersDir.listFiles().filter { it.extension == "yml" }) { for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
try { try {
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) } val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
YamlOnlineSource(map).let { put(it.id, it) } sources.add(YamlOnlineSource(map))
} catch (e: Exception) { } catch (e: Exception) {
Timber.e("Error loading source from file. Bad format?") 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
} }
} }

View file

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

View file

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

View file

@ -1,13 +1,3 @@
package eu.kanade.tachiyomi.data.source.model package eu.kanade.tachiyomi.data.source.model
import eu.kanade.tachiyomi.data.database.models.Manga data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
class MangasPage(val page: Int) {
val mangas: MutableList<Manga> = mutableListOf()
lateinit var url: String
var nextPageUrl: String? = null
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,40 +1,32 @@
package eu.kanade.tachiyomi.data.source.online 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.GET
import eu.kanade.tachiyomi.data.network.NetworkHelper import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.network.asObservableSuccess import eu.kanade.tachiyomi.data.network.asObservableSuccess
import eu.kanade.tachiyomi.data.network.newCallWithProgress import eu.kanade.tachiyomi.data.network.newCallWithProgress
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.CatalogueSource
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.UrlUtil
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy 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. * A simple implementation for sources from a website.
*/ */
abstract class OnlineSource() : Source { abstract class OnlineSource : CatalogueSource {
/** /**
* Network service. * Network service.
*/ */
val network: NetworkHelper by injectLazy() val network: NetworkHelper by injectLazy()
/**
* Chapter cache.
*/
val chapterCache: ChapterCache by injectLazy()
/** /**
* Preferences helper. * Preferences helper.
*/ */
@ -46,24 +38,26 @@ abstract class OnlineSource() : Source {
abstract val baseUrl: String 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. * Headers used for requests.
*/ */
val headers by lazy { headersBuilder().build() } val headers: Headers by lazy { headersBuilder().build() }
/**
* Genre filters.
*/
val filters by lazy { getFilterList() }
/** /**
* Default network client for doing requests. * 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 * Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method. * override this method.
* *
* @param page the page object where the information will be saved, like the list of manga, * @param page the page number to retrieve.
* the current page and the next page url.
*/ */
open fun fetchPopularManga(page: MangasPage): Observable<MangasPage> = client override fun fetchPopularManga(page: Int): Observable<MangasPage> {
.newCall(popularMangaRequest(page)) return client.newCall(popularMangaRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
popularMangaParse(response, page) popularMangaParse(response)
page }
} }
/** /**
* Returns the request for the popular manga given the page. Override only if it's needed to * Returns the request for the popular manga given the page.
* send different headers or request method like POST.
* *
* @param page the page object. * @param page the page number to retrieve.
*/ */
open protected fun popularMangaRequest(page: MangasPage): Request { abstract protected fun popularMangaRequest(page: Int): Request
if (page.page == 1) {
page.url = popularMangaInitialUrl()
}
return GET(page.url, headers)
}
/** /**
* Returns the absolute url of the first page to popular manga. * Parses the response from the site and returns a [MangasPage] object.
*/
abstract protected fun popularMangaInitialUrl(): String
/**
* Parse the response from the site. It should add a list of manga and the absolute url to the
* next page (if it has a next one) to [page].
* *
* @param response the response from the site. * @param 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 * Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method. * override this method.
* *
* @param page the page object where the information will be saved, like the list of manga, * @param page the page number to retrieve.
* the current page and the next page url.
* @param query the search query. * @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 override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
.newCall(searchMangaRequest(page, query, filters)) return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
searchMangaParse(response, page, query, filters) searchMangaParse(response)
page }
} }
/** /**
* Returns the request for the search manga given the page. Override only if it's needed to * Returns the request for the search manga given the page.
* send different headers or request method like POST.
* *
* @param page the page object. * @param page the page number to retrieve.
* @param query the search query. * @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 { abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
if (page.page == 1) {
page.url = searchMangaInitialUrl(query, filters)
}
return GET(page.url, headers)
}
/** /**
* Returns the absolute url of the first page to popular manga. * Parses the response from the site and returns a [MangasPage] object.
*
* @param query the search query.
*/
abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String
/**
* Parse the response from the site. It should add a list of manga and the absolute url to the
* next page (if it has a next one) to [page].
* *
* @param response the response from the site. * @param 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 override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
.newCall(latestUpdatesRequest(page)) return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
latestUpdatesParse(response, page) latestUpdatesParse(response)
page }
} }
/** /**
* Returns the request for latest manga given the page. * Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/ */
open protected fun latestUpdatesRequest(page: MangasPage): Request { abstract protected fun latestUpdatesRequest(page: Int): Request
if (page.page == 1) {
page.url = latestUpdatesInitialUrl()
}
return GET(page.url, headers)
}
/** /**
* 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 abstract protected fun latestUpdatesParse(response: Response): MangasPage
/**
* Same as [popularMangaParse], but for latest manga.
*/
abstract protected fun latestUpdatesParse(response: Response, page: MangasPage)
/** /**
* Returns an observable with the updated details for a manga. Normally it's not needed to * Returns an observable with the updated details for a manga. Normally it's not needed to
@ -209,33 +170,30 @@ abstract class OnlineSource() : Source {
* *
* @param manga the manga to be updated. * @param manga the manga to be updated.
*/ */
override fun fetchMangaDetails(manga: Manga): Observable<Manga> = client override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
Manga.create(manga.url, id).apply { mangaDetailsParse(response).apply { initialized = true }
mangaDetailsParse(response, this)
initialized = true
} }
} }
/** /**
* Returns the request for updating a manga. Override only if it's needed to override the url, * Returns the request for the details of a manga. Override only if it's needed to change the
* send different headers or request method like POST. * url, send different headers or request method like POST.
* *
* @param manga the manga to be updated. * @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) 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 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 * 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. * @param manga the manga to look for chapters.
*/ */
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> = client override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
.newCall(chapterListRequest(manga)) return client.newCall(chapterListRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
mutableListOf<Chapter>().apply { chapterListParse(response)
chapterListParse(response, this)
if (isEmpty()) {
throw Exception("No chapters found")
}
} }
} }
@ -261,43 +215,27 @@ abstract class OnlineSource() : Source {
* *
* @param manga the manga to look for chapters. * @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) 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 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 * Returns an observable with the page list for a chapter.
* the local cache, otherwise fallbacks to network calling [fetchPageListFromNetwork].
* *
* @param chapter the chapter whose page list has to be fetched. * @param chapter the chapter whose page list has to be fetched.
*/ */
final override fun fetchPageList(chapter: Chapter): Observable<List<Page>> = chapterCache override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
.getPageListFromCache(getChapterCacheKey(chapter)) return client.newCall(pageListRequest(chapter))
.onErrorResumeNext { fetchPageListFromNetwork(chapter) }
/**
* Returns an observable with the page list for a chapter. Normally it's not needed to override
* this method.
*
* @param chapter the chapter whose page list has to be fetched.
*/
open fun fetchPageListFromNetwork(chapter: Chapter): Observable<List<Page>> = client
.newCall(pageListRequest(chapter))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
mutableListOf<Page>().apply { pageListParse(response)
pageListParse(response, this)
if (isEmpty()) {
throw Exception("Page list is empty")
}
} }
} }
@ -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 * 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. * 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) 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 response the response from the site.
* @param pages the page list to be filled.
*/ */
abstract protected fun pageListParse(response: Response, pages: MutableList<Page>) abstract protected fun pageListParse(response: Response): List<Page>
/**
* Returns the key for the page list to be stored in [ChapterCache].
*/
private fun getChapterCacheKey(chapter: Chapter) = "$id${chapter.url}"
/** /**
* Returns an observable with the page containing the source url of the image. If there's any * 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. * @param page the page whose source image has to be fetched.
*/ */
open protected fun fetchImageUrl(page: Page): Observable<Page> { open fun fetchImageUrl(page: Page): Observable<String> {
page.status = Page.LOAD_PAGE return client.newCall(imageUrlRequest(page))
return client
.newCall(imageUrlRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { imageUrlParse(it) } .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. * @param response the response from the site.
*/ */
abstract protected fun imageUrlParse(response: Response): String 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. * Returns an observable with the response of the source image.
* *
* @param page the page whose source image has to be downloaded. * @param page the page whose source image has to be downloaded.
*/ */
fun imageResponse(page: Page): Observable<Response> = client fun fetchImage(page: Page): Observable<Response> {
.newCallWithProgress(imageRequest(page), page) return client.newCallWithProgress(imageRequest(page), page)
.asObservableSuccess() .asObservableSuccess()
}
/** /**
* Returns the request for getting the source image. Override only if it's needed to override * 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 * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
* network and copies it to the cache calling [cacheImage]. * 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> { fun SChapter.setUrlWithoutDomain(url: String) {
val imageUrl = page.imageUrl ?: return Observable.just(page) this.url = getUrlWithoutDomain(url)
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]. * 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> { fun SManga.setUrlWithoutDomain(url: String) {
page.status = Page.DOWNLOAD_IMAGE this.url = getUrlWithoutDomain(url)
return imageResponse(page)
.doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) }
.map { page }
} }
/**
// Utility methods * Returns the url of the given string without the scheme and domain.
*
fun fetchAllImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages) * @param orig the full url.
.filter { !it.imageUrl.isNullOrEmpty() } */
.mergeWith(fetchRemainingImageUrlsFromPageList(pages)) private fun getUrlWithoutDomain(orig: String): String {
try {
fun fetchRemainingImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages) val uri = URI(orig)
.filter { it.imageUrl.isNullOrEmpty() } var out = uri.path
.concatMap { fetchImageUrl(it) } if (uri.query != null)
out += "?" + uri.query
fun savePageList(chapter: Chapter, pages: List<Page>?) { if (uri.fragment != null)
if (pages != null) { out += "#" + uri.fragment
chapterCache.putPageListToCache(getChapterCacheKey(chapter), pages) 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 * 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]. * 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 chapter the chapter to be added.
* @param manga the manga of the chapter. * @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) * Returns the list of filters for the source.
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) override fun getFilterList() = FilterList()
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()
} }

View file

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

View file

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.data.source.online 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.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page 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 eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document 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. * 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 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() val document = response.asJsoup()
for (element in document.select(popularMangaSelector())) {
Manga.create(id).apply { val mangas = document.select(popularMangaSelector()).map { element ->
popularMangaFromElement(element, this) popularMangaFromElement(element)
page.mangas.add(this)
}
} }
popularMangaNextPageSelector()?.let { selector -> val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.absUrl("href") document.select(selector).first()
} } != null
return MangasPage(mangas, hasNextPage)
} }
/** /**
@ -40,13 +39,12 @@ abstract class ParsedOnlineSource() : OnlineSource() {
abstract protected fun popularMangaSelector(): String abstract protected fun popularMangaSelector(): String
/** /**
* Fills [manga] with the given [element]. Most sites only show the title and the url, it's * Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally safe to fill only those two values. * totally fine to fill only those two values.
* *
* @param element an element obtained from [popularMangaSelector]. * @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 * 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? 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 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() val document = response.asJsoup()
for (element in document.select(searchMangaSelector())) {
Manga.create(id).apply { val mangas = document.select(searchMangaSelector()).map { element ->
searchMangaFromElement(element, this) searchMangaFromElement(element)
page.mangas.add(this)
}
} }
searchMangaNextPageSelector()?.let { selector -> val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.absUrl("href") document.select(selector).first()
} } != null
return MangasPage(mangas, hasNextPage)
} }
/** /**
@ -81,13 +77,12 @@ abstract class ParsedOnlineSource() : OnlineSource() {
abstract protected fun searchMangaSelector(): String abstract protected fun searchMangaSelector(): String
/** /**
* Fills [manga] with the given [element]. Most sites only show the title and the url, it's * Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally safe to fill only those two values. * totally fine to fill only those two values.
* *
* @param element an element obtained from [searchMangaSelector]. * @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 * 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? 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() val document = response.asJsoup()
for (element in document.select(latestUpdatesSelector())) {
Manga.create(id).apply { val mangas = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element, this) latestUpdatesFromElement(element)
page.mangas.add(this)
}
} }
latestUpdatesNextPageSelector()?.let { selector -> val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.absUrl("href") 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 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? 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 response the response from the site.
* @param manga the manga to fill.
*/ */
override fun mangaDetailsParse(response: Response, manga: Manga) { override fun mangaDetailsParse(response: Response): SManga {
mangaDetailsParse(response.asJsoup(), manga) 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 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 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() val document = response.asJsoup()
return document.select(chapterListSelector()).map { chapterFromElement(it) }
for (element in document.select(chapterListSelector())) {
Chapter.create().apply {
chapterFromElement(element, this)
chapters.add(this)
}
}
} }
/** /**
@ -168,30 +160,27 @@ abstract class ParsedOnlineSource() : OnlineSource() {
abstract protected fun chapterListSelector(): String 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 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 response the response from the site.
* @param pages the list of pages to fill.
*/ */
override fun pageListParse(response: Response, pages: MutableList<Page>) { override fun pageListParse(response: Response): List<Page> {
pageListParse(response.asJsoup(), pages) return pageListParse(response.asJsoup())
} }
/** /**
* Fills [pages] from the given [document]. * Returns a page list from the given document.
* *
* @param document the parsed 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. * Parse the response from the site and returns the absolute url to the source image.

View file

@ -1,11 +1,8 @@
package eu.kanade.tachiyomi.data.source.online 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.GET
import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.attrOrText import eu.kanade.tachiyomi.util.attrOrText
import okhttp3.Request import okhttp3.Request
@ -36,92 +33,108 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
} }
override val id = map.id.let { 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 { // Ugly, but needed after the changes
if (page.page == 1) { var popularNextPage: String? = null
page.url = popularMangaInitialUrl() 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()) { return when (map.popular.method?.toLowerCase()) {
"post" -> POST(page.url, headers, map.popular.createForm()) "post" -> POST(url, headers, map.popular.createForm())
else -> GET(page.url, headers) else -> GET(url, headers)
} }
} }
override fun popularMangaInitialUrl() = map.popular.url override fun popularMangaParse(response: Response): MangasPage {
override fun popularMangaParse(response: Response, page: MangasPage) {
val document = response.asJsoup() 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() title = element.text()
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
page.mangas.add(this)
} }
} }
map.popular.next_url_css?.let { selector -> popularNextPage = map.popular.next_url_css?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.absUrl("href") document.select(selector).first()?.absUrl("href")
}
} }
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request { return MangasPage(mangas, popularNextPage != null)
if (page.page == 1) { }
page.url = searchMangaInitialUrl(query, filters)
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()) { return when (map.search.method?.toLowerCase()) {
"post" -> POST(page.url, headers, map.search.createForm()) "post" -> POST(url, headers, map.search.createForm())
else -> GET(page.url, headers) else -> GET(url, headers)
} }
} }
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = map.search.url.replace("\$query", query) override fun searchMangaParse(response: Response): MangasPage {
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
val document = response.asJsoup() 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() title = element.text()
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
page.mangas.add(this)
} }
} }
map.search.next_url_css?.let { selector -> searchNextPage = map.search.next_url_css?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.absUrl("href") document.select(selector).first()?.absUrl("href")
}
} }
override fun latestUpdatesRequest(page: MangasPage): Request { return MangasPage(mangas, searchNextPage != null)
if (page.page == 1) { }
page.url = latestUpdatesInitialUrl()
override fun latestUpdatesRequest(page: Int): Request {
val url = if (page == 1) {
latestNextPage = null
map.latestupdates!!.url
} else {
latestNextPage!!
} }
return when (map.latestupdates!!.method?.toLowerCase()) { return when (map.latestupdates!!.method?.toLowerCase()) {
"post" -> POST(page.url, headers, map.latestupdates.createForm()) "post" -> POST(url, headers, map.latestupdates.createForm())
else -> GET(page.url, headers) else -> GET(url, headers)
} }
} }
override fun latestUpdatesInitialUrl() = map.latestupdates!!.url override fun latestUpdatesParse(response: Response): MangasPage {
override fun latestUpdatesParse(response: Response, page: MangasPage) {
val document = response.asJsoup() 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() title = element.text()
setUrlWithoutDomain(element.attr("href")) setUrlWithoutDomain(element.attr("href"))
page.mangas.add(this)
} }
} }
map.latestupdates.next_url_css?.let { selector -> popularNextPage = map.latestupdates.next_url_css?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.absUrl("href") 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 document = response.asJsoup()
val manga = SManga.create()
with(map.manga) { with(map.manga) {
val pool = parts.get(document) val pool = parts.get(document)
@ -130,18 +143,21 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
manga.description = summary?.process(document, pool) manga.description = summary?.process(document, pool)
manga.thumbnail_url = cover?.process(document, pool) manga.thumbnail_url = cover?.process(document, pool)
manga.genre = genres?.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 document = response.asJsoup()
val chapters = mutableListOf<SChapter>()
with(map.chapters) { with(map.chapters) {
val pool = emptyMap<String, Element>() val pool = emptyMap<String, Element>()
val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH) val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
for (element in document.select(chapter_css)) { for (element in document.select(chapter_css)) {
val chapter = Chapter.create() val chapter = SChapter.create()
element.select(title).first().let { element.select(title).first().let {
chapter.name = it.text() chapter.name = it.text()
chapter.setUrlWithoutDomain(it.attr("href")) chapter.setUrlWithoutDomain(it.attr("href"))
@ -151,12 +167,15 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
chapters.add(chapter) 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 body = response.body().string()
val url = response.request().url().toString() val url = response.request().url().toString()
val pages = mutableListOf<Page>()
// TODO lazy initialization in Kotlin 1.1 // TODO lazy initialization in Kotlin 1.1
val document = Jsoup.parse(body, url) val document = Jsoup.parse(body, url)
@ -194,6 +213,7 @@ class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
page.imageUrl = url page.imageUrl = url
} }
} }
return pages
} }
override fun imageUrlParse(response: Response): String { override fun imageUrlParse(response: Response): String {

View file

@ -2,7 +2,7 @@
package eu.kanade.tachiyomi.data.source.online 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.FormBody
import okhttp3.RequestBody import okhttp3.RequestBody
import org.jsoup.nodes.Document 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 { fun getStatus(document: Element, cache: Map<String, Element>): Int {
val text = process(document, cache) val text = process(document, cache)
complete?.let { complete?.let {
if (text.contains(it)) return Manga.COMPLETED if (text.contains(it)) return SManga.COMPLETED
} }
ongoing?.let { ongoing?.let {
if (text.contains(it)) return Manga.ONGOING if (text.contains(it)) return SManga.ONGOING
} }
licensed?.let { licensed?.let {
if (text.contains(it)) return Manga.LICENSED if (text.contains(it)) return SManga.LICENSED
} }
return Manga.UNKNOWN return SManga.UNKNOWN
} }
} }

View file

@ -1,13 +1,10 @@
package eu.kanade.tachiyomi.data.source.online.english package eu.kanade.tachiyomi.data.source.online.english
import android.text.Html 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.GET
import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.network.asObservable import eu.kanade.tachiyomi.data.network.asObservable
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.LoginSource import eu.kanade.tachiyomi.data.source.online.LoginSource
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@ -25,7 +22,9 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.regex.Pattern 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" override val name = "Batoto"
@ -56,70 +55,46 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
.add("Referer", "http://bato.to/reader") .add("Referer", "http://bato.to/reader")
.build() .build()
override fun popularMangaInitialUrl() = "$baseUrl/search_ajax?order_cond=views&order=desc&p=1" override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/search_ajax?order_cond=views&order=desc&p=$page", headers)
override fun latestUpdatesInitialUrl() = "$baseUrl/search_ajax?order_cond=update&order=desc&p=1"
override fun popularMangaParse(response: Response, page: MangasPage) {
val document = response.asJsoup()
for (element in document.select(popularMangaSelector())) {
Manga.create(id).apply {
popularMangaFromElement(element, this)
page.mangas.add(this)
}
} }
page.nextPageUrl = document.select(popularMangaNextPageSelector()).first()?.let { override fun latestUpdatesRequest(page: Int): Request {
"$baseUrl/search_ajax?order_cond=views&order=desc&p=${page.page + 1}" return GET("$baseUrl/search_ajax?order_cond=update&order=desc&p=$page", headers)
}
}
override fun latestUpdatesParse(response: Response, page: MangasPage) {
val document = response.asJsoup()
for (element in document.select(latestUpdatesSelector())) {
Manga.create(id).apply {
latestUpdatesFromElement(element, this)
page.mangas.add(this)
}
}
page.nextPageUrl = document.select(latestUpdatesNextPageSelector()).first()?.let {
"$baseUrl/search_ajax?order_cond=update&order=desc&p=${page.page + 1}"
}
} }
override fun popularMangaSelector() = "tr:has(a)" override fun popularMangaSelector() = "tr:has(a)"
override fun latestUpdatesSelector() = "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 { element.select("a[href^=http://bato.to]").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text().trim() manga.title = it.text().trim()
} }
return manga
} }
override fun latestUpdatesFromElement(element: Element, manga: Manga) { override fun latestUpdatesFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun popularMangaNextPageSelector() = "#show_more_row" override fun popularMangaNextPageSelector() = "#show_more_row"
override fun latestUpdatesNextPageSelector() = "#show_more_row" override fun latestUpdatesNextPageSelector() = "#show_more_row"
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>) = searchMangaUrl(query, filters, 1) override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
private fun searchMangaUrl(query: String, filterStates: List<Filter<*>>, page: Int): String {
val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder() val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c") if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
var genres = "" var genres = ""
for (filter in if (filterStates.isEmpty()) filters else filterStates) { filters.forEach { filter ->
when (filter) { when (filter) {
is Status -> if (filter.state != Filter.TriState.STATE_IGNORE) { is Status -> if (!filter.isIgnored()) {
url.addQueryParameter("completed", if (filter.state == Filter.TriState.STATE_EXCLUDE) "i" else "c") url.addQueryParameter("completed", if (filter.isExcluded()) "i" else "c")
} }
is Genre -> if (filter.state != Filter.TriState.STATE_IGNORE) { is Genre -> if (!filter.isIgnored()) {
genres += (if (filter.state == Filter.TriState.STATE_EXCLUDE) ";e" else ";i") + filter.id genres += (if (filter.isExcluded()) ";e" else ";i") + filter.id
} }
is TextField -> { is TextField -> {
if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state) 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) if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
url.addQueryParameter("p", page.toString()) url.addQueryParameter("p", page.toString())
return url.toString() return GET(url.toString(), headers)
}
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)
}
} }
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element, manga: Manga) { override fun searchMangaFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun mangaDetailsRequest(manga: Manga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
val mangaId = manga.url.substringAfterLast("r") val mangaId = manga.url.substringAfterLast("r")
return GET("$baseUrl/comic_pop?id=$mangaId", headers) 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 tbody = document.select("tbody").first()
val artistElement = tbody.select("tr:contains(Author/Artist:)").first() val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
val manga = SManga.create()
manga.author = artistElement.selectText("td:eq(1)") manga.author = artistElement.selectText("td:eq(1)")
manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author
manga.description = tbody.selectText("tr:contains(Description:) > td:eq(1)") 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.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.status = parseStatus(document.selectText("tr:contains(Status:) > td:eq(1)"))
manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ") manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ")
return manga
} }
private fun parseStatus(status: String?) = when (status) { private fun parseStatus(status: String?) = when (status) {
"Ongoing" -> Manga.ONGOING "Ongoing" -> SManga.ONGOING
"Complete" -> Manga.COMPLETED "Complete" -> SManga.COMPLETED
else -> Manga.UNKNOWN else -> SManga.UNKNOWN
} }
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) { override fun chapterListParse(response: Response): List<SChapter> {
val body = response.body().string() val body = response.body().string()
val matcher = staffNotice.matcher(body) val matcher = staffNotice.matcher(body)
if (matcher.find()) { if (matcher.find()) {
@Suppress("DEPRECATION")
val notice = Html.fromHtml(matcher.group(1)).toString().trim() val notice = Html.fromHtml(matcher.group(1)).toString().trim()
throw Exception(notice) throw Exception(notice)
} }
val document = response.asJsoup(body) val document = response.asJsoup(body)
return document.select(chapterListSelector()).map { chapterFromElement(it) }
for (element in document.select(chapterListSelector())) {
Chapter.create().apply {
chapterFromElement(element, this)
chapters.add(this)
}
}
} }
override fun chapterListSelector() = "tr.row.lang_English.chapter_row" override fun 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 urlElement = element.select("a[href^=http://bato.to/reader").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = element.select("td").getOrNull(4)?.let { chapter.date_upload = element.select("td").getOrNull(4)?.let {
parseDateFromElement(it) parseDateFromElement(it)
} ?: 0 } ?: 0
return chapter
} }
private fun parseDateFromElement(dateElement: Element): Long { private fun parseDateFromElement(dateElement: Element): Long {
@ -246,12 +199,13 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
return date.time return date.time
} }
override fun pageListRequest(chapter: Chapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val id = chapter.url.substringAfterLast("#") val id = chapter.url.substringAfterLast("#")
return GET("$baseUrl/areader?id=$id&p=1", pageHeaders) 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() val selectElement = document.select("#page_select").first()
if (selectElement != null) { if (selectElement != null) {
for ((i, element) in selectElement.select("option").withIndex()) { 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"))) pages.add(Page(i, "", element.attr("src")))
} }
} }
return pages
} }
override fun imageUrlRequest(page: Page): Request { 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" } 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()) { if (!isLogged()) {
val username = preferences.sourceUsername(this) val username = preferences.sourceUsername(this)
val password = preferences.sourcePassword(this) val password = preferences.sourcePassword(this)
@ -328,7 +283,7 @@ class Batoto(override val id: Int) : ParsedOnlineSource(), LoginSource {
override fun toString(): String = name 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 Genre(name: String, val id: Int) : Filter.TriState(name)
private class TextField(name: String, val key: String) : Filter.Text(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) 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})` // const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Genre("${el.textContent.trim()}", ${id})`
// }).join(',\n') // }).join(',\n')
// on https://bato.to/search // on https://bato.to/search
override fun getFilterList(): List<Filter<*>> = listOf( override fun getFilterList() = FilterList(
TextField("Author", "artist_name"), 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"))), 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(), Status(),

View file

@ -1,11 +1,8 @@
package eu.kanade.tachiyomi.data.source.online.english 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.GET
import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -16,7 +13,9 @@ import org.jsoup.nodes.Element
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.regex.Pattern import java.util.regex.Pattern
class Kissmanga(override val id: Int) : ParsedOnlineSource() { class Kissmanga : ParsedOnlineSource() {
override val id: Long = 4
override val name = "Kissmanga" override val name = "Kissmanga"
@ -28,38 +27,40 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
override val client: OkHttpClient = network.cloudflareClient 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 popularMangaSelector() = "table.listing tr:gt(1)"
override fun latestUpdatesSelector() = "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 { element.select("td a:eq(0)").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
return manga
} }
override fun latestUpdatesFromElement(element: Element, manga: Manga) { override fun latestUpdatesFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun popularMangaNextPageSelector() = "li > a:contains( Next)" override fun popularMangaNextPageSelector() = "li > a:contains( Next)"
override fun latestUpdatesNextPageSelector(): String = "ul.pager > 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 { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query, filters)
}
val form = FormBody.Builder().apply { val form = FormBody.Builder().apply {
add("mangaName", query) 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) { when (filter) {
is Author -> add("authorArtist", filter.state) is Author -> add("authorArtist", filter.state)
is Status -> add("status", arrayOf("", "Completed", "Ongoing")[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 searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element, manga: Manga) { override fun searchMangaFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun searchMangaNextPageSelector() = null 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 infoElement = document.select("div.barContent").first()
val manga = SManga.create()
manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text() manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text() manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text() manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text()
manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) } manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src") manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
return manga
} }
fun parseStatus(status: String) = when { fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING status.contains("Ongoing") -> SManga.ONGOING
status.contains("Completed") -> Manga.COMPLETED status.contains("Completed") -> SManga.COMPLETED
else -> Manga.UNKNOWN else -> SManga.UNKNOWN
} }
override fun chapterListSelector() = "table.listing tr:gt(1)" 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 urlElement = element.select("a").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("MM/dd/yyyy").parse(it).time SimpleDateFormat("MM/dd/yyyy").parse(it).time
} ?: 0 } ?: 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 //language=RegExp
val p = Pattern.compile("""lstImages.push\("(.+?)"""") val p = Pattern.compile("""lstImages.push\("(.+?)"""")
val m = p.matcher(response.body().string()) val m = p.matcher(response.body().string())
@ -119,10 +123,11 @@ class Kissmanga(override val id: Int) : ParsedOnlineSource() {
while (m.find()) { while (m.find()) {
pages.add(Page(i++, "", m.group(1))) pages.add(Page(i++, "", m.group(1)))
} }
return pages
} }
// Not used override fun pageListParse(document: Document): List<Page> {
override fun pageListParse(document: Document, pages: MutableList<Page>) { throw Exception("Not used")
} }
override fun imageUrlRequest(page: Page) = GET(page.url) 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 Status() : Filter.TriState("Completed")
private class Author() : Filter.Text("Author") 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') // $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
// on http://kissmanga.com/AdvanceSearch // on http://kissmanga.com/AdvanceSearch
override fun getFilterList(): List<Filter<*>> = listOf( override fun getFilterList() = FilterList(
Author(), Author(),
Status(), Status(),
Filter.Header("Genres"), Filter.Header("Genres"),
Genre("Action", 0), Genre("4-Koma"),
Genre("Adult", 1), Genre("Action"),
Genre("Adventure", 2), Genre("Adult"),
Genre("Comedy", 3), Genre("Adventure"),
Genre("Comic", 4), Genre("Comedy"),
Genre("Cooking", 5), Genre("Comic"),
Genre("Doujinshi", 6), Genre("Cooking"),
Genre("Drama", 7), Genre("Doujinshi"),
Genre("Ecchi", 8), Genre("Drama"),
Genre("Fantasy", 9), Genre("Ecchi"),
Genre("Gender Bender", 10), Genre("Fantasy"),
Genre("Harem", 11), Genre("Gender Bender"),
Genre("Historical", 12), Genre("Harem"),
Genre("Horror", 13), Genre("Historical"),
Genre("Josei", 14), Genre("Horror"),
Genre("Lolicon", 15), Genre("Josei"),
Genre("Manga", 16), Genre("Lolicon"),
Genre("Manhua", 17), Genre("Manga"),
Genre("Manhwa", 18), Genre("Manhua"),
Genre("Martial Arts", 19), Genre("Manhwa"),
Genre("Mature", 20), Genre("Martial Arts"),
Genre("Mecha", 21), Genre("Mature"),
Genre("Medical", 22), Genre("Mecha"),
Genre("Music", 23), Genre("Medical"),
Genre("Mystery", 24), Genre("Music"),
Genre("One shot", 25), Genre("Mystery"),
Genre("Psychological", 26), Genre("One shot"),
Genre("Romance", 27), Genre("Psychological"),
Genre("School Life", 28), Genre("Romance"),
Genre("Sci-fi", 29), Genre("School Life"),
Genre("Seinen", 30), Genre("Sci-fi"),
Genre("Shotacon", 31), Genre("Seinen"),
Genre("Shoujo", 32), Genre("Shotacon"),
Genre("Shoujo Ai", 33), Genre("Shoujo"),
Genre("Shounen", 34), Genre("Shoujo Ai"),
Genre("Shounen Ai", 35), Genre("Shounen"),
Genre("Slice of Life", 36), Genre("Shounen Ai"),
Genre("Smut", 37), Genre("Slice of Life"),
Genre("Sports", 38), Genre("Smut"),
Genre("Supernatural", 39), Genre("Sports"),
Genre("Tragedy", 40), Genre("Supernatural"),
Genre("Webtoon", 41), Genre("Tragedy"),
Genre("Yaoi", 42), Genre("Webtoon"),
Genre("Yuri", 43) Genre("Yaoi"),
Genre("Yuri")
) )
} }

View file

@ -1,19 +1,19 @@
package eu.kanade.tachiyomi.data.source.online.english package eu.kanade.tachiyomi.data.source.online.english
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Response import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class Mangafox(override val id: Int) : ParsedOnlineSource() { class Mangafox : ParsedOnlineSource() {
override val id: Long = 3
override val name = "Mangafox" override val name = "Mangafox"
@ -23,32 +23,40 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
override val supportsLatest = true 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 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 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 { element.select("a.title").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
return manga
} }
override fun latestUpdatesFromElement(element: Element, manga: Manga) { override fun latestUpdatesFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun popularMangaNextPageSelector() = "a:has(span.next)" override fun popularMangaNextPageSelector() = "a:has(span.next)"
override fun latestUpdatesNextPageSelector() = "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) 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) { when (filter) {
is Genre -> url.addQueryParameter(filter.id, filter.state.toString()) is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
is TextField -> url.addQueryParameter(filter.key, filter.state) 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") 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 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 { element.select("a.title").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
return manga
} }
override fun searchMangaNextPageSelector() = "a:has(span.next)" 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 infoElement = document.select("div#title").first()
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first() val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
val sideInfoElement = document.select("#series_info").first() val sideInfoElement = document.select("#series_info").first()
val manga = SManga.create()
manga.author = rowElement.select("td:eq(1)").first()?.text() manga.author = rowElement.select("td:eq(1)").first()?.text()
manga.artist = rowElement.select("td:eq(2)").first()?.text() manga.artist = rowElement.select("td:eq(2)").first()?.text()
manga.genre = rowElement.select("td:eq(3)").first()?.text() manga.genre = rowElement.select("td:eq(3)").first()?.text()
manga.description = infoElement.select("p.summary").first()?.text() manga.description = infoElement.select("p.summary").first()?.text()
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) } manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src") manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
return manga
} }
private fun parseStatus(status: String) = when { private fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING status.contains("Ongoing") -> SManga.ONGOING
status.contains("Completed") -> Manga.COMPLETED status.contains("Completed") -> SManga.COMPLETED
else -> Manga.UNKNOWN else -> SManga.UNKNOWN
} }
override fun chapterListSelector() = "div#chapters li div" 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 urlElement = element.select("a.tips").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter
} }
private fun parseChapterDate(date: String): Long { 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>) { override fun pageListParse(document: Document): List<Page> {
val document = response.asJsoup() 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 { document.select("select.m").first()?.select("option:not([value=0])")?.forEach {
pages.add(Page(pages.size, "$url/${it.attr("value")}.html")) pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
} }
} return pages
// Not used, overrides parent.
override fun pageListParse(document: Document, pages: MutableList<Page>) {
} }
override fun imageUrlParse(document: Document): String { 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') // $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
// on http://mangafox.me/search.php // on http://mangafox.me/search.php
override fun getFilterList(): List<Filter<*>> = listOf( override fun getFilterList() = FilterList(
TextField("Author", "author"), TextField("Author", "author"),
TextField("Artist", "artist"), TextField("Artist", "artist"),
ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga", "1"), ListValue("Korean Manhwa", "2"), ListValue("Chinese Manhua", "3"))), ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga", "1"), ListValue("Korean Manhwa", "2"), ListValue("Chinese Manhua", "3"))),

View file

@ -1,17 +1,19 @@
package eu.kanade.tachiyomi.data.source.online.english package eu.kanade.tachiyomi.data.source.online.english
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class Mangahere(override val id: Int) : ParsedOnlineSource() { class Mangahere : ParsedOnlineSource() {
override val id: Long = 2
override val name = "Mangahere" override val name = "Mangahere"
@ -21,36 +23,42 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
override val supportsLatest = true override val supportsLatest = true
override fun popularMangaInitialUrl() = "$baseUrl/directory/?views.za"
override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?last_chapter_time.za"
override fun popularMangaSelector() = "div.directory_list > ul > li" override fun popularMangaSelector() = "div.directory_list > ul > li"
override fun latestUpdatesSelector() = "div.directory_list > ul > li" override fun 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 { element.select(query).first().let {
manga.setUrlWithoutDomain(it.attr("href")) 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() 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) { override fun popularMangaFromElement(element: Element): SManga {
mangaFromElement("div.title > a", element, manga) return mangaFromElement("div.title > a", element)
} }
override fun latestUpdatesFromElement(element: Element, manga: Manga) { override fun latestUpdatesFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun popularMangaNextPageSelector() = "div.next-page > a.next" override fun popularMangaNextPageSelector() = "div.next-page > a.next"
override fun latestUpdatesNextPageSelector() = "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) 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) { when (filter) {
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state]) is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
is Genre -> url.addQueryParameter(filter.id, filter.state.toString()) 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") 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 searchMangaSelector() = "div.result_search > dl:has(dt)"
override fun searchMangaFromElement(element: Element, manga: Manga) { override fun searchMangaFromElement(element: Element): SManga {
mangaFromElement("a.manga_info", element, manga) return mangaFromElement("a.manga_info", element)
} }
override fun searchMangaNextPageSelector() = "div.next-page > a.next" 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 detailElement = document.select(".manga_detail_top").first()
val infoElement = detailElement.select(".detail_topText").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.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.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.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less") manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) } manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src") manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
return manga
} }
private fun parseStatus(status: String) = when { private fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING status.contains("Ongoing") -> SManga.ONGOING
status.contains("Completed") -> Manga.COMPLETED status.contains("Completed") -> SManga.COMPLETED
else -> Manga.UNKNOWN else -> SManga.UNKNOWN
} }
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li" 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 parentEl = element.select("span.left").first()
val urlElement = parentEl.select("a").first() val urlElement = parentEl.select("a").first()
@ -106,9 +116,11 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
title = " - " + title title = " - " + title
} }
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() + volume + title chapter.name = urlElement.text() + volume + title
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter
} }
private fun parseChapterDate(date: String): Long { 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 { document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
pages.add(Page(pages.size, it.attr("value"))) pages.add(Page(pages.size, it.attr("value")))
} }
pages.getOrNull(0)?.imageUrl = imageUrlParse(document) pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
return pages
} }
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src") 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') // [...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 // http://www.mangahere.co/advsearch.htm
override fun getFilterList(): List<Filter<*>> = listOf( override fun getFilterList() = FilterList(
TextField("Author", "author"), TextField("Author", "author"),
TextField("Artist", "artist"), 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"))), ListField("Type", "direction", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga (read from right to left)", "rl"), ListValue("Korean Manhwa (read from left to right)", "lr"))),

View file

@ -1,22 +1,19 @@
package eu.kanade.tachiyomi.data.source.online.english 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.network.POST
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.regex.Pattern import java.util.regex.Pattern
class Mangasee(override val id: Int) : ParsedOnlineSource() { class Mangasee : ParsedOnlineSource() {
override val id: Long = 9
override val name = "Mangasee" override val name = "Mangasee"
@ -30,46 +27,32 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
private val indexPattern = Pattern.compile("-index-(.*?)-") 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 popularMangaSelector() = "div.requested > div.row"
override fun popularMangaRequest(page: MangasPage): Request { override fun popularMangaRequest(page: Int): Request {
if (page.page == 1) { val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending&todo=1")
page.url = popularMangaInitialUrl()
}
val (body, requestUrl) = convertQueryToPost(page)
return POST(requestUrl, headers, body.build()) return POST(requestUrl, headers, body.build())
} }
override fun popularMangaParse(response: Response, page: MangasPage) { override fun popularMangaFromElement(element: Element): SManga {
val document = response.asJsoup() val manga = SManga.create()
for (element in document.select(popularMangaSelector())) {
Manga.create(id).apply {
popularMangaFromElement(element, this)
page.mangas.add(this)
}
}
page.nextPageUrl = page.url
}
override fun popularMangaFromElement(element: Element, manga: Manga) {
element.select("a.resultLink").first().let { element.select("a.resultLink").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
return manga
} }
// Not used, overrides parent. override fun popularMangaNextPageSelector() = "button.requestMore"
override fun popularMangaNextPageSelector() = ""
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() val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
if (!query.isEmpty()) url.addQueryParameter("keyword", query) if (!query.isEmpty()) url.addQueryParameter("keyword", query)
var genres: String? = null var genres: String? = null
var genresNo: 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) { when (filter) {
is Sort -> filter.values[filter.state].keys.forEachIndexed { i, s -> is Sort -> filter.values[filter.state].keys.forEachIndexed { i, s ->
url.addQueryParameter(s, filter.values[filter.state].values[i]) 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 (genres != null) url.addQueryParameter("genre", genres)
if (genresNo != null) url.addQueryParameter("genreNo", genresNo) if (genresNo != null) url.addQueryParameter("genreNo", genresNo)
return url.toString()
}
override fun searchMangaSelector() = "div.searchResults > div.requested > div.row" val (body, requestUrl) = convertQueryToPost(page, url.toString())
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query, filters)
}
val (body, requestUrl) = convertQueryToPost(page)
return POST(requestUrl, headers, body.build()) return POST(requestUrl, headers, body.build())
} }
private fun convertQueryToPost(page: MangasPage): Pair<FormBody.Builder, String> { private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
val url = HttpUrl.parse(page.url) val url = HttpUrl.parse(url)
val body = FormBody.Builder().add("page", page.page.toString()) val body = FormBody.Builder().add("page", page.toString())
for (i in 0..url.querySize() - 1) { for (i in 0..url.querySize() - 1) {
body.add(url.queryParameterName(i), url.queryParameterValue(i)) body.add(url.queryParameterName(i), url.queryParameterValue(i))
} }
@ -107,63 +82,57 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
return Pair(body, requestUrl) return Pair(body, requestUrl)
} }
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) { override fun searchMangaFromElement(element: Element): SManga {
val document = response.asJsoup() val manga = SManga.create()
for (element in document.select(popularMangaSelector())) {
Manga.create(id).apply {
popularMangaFromElement(element, this)
page.mangas.add(this)
}
}
page.nextPageUrl = page.url
}
override fun searchMangaFromElement(element: Element, manga: Manga) {
element.select("a.resultLink").first().let { element.select("a.resultLink").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
return manga
} }
// Not used, overrides parent. override fun searchMangaNextPageSelector() = "button.requestMore"
override fun searchMangaNextPageSelector() = ""
override fun mangaDetailsParse(document: Document, manga: Manga) { override fun mangaDetailsParse(document: Document): SManga {
val detailElement = document.select("div.well > div.row").first() val detailElement = document.select("div.well > div.row").first()
val manga = SManga.create()
manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text() 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.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.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) } manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src") manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
return manga
} }
private fun parseStatus(status: String) = when { private fun parseStatus(status: String) = when {
status.contains("Ongoing (Scan)") -> Manga.ONGOING status.contains("Ongoing (Scan)") -> SManga.ONGOING
status.contains("Complete (Scan)") -> Manga.COMPLETED status.contains("Complete (Scan)") -> SManga.COMPLETED
else -> Manga.UNKNOWN else -> SManga.UNKNOWN
} }
override fun chapterListSelector() = "div.chapter-list > a" 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 urlElement = element.select("a").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: "" chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: ""
chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0
return chapter
} }
private fun parseChapterDate(dateAsString: String): Long { private fun parseChapterDate(dateAsString: String): Long {
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time
} }
override fun pageListParse(response: Response, pages: MutableList<Page>) { override fun pageListParse(document: Document): List<Page> {
val document = response.asJsoup() val fullUrl = document.baseUri()
val fullUrl = response.request().url().toString()
val url = fullUrl.substringBeforeLast('/') val url = fullUrl.substringBeforeLast('/')
val pages = mutableListOf<Page>()
val series = document.select("input.IndexName").first().attr("value") val series = document.select("input.IndexName").first().attr("value")
val chapter = document.select("span.CurChapter").first().text() val chapter = document.select("span.CurChapter").first().text()
var index = "" 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.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html"))
} }
pages.getOrNull(0)?.imageUrl = imageUrlParse(document) pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
} return pages
// Not used, overrides parent.
override fun pageListParse(document: Document, pages: MutableList<Page>) {
} }
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src") 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') // [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
// http://mangasee.co/advanced-search/ // http://mangasee.co/advanced-search/
override fun getFilterList(): List<Filter<*>> = listOf( override fun getFilterList() = FilterList(
TextField("Years", "year"), TextField("Years", "year"),
TextField("Author", "author"), TextField("Author", "author"),
Sort("Sort By", arrayOf(SortOption("Alphabetical A-Z", emptyArray(), emptyArray()), Sort("Sort By", arrayOf(SortOption("Alphabetical A-Z", emptyArray(), emptyArray()),
@ -249,34 +215,18 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
Genre("Yuri") Genre("Yuri")
) )
override fun latestUpdatesInitialUrl(): String = "http://mangaseeonline.net/home/latest.request.php" override fun latestUpdatesNextPageSelector() = "button.requestMore"
// Not used, overrides parent.
override fun latestUpdatesNextPageSelector(): String = ""
override fun latestUpdatesSelector(): String = "a.latestSeries" override fun latestUpdatesSelector(): String = "a.latestSeries"
override fun latestUpdatesRequest(page: MangasPage): Request { override fun latestUpdatesRequest(page: Int): Request {
if (page.page == 1) { val url = "http://mangaseeonline.net/home/latest.request.php"
page.url = latestUpdatesInitialUrl() val (body, requestUrl) = convertQueryToPost(page, url)
}
val (body, requestUrl) = convertQueryToPost(page)
return POST(requestUrl, headers, body.build()) return POST(requestUrl, headers, body.build())
} }
override fun latestUpdatesParse(response: Response, page: MangasPage) { override fun latestUpdatesFromElement(element: Element): SManga {
val document = response.asJsoup() val manga = SManga.create()
for (element in document.select(latestUpdatesSelector())) {
Manga.create(id).apply {
latestUpdatesFromElement(element, this)
page.mangas.add(this)
}
}
page.nextPageUrl = page.url
}
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
element.select("a.latestSeries").first().let { element.select("a.latestSeries").first().let {
val chapterUrl = it.attr("href") val chapterUrl = it.attr("href")
val indexOfMangaUrl = chapterUrl.indexOf("-chapter-") val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
@ -288,6 +238,7 @@ class Mangasee(override val id: Int) : ParsedOnlineSource() {
manga.setUrlWithoutDomain("/manga" + mangaUrl) manga.setUrlWithoutDomain("/manga" + mangaUrl)
manga.title = title manga.title = title
} }
return manga
} }
} }

View file

@ -1,10 +1,8 @@
package eu.kanade.tachiyomi.data.source.online.english package eu.kanade.tachiyomi.data.source.online.english
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -13,7 +11,9 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.util.* import java.util.*
class Readmangatoday(override val id: Int) : ParsedOnlineSource() { class Readmangatoday : ParsedOnlineSource() {
override val id: Long = 8
override val name = "ReadMangaToday" override val name = "ReadMangaToday"
@ -33,41 +33,39 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
add("X-Requested-With", "XMLHttpRequest") 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 popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
override fun latestUpdatesSelector() = "div.hot-manga > div.style-grid > 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 { element.select("div.title > h2 > a").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title") manga.title = it.attr("title")
} }
return manga
} }
override fun latestUpdatesFromElement(element: Element, manga: Manga) { override fun latestUpdatesFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)" 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 latestUpdatesNextPageSelector() = "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 searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val builder = okhttp3.FormBody.Builder() val builder = okhttp3.FormBody.Builder()
builder.add("manga-name", query) 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) { when (filter) {
is TextField -> builder.add(filter.key, filter.state) is TextField -> builder.add(filter.key, filter.state)
is Type -> builder.add("type", arrayOf("all", "japanese", "korean", "chinese")[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) { is Genre -> when (filter.state) {
Filter.TriState.STATE_INCLUDE -> builder.add("include[]", filter.id.toString()) Filter.TriState.STATE_INCLUDE -> builder.add("include[]", filter.id.toString())
Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", 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 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 { element.select("div.title > h2 > a").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title") manga.title = it.attr("title")
} }
return manga
} }
override fun searchMangaNextPageSelector() = "div.next-page > a.next" 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 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.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.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.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text()
manga.description = detailElement.select("li.movie-detail").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.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") manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src")
return manga
} }
private fun parseStatus(status: String) = when { private fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING status.contains("Ongoing") -> SManga.ONGOING
status.contains("Completed") -> Manga.COMPLETED status.contains("Completed") -> SManga.COMPLETED
else -> Manga.UNKNOWN else -> SManga.UNKNOWN
} }
override fun chapterListSelector() = "ul.chp_lst > li" 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 urlElement = element.select("a").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.select("span.val").text() chapter.name = urlElement.select("span.val").text()
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter
} }
private fun parseChapterDate(date: String): Long { private fun parseChapterDate(date: String): Long {
@ -125,7 +128,7 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
if (dateWords.size == 3) { if (dateWords.size == 3) {
val timeAgo = Integer.parseInt(dateWords[0]) val timeAgo = Integer.parseInt(dateWords[0])
var date: Calendar = Calendar.getInstance() val date: Calendar = Calendar.getInstance()
if (dateWords[1].contains("Minute")) { if (dateWords[1].contains("Minute")) {
date.add(Calendar.MINUTE, -timeAgo) date.add(Calendar.MINUTE, -timeAgo)
@ -141,17 +144,19 @@ class Readmangatoday(override val id: Int) : ParsedOnlineSource() {
date.add(Calendar.YEAR, -timeAgo) date.add(Calendar.YEAR, -timeAgo)
} }
return date.getTimeInMillis() return date.timeInMillis
} }
return 0L 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 { document.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option").forEach {
pages.add(Page(pages.size, it.attr("value"))) pages.add(Page(pages.size, it.attr("value")))
} }
pages.getOrNull(0)?.imageUrl = imageUrlParse(document) pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
return pages
} }
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src") override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
@ -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') // [...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 // http://www.readmanga.today/advanced-search
override fun getFilterList(): List<Filter<*>> = listOf( override fun getFilterList() = FilterList(
TextField("Author", "author-name"), TextField("Author", "author-name"),
TextField("Artist", "artist-name"), TextField("Artist", "artist-name"),
Type(), Type(),

View file

@ -1,16 +1,19 @@
package eu.kanade.tachiyomi.data.source.online.german package eu.kanade.tachiyomi.data.source.online.german
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.model.FilterList
import eu.kanade.tachiyomi.data.source.model.Page 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.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.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
class WieManga(override val id: Int) : ParsedOnlineSource() { class WieManga : ParsedOnlineSource() {
override val id: Long = 10
override val name = "Wie Manga!" override val name = "Wie Manga!"
@ -20,50 +23,61 @@ class WieManga(override val id: Int) : ParsedOnlineSource() {
override val supportsLatest = true 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 popularMangaSelector() = ".booklist td > div"
override fun latestUpdatesSelector() = ".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 image = element.select("dt img")
val title = element.select("dd a:first-child") val title = element.select("dd a:first-child")
val manga = SManga.create()
manga.setUrlWithoutDomain(title.attr("href")) manga.setUrlWithoutDomain(title.attr("href"))
manga.title = title.text() manga.title = title.text()
manga.thumbnail_url = image.attr("src") manga.thumbnail_url = image.attr("src")
return manga
} }
override fun latestUpdatesFromElement(element: Element, manga: Manga) { override fun latestUpdatesFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun popularMangaNextPageSelector() = null override fun popularMangaNextPageSelector() = null
override fun latestUpdatesNextPageSelector() = 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 searchMangaSelector() = ".searchresult td > div"
override fun searchMangaFromElement(element: Element, manga: Manga) { override fun searchMangaFromElement(element: Element): SManga {
val image = element.select(".resultimg img") val image = element.select(".resultimg img")
val title = element.select(".resultbookname") val title = element.select(".resultbookname")
val manga = SManga.create()
manga.setUrlWithoutDomain(title.attr("href")) manga.setUrlWithoutDomain(title.attr("href"))
manga.title = title.text() manga.title = title.text()
manga.thumbnail_url = image.attr("src") manga.thumbnail_url = image.attr("src")
return manga
} }
override fun searchMangaNextPageSelector() = ".pagetor a.l" 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 imageElement = document.select(".bookmessgae tr > td:nth-child(1)").first()
val infoElement = document.select(".bookmessgae tr > td:nth-child(2)").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.author = infoElement.select("dd:nth-of-type(2) a").first()?.text()
manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text() manga.artist = infoElement.select("dd:nth-of-type(3) a").first()?.text()
manga.description = infoElement.select("dl > dt:last-child").first()?.text()?.replaceFirst("Beschreibung", "") manga.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") if (manga.artist == "RSS")
manga.artist = null manga.artist = null
return manga
} }
override fun chapterListSelector() = ".chapterlist tr:not(:first-child)" 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 urlElement = element.select(".col1 a").first()
val dateElement = element.select(".col3 a").first() val dateElement = element.select(".col3 a").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = dateElement?.text()?.let { parseChapterDate(it) } ?: 0
return chapter
} }
private fun parseChapterDate(date: String): Long { private fun parseChapterDate(date: String): Long {
return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time return SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(date).time
} }
override fun pageListParse(response: Response, pages: MutableList<Page>) { override fun pageListParse(document: Document): List<Page> {
val document = response.asJsoup() val pages = mutableListOf<Page>()
document.select("select#page").first().select("option").forEach { document.select("select#page").first().select("option").forEach {
pages.add(Page(pages.size, it.attr("value"))) pages.add(Page(pages.size, it.attr("value")))
} }
} return pages
override fun pageListParse(document: Document, pages: MutableList<Page>) {
} }
override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src") override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")

View file

@ -1,18 +1,19 @@
package eu.kanade.tachiyomi.data.source.online.russian package eu.kanade.tachiyomi.data.source.online.russian
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class Mangachan(override val id: Int) : ParsedOnlineSource() { class Mangachan : ParsedOnlineSource() {
override val id: Long = 7
override val name = "Mangachan" override val name = "Mangachan"
@ -22,23 +23,28 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
override val supportsLatest = true 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 searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = if (query.isNotEmpty()) {
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String { "$baseUrl/?do=search&subaction=search&story=$query"
if (query.isNotEmpty()) {
return "$baseUrl/?do=search&subaction=search&story=$query"
} else { } else {
val filt = filters.filter { it.state != Filter.TriState.STATE_IGNORE } val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
if (filt.isNotEmpty()) { if (filt.isNotEmpty()) {
var genres = "" var genres = ""
filt.forEach { genres += (if (it.state == Filter.TriState.STATE_EXCLUDE) "-" else "") + (it as Genre).id + '+' } filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
return "$baseUrl/tags/${genres.dropLast(1)}" "$baseUrl/tags/${genres.dropLast(1)}"
} else { } 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" override fun popularMangaSelector() = "div.content_row"
@ -47,22 +53,26 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
override fun searchMangaSelector() = popularMangaSelector() 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 { element.select("h2 > a").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() 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 { element.select("a:nth-child(1)").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() manga.title = it.text()
} }
return manga
} }
override fun searchMangaFromElement(element: Element, manga: Manga) { override fun searchMangaFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun popularMangaNextPageSelector() = "a:contains(Вперед)" override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
@ -73,74 +83,80 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
private fun searchGenresNextPageSelector() = popularMangaNextPageSelector() 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() val document = response.asJsoup()
for (element in document.select(searchMangaSelector())) { val mangas = document.select(searchMangaSelector()).map { element ->
Manga.create(id).apply { searchMangaFromElement(element)
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
}
} }
searchGenresNextPageSelector().let { selector -> // FIXME
if (page.nextPageUrl.isNullOrEmpty() && !allIgnore) { // val allIgnore = filters.all { it.state == Filter.TriState.STATE_IGNORE }
val url = document.select(selector).first()?.attr("href") // searchMangaNextPageSelector().let { selector ->
page.nextPageUrl = searchMangaInitialUrl(query, filters) + url // 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 infoElement = document.select("table.mangatitle").first()
val descElement = document.select("div#description").first() val descElement = document.select("div#description").first()
val imgElement = document.select("img#cover").first() val imgElement = document.select("img#cover").first()
val manga = SManga.create()
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text() manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text() manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text()) manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
manga.description = descElement.textNodes().first().text() manga.description = descElement.textNodes().first().text()
manga.thumbnail_url = baseUrl + imgElement.attr("src") manga.thumbnail_url = baseUrl + imgElement.attr("src")
return manga
} }
private fun parseStatus(element: String): Int { private fun parseStatus(element: String): Int {
when { when {
element.contains("перевод завершен") -> return Manga.COMPLETED element.contains("перевод завершен") -> return SManga.COMPLETED
element.contains("перевод продолжается") -> return Manga.ONGOING element.contains("перевод продолжается") -> return SManga.ONGOING
else -> return Manga.UNKNOWN else -> return SManga.UNKNOWN
} }
} }
override fun chapterListSelector() = "table.table_cha tr:gt(1)" 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 urlElement = element.select("a").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = element.select("div.date").first()?.text()?.let { chapter.date_upload = element.select("div.date").first()?.text()?.let {
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
} ?: 0 } ?: 0
return chapter
} }
override fun pageListParse(response: Response, pages: MutableList<Page>) { override fun pageListParse(response: Response): List<Page> {
val html = response.body().string() val html = response.body().string()
val beginIndex = html.indexOf("fullimg\":[") + 10 val beginIndex = html.indexOf("fullimg\":[") + 10
val endIndex = html.indexOf(",]", beginIndex) val endIndex = html.indexOf(",]", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "") val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
val pageUrls = trimmedHtml.split(',') 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) = "" override fun imageUrlParse(document: Document) = ""
@ -152,7 +168,7 @@ class Mangachan(override val id: Int) : ParsedOnlineSource() {
* return `Genre("${id.replace("_", " ")}")` }).join(',\n') * return `Genre("${id.replace("_", " ")}")` }).join(',\n')
* on http://mangachan.me/ * on http://mangachan.me/
*/ */
override fun getFilterList(): List<Filter<*>> = listOf( override fun getFilterList() = FilterList(
Genre("18 плюс"), Genre("18 плюс"),
Genre("bdsm"), Genre("bdsm"),
Genre("арт"), Genre("арт"),

View file

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.data.source.online.russian package eu.kanade.tachiyomi.data.source.online.russian
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -11,7 +11,9 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
class Mintmanga(override val id: Int) : ParsedOnlineSource() { class Mintmanga : ParsedOnlineSource() {
override val id: Long = 6
override val name = "Mintmanga" override val name = "Mintmanga"
@ -21,77 +23,89 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
override val supportsLatest = true 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 latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
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 popularMangaSelector() = "div.desc"
override fun latestUpdatesSelector() = "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 { element.select("h3 > a").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title") manga.title = it.attr("title")
} }
return manga
} }
override fun latestUpdatesFromElement(element: Element, manga: Manga) { override fun latestUpdatesFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun popularMangaNextPageSelector() = "a.nextLink" override fun popularMangaNextPageSelector() = "a.nextLink"
override fun latestUpdatesNextPageSelector() = "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 searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element, manga: Manga) { override fun searchMangaFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
// max 200 results // max 200 results
override fun searchMangaNextPageSelector() = null 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 infoElement = document.select("div.leftContent").first()
val manga = SManga.create()
manga.author = infoElement.select("span.elem_author").first()?.text() manga.author = infoElement.select("span.elem_author").first()?.text()
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",") manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
manga.description = infoElement.select("div.manga-description").text() manga.description = infoElement.select("div.manga-description").text()
manga.status = parseStatus(infoElement.html()) manga.status = parseStatus(infoElement.html())
manga.thumbnail_url = infoElement.select("img").attr("data-full") manga.thumbnail_url = infoElement.select("img").attr("data-full")
return manga
} }
private fun parseStatus(element: String): Int { private fun parseStatus(element: String): Int {
when { when {
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
else -> return Manga.UNKNOWN else -> return SManga.UNKNOWN
} }
} }
override fun chapterListSelector() = "div.chapters-link tbody tr" 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 urlElement = element.select("a").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1") chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
chapter.name = urlElement.text().replace(" новое", "") chapter.name = urlElement.text().replace(" новое", "")
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
} ?: 0 } ?: 0
return chapter
} }
override fun prepareNewChapter(chapter: Chapter, manga: Manga) { override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
chapter.chapter_number = -2f 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 html = response.body().string()
val beginIndex = html.indexOf("rm_h.init( [") val beginIndex = html.indexOf("rm_h.init( [")
val endIndex = html.indexOf("], 0, false);", beginIndex) val endIndex = html.indexOf("], 0, false);", beginIndex)
@ -100,14 +114,18 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
val p = Pattern.compile("'.+?','.+?',\".+?\"") val p = Pattern.compile("'.+?','.+?',\".+?\"")
val m = p.matcher(trimmedHtml) val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>()
var i = 0 var i = 0
while (m.find()) { while (m.find()) {
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',') val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2])) pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
} }
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) = "" override fun imageUrlParse(document: Document) = ""
@ -119,7 +137,7 @@ class Mintmanga(override val id: Int) : ParsedOnlineSource() {
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
* on http://mintmanga.com/search * on http://mintmanga.com/search
*/ */
override fun getFilterList(): List<Filter<*>> = listOf( override fun getFilterList() = FilterList(
Genre("арт", "el_2220"), Genre("арт", "el_2220"),
Genre("бара", "el_1353"), Genre("бара", "el_1353"),
Genre("боевик", "el_1346"), Genre("боевик", "el_1346"),

View file

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.data.source.online.russian package eu.kanade.tachiyomi.data.source.online.russian
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -11,7 +11,9 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
class Readmanga(override val id: Int) : ParsedOnlineSource() { class Readmanga : ParsedOnlineSource() {
override val id: Long = 5
override val name = "Readmanga" override val name = "Readmanga"
@ -21,77 +23,89 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
override val supportsLatest = true 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 popularMangaSelector() = "div.desc"
override fun latestUpdatesSelector() = "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 { element.select("h3 > a").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.attr("title") manga.title = it.attr("title")
} }
return manga
} }
override fun latestUpdatesFromElement(element: Element, manga: Manga) { override fun latestUpdatesFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
override fun popularMangaNextPageSelector() = "a.nextLink" override fun popularMangaNextPageSelector() = "a.nextLink"
override fun latestUpdatesNextPageSelector() = "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 searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element, manga: Manga) { override fun searchMangaFromElement(element: Element): SManga {
popularMangaFromElement(element, manga) return popularMangaFromElement(element)
} }
// max 200 results // max 200 results
override fun searchMangaNextPageSelector() = null 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 infoElement = document.select("div.leftContent").first()
val manga = SManga.create()
manga.author = infoElement.select("span.elem_author").first()?.text() manga.author = infoElement.select("span.elem_author").first()?.text()
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",") manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
manga.description = infoElement.select("div.manga-description").text() manga.description = infoElement.select("div.manga-description").text()
manga.status = parseStatus(infoElement.html()) manga.status = parseStatus(infoElement.html())
manga.thumbnail_url = infoElement.select("img").attr("data-full") manga.thumbnail_url = infoElement.select("img").attr("data-full")
return manga
} }
private fun parseStatus(element: String): Int { private fun parseStatus(element: String): Int {
when { when {
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return Manga.LICENSED element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return Manga.COMPLETED element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
element.contains("<b>Перевод:</b> продолжается") -> return Manga.ONGOING element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
else -> return Manga.UNKNOWN else -> return SManga.UNKNOWN
} }
} }
override fun chapterListSelector() = "div.chapters-link tbody tr" 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 urlElement = element.select("a").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1") chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
chapter.name = urlElement.text().replace(" новое", "") chapter.name = urlElement.text().replace(" новое", "")
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let { chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
} ?: 0 } ?: 0
return chapter
} }
override fun prepareNewChapter(chapter: Chapter, manga: Manga) { override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
chapter.chapter_number = -2f 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 html = response.body().string()
val beginIndex = html.indexOf("rm_h.init( [") val beginIndex = html.indexOf("rm_h.init( [")
val endIndex = html.indexOf("], 0, false);", beginIndex) val endIndex = html.indexOf("], 0, false);", beginIndex)
@ -100,14 +114,18 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
val p = Pattern.compile("'.+?','.+?',\".+?\"") val p = Pattern.compile("'.+?','.+?',\".+?\"")
val m = p.matcher(trimmedHtml) val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>()
var i = 0 var i = 0
while (m.find()) { while (m.find()) {
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',') val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2])) pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
} }
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) = "" override fun imageUrlParse(document: Document) = ""
@ -119,7 +137,7 @@ class Readmanga(override val id: Int) : ParsedOnlineSource() {
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
* on http://readmanga.me/search * on http://readmanga.me/search
*/ */
override fun getFilterList(): List<Filter<*>> = listOf( override fun getFilterList() = FilterList(
Genre("арт", "el_5685"), Genre("арт", "el_5685"),
Genre("боевик", "el_2155"), Genre("боевик", "el_2155"),
Genre("боевые искусства", "el_2143"), Genre("боевые искусства", "el_2143"),

View file

@ -14,6 +14,7 @@ import com.afollestad.materialdialogs.MaterialDialog
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga 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.data.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
@ -32,7 +33,6 @@ import nucleus.factory.RequiresPresenter
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import timber.log.Timber
import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.MILLISECONDS
/** /**
@ -104,6 +104,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
private val toolbar: Toolbar private val toolbar: Toolbar
get() = (activity as MainActivity).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. * Navigation view containing filter items.
*/ */
@ -201,8 +206,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
} else if (source != presenter.source) { } else if (source != presenter.source) {
selectedIndex = position selectedIndex = position
showProgressBar() showProgressBar()
glm.scrollToPositionWithOffset(0, 0) adapter.clear()
llm.scrollToPositionWithOffset(0, 0)
presenter.setActiveSource(source) presenter.setActiveSource(source)
navView?.setFilters(presenter.sourceFilters) navView?.setFilters(presenter.sourceFilters)
activity.invalidateOptionsMenu() activity.invalidateOptionsMenu()
@ -233,14 +237,14 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
} }
navView.onSearchClicked = { navView.onSearchClicked = {
val allDefault = (0..navView.adapter.items.lastIndex) val allDefault = navView.adapter.items.hasSameState(presenter.source.getFilterList())
.none { navView.adapter.items[it].state != presenter.source.filters[it].state } showProgressBar()
adapter.clear()
presenter.setSourceFilter(if (allDefault) emptyList() else navView.adapter.items) presenter.setSourceFilter(if (allDefault) FilterList() else navView.adapter.items)
} }
navView.onResetClicked = { navView.onResetClicked = {
presenter.appliedFilters = emptyList() presenter.appliedFilters = FilterList()
val newFilters = presenter.source.getFilterList() val newFilters = presenter.source.getFilterList()
presenter.sourceFilters = newFilters presenter.sourceFilters = newFilters
navView.setFilters(newFilters) navView.setFilters(newFilters)
@ -277,7 +281,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
// Setup filters button // Setup filters button
menu.findItem(R.id.action_set_filter).apply { menu.findItem(R.id.action_set_filter).apply {
icon.mutate() icon.mutate()
if (presenter.source.filters.isEmpty()) { if (presenter.sourceFilters.isEmpty()) {
isEnabled = false isEnabled = false
icon.alpha = 128 icon.alpha = 128
} else { } else {
@ -355,8 +359,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
return return
showProgressBar() showProgressBar()
catalogue_grid.layoutManager.scrollToPosition(0) adapter.clear()
catalogue_list.layoutManager.scrollToPosition(0)
presenter.restartPager(newQuery) presenter.restartPager(newQuery)
} }
@ -394,9 +397,11 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
*/ */
fun onAddPageError(error: Throwable) { fun onAddPageError(error: Throwable) {
hideProgressBar() 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) { setAction(R.string.action_retry) {
showProgressBar() showProgressBar()
presenter.requestNext() presenter.requestNext()
@ -456,6 +461,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
*/ */
private fun showProgressBar() { private fun showProgressBar() {
progress.visibility = ProgressBar.VISIBLE progress.visibility = ProgressBar.VISIBLE
snack?.dismiss()
snack = null
} }
/** /**
@ -463,6 +470,8 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
*/ */
private fun showGridProgressBar() { private fun showGridProgressBar() {
progress_grid.visibility = ProgressBar.VISIBLE progress_grid.visibility = ProgressBar.VISIBLE
snack?.dismiss()
snack = null
} }
/** /**

View file

@ -9,7 +9,8 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.TextView import android.widget.TextView
import eu.kanade.tachiyomi.R 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.dpToPx
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
@ -38,14 +39,14 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
reset_btn.setOnClickListener { onResetClicked() } reset_btn.setOnClickListener { onResetClicked() }
} }
fun setFilters(items: List<Filter<*>>) { fun setFilters(items: FilterList) {
adapter.items = items adapter.items = items
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
inner class Adapter : RecyclerView.Adapter<Holder>() { inner class Adapter : RecyclerView.Adapter<Holder>() {
var items: List<Filter<*>> = emptyList() var items: FilterList = FilterList()
override fun getItemCount(): Int { override fun getItemCount(): Int {
return items.size return items.size

View file

@ -1,28 +1,32 @@
package eu.kanade.tachiyomi.ui.catalogue 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.model.MangasPage
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
import rx.Observable 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> { override fun requestNext(): Observable<MangasPage> {
val lastPage = lastPage val page = currentPage
val page = if (lastPage == null)
MangasPage(1)
else
MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! }
val observable = if (query.isBlank() && filters.isEmpty()) val observable = if (query.isBlank() && filters.isEmpty())
source.fetchPopularManga(page) source.fetchPopularManga(page)
else else
source.fetchSearchManga(page, query, filters) source.fetchSearchManga(page, query, filters)
return transformer(observable) return observable
.doOnNext { results.onNext(it) } .subscribeOn(Schedulers.io())
.doOnNext { this@CataloguePager.lastPage = it } .observeOn(AndroidSchedulers.mainThread())
.doOnNext {
if (it.mangas.isNotEmpty()) {
onPageReceived(it)
} else {
throw NoResultsException()
}
}
} }
} }

View file

@ -6,12 +6,12 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.CatalogueSource
import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.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.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 eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
@ -55,7 +55,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
/** /**
* Active source. * Active source.
*/ */
lateinit var source: OnlineSource lateinit var source: CatalogueSource
private set private set
/** /**
@ -67,12 +67,12 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
/** /**
* Modifiable list of filters. * 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. * 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. * Pager containing a list of manga results.
@ -136,7 +136,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* @param query the query. * @param query the query.
* @param filters the current state of the filters (for search mode). * @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.query = query
this.appliedFilters = filters this.appliedFilters = filters
@ -145,11 +145,17 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
// Create a new pager. // Create a new pager.
pager = createPager(query, filters) pager = createPager(query, filters)
val sourceId = source.id
// Prepare the pager. // Prepare the pager.
pagerSubscription?.let { remove(it) } pagerSubscription?.let { remove(it) }
pagerSubscription = pager.results() pagerSubscription = pager.results()
.subscribeReplay({ view, page -> .observeOn(Schedulers.io())
view.onAddPage(page.page, page.mangas) .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 -> }, { view, error ->
Timber.e(error) Timber.e(error)
}) })
@ -165,7 +171,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
if (!hasNextPage()) return if (!hasNextPage()) return
pageSubscription?.let { remove(it) } pageSubscription?.let { remove(it) }
pageSubscription = pager.requestNext { getPageTransformer(it) } pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ view, page -> .subscribeFirst({ view, page ->
// Nothing to do when onNext is emitted. // Nothing to do when onNext is emitted.
}, CatalogueFragment::onAddPageError) }, CatalogueFragment::onAddPageError)
@ -175,7 +181,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* Returns true if the last fetched page has a next page. * Returns true if the last fetched page has a next page.
*/ */
fun hasNextPage(): Boolean { fun hasNextPage(): Boolean {
return pager.hasNextPage() return pager.hasNextPage
} }
/** /**
@ -183,12 +189,12 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* *
* @param source the new active source. * @param source the new active source.
*/ */
fun setActiveSource(source: OnlineSource) { fun setActiveSource(source: CatalogueSource) {
prefs.lastUsedCatalogueSource().set(source.id) prefs.lastUsedCatalogueSource().set(source.id)
this.source = source this.source = source
sourceFilters = source.getFilterList() 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?.let { remove(it) }
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io()) initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
.flatMap { Observable.from(it) } .flatMap { Observable.from(it) }
.filter { !it.initialized } .filter { it.thumbnail_url == null && !it.initialized }
.concatMap { getMangaDetailsObservable(it) } .concatMap { getMangaDetailsObservable(it) }
.onBackpressureBuffer() .onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -221,41 +227,21 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
.apply { add(this) } .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 * 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. * 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. * @return a manga from the database.
*/ */
private fun networkToLocalManga(networkManga: Manga): Manga { private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
var localManga = db.getManga(networkManga.url, source.id).executeAsBlocking() var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
if (localManga == null) { if (localManga == null) {
val result = db.insertManga(networkManga).executeAsBlocking() val newManga = Manga.create(sManga.url, sManga.title, sourceId)
networkManga.id = result.insertedId() newManga.copyFrom(sManga)
localManga = networkManga val result = db.insertManga(newManga).executeAsBlocking()
newManga.id = result.insertedId()
localManga = newManga
} }
return localManga return localManga
} }
@ -279,6 +265,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
return source.fetchMangaDetails(manga) return source.fetchMangaDetails(manga)
.flatMap { networkManga -> .flatMap { networkManga ->
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.initialized = true
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
Observable.just(manga) Observable.just(manga)
} }
@ -290,13 +277,13 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* *
* @return a source. * @return a source.
*/ */
fun getLastUsedSource(): OnlineSource { fun getLastUsedSource(): CatalogueSource {
val id = prefs.lastUsedCatalogueSource().get() ?: -1 val id = prefs.lastUsedCatalogueSource().get() ?: -1
val source = sourceManager.get(id) val source = sourceManager.get(id)
if (!isValidSource(source)) { if (!isValidSource(source)) {
return findFirstValidSource() 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. * @return the index of the first valid source.
*/ */
fun findFirstValidSource(): OnlineSource { fun findFirstValidSource(): CatalogueSource {
return sources.first { isValidSource(it) } return sources.first { isValidSource(it) }
} }
/** /**
* Returns a list of enabled sources ordered by language and name. * 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 languages = prefs.enabledLanguages().getOrDefault()
val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault() val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
@ -336,7 +323,7 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
languages.add("en") languages.add("en")
} }
return sourceManager.getOnlineSources() return sourceManager.getCatalogueSources()
.filter { it.lang in languages } .filter { it.lang in languages }
.filterNot { it.id.toString() in hiddenCatalogues } .filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" } .sortedBy { "(${it.lang}) ${it.name}" }
@ -365,13 +352,13 @@ open class CataloguePresenter : BasePresenter<CatalogueFragment>() {
/** /**
* Set the filter states for the current source. * 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) 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) return CataloguePager(source, query, filters)
} }

View file

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.ui.catalogue
class NoResultsException : Exception()

View file

@ -1,25 +1,31 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.MangasPage
import rx.subjects.PublishSubject import eu.kanade.tachiyomi.data.source.model.SManga
import rx.Observable import rx.Observable
/** /**
* A general pager for source requests (latest updates, popular, search) * 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() return results.asObservable()
} }
fun hasNextPage(): Boolean { abstract fun requestNext(): Observable<MangasPage>
return lastPage == null || lastPage?.nextPageUrl != null
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>
} }

View file

@ -1,28 +1,22 @@
package eu.kanade.tachiyomi.ui.latest_updates 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.model.MangasPage
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.ui.catalogue.Pager import eu.kanade.tachiyomi.ui.catalogue.Pager
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
/** /**
* LatestUpdatesPager inherited from the general Pager. * 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> { override fun requestNext(): Observable<MangasPage> {
val lastPage = lastPage return source.fetchLatestUpdates(currentPage)
.subscribeOn(Schedulers.io())
val page = if (lastPage == null) .observeOn(AndroidSchedulers.mainThread())
MangasPage(1) .doOnNext { onPageReceived(it) }
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 }
} }
} }

View file

@ -1,26 +1,26 @@
package eu.kanade.tachiyomi.ui.latest_updates 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.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.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.Pager import eu.kanade.tachiyomi.ui.catalogue.Pager
import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter
/** /**
* Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter. * Presenter of [LatestUpdatesFragment]. Inherit CataloguePresenter.
*/ */
class LatestUpdatesPresenter : CataloguePresenter() { class LatestUpdatesPresenter : CataloguePresenter() {
override fun createPager(query: String, filters: List<Filter<*>>): Pager { override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source) return LatestUpdatesPager(source)
} }
override fun getEnabledSources(): List<OnlineSource> { override fun getEnabledSources(): List<CatalogueSource> {
return super.getEnabledSources().filter { it.supportsLatest } return super.getEnabledSources().filter { it.supportsLatest }
} }
override fun isValidSource(source: Source?): Boolean { override fun isValidSource(source: Source?): Boolean {
return super.isValidSource(source) && (source as OnlineSource).supportsLatest return super.isValidSource(source) && (source as CatalogueSource).supportsLatest
} }
} }

View file

@ -125,7 +125,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
*/ */
private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> { private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
// Cached list of downloaded manga directories given a source id. // 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. // Cached list of downloaded chapter directories for a manga.
val chapterDirectories = mutableMapOf<Long, Boolean>() val chapterDirectories = mutableMapOf<Long, Boolean>()

View file

@ -197,7 +197,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
/** /**
* Returns an observable that updates the chapter list with the latest from the source. * 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()) .subscribeOn(Schedulers.io())
.map { syncChaptersWithSource(db, it, manga, source) } .map { syncChaptersWithSource(db, it, manga, source) }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())

View file

@ -15,6 +15,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.Source 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.data.source.online.OnlineSource
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.ui.manga.MangaActivity import eu.kanade.tachiyomi.ui.manga.MangaActivity
@ -122,9 +123,9 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
// Update status TextView. // Update status TextView.
manga_status.setText(when (manga.status) { manga_status.setText(when (manga.status) {
Manga.ONGOING -> R.string.ongoing SManga.ONGOING -> R.string.ongoing
Manga.COMPLETED -> R.string.completed SManga.COMPLETED -> R.string.completed
Manga.LICENSED -> R.string.licensed SManga.LICENSED -> R.string.licensed
else -> R.string.unknown else -> R.string.unknown
}) })

View file

@ -99,9 +99,10 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
* @return manga information. * @return manga information.
*/ */
private fun fetchMangaObs(): Observable<Manga> { private fun fetchMangaObs(): Observable<Manga> {
return source.fetchMangaDetails(manga) return Observable.defer { source.fetchMangaDetails(manga) }
.flatMap { networkManga -> .flatMap { networkManga ->
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.initialized = true
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
Observable.just<Manga>(manga) Observable.just<Manga>(manga)
} }

View file

@ -4,6 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.source.online.fetchImageFromCacheThenNet
import eu.kanade.tachiyomi.data.source.online.fetchPageListFromCacheThenNet
import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.util.plusAssign
import rx.Observable import rx.Observable
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
@ -36,9 +39,11 @@ class ChapterLoader(
} }
private fun prepareOnlineReading() { private fun prepareOnlineReading() {
if (source !is OnlineSource) return
subscriptions += Observable.defer { Observable.just(queue.take().page) } subscriptions += Observable.defer { Observable.just(queue.take().page) }
.filter { it.status == Page.QUEUE } .filter { it.status == Page.QUEUE }
.concatMap { source.fetchImage(it) } .concatMap { source.fetchImageFromCacheThenNet(it) }
.repeat() .repeat()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe({ .subscribe({
@ -57,6 +62,10 @@ class ChapterLoader(
Observable.just(chapter.pages!!) Observable.just(chapter.pages!!)
} }
.doOnNext { 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 // Now that the number of pages is known, fix the requested page if the last one
// was requested. // was requested.
if (chapter.requestedPage == -1) { if (chapter.requestedPage == -1) {
@ -76,8 +85,8 @@ class ChapterLoader(
// Fetch the page list from disk. // Fetch the page list from disk.
downloadManager.buildPageList(source, manga, chapter) downloadManager.buildPageList(source, manga, chapter)
} else { } else {
// Fetch the page list from cache or fallback to network (source as? OnlineSource)?.fetchPageListFromCacheThenNet(chapter)
source.fetchPageList(chapter) ?: source.fetchPageList(chapter)
} }
} }
.doOnNext { pages -> .doOnNext { pages ->
@ -111,6 +120,8 @@ class ChapterLoader(
queue.offer(PriorityPage(page, 2)) queue.offer(PriorityPage(page, 2))
} }
private data class PriorityPage(val page: Page, val priority: Int): Comparable<PriorityPage> { private data class PriorityPage(val page: Page, val priority: Int): Comparable<PriorityPage> {
companion object { companion object {

View file

@ -372,7 +372,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
Observable.fromCallable { Observable.fromCallable {
// Cache current page list progress for online chapters to allow a faster reopen // Cache current page list progress for online chapters to allow a faster reopen
if (!chapter.isDownloaded) { if (!chapter.isDownloaded) {
source.let { if (it is OnlineSource) it.savePageList(chapter, pages) } source.let {
if (it is OnlineSource) chapterCache.putPageListToCache(chapter, pages)
}
} }
try { try {

View file

@ -130,7 +130,7 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
*/ */
private fun setDownloadedChapters(chapters: List<RecentChapter>) { private fun setDownloadedChapters(chapters: List<RecentChapter>) {
// Cached list of downloaded manga directories. // 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. // Cached list of downloaded chapter directories for a manga.
val chapterDirectories = mutableMapOf<Long, Array<UniFile>>() val chapterDirectories = mutableMapOf<Long, Array<UniFile>>()

View file

@ -123,13 +123,14 @@ class SettingsSourcesFragment : SettingsFragment() {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SOURCE_CHANGE_REQUEST) { if (requestCode == SOURCE_CHANGE_REQUEST && data != null) {
val pref = findPreference(getSourceKey(resultCode)) as? LoginCheckBoxPreference val sourceId = data.getLongExtra("key", -1L)
val pref = findPreference(getSourceKey(sourceId)) as? LoginCheckBoxPreference
pref?.notifyChanged() pref?.notifyChanged()
} }
} }
private fun getSourceKey(sourceId: Int): String { private fun getSourceKey(sourceId: Long): String {
return "source_$sourceId" return "source_$sourceId"
} }

View file

@ -81,8 +81,9 @@ class SettingsTrackingFragment : SettingsFragment() {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SYNC_CHANGE_REQUEST) { if (requestCode == SYNC_CHANGE_REQUEST && data != null) {
updatePreference(resultCode) val serviceId = data.getIntExtra("key", -1)
updatePreference(serviceId)
} }
} }

View file

@ -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.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.model.SChapter
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import java.util.* 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. * Helper method for syncing the list of chapters from the source with the ones from the database.
* *
* @param db 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 manga the manga of the chapters.
* @param source the source of the chapters. * @param source the source of the chapters.
* @return a pair of new insertions and deletions. * @return a pair of new insertions and deletions.
*/ */
fun syncChaptersWithSource(db: DatabaseHelper, fun syncChaptersWithSource(db: DatabaseHelper,
sourceChapters: List<Chapter>, rawSourceChapters: List<SChapter>,
manga: Manga, manga: Manga,
source: Source) : Pair<List<Chapter>, List<Chapter>> { source: Source) : Pair<List<Chapter>, List<Chapter>> {
if (rawSourceChapters.isEmpty()) {
throw Exception("No chapters found")
}
// Chapters from db. // Chapters from db.
val dbChapters = db.getChapters(manga).executeAsBlocking() val dbChapters = db.getChapters(manga).executeAsBlocking()
// Fix manga id and order in source. val sourceChapters = rawSourceChapters.mapIndexed { i, sChapter ->
sourceChapters.forEachIndexed { i, chapter -> Chapter.create().apply {
chapter.manga_id = manga.id copyFrom(sChapter)
chapter.source_order = i manga_id = manga.id
source_order = i
}
} }
// Chapters from the source not in db. // Chapters from the source not in db.

View file

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

View file

@ -21,10 +21,11 @@ fun View.getCoordinates() = Point((left + right) / 2, (top + bottom) / 2)
* @param length the duration of the snack. * @param length the duration of the snack.
* @param f a function to execute in the snack, allowing for example to define a custom action. * @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 snack = Snackbar.make(this, message, length)
val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView
textView.setTextColor(Color.WHITE) textView.setTextColor(Color.WHITE)
snack.f() snack.f()
snack.show() snack.show()
return snack
} }

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.widget.preference package eu.kanade.tachiyomi.widget.preference
import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
@ -70,7 +71,8 @@ abstract class LoginDialogPreference : DialogFragment() {
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog) 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() protected abstract fun checkLogin()

View file

@ -19,7 +19,7 @@ class SourceLoginDialog : LoginDialogPreference() {
fun newInstance(source: Source): LoginDialogPreference { fun newInstance(source: Source): LoginDialogPreference {
val fragment = SourceLoginDialog() val fragment = SourceLoginDialog()
val bundle = Bundle(1) val bundle = Bundle(1)
bundle.putInt("key", source.id) bundle.putLong("key", source.id)
fragment.arguments = bundle fragment.arguments = bundle
return fragment return fragment
} }
@ -32,7 +32,7 @@ class SourceLoginDialog : LoginDialogPreference() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val sourceId = arguments.getInt("key") val sourceId = arguments.getLong("key")
source = sourceManager.get(sourceId) as LoginSource source = sourceManager.get(sourceId) as LoginSource
} }

View file

@ -64,7 +64,7 @@
<string name="pref_enable_automatic_updates_key">automatic_updates</string> <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_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> <string name="pref_download_new_key">download_new</string>

View file

@ -9,12 +9,13 @@ import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.SChapter
import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.OnlineSource
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith 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.mockito.Mockito.* import org.mockito.Mockito.*
import org.robolectric.Robolectric import org.robolectric.Robolectric
@ -51,7 +52,7 @@ class LibraryUpdateServiceTest {
service = Robolectric.setupService(LibraryUpdateService::class.java) service = Robolectric.setupService(LibraryUpdateService::class.java)
source = mock(OnlineSource::class.java) source = mock(OnlineSource::class.java)
`when`(service.sourceManager.get(anyInt())).thenReturn(source) `when`(service.sourceManager.get(anyLong())).thenReturn(source)
} }
@Test @Test
@ -91,7 +92,7 @@ class LibraryUpdateServiceTest {
// One of the updates will fail // One of the updates will fail
`when`(source.fetchChapterList(favManga[0])).thenReturn(Observable.just(chapters)) `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)) `when`(source.fetchChapterList(favManga[2])).thenReturn(Observable.just(chapters3))
val intent = Intent() val intent = Intent()
@ -117,8 +118,7 @@ class LibraryUpdateServiceTest {
private fun createManga(vararg urls: String): List<Manga> { private fun createManga(vararg urls: String): List<Manga> {
val list = ArrayList<Manga>() val list = ArrayList<Manga>()
for (url in urls) { for (url in urls) {
val m = Manga.create(url) val m = Manga.create(url, url.substring(1))
m.title = url.substring(1)
m.favorite = true m.favorite = true
list.add(m) list.add(m)
} }