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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

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

View File

@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.data.source
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Environment
import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
@ -18,29 +21,47 @@ import java.io.File
open class SourceManager(private val context: Context) {
private val sourcesMap = createSources()
private val sourcesMap = mutableMapOf<Long, Source>()
open fun get(sourceKey: Int): Source? {
init {
createSources()
}
open fun get(sourceKey: Long): Source? {
return sourcesMap[sourceKey]
}
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
fun getOnlineSources() = sourcesMap.values.filterIsInstance<OnlineSource>()
private fun createOnlineSourceList(): List<Source> = listOf(
Batoto(1),
Mangahere(2),
Mangafox(3),
Kissmanga(4),
Readmanga(5),
Mintmanga(6),
Mangachan(7),
Readmangatoday(8),
Mangasee(9),
WieManga(10)
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
private fun createSources() {
createExtensionSources().forEach { registerSource(it) }
createYamlSources().forEach { registerSource(it) }
createInternalSources().forEach { registerSource(it) }
}
private fun registerSource(source: Source, overwrite: Boolean = false) {
if (overwrite || !sourcesMap.containsKey(source.id)) {
sourcesMap.put(source.id, source)
}
}
private fun createInternalSources(): List<Source> = listOf(
Batoto(),
Mangahere(),
Mangafox(),
Kissmanga(),
Readmanga(),
Mintmanga(),
Mangachan(),
Readmangatoday(),
Mangasee(),
WieManga()
)
private fun createSources(): Map<Int, Source> = hashMapOf<Int, Source>().apply {
createOnlineSourceList().forEach { put(it.id, it) }
private fun createYamlSources(): List<Source> {
val sources = mutableListOf<Source>()
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
File.separator + context.getString(R.string.app_name), "parsers")
@ -50,12 +71,89 @@ open class SourceManager(private val context: Context) {
for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
try {
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
YamlOnlineSource(map).let { put(it.id, it) }
sources.add(YamlOnlineSource(map))
} catch (e: Exception) {
Timber.e("Error loading source from file. Bad format?")
}
}
}
return sources
}
private fun createExtensionSources(): List<OnlineSource> {
val pkgManager = context.packageManager
val flags = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
val installedPkgs = pkgManager.getInstalledPackages(flags)
val extPkgs = installedPkgs.filter { it.reqFeatures.orEmpty().any { it.name == FEATURE } }
val sources = mutableListOf<OnlineSource>()
for (pkgInfo in extPkgs) {
val appInfo = pkgManager.getApplicationInfo(pkgInfo.packageName,
PackageManager.GET_META_DATA) ?: continue
val data = appInfo.metaData
val extName = data.getString(NAME)
val version = data.getInt(VERSION)
val sourceClass = extendClassName(data.getString(SOURCE), pkgInfo.packageName)
val ext = Extension(extName, appInfo, version, sourceClass)
if (!validateExtension(ext)) {
continue
}
val instance = loadExtension(ext, pkgManager)
if (instance == null) {
Timber.e("Extension error: failed to instance $extName")
continue
}
sources.add(instance)
}
return sources
}
private fun validateExtension(ext: Extension): Boolean {
if (ext.version < LIB_VERSION_MIN || ext.version > LIB_VERSION_MAX) {
Timber.e("Extension error: ${ext.name} has version ${ext.version}, while only versions "
+ "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed")
return false
}
return true
}
private fun loadExtension(ext: Extension, pkgManager: PackageManager): OnlineSource? {
return try {
val classLoader = PathClassLoader(ext.appInfo.sourceDir, null, context.classLoader)
val resources = pkgManager.getResourcesForApplication(ext.appInfo)
Class.forName(ext.sourceClass, false, classLoader).newInstance() as? OnlineSource
} catch (e: Exception) {
null
} catch (e: LinkageError) {
null
}
}
private fun extendClassName(className: String, packageName: String): String {
return if (className.startsWith(".")) {
packageName + className
} else {
className
}
}
class Extension(val name: String,
val appInfo: ApplicationInfo,
val version: Int,
val sourceClass: String)
private companion object {
const val FEATURE = "tachiyomi.extension"
const val NAME = "tachiyomi.extension.name"
const val VERSION = "tachiyomi.extension.version"
const val SOURCE = "tachiyomi.extension.source"
const val LIB_VERSION_MIN = 1
const val LIB_VERSION_MAX = 1
}
}

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
import eu.kanade.tachiyomi.data.database.models.Manga
class MangasPage(val page: Int) {
val mangas: MutableList<Manga> = mutableListOf()
lateinit var url: String
var nextPageUrl: String? = null
}
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)

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
import android.net.Uri
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.network.asObservableSuccess
import eu.kanade.tachiyomi.data.network.newCallWithProgress
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.UrlUtil
import eu.kanade.tachiyomi.data.source.CatalogueSource
import eu.kanade.tachiyomi.data.source.model.*
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.URI
import java.net.URISyntaxException
import java.security.MessageDigest
/**
* A simple implementation for sources from a website.
*/
abstract class OnlineSource() : Source {
abstract class OnlineSource : CatalogueSource {
/**
* Network service.
*/
val network: NetworkHelper by injectLazy()
/**
* Chapter cache.
*/
val chapterCache: ChapterCache by injectLazy()
/**
* Preferences helper.
*/
@ -46,24 +38,26 @@ abstract class OnlineSource() : Source {
abstract val baseUrl: String
/**
* An ISO 639-1 compliant language code (two characters in lower case).
* Version id used to generate the source id. If the site completely changes and urls are
* incompatible, you may increase this value and it'll be considered as a new source.
*/
abstract val lang: String
open val versionId = 1
/**
* Whether the source has support for latest updates.
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
* of the MD5 of the string: sourcename/language/versionId
* Note the generated id sets the sign bit to 0.
*/
abstract val supportsLatest: Boolean
override val id by lazy {
val key = "${name.toLowerCase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE
}
/**
* Headers used for requests.
*/
val headers by lazy { headersBuilder().build() }
/**
* Genre filters.
*/
val filters by lazy { getFilterList() }
val headers: Headers by lazy { headersBuilder().build() }
/**
* Default network client for doing requests.
@ -87,121 +81,88 @@ abstract class OnlineSource() : Source {
* Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method.
*
* @param page the page object where the information will be saved, like the list of manga,
* the current page and the next page url.
* @param page the page number to retrieve.
*/
open fun fetchPopularManga(page: MangasPage): Observable<MangasPage> = client
.newCall(popularMangaRequest(page))
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response ->
popularMangaParse(response, page)
page
popularMangaParse(response)
}
}
/**
* Returns the request for the popular manga given the page. Override only if it's needed to
* send different headers or request method like POST.
* Returns the request for the popular manga given the page.
*
* @param page the page object.
* @param page the page number to retrieve.
*/
open protected fun popularMangaRequest(page: MangasPage): Request {
if (page.page == 1) {
page.url = popularMangaInitialUrl()
}
return GET(page.url, headers)
}
abstract protected fun popularMangaRequest(page: Int): Request
/**
* Returns the absolute url of the first page to popular manga.
*/
abstract protected fun popularMangaInitialUrl(): String
/**
* Parse the response from the site. It should add a list of manga and the absolute url to the
* next page (if it has a next one) to [page].
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
* @param page the page object to be filled.
*/
abstract protected fun popularMangaParse(response: Response, page: MangasPage)
abstract protected fun popularMangaParse(response: Response): MangasPage
/**
* Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method.
*
* @param page the page object where the information will be saved, like the list of manga,
* the current page and the next page url.
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter<*>>): Observable<MangasPage> = client
.newCall(searchMangaRequest(page, query, filters))
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response, page, query, filters)
page
searchMangaParse(response)
}
}
/**
* Returns the request for the search manga given the page. Override only if it's needed to
* send different headers or request method like POST.
* Returns the request for the search manga given the page.
*
* @param page the page object.
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter<*>>): Request {
if (page.page == 1) {
page.url = searchMangaInitialUrl(query, filters)
}
return GET(page.url, headers)
}
abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
/**
* Returns the absolute url of the first page to popular manga.
*
* @param query the search query.
*/
abstract protected fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String
/**
* Parse the response from the site. It should add a list of manga and the absolute url to the
* next page (if it has a next one) to [page].
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
* @param page the page object to be filled.
* @param query the search query.
*/
abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>)
abstract protected fun searchMangaParse(response: Response): MangasPage
/**
* Returns an observable containing a page with a list of latest manga.
* Returns an observable containing a page with a list of latest manga updates.
*
* @param page the page number to retrieve.
*/
open fun fetchLatestUpdates(page: MangasPage): Observable<MangasPage> = client
.newCall(latestUpdatesRequest(page))
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.map { response ->
latestUpdatesParse(response, page)
page
latestUpdatesParse(response)
}
}
/**
* Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/
open protected fun latestUpdatesRequest(page: MangasPage): Request {
if (page.page == 1) {
page.url = latestUpdatesInitialUrl()
}
return GET(page.url, headers)
}
abstract protected fun latestUpdatesRequest(page: Int): Request
/**
* Returns the absolute url of the first page to latest manga.
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
abstract protected fun latestUpdatesInitialUrl(): String
/**
* Same as [popularMangaParse], but for latest manga.
*/
abstract protected fun latestUpdatesParse(response: Response, page: MangasPage)
abstract protected fun latestUpdatesParse(response: Response): MangasPage
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
@ -209,33 +170,30 @@ abstract class OnlineSource() : Source {
*
* @param manga the manga to be updated.
*/
override fun fetchMangaDetails(manga: Manga): Observable<Manga> = client
.newCall(mangaDetailsRequest(manga))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.map { response ->
Manga.create(manga.url, id).apply {
mangaDetailsParse(response, this)
initialized = true
mangaDetailsParse(response).apply { initialized = true }
}
}
/**
* Returns the request for updating a manga. Override only if it's needed to override the url,
* send different headers or request method like POST.
* Returns the request for the details of a manga. Override only if it's needed to change the
* url, send different headers or request method like POST.
*
* @param manga the manga to be updated.
*/
open fun mangaDetailsRequest(manga: Manga): Request {
open fun mangaDetailsRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers)
}
/**
* Parse the response from the site. It should fill [manga].
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
* @param manga the manga whose fields have to be filled.
*/
abstract protected fun mangaDetailsParse(response: Response, manga: Manga)
abstract protected fun mangaDetailsParse(response: Response): SManga
/**
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
@ -243,15 +201,11 @@ abstract class OnlineSource() : Source {
*
* @param manga the manga to look for chapters.
*/
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> = client
.newCall(chapterListRequest(manga))
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response ->
mutableListOf<Chapter>().apply {
chapterListParse(response, this)
if (isEmpty()) {
throw Exception("No chapters found")
}
chapterListParse(response)
}
}
@ -261,43 +215,27 @@ abstract class OnlineSource() : Source {
*
* @param manga the manga to look for chapters.
*/
open protected fun chapterListRequest(manga: Manga): Request {
open protected fun chapterListRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers)
}
/**
* Parse the response from the site. It should fill [chapters].
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
* @param chapters the chapter list to be filled.
*/
abstract protected fun chapterListParse(response: Response, chapters: MutableList<Chapter>)
abstract protected fun chapterListParse(response: Response): List<SChapter>
/**
* Returns an observable with the page list for a chapter. It tries to return the page list from
* the local cache, otherwise fallbacks to network calling [fetchPageListFromNetwork].
* Returns an observable with the page list for a chapter.
*
* @param chapter the chapter whose page list has to be fetched.
*/
final override fun fetchPageList(chapter: Chapter): Observable<List<Page>> = chapterCache
.getPageListFromCache(getChapterCacheKey(chapter))
.onErrorResumeNext { fetchPageListFromNetwork(chapter) }
/**
* Returns an observable with the page list for a chapter. Normally it's not needed to override
* this method.
*
* @param chapter the chapter whose page list has to be fetched.
*/
open fun fetchPageListFromNetwork(chapter: Chapter): Observable<List<Page>> = client
.newCall(pageListRequest(chapter))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
mutableListOf<Page>().apply {
pageListParse(response, this)
if (isEmpty()) {
throw Exception("Page list is empty")
}
pageListParse(response)
}
}
@ -305,24 +243,18 @@ abstract class OnlineSource() : Source {
* Returns the request for getting the page list. Override only if it's needed to override the
* url, send different headers or request method like POST.
*
* @param chapter the chapter whose page list has to be fetched
* @param chapter the chapter whose page list has to be fetched.
*/
open protected fun pageListRequest(chapter: Chapter): Request {
open protected fun pageListRequest(chapter: SChapter): Request {
return GET(baseUrl + chapter.url, headers)
}
/**
* Parse the response from the site. It should fill [pages].
* Parses the response from the site and returns a list of pages.
*
* @param response the response from the site.
* @param pages the page list to be filled.
*/
abstract protected fun pageListParse(response: Response, pages: MutableList<Page>)
/**
* Returns the key for the page list to be stored in [ChapterCache].
*/
private fun getChapterCacheKey(chapter: Chapter) = "$id${chapter.url}"
abstract protected fun pageListParse(response: Response): List<Page>
/**
* Returns an observable with the page containing the source url of the image. If there's any
@ -330,16 +262,10 @@ abstract class OnlineSource() : Source {
*
* @param page the page whose source image has to be fetched.
*/
open protected fun fetchImageUrl(page: Page): Observable<Page> {
page.status = Page.LOAD_PAGE
return client
.newCall(imageUrlRequest(page))
open fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page))
.asObservableSuccess()
.map { imageUrlParse(it) }
.doOnError { page.status = Page.ERROR }
.onErrorReturn { null }
.doOnNext { page.imageUrl = it }
.map { page }
}
/**
@ -353,31 +279,21 @@ abstract class OnlineSource() : Source {
}
/**
* Parse the response from the site. It should return the absolute url to the source image.
* Parses the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
abstract protected fun imageUrlParse(response: Response): String
/**
* Returns an observable of the page with the downloaded image.
*
* @param page the page whose source image has to be downloaded.
*/
final override fun fetchImage(page: Page): Observable<Page> =
if (page.imageUrl.isNullOrEmpty())
fetchImageUrl(page).flatMap { getCachedImage(it) }
else
getCachedImage(page)
/**
* Returns an observable with the response of the source image.
*
* @param page the page whose source image has to be downloaded.
*/
fun imageResponse(page: Page): Observable<Response> = client
.newCallWithProgress(imageRequest(page), page)
fun fetchImage(page: Page): Observable<Response> {
return client.newCallWithProgress(imageRequest(page), page)
.asObservableSuccess()
}
/**
* Returns the request for getting the source image. Override only if it's needed to override
@ -390,68 +306,44 @@ abstract class OnlineSource() : Source {
}
/**
* Returns an observable of the page that gets the image from the chapter or fallbacks to
* network and copies it to the cache calling [cacheImage].
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
* database and the urls could still work after a domain change.
*
* @param page the page.
* @param url the full url to the chapter.
*/
fun getCachedImage(page: Page): Observable<Page> {
val imageUrl = page.imageUrl ?: return Observable.just(page)
return Observable.just(page)
.flatMap {
if (!chapterCache.isImageInCache(imageUrl)) {
cacheImage(page)
} else {
Observable.just(page)
}
}
.doOnNext {
page.uri = Uri.fromFile(chapterCache.getImageFile(imageUrl))
page.status = Page.READY
}
.doOnError { page.status = Page.ERROR }
.onErrorReturn { page }
fun SChapter.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url)
}
/**
* Returns an observable of the page that downloads the image to [ChapterCache].
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from
* database and the urls could still work after a domain change.
*
* @param page the page.
* @param url the full url to the manga.
*/
private fun cacheImage(page: Page): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE
return imageResponse(page)
.doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) }
.map { page }
fun SManga.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url)
}
// Utility methods
fun fetchAllImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
.filter { !it.imageUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
fun fetchRemainingImageUrlsFromPageList(pages: List<Page>) = Observable.from(pages)
.filter { it.imageUrl.isNullOrEmpty() }
.concatMap { fetchImageUrl(it) }
fun savePageList(chapter: Chapter, pages: List<Page>?) {
if (pages != null) {
chapterCache.putPageListToCache(getChapterCacheKey(chapter), pages)
/**
* Returns the url of the given string without the scheme and domain.
*
* @param orig the full url.
*/
private fun getUrlWithoutDomain(orig: String): String {
try {
val uri = URI(orig)
var out = uri.path
if (uri.query != null)
out += "?" + uri.query
if (uri.fragment != null)
out += "#" + uri.fragment
return out
} catch (e: URISyntaxException) {
return orig
}
}
fun Chapter.setUrlWithoutDomain(url: String) {
this.url = UrlUtil.getPath(url)
}
fun Manga.setUrlWithoutDomain(url: String) {
this.url = UrlUtil.getPath(url)
}
/**
* Called before inserting a new chapter into database. Use it if you need to override chapter
* fields, like the title or the chapter number. Do not change anything to [manga].
@ -459,22 +351,11 @@ abstract class OnlineSource() : Source {
* @param chapter the chapter to be added.
* @param manga the manga of the chapter.
*/
open fun prepareNewChapter(chapter: Chapter, manga: Manga) {
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
}
sealed class Filter<T>(val name: String, var state: T) {
open class Header(name: String) : Filter<Any>(name, 0)
abstract class List<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
companion object {
const val STATE_IGNORE = 0
const val STATE_INCLUDE = 1
const val STATE_EXCLUDE = 2
}
}
}
open fun getFilterList(): List<Filter<*>> = emptyList()
/**
* Returns the list of filters for the source.
*/
override fun getFilterList() = FilterList()
}

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
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.model.SChapter
import eu.kanade.tachiyomi.data.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
@ -12,26 +12,25 @@ import org.jsoup.nodes.Element
/**
* A simple implementation for sources from a website using Jsoup, an HTML parser.
*/
abstract class ParsedOnlineSource() : OnlineSource() {
abstract class ParsedOnlineSource : OnlineSource() {
/**
* Parse the response from the site and fills [page].
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
* @param page the page object to be filled.
*/
override fun popularMangaParse(response: Response, page: MangasPage) {
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
for (element in document.select(popularMangaSelector())) {
Manga.create(id).apply {
popularMangaFromElement(element, this)
page.mangas.add(this)
}
val mangas = document.select(popularMangaSelector()).map { element ->
popularMangaFromElement(element)
}
popularMangaNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
}
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
/**
@ -40,13 +39,12 @@ abstract class ParsedOnlineSource() : OnlineSource() {
abstract protected fun popularMangaSelector(): String
/**
* Fills [manga] with the given [element]. Most sites only show the title and the url, it's
* totally safe to fill only those two values.
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [popularMangaSelector].
* @param manga the manga to fill.
*/
abstract protected fun popularMangaFromElement(element: Element, manga: Manga)
abstract protected fun popularMangaFromElement(element: Element): SManga
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
@ -55,24 +53,22 @@ abstract class ParsedOnlineSource() : OnlineSource() {
abstract protected fun popularMangaNextPageSelector(): String?
/**
* Parse the response from the site and fills [page].
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
* @param page the page object to be filled.
* @param query the search query.
*/
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter<*>>) {
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
for (element in document.select(searchMangaSelector())) {
Manga.create(id).apply {
searchMangaFromElement(element, this)
page.mangas.add(this)
}
val mangas = document.select(searchMangaSelector()).map { element ->
searchMangaFromElement(element)
}
searchMangaNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
}
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
/**
@ -81,13 +77,12 @@ abstract class ParsedOnlineSource() : OnlineSource() {
abstract protected fun searchMangaSelector(): String
/**
* Fills [manga] with the given [element]. Most sites only show the title and the url, it's
* totally safe to fill only those two values.
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [searchMangaSelector].
* @param manga the manga to fill.
*/
abstract protected fun searchMangaFromElement(element: Element, manga: Manga)
abstract protected fun searchMangaFromElement(element: Element): SManga
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
@ -96,70 +91,67 @@ abstract class ParsedOnlineSource() : OnlineSource() {
abstract protected fun searchMangaNextPageSelector(): String?
/**
* Parse the response from the site for latest updates and fills [page].
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response, page: MangasPage) {
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
for (element in document.select(latestUpdatesSelector())) {
Manga.create(id).apply {
latestUpdatesFromElement(element, this)
page.mangas.add(this)
}
val mangas = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element)
}
latestUpdatesNextPageSelector()?.let { selector ->
page.nextPageUrl = document.select(selector).first()?.absUrl("href")
}
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
/**
* Returns the Jsoup selector similar to [popularMangaSelector], but for latest updates.
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
abstract protected fun latestUpdatesSelector(): String
/**
* Fills [manga] with the given [element]. For latest updates.
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [latestUpdatesSelector].
*/
abstract protected fun latestUpdatesFromElement(element: Element, manga: Manga)
abstract protected fun latestUpdatesFromElement(element: Element): SManga
/**
* Returns the Jsoup selector that returns the <a> tag, like [popularMangaNextPageSelector].
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
abstract protected fun latestUpdatesNextPageSelector(): String?
/**
* Parse the response from the site and fills the details of [manga].
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
* @param manga the manga to fill.
*/
override fun mangaDetailsParse(response: Response, manga: Manga) {
mangaDetailsParse(response.asJsoup(), manga)
override fun mangaDetailsParse(response: Response): SManga {
return mangaDetailsParse(response.asJsoup())
}
/**
* Fills the details of [manga] from the given [document].
* Returns the details of the manga from the given [document].
*
* @param document the parsed document.
* @param manga the manga to fill.
*/
abstract protected fun mangaDetailsParse(document: Document, manga: Manga)
abstract protected fun mangaDetailsParse(document: Document): SManga
/**
* Parse the response from the site and fills the chapter list.
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
* @param chapters the list of chapters to fill.
*/
override fun chapterListParse(response: Response, chapters: MutableList<Chapter>) {
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
for (element in document.select(chapterListSelector())) {
Chapter.create().apply {
chapterFromElement(element, this)
chapters.add(this)
}
}
return document.select(chapterListSelector()).map { chapterFromElement(it) }
}
/**
@ -168,30 +160,27 @@ abstract class ParsedOnlineSource() : OnlineSource() {
abstract protected fun chapterListSelector(): String
/**
* Fills [chapter] with the given [element].
* Returns a chapter from the given element.
*
* @param element an element obtained from [chapterListSelector].
* @param chapter the chapter to fill.
*/
abstract protected fun chapterFromElement(element: Element, chapter: Chapter)
abstract protected fun chapterFromElement(element: Element): SChapter
/**
* Parse the response from the site and fills the page list.
* Parses the response from the site and returns the page list.
*
* @param response the response from the site.
* @param pages the list of pages to fill.
*/
override fun pageListParse(response: Response, pages: MutableList<Page>) {
pageListParse(response.asJsoup(), pages)
override fun pageListParse(response: Response): List<Page> {
return pageListParse(response.asJsoup())
}
/**
* Fills [pages] from the given [document].
* Returns a page list from the given document.
*
* @param document the parsed document.
* @param pages the list of pages to fill.
*/
abstract protected fun pageListParse(document: Document, pages: MutableList<Page>)
abstract protected fun pageListParse(document: Document): List<Page>
/**
* Parse the response from the site and returns the absolute url to the source image.

View File

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

View File

@ -2,7 +2,7 @@
package eu.kanade.tachiyomi.data.source.online
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.model.SManga
import okhttp3.FormBody
import okhttp3.RequestBody
import org.jsoup.nodes.Document
@ -164,15 +164,15 @@ class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
fun getStatus(document: Element, cache: Map<String, Element>): Int {
val text = process(document, cache)
complete?.let {
if (text.contains(it)) return Manga.COMPLETED
if (text.contains(it)) return SManga.COMPLETED
}
ongoing?.let {
if (text.contains(it)) return Manga.ONGOING
if (text.contains(it)) return SManga.ONGOING
}
licensed?.let {
if (text.contains(it)) return Manga.LICENSED
if (text.contains(it)) return SManga.LICENSED
}
return Manga.UNKNOWN
return SManga.UNKNOWN
}
}

View File

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

View File

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

View File

@ -1,19 +1,19 @@
package eu.kanade.tachiyomi.data.source.online.english
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl
import okhttp3.Response
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
class Mangafox(override val id: Int) : ParsedOnlineSource() {
class Mangafox : ParsedOnlineSource() {
override val id: Long = 3
override val name = "Mangafox"
@ -23,32 +23,40 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
override val supportsLatest = true
override fun popularMangaInitialUrl() = "$baseUrl/directory/"
override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?latest"
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
override fun popularMangaRequest(page: Int): Request {
val pageStr = if (page != 1) "$page.htm" else ""
return GET("$baseUrl/directory/$pageStr", headers)
}
override fun latestUpdatesSelector() = "div#mangalist > ul.list > li"
override fun popularMangaFromElement(element: Element, manga: Manga) {
override fun latestUpdatesRequest(page: Int): Request {
val pageStr = if (page != 1) "$page.htm" else ""
return GET("$baseUrl/directory/$pageStr?latest")
}
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a.title").first().let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text()
}
return manga
}
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
popularMangaFromElement(element, manga)
override fun latestUpdatesFromElement(element: Element): SManga {
return popularMangaFromElement(element)
}
override fun popularMangaNextPageSelector() = "a:has(span.next)"
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
for (filter in if (filters.isEmpty()) this@Mangafox.filters else filters) {
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
is TextField -> url.addQueryParameter(filter.key, filter.state)
@ -56,47 +64,54 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
is Order -> url.addQueryParameter("order", if (filter.state) "az" else "za")
}
}
return url.toString()
url.addQueryParameter("page", page.toString())
return GET(url.toString(), headers)
}
override fun searchMangaSelector() = "div#mangalist > ul.list > li"
override fun searchMangaFromElement(element: Element, manga: Manga) {
override fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a.title").first().let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text()
}
return manga
}
override fun searchMangaNextPageSelector() = "a:has(span.next)"
override fun mangaDetailsParse(document: Document, manga: Manga) {
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select("div#title").first()
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
val sideInfoElement = document.select("#series_info").first()
val manga = SManga.create()
manga.author = rowElement.select("td:eq(1)").first()?.text()
manga.artist = rowElement.select("td:eq(2)").first()?.text()
manga.genre = rowElement.select("td:eq(3)").first()?.text()
manga.description = infoElement.select("p.summary").first()?.text()
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
return manga
}
private fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING
status.contains("Completed") -> Manga.COMPLETED
else -> Manga.UNKNOWN
status.contains("Ongoing") -> SManga.ONGOING
status.contains("Completed") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun chapterListSelector() = "div#chapters li div"
override fun chapterFromElement(element: Element, chapter: Chapter) {
override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.select("a.tips").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter
}
private fun parseChapterDate(date: String): Long {
@ -124,17 +139,14 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
}
}
override fun pageListParse(response: Response, pages: MutableList<Page>) {
val document = response.asJsoup()
override fun pageListParse(document: Document): List<Page> {
val url = document.baseUri().substringBeforeLast('/')
val url = response.request().url().toString().substringBeforeLast('/')
val pages = mutableListOf<Page>()
document.select("select.m").first()?.select("option:not([value=0])")?.forEach {
pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
}
}
// Not used, overrides parent.
override fun pageListParse(document: Document, pages: MutableList<Page>) {
return pages
}
override fun imageUrlParse(document: Document): String {
@ -157,7 +169,7 @@ class Mangafox(override val id: Int) : ParsedOnlineSource() {
// $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
// on http://mangafox.me/search.php
override fun getFilterList(): List<Filter<*>> = listOf(
override fun getFilterList() = FilterList(
TextField("Author", "author"),
TextField("Artist", "artist"),
ListField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga", "1"), ListValue("Korean Manhwa", "2"), ListValue("Chinese Manhua", "3"))),

View File

@ -1,17 +1,19 @@
package eu.kanade.tachiyomi.data.source.online.english
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.source.model.*
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
import okhttp3.HttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
class Mangahere(override val id: Int) : ParsedOnlineSource() {
class Mangahere : ParsedOnlineSource() {
override val id: Long = 2
override val name = "Mangahere"
@ -21,36 +23,42 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
override val supportsLatest = true
override fun popularMangaInitialUrl() = "$baseUrl/directory/?views.za"
override fun latestUpdatesInitialUrl() = "$baseUrl/directory/?last_chapter_time.za"
override fun popularMangaSelector() = "div.directory_list > ul > li"
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
private fun mangaFromElement(query: String, element: Element, manga: Manga) {
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/directory/$page.htm?views.za", headers)
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/directory/$page.htm?last_chapter_time.za", headers)
}
private fun mangaFromElement(query: String, element: Element): SManga {
val manga = SManga.create()
element.select(query).first().let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text()
}
return manga
}
override fun popularMangaFromElement(element: Element, manga: Manga) {
mangaFromElement("div.title > a", element, manga)
override fun popularMangaFromElement(element: Element): SManga {
return mangaFromElement("div.title > a", element)
}
override fun latestUpdatesFromElement(element: Element, manga: Manga) {
popularMangaFromElement(element, manga)
override fun latestUpdatesFromElement(element: Element): SManga {
return popularMangaFromElement(element)
}
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
override fun searchMangaInitialUrl(query: String, filters: List<Filter<*>>): String {
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
for (filter in if (filters.isEmpty()) this@Mangahere.filters else filters) {
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
is Genre -> url.addQueryParameter(filter.id, filter.state.toString())
@ -59,39 +67,41 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
is Order -> url.addQueryParameter("order", if (filter.state) "az" else "za")
}
}
return url.toString()
url.addQueryParameter("page", page.toString())
return GET(url.toString(), headers)
}
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
override fun searchMangaFromElement(element: Element, manga: Manga) {
mangaFromElement("a.manga_info", element, manga)
override fun searchMangaFromElement(element: Element): SManga {
return mangaFromElement("a.manga_info", element)
}
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
override fun mangaDetailsParse(document: Document, manga: Manga) {
override fun mangaDetailsParse(document: Document): SManga {
val detailElement = document.select(".manga_detail_top").first()
val infoElement = detailElement.select(".detail_topText").first()
val manga = SManga.create()
manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text()
manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text()
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
return manga
}
private fun parseStatus(status: String) = when {
status.contains("Ongoing") -> Manga.ONGOING
status.contains("Completed") -> Manga.COMPLETED
else -> Manga.UNKNOWN
status.contains("Ongoing") -> SManga.ONGOING
status.contains("Completed") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
override fun chapterFromElement(element: Element, chapter: Chapter) {
override fun chapterFromElement(element: Element): SChapter {
val parentEl = element.select("span.left").first()
val urlElement = parentEl.select("a").first()
@ -106,9 +116,11 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
title = " - " + title
}
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() + volume + title
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter
}
private fun parseChapterDate(date: String): Long {
@ -136,11 +148,13 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
}
}
override fun pageListParse(document: Document, pages: MutableList<Page>) {
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
pages.add(Page(pages.size, it.attr("value")))
}
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
return pages
}
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
@ -157,7 +171,7 @@ class Mangahere(override val id: Int) : ParsedOnlineSource() {
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Genre("${el.nextSibling.nextSibling.textContent.trim()}", "${el.getAttribute('name')}")`).join(',\n')
// http://www.mangahere.co/advsearch.htm
override fun getFilterList(): List<Filter<*>> = listOf(
override fun getFilterList() = FilterList(
TextField("Author", "author"),
TextField("Artist", "artist"),
ListField("Type", "direction", arrayOf(ListValue("Any", ""), ListValue("Japanese Manga (read from right to left)", "rl"), ListValue("Korean Manhwa (read from left to right)", "lr"))),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.data.source.model.MangasPage
import rx.subjects.PublishSubject
import eu.kanade.tachiyomi.data.source.model.SManga
import rx.Observable
/**
* A general pager for source requests (latest updates, popular, search)
*/
abstract class Pager {
abstract class Pager(var currentPage: Int = 1) {
protected var lastPage: MangasPage? = null
var hasNextPage = true
private set
protected val results = PublishSubject.create<MangasPage>()
protected val results: PublishRelay<Pair<Int, List<SManga>>> = PublishRelay.create()
fun results(): Observable<MangasPage> {
fun results(): Observable<Pair<Int, List<SManga>>> {
return results.asObservable()
}
fun hasNextPage(): Boolean {
return lastPage == null || lastPage?.nextPageUrl != null
abstract fun requestNext(): Observable<MangasPage>
fun onPageReceived(mangasPage: MangasPage) {
val page = currentPage
currentPage++
hasNextPage = mangasPage.hasNextPage && !mangasPage.mangas.isEmpty()
results.call(Pair(page, mangasPage.mangas))
}
abstract fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage>
}

View File

@ -1,28 +1,22 @@
package eu.kanade.tachiyomi.ui.latest_updates
import eu.kanade.tachiyomi.data.source.CatalogueSource
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.ui.catalogue.Pager
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
/**
* LatestUpdatesPager inherited from the general Pager.
*/
class LatestUpdatesPager(val source: OnlineSource): Pager() {
class LatestUpdatesPager(val source: CatalogueSource): Pager() {
override fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
val lastPage = lastPage
val page = if (lastPage == null)
MangasPage(1)
else
MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! }
val observable = source.fetchLatestUpdates(page)
return transformer(observable)
.doOnNext { results.onNext(it) }
.doOnNext { this@LatestUpdatesPager.lastPage = it }
override fun requestNext(): Observable<MangasPage> {
return source.fetchLatestUpdates(currentPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { onPageReceived(it) }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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 f a function to execute in the snack, allowing for example to define a custom action.
*/
inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit) {
inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit): Snackbar {
val snack = Snackbar.make(this, message, length)
val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView
textView.setTextColor(Color.WHITE)
snack.f()
snack.show()
return snack
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.widget.preference
import android.app.Activity
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
@ -70,7 +71,8 @@ abstract class LoginDialogPreference : DialogFragment() {
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
targetFragment?.onActivityResult(targetRequestCode, arguments.getInt("key"), Intent())
val intent = Intent().putExtras(arguments)
targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_OK, intent)
}
protected abstract fun checkLogin()

View File

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

View File

@ -64,7 +64,7 @@
<string name="pref_enable_automatic_updates_key">automatic_updates</string>
<string name="pref_display_catalogue_as_list">pref_display_catalogue_as_list</string>
<string name="pref_last_catalogue_source_key">pref_last_catalogue_source_key</string>
<string name="pref_last_catalogue_source_key">last_catalogue_source</string>
<string name="pref_download_new_key">download_new</string>

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