Move source and network outside data
This commit is contained in:
parent
a4c145c1ef
commit
706163e7a6
@ -6,9 +6,9 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
|
@ -6,7 +6,7 @@ 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.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.saveTo
|
||||
import okhttp3.Response
|
||||
|
@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import java.io.Serializable
|
||||
|
||||
interface Chapter : SChapter, Serializable {
|
||||
|
@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
interface Manga : SManga {
|
||||
|
||||
|
@ -6,8 +6,8 @@ import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.util.DiskUtil
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
|
@ -5,8 +5,8 @@ import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
|
@ -10,10 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||
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.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
|
||||
import eu.kanade.tachiyomi.util.RetryWithDelay
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
|
@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.data.download.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import rx.subjects.PublishSubject
|
||||
|
||||
class Download(val source: OnlineSource, val manga: Manga, val chapter: Chapter) {
|
||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.download.model
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.download.DownloadStore
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
@ -8,7 +8,7 @@ import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.GlideModule
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.InputStream
|
||||
|
@ -8,8 +8,8 @@ import com.bumptech.glide.load.model.*
|
||||
import com.bumptech.glide.load.model.stream.StreamModelLoader
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
@ -20,9 +20,9 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.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.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import rx.Observable
|
||||
|
@ -7,7 +7,7 @@ import android.preference.PreferenceManager
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import com.f2prateek.rx.preferences.RxSharedPreferences
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import java.io.File
|
||||
|
||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track
|
||||
import android.support.annotation.CallSuper
|
||||
import android.support.annotation.DrawableRes
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import okhttp3.OkHttpClient
|
||||
import rx.Completable
|
||||
|
@ -5,7 +5,7 @@ import com.github.salomonbrys.kotson.int
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.ResponseBody
|
||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.track.kitsu
|
||||
import com.github.salomonbrys.kotson.*
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
|
@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
import android.net.Uri
|
||||
import android.util.Xml
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.network.asObservable
|
||||
import eu.kanade.tachiyomi.data.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.util.selectInt
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
|
@ -8,10 +8,10 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.data.network.ProgressListener
|
||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.ProgressListener
|
||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||
import eu.kanade.tachiyomi.util.registerLocalReceiver
|
||||
import eu.kanade.tachiyomi.util.saveTo
|
||||
import eu.kanade.tachiyomi.util.sendLocalBroadcastSync
|
||||
|
@ -1,80 +1,80 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
import com.squareup.duktape.Duktape
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interceptor {
|
||||
|
||||
//language=RegExp
|
||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
||||
|
||||
//language=RegExp
|
||||
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
||||
|
||||
//language=RegExp
|
||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
// Check if we already solved a challenge
|
||||
if (response.code() != 503 &&
|
||||
cookies.get(response.request().url()).any { it.name() == "cf_clearance" }) {
|
||||
return response
|
||||
}
|
||||
|
||||
// Check if Cloudflare anti-bot is on
|
||||
if ("URL=/cdn-cgi/" in response.header("Refresh", "")
|
||||
&& response.header("Server", "") == "cloudflare-nginx") {
|
||||
return chain.proceed(resolveChallenge(response))
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private fun resolveChallenge(response: Response): Request {
|
||||
val duktape = Duktape.create()
|
||||
try {
|
||||
val originalRequest = response.request()
|
||||
val domain = originalRequest.url().host()
|
||||
val content = response.body().string()
|
||||
|
||||
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
||||
Thread.sleep(4000)
|
||||
|
||||
val operation = operationPattern.find(content)?.groups?.get(1)?.value
|
||||
val challenge = challengePattern.find(content)?.groups?.get(1)?.value
|
||||
val pass = passPattern.find(content)?.groups?.get(1)?.value
|
||||
|
||||
if (operation == null || challenge == null || pass == null) {
|
||||
throw RuntimeException("Failed resolving Cloudflare challenge")
|
||||
}
|
||||
|
||||
val js = operation
|
||||
//language=RegExp
|
||||
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
||||
//language=RegExp
|
||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||
.replace("\n", "")
|
||||
|
||||
val result = (duktape.evaluate(js) as Double).toInt()
|
||||
|
||||
val answer = "${result + domain.length}"
|
||||
|
||||
val url = HttpUrl.parse("http://$domain/cdn-cgi/l/chk_jschl").newBuilder()
|
||||
.addQueryParameter("jschl_vc", challenge)
|
||||
.addQueryParameter("pass", pass)
|
||||
.addQueryParameter("jschl_answer", answer)
|
||||
.toString()
|
||||
|
||||
val referer = originalRequest.url().toString()
|
||||
return GET(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
|
||||
} finally {
|
||||
duktape.close()
|
||||
}
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import com.squareup.duktape.Duktape
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interceptor {
|
||||
|
||||
//language=RegExp
|
||||
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
|
||||
|
||||
//language=RegExp
|
||||
private val passPattern = Regex("""name="pass" value="(.+?)"""")
|
||||
|
||||
//language=RegExp
|
||||
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
// Check if we already solved a challenge
|
||||
if (response.code() != 503 &&
|
||||
cookies.get(response.request().url()).any { it.name() == "cf_clearance" }) {
|
||||
return response
|
||||
}
|
||||
|
||||
// Check if Cloudflare anti-bot is on
|
||||
if ("URL=/cdn-cgi/" in response.header("Refresh", "")
|
||||
&& response.header("Server", "") == "cloudflare-nginx") {
|
||||
return chain.proceed(resolveChallenge(response))
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private fun resolveChallenge(response: Response): Request {
|
||||
val duktape = Duktape.create()
|
||||
try {
|
||||
val originalRequest = response.request()
|
||||
val domain = originalRequest.url().host()
|
||||
val content = response.body().string()
|
||||
|
||||
// CloudFlare requires waiting 4 seconds before resolving the challenge
|
||||
Thread.sleep(4000)
|
||||
|
||||
val operation = operationPattern.find(content)?.groups?.get(1)?.value
|
||||
val challenge = challengePattern.find(content)?.groups?.get(1)?.value
|
||||
val pass = passPattern.find(content)?.groups?.get(1)?.value
|
||||
|
||||
if (operation == null || challenge == null || pass == null) {
|
||||
throw RuntimeException("Failed resolving Cloudflare challenge")
|
||||
}
|
||||
|
||||
val js = operation
|
||||
//language=RegExp
|
||||
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
|
||||
//language=RegExp
|
||||
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
|
||||
.replace("\n", "")
|
||||
|
||||
val result = (duktape.evaluate(js) as Double).toInt()
|
||||
|
||||
val answer = "${result + domain.length}"
|
||||
|
||||
val url = HttpUrl.parse("http://$domain/cdn-cgi/l/chk_jschl").newBuilder()
|
||||
.addQueryParameter("jschl_vc", challenge)
|
||||
.addQueryParameter("pass", pass)
|
||||
.addQueryParameter("jschl_answer", answer)
|
||||
.toString()
|
||||
|
||||
val referer = originalRequest.url().toString()
|
||||
return GET(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
|
||||
} finally {
|
||||
duktape.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,38 +1,38 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
|
||||
class NetworkHelper(context: Context) {
|
||||
|
||||
private val cacheDir = File(context.cacheDir, "network_cache")
|
||||
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
|
||||
private val cookieManager = PersistentCookieJar(context)
|
||||
|
||||
val client = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
.cache(Cache(cacheDir, cacheSize))
|
||||
.build()
|
||||
|
||||
val forceCacheClient = client.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.removeHeader("Pragma")
|
||||
.header("Cache-Control", "max-age=600")
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
val cloudflareClient = client.newBuilder()
|
||||
.addInterceptor(CloudflareInterceptor(cookies))
|
||||
.build()
|
||||
|
||||
val cookies: PersistentCookieStore
|
||||
get() = cookieManager.store
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
|
||||
class NetworkHelper(context: Context) {
|
||||
|
||||
private val cacheDir = File(context.cacheDir, "network_cache")
|
||||
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
|
||||
private val cookieManager = PersistentCookieJar(context)
|
||||
|
||||
val client = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
.cache(Cache(cacheDir, cacheSize))
|
||||
.build()
|
||||
|
||||
val forceCacheClient = client.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.removeHeader("Pragma")
|
||||
.header("Cache-Control", "max-age=600")
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
val cloudflareClient = client.newBuilder()
|
||||
.addInterceptor(CloudflareInterceptor(cookies))
|
||||
.build()
|
||||
|
||||
val cookies: PersistentCookieStore
|
||||
get() = cookieManager.store
|
||||
|
||||
}
|
@ -1,70 +1,70 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
import okhttp3.Call
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
fun Call.asObservable(): Observable<Response> {
|
||||
return Observable.create { subscriber ->
|
||||
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||
val call = clone()
|
||||
|
||||
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
||||
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
|
||||
override fun request(n: Long) {
|
||||
if (n == 0L || !compareAndSet(false, true)) return
|
||||
|
||||
try {
|
||||
val response = call.execute()
|
||||
if (!subscriber.isUnsubscribed) {
|
||||
subscriber.onNext(response)
|
||||
subscriber.onCompleted()
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
if (!subscriber.isUnsubscribed) {
|
||||
subscriber.onError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun unsubscribe() {
|
||||
call.cancel()
|
||||
}
|
||||
|
||||
override fun isUnsubscribed(): Boolean {
|
||||
return call.isCanceled
|
||||
}
|
||||
}
|
||||
|
||||
subscriber.add(requestArbiter)
|
||||
subscriber.setProducer(requestArbiter)
|
||||
}
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw Exception("HTTP error ${response.code()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
val progressClient = newBuilder()
|
||||
.cache(null)
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.body(ProgressResponseBody(originalResponse.body(), listener))
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
return progressClient.newCall(request)
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.Call
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
fun Call.asObservable(): Observable<Response> {
|
||||
return Observable.create { subscriber ->
|
||||
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||
val call = clone()
|
||||
|
||||
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
||||
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
|
||||
override fun request(n: Long) {
|
||||
if (n == 0L || !compareAndSet(false, true)) return
|
||||
|
||||
try {
|
||||
val response = call.execute()
|
||||
if (!subscriber.isUnsubscribed) {
|
||||
subscriber.onNext(response)
|
||||
subscriber.onCompleted()
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
if (!subscriber.isUnsubscribed) {
|
||||
subscriber.onError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun unsubscribe() {
|
||||
call.cancel()
|
||||
}
|
||||
|
||||
override fun isUnsubscribed(): Boolean {
|
||||
return call.isCanceled
|
||||
}
|
||||
}
|
||||
|
||||
subscriber.add(requestArbiter)
|
||||
subscriber.setProducer(requestArbiter)
|
||||
}
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw Exception("HTTP error ${response.code()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
val progressClient = newBuilder()
|
||||
.cache(null)
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
.body(ProgressResponseBody(originalResponse.body(), listener))
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
return progressClient.newCall(request)
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class PersistentCookieJar(context: Context) : CookieJar {
|
||||
|
||||
val store = PersistentCookieStore(context)
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
store.addAll(url, cookies)
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return store.get(url)
|
||||
}
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class PersistentCookieJar(context: Context) : CookieJar {
|
||||
|
||||
val store = PersistentCookieStore(context)
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
store.addAll(url, cookies)
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return store.get(url)
|
||||
}
|
||||
}
|
@ -1,75 +1,75 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import java.net.URI
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class PersistentCookieStore(context: Context) {
|
||||
|
||||
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
||||
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
||||
|
||||
init {
|
||||
for ((key, value) in prefs.all) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val cookies = value as? Set<String>
|
||||
if (cookies != null) {
|
||||
try {
|
||||
val url = HttpUrl.parse("http://$key")
|
||||
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
cookieMap.put(key, nonExpiredCookies)
|
||||
} catch (e: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
||||
synchronized(this) {
|
||||
val key = url.uri().host
|
||||
|
||||
// Append or replace the cookies for this domain.
|
||||
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
|
||||
for (cookie in cookies) {
|
||||
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
||||
val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
|
||||
if (pos == -1) {
|
||||
cookiesForDomain.add(cookie)
|
||||
} else {
|
||||
cookiesForDomain[pos] = cookie
|
||||
}
|
||||
}
|
||||
cookieMap.put(key, cookiesForDomain)
|
||||
|
||||
// Get cookies to be stored in disk
|
||||
val newValues = cookiesForDomain.asSequence()
|
||||
.filter { it.persistent() && !it.hasExpired() }
|
||||
.map { it.toString() }
|
||||
.toSet()
|
||||
|
||||
prefs.edit().putStringSet(key, newValues).apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
synchronized(this) {
|
||||
prefs.edit().clear().apply()
|
||||
cookieMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun get(url: HttpUrl) = get(url.uri().host)
|
||||
|
||||
fun get(uri: URI) = get(uri.host)
|
||||
|
||||
private fun get(url: String): List<Cookie> {
|
||||
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||
}
|
||||
|
||||
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
|
||||
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import java.net.URI
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class PersistentCookieStore(context: Context) {
|
||||
|
||||
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
||||
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
||||
|
||||
init {
|
||||
for ((key, value) in prefs.all) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val cookies = value as? Set<String>
|
||||
if (cookies != null) {
|
||||
try {
|
||||
val url = HttpUrl.parse("http://$key")
|
||||
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
cookieMap.put(key, nonExpiredCookies)
|
||||
} catch (e: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
||||
synchronized(this) {
|
||||
val key = url.uri().host
|
||||
|
||||
// Append or replace the cookies for this domain.
|
||||
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
|
||||
for (cookie in cookies) {
|
||||
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
||||
val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
|
||||
if (pos == -1) {
|
||||
cookiesForDomain.add(cookie)
|
||||
} else {
|
||||
cookiesForDomain[pos] = cookie
|
||||
}
|
||||
}
|
||||
cookieMap.put(key, cookiesForDomain)
|
||||
|
||||
// Get cookies to be stored in disk
|
||||
val newValues = cookiesForDomain.asSequence()
|
||||
.filter { it.persistent() && !it.hasExpired() }
|
||||
.map { it.toString() }
|
||||
.toSet()
|
||||
|
||||
prefs.edit().putStringSet(key, newValues).apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAll() {
|
||||
synchronized(this) {
|
||||
prefs.edit().clear().apply()
|
||||
cookieMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun get(url: HttpUrl) = get(url.uri().host)
|
||||
|
||||
fun get(uri: URI) = get(uri.host)
|
||||
|
||||
private fun get(url: String): List<Cookie> {
|
||||
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||
}
|
||||
|
||||
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
interface ProgressListener {
|
||||
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
interface ProgressListener {
|
||||
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
|
||||
}
|
@ -1,40 +1,40 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.ResponseBody
|
||||
import okio.*
|
||||
import java.io.IOException
|
||||
|
||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
||||
|
||||
private val bufferedSource: BufferedSource by lazy {
|
||||
Okio.buffer(source(responseBody.source()))
|
||||
}
|
||||
|
||||
override fun contentType(): MediaType {
|
||||
return responseBody.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
return responseBody.contentLength()
|
||||
}
|
||||
|
||||
override fun source(): BufferedSource {
|
||||
return bufferedSource
|
||||
}
|
||||
|
||||
private fun source(source: Source): Source {
|
||||
return object : ForwardingSource(source) {
|
||||
internal var totalBytesRead = 0L
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||
return bytesRead
|
||||
}
|
||||
}
|
||||
}
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.ResponseBody
|
||||
import okio.*
|
||||
import java.io.IOException
|
||||
|
||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
||||
|
||||
private val bufferedSource: BufferedSource by lazy {
|
||||
Okio.buffer(source(responseBody.source()))
|
||||
}
|
||||
|
||||
override fun contentType(): MediaType {
|
||||
return responseBody.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
return responseBody.contentLength()
|
||||
}
|
||||
|
||||
override fun source(): BufferedSource {
|
||||
return bufferedSource
|
||||
}
|
||||
|
||||
private fun source(source: Source): Source {
|
||||
return object : ForwardingSource(source) {
|
||||
internal var totalBytesRead = 0L
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||
return bytesRead
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +1,32 @@
|
||||
package eu.kanade.tachiyomi.data.network
|
||||
|
||||
import okhttp3.*
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
|
||||
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
||||
private val DEFAULT_HEADERS = Headers.Builder().build()
|
||||
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
|
||||
|
||||
fun GET(url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
||||
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun POST(url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
body: RequestBody = DEFAULT_BODY,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
||||
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.post(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.*
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
|
||||
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
|
||||
private val DEFAULT_HEADERS = Headers.Builder().build()
|
||||
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
|
||||
|
||||
fun GET(url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
||||
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun POST(url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
body: RequestBody = DEFAULT_BODY,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
|
||||
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.post(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
@ -1,46 +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
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import rx.Observable
|
||||
|
||||
interface CatalogueSource : Source {
|
||||
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
*/
|
||||
val lang: String
|
||||
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
*/
|
||||
val supportsLatest: Boolean
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
fun fetchPopularManga(page: Int): Observable<MangasPage>
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage>
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
fun fetchLatestUpdates(page: Int): Observable<MangasPage>
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
fun getFilterList(): FilterList
|
||||
}
|
@ -1,44 +1,44 @@
|
||||
package eu.kanade.tachiyomi.data.source
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
||||
*/
|
||||
interface Source {
|
||||
|
||||
/**
|
||||
* Id for the source. Must be unique.
|
||||
*/
|
||||
val id: Long
|
||||
|
||||
/**
|
||||
* Name of the source.
|
||||
*/
|
||||
val name: String
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
*/
|
||||
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: SManga): Observable<List<SChapter>>
|
||||
|
||||
/**
|
||||
* Returns an observable with the list of pages a chapter has.
|
||||
*
|
||||
* @param chapter the chapter.
|
||||
*/
|
||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>>
|
||||
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
||||
*/
|
||||
interface Source {
|
||||
|
||||
/**
|
||||
* Id for the source. Must be unique.
|
||||
*/
|
||||
val id: Long
|
||||
|
||||
/**
|
||||
* Name of the source.
|
||||
*/
|
||||
val name: String
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
*/
|
||||
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: SManga): Observable<List<SChapter>>
|
||||
|
||||
/**
|
||||
* Returns an observable with the list of pages a chapter has.
|
||||
*
|
||||
* @param chapter the chapter.
|
||||
*/
|
||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>>
|
||||
|
||||
}
|
@ -1,159 +1,159 @@
|
||||
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
|
||||
import eu.kanade.tachiyomi.data.source.online.english.*
|
||||
import eu.kanade.tachiyomi.data.source.online.german.WieManga
|
||||
import eu.kanade.tachiyomi.data.source.online.russian.Mangachan
|
||||
import eu.kanade.tachiyomi.data.source.online.russian.Mintmanga
|
||||
import eu.kanade.tachiyomi.data.source.online.russian.Readmanga
|
||||
import eu.kanade.tachiyomi.util.hasPermission
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
open class SourceManager(private val context: Context) {
|
||||
|
||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||
|
||||
init {
|
||||
createSources()
|
||||
}
|
||||
|
||||
open fun get(sourceKey: Long): Source? {
|
||||
return sourcesMap[sourceKey]
|
||||
}
|
||||
|
||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance<OnlineSource>()
|
||||
|
||||
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 createYamlSources(): List<Source> {
|
||||
val sources = mutableListOf<Source>()
|
||||
|
||||
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||
File.separator + context.getString(R.string.app_name), "parsers")
|
||||
|
||||
if (parsersDir.exists() && context.hasPermission(READ_EXTERNAL_STORAGE)) {
|
||||
val yaml = Yaml()
|
||||
for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
|
||||
try {
|
||||
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
|
||||
sources.add(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
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Environment
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.source.online.YamlOnlineSource
|
||||
import eu.kanade.tachiyomi.source.online.english.*
|
||||
import eu.kanade.tachiyomi.source.online.german.WieManga
|
||||
import eu.kanade.tachiyomi.source.online.russian.Mangachan
|
||||
import eu.kanade.tachiyomi.source.online.russian.Mintmanga
|
||||
import eu.kanade.tachiyomi.source.online.russian.Readmanga
|
||||
import eu.kanade.tachiyomi.util.hasPermission
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
open class SourceManager(private val context: Context) {
|
||||
|
||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||
|
||||
init {
|
||||
createSources()
|
||||
}
|
||||
|
||||
open fun get(sourceKey: Long): Source? {
|
||||
return sourcesMap[sourceKey]
|
||||
}
|
||||
|
||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance<OnlineSource>()
|
||||
|
||||
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 createYamlSources(): List<Source> {
|
||||
val sources = mutableListOf<Source>()
|
||||
|
||||
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||
File.separator + context.getString(R.string.app_name), "parsers")
|
||||
|
||||
if (parsersDir.exists() && context.hasPermission(READ_EXTERNAL_STORAGE)) {
|
||||
val yaml = Yaml()
|
||||
for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
|
||||
try {
|
||||
val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
|
||||
sources.add(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
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
sealed class Filter<T>(val name: String, var state: T) {
|
||||
open class Header(name: String) : Filter<Any>(name, 0)
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
|
||||
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
|
||||
|
||||
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
|
||||
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
|
||||
|
||||
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
|
||||
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
@ -1,47 +1,47 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.network.ProgressListener
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
|
||||
import rx.subjects.Subject
|
||||
|
||||
class Page(
|
||||
val index: Int,
|
||||
val url: String = "",
|
||||
var imageUrl: String? = null,
|
||||
@Transient var uri: Uri? = null
|
||||
) : ProgressListener {
|
||||
|
||||
val number: Int
|
||||
get() = index + 1
|
||||
|
||||
@Transient lateinit var chapter: ReaderChapter
|
||||
|
||||
@Transient @Volatile var status: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
statusSubject?.onNext(value)
|
||||
}
|
||||
|
||||
@Transient @Volatile var progress: Int = 0
|
||||
|
||||
@Transient private var statusSubject: Subject<Int, Int>? = null
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
progress = (100 * bytesRead / contentLength).toInt()
|
||||
}
|
||||
|
||||
fun setStatusSubject(subject: Subject<Int, Int>?) {
|
||||
this.statusSubject = subject
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val QUEUE = 0
|
||||
const val LOAD_PAGE = 1
|
||||
const val DOWNLOAD_IMAGE = 2
|
||||
const val READY = 3
|
||||
const val ERROR = 4
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.ProgressListener
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
|
||||
import rx.subjects.Subject
|
||||
|
||||
class Page(
|
||||
val index: Int,
|
||||
val url: String = "",
|
||||
var imageUrl: String? = null,
|
||||
@Transient var uri: Uri? = null
|
||||
) : ProgressListener {
|
||||
|
||||
val number: Int
|
||||
get() = index + 1
|
||||
|
||||
@Transient lateinit var chapter: ReaderChapter
|
||||
|
||||
@Transient @Volatile var status: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
statusSubject?.onNext(value)
|
||||
}
|
||||
|
||||
@Transient @Volatile var progress: Int = 0
|
||||
|
||||
@Transient private var statusSubject: Subject<Int, Int>? = null
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
progress = (100 * bytesRead / contentLength).toInt()
|
||||
}
|
||||
|
||||
fun setStatusSubject(subject: Subject<Int, Int>?) {
|
||||
this.statusSubject = subject
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val QUEUE = 0
|
||||
const val LOAD_PAGE = 1
|
||||
const val DOWNLOAD_IMAGE = 2
|
||||
const val READY = 3
|
||||
const val ERROR = 4
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
class SChapterImpl : SChapter {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.source.model
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
class SMangaImpl : SManga {
|
||||
|
@ -1,15 +1,15 @@
|
||||
package eu.kanade.tachiyomi.data.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
|
||||
interface LoginSource : Source {
|
||||
|
||||
fun isLogged(): Boolean
|
||||
|
||||
fun login(username: String, password: String): Observable<Boolean>
|
||||
|
||||
fun isAuthenticationSuccessful(response: Response): Boolean
|
||||
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
|
||||
interface LoginSource : Source {
|
||||
|
||||
fun isLogged(): Boolean
|
||||
|
||||
fun login(username: String, password: String): Observable<Boolean>
|
||||
|
||||
fun isAuthenticationSuccessful(response: Response): Boolean
|
||||
|
||||
}
|
@ -1,361 +1,361 @@
|
||||
package eu.kanade.tachiyomi.data.source.online
|
||||
|
||||
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.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 : CatalogueSource {
|
||||
|
||||
/**
|
||||
* Network service.
|
||||
*/
|
||||
val network: NetworkHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
*/
|
||||
abstract val baseUrl: String
|
||||
|
||||
/**
|
||||
* Version id used to generate the source id. If the site completely changes and urls are
|
||||
* incompatible, you may increase this value and it'll be considered as a new source.
|
||||
*/
|
||||
open val versionId = 1
|
||||
|
||||
/**
|
||||
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||
* of the MD5 of the string: sourcename/language/versionId
|
||||
* Note the generated id sets the sign bit to 0.
|
||||
*/
|
||||
override val id by lazy {
|
||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
(0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* Headers used for requests.
|
||||
*/
|
||||
val headers: Headers by lazy { headersBuilder().build() }
|
||||
|
||||
/**
|
||||
* Default network client for doing requests.
|
||||
*/
|
||||
open val client: OkHttpClient
|
||||
get() = network.client
|
||||
|
||||
/**
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
open protected fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible name of the source.
|
||||
*/
|
||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
popularMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
abstract protected fun popularMangaRequest(page: Int): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun popularMangaParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun searchMangaParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
latestUpdatesParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
abstract protected fun latestUpdatesRequest(page: Int): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun latestUpdatesParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the details of a manga. Override only if it's needed to change the
|
||||
* url, send different headers or request method like POST.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
open fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun mangaDetailsParse(response: Response): SManga
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for updating the chapter list. Override only if it's needed to override
|
||||
* the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
open protected fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun chapterListParse(response: Response): List<SChapter>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page list for a chapter.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
pageListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the page list. Override only if it's needed to override the
|
||||
* url, send different headers or request method like POST.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
open protected fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(baseUrl + chapter.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun pageListParse(response: Response): List<Page>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page containing the source url of the image. If there's any
|
||||
* error, it will return null instead of throwing an exception.
|
||||
*
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
open fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return client.newCall(imageUrlRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { imageUrlParse(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the url to the source image. Override only if it's needed to
|
||||
* override the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
open protected fun imageUrlRequest(page: Page): Request {
|
||||
return GET(page.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun imageUrlParse(response: Response): String
|
||||
|
||||
/**
|
||||
* Returns an observable with the response of the source image.
|
||||
*
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
fun fetchImage(page: Page): Observable<Response> {
|
||||
return client.newCallWithProgress(imageRequest(page), page)
|
||||
.asObservableSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the source image. Override only if it's needed to override
|
||||
* the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
open protected fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
|
||||
* database and the urls could still work after a domain change.
|
||||
*
|
||||
* @param url the full url to the chapter.
|
||||
*/
|
||||
fun SChapter.setUrlWithoutDomain(url: String) {
|
||||
this.url = getUrlWithoutDomain(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from
|
||||
* database and the urls could still work after a domain change.
|
||||
*
|
||||
* @param url the full url to the manga.
|
||||
*/
|
||||
fun SManga.setUrlWithoutDomain(url: String) {
|
||||
this.url = getUrlWithoutDomain(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url of the given string without the scheme and domain.
|
||||
*
|
||||
* @param orig the full url.
|
||||
*/
|
||||
private fun getUrlWithoutDomain(orig: String): String {
|
||||
try {
|
||||
val uri = URI(orig)
|
||||
var out = uri.path
|
||||
if (uri.query != null)
|
||||
out += "?" + uri.query
|
||||
if (uri.fragment != null)
|
||||
out += "#" + uri.fragment
|
||||
return out
|
||||
} catch (e: URISyntaxException) {
|
||||
return orig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before inserting a new chapter into database. Use it if you need to override chapter
|
||||
* fields, like the title or the chapter number. Do not change anything to [manga].
|
||||
*
|
||||
* @param chapter the chapter to be added.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
override fun getFilterList() = FilterList()
|
||||
}
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* A simple implementation for sources from a website.
|
||||
*/
|
||||
abstract class OnlineSource : CatalogueSource {
|
||||
|
||||
/**
|
||||
* Network service.
|
||||
*/
|
||||
val network: NetworkHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||
*/
|
||||
abstract val baseUrl: String
|
||||
|
||||
/**
|
||||
* Version id used to generate the source id. If the site completely changes and urls are
|
||||
* incompatible, you may increase this value and it'll be considered as a new source.
|
||||
*/
|
||||
open val versionId = 1
|
||||
|
||||
/**
|
||||
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||
* of the MD5 of the string: sourcename/language/versionId
|
||||
* Note the generated id sets the sign bit to 0.
|
||||
*/
|
||||
override val id by lazy {
|
||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
(0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* Headers used for requests.
|
||||
*/
|
||||
val headers: Headers by lazy { headersBuilder().build() }
|
||||
|
||||
/**
|
||||
* Default network client for doing requests.
|
||||
*/
|
||||
open val client: OkHttpClient
|
||||
get() = network.client
|
||||
|
||||
/**
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
open protected fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible name of the source.
|
||||
*/
|
||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
popularMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
abstract protected fun popularMangaRequest(page: Int): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun popularMangaParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
abstract protected fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun searchMangaParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
latestUpdatesParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
*
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
abstract protected fun latestUpdatesRequest(page: Int): Request
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun latestUpdatesParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the details of a manga. Override only if it's needed to change the
|
||||
* url, send different headers or request method like POST.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
open fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun mangaDetailsParse(response: Response): SManga
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for updating the chapter list. Override only if it's needed to override
|
||||
* the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
open protected fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun chapterListParse(response: Response): List<SChapter>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page list for a chapter.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
pageListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the page list. Override only if it's needed to override the
|
||||
* url, send different headers or request method like POST.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
open protected fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(baseUrl + chapter.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun pageListParse(response: Response): List<Page>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page containing the source url of the image. If there's any
|
||||
* error, it will return null instead of throwing an exception.
|
||||
*
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
open fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return client.newCall(imageUrlRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { imageUrlParse(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the url to the source image. Override only if it's needed to
|
||||
* override the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
open protected fun imageUrlRequest(page: Page): Request {
|
||||
return GET(page.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
abstract protected fun imageUrlParse(response: Response): String
|
||||
|
||||
/**
|
||||
* Returns an observable with the response of the source image.
|
||||
*
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
fun fetchImage(page: Page): Observable<Response> {
|
||||
return client.newCallWithProgress(imageRequest(page), page)
|
||||
.asObservableSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the source image. Override only if it's needed to override
|
||||
* the url, send different headers or request method like POST.
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
open protected fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
|
||||
* database and the urls could still work after a domain change.
|
||||
*
|
||||
* @param url the full url to the chapter.
|
||||
*/
|
||||
fun SChapter.setUrlWithoutDomain(url: String) {
|
||||
this.url = getUrlWithoutDomain(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from
|
||||
* database and the urls could still work after a domain change.
|
||||
*
|
||||
* @param url the full url to the manga.
|
||||
*/
|
||||
fun SManga.setUrlWithoutDomain(url: String) {
|
||||
this.url = getUrlWithoutDomain(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url of the given string without the scheme and domain.
|
||||
*
|
||||
* @param orig the full url.
|
||||
*/
|
||||
private fun getUrlWithoutDomain(orig: String): String {
|
||||
try {
|
||||
val uri = URI(orig)
|
||||
var out = uri.path
|
||||
if (uri.query != null)
|
||||
out += "?" + uri.query
|
||||
if (uri.fragment != null)
|
||||
out += "#" + uri.fragment
|
||||
return out
|
||||
} catch (e: URISyntaxException) {
|
||||
return orig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before inserting a new chapter into database. Use it if you need to override chapter
|
||||
* fields, like the title or the chapter number. Do not change anything to [manga].
|
||||
*
|
||||
* @param chapter the chapter to be added.
|
||||
* @param manga the manga of the chapter.
|
||||
*/
|
||||
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
override fun getFilterList() = FilterList()
|
||||
}
|
@ -1,98 +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) }
|
||||
}
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
|
||||
// TODO: this should be handled with a different approach.
|
||||
|
||||
/**
|
||||
* Chapter cache.
|
||||
*/
|
||||
private val chapterCache: ChapterCache by injectLazy()
|
||||
|
||||
/**
|
||||
* Returns an observable with the page list for a chapter. It tries to return the page list from
|
||||
* the local cache, otherwise fallbacks to network.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
fun OnlineSource.fetchPageListFromCacheThenNet(chapter: Chapter): Observable<List<Page>> {
|
||||
return chapterCache
|
||||
.getPageListFromCache(chapter)
|
||||
.onErrorResumeNext { fetchPageList(chapter) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of the page with the downloaded image.
|
||||
*
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
fun OnlineSource.fetchImageFromCacheThenNet(page: Page): Observable<Page> {
|
||||
return if (page.imageUrl.isNullOrEmpty())
|
||||
getImageUrl(page).flatMap { getCachedImage(it) }
|
||||
else
|
||||
getCachedImage(page)
|
||||
}
|
||||
|
||||
fun OnlineSource.getImageUrl(page: Page): Observable<Page> {
|
||||
page.status = Page.LOAD_PAGE
|
||||
return fetchImageUrl(page)
|
||||
.doOnError { page.status = Page.ERROR }
|
||||
.onErrorReturn { null }
|
||||
.doOnNext { page.imageUrl = it }
|
||||
.map { page }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of the page that gets the image from the chapter or fallbacks to
|
||||
* network and copies it to the cache calling [cacheImage].
|
||||
*
|
||||
* @param page the page.
|
||||
*/
|
||||
fun OnlineSource.getCachedImage(page: Page): Observable<Page> {
|
||||
val imageUrl = page.imageUrl ?: return Observable.just(page)
|
||||
|
||||
return Observable.just(page)
|
||||
.flatMap {
|
||||
if (!chapterCache.isImageInCache(imageUrl)) {
|
||||
cacheImage(page)
|
||||
} else {
|
||||
Observable.just(page)
|
||||
}
|
||||
}
|
||||
.doOnNext {
|
||||
page.uri = Uri.fromFile(chapterCache.getImageFile(imageUrl))
|
||||
page.status = Page.READY
|
||||
}
|
||||
.doOnError { page.status = Page.ERROR }
|
||||
.onErrorReturn { page }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of the page that downloads the image to [ChapterCache].
|
||||
*
|
||||
* @param page the page.
|
||||
*/
|
||||
private fun OnlineSource.cacheImage(page: Page): Observable<Page> {
|
||||
page.status = Page.DOWNLOAD_IMAGE
|
||||
return fetchImage(page)
|
||||
.doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) }
|
||||
.map { page }
|
||||
}
|
||||
|
||||
fun OnlineSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { !it.imageUrl.isNullOrEmpty() }
|
||||
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
||||
}
|
||||
|
||||
fun OnlineSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { it.imageUrl.isNullOrEmpty() }
|
||||
.concatMap { getImageUrl(it) }
|
||||
}
|
@ -1,200 +1,200 @@
|
||||
package eu.kanade.tachiyomi.data.source.online
|
||||
|
||||
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
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
/**
|
||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||
*/
|
||||
abstract class ParsedOnlineSource : OnlineSource() {
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||
*/
|
||||
abstract protected fun popularMangaSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||
* totally fine to fill only those two values.
|
||||
*
|
||||
* @param element an element obtained from [popularMangaSelector].
|
||||
*/
|
||||
abstract protected fun popularMangaFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
* there's no next page.
|
||||
*/
|
||||
abstract protected fun popularMangaNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||
*/
|
||||
abstract protected fun searchMangaSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||
* totally fine to fill only those two values.
|
||||
*
|
||||
* @param element an element obtained from [searchMangaSelector].
|
||||
*/
|
||||
abstract protected fun searchMangaFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
* there's no next page.
|
||||
*/
|
||||
abstract protected fun searchMangaNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||
*/
|
||||
abstract protected fun latestUpdatesSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||
* totally fine to fill only those two values.
|
||||
*
|
||||
* @param element an element obtained from [latestUpdatesSelector].
|
||||
*/
|
||||
abstract protected fun latestUpdatesFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
* there's no next page.
|
||||
*/
|
||||
abstract protected fun latestUpdatesNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return mangaDetailsParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the details of the manga from the given [document].
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
abstract protected fun mangaDetailsParse(document: Document): SManga
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
|
||||
*/
|
||||
abstract protected fun chapterListSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a chapter from the given element.
|
||||
*
|
||||
* @param element an element obtained from [chapterListSelector].
|
||||
*/
|
||||
abstract protected fun chapterFromElement(element: Element): SChapter
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the page list.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return pageListParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a page list from the given document.
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
abstract protected fun pageListParse(document: Document): List<Page>
|
||||
|
||||
/**
|
||||
* Parse the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
return imageUrlParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute url to the source image from the document.
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
abstract protected fun imageUrlParse(document: Document): String
|
||||
}
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
/**
|
||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||
*/
|
||||
abstract class ParsedOnlineSource : OnlineSource() {
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||
*/
|
||||
abstract protected fun popularMangaSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||
* totally fine to fill only those two values.
|
||||
*
|
||||
* @param element an element obtained from [popularMangaSelector].
|
||||
*/
|
||||
abstract protected fun popularMangaFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
* there's no next page.
|
||||
*/
|
||||
abstract protected fun popularMangaNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||
*/
|
||||
abstract protected fun searchMangaSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||
* totally fine to fill only those two values.
|
||||
*
|
||||
* @param element an element obtained from [searchMangaSelector].
|
||||
*/
|
||||
abstract protected fun searchMangaFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
* there's no next page.
|
||||
*/
|
||||
abstract protected fun searchMangaNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a [MangasPage] object.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
|
||||
*/
|
||||
abstract protected fun latestUpdatesSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
|
||||
* totally fine to fill only those two values.
|
||||
*
|
||||
* @param element an element obtained from [latestUpdatesSelector].
|
||||
*/
|
||||
abstract protected fun latestUpdatesFromElement(element: Element): SManga
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||
* there's no next page.
|
||||
*/
|
||||
abstract protected fun latestUpdatesNextPageSelector(): String?
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return mangaDetailsParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the details of the manga from the given [document].
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
abstract protected fun mangaDetailsParse(document: Document): SManga
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
|
||||
*/
|
||||
abstract protected fun chapterListSelector(): String
|
||||
|
||||
/**
|
||||
* Returns a chapter from the given element.
|
||||
*
|
||||
* @param element an element obtained from [chapterListSelector].
|
||||
*/
|
||||
abstract protected fun chapterFromElement(element: Element): SChapter
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the page list.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return pageListParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a page list from the given document.
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
abstract protected fun pageListParse(document: Document): List<Page>
|
||||
|
||||
/**
|
||||
* Parse the response from the site and returns the absolute url to the source image.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
return imageUrlParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute url to the source image from the document.
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
abstract protected fun imageUrlParse(document: Document): String
|
||||
}
|
@ -1,232 +1,232 @@
|
||||
package eu.kanade.tachiyomi.data.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.source.model.*
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.attrOrText
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
||||
|
||||
val map = YamlSourceNode(mappings)
|
||||
|
||||
override val name: String
|
||||
get() = map.name
|
||||
|
||||
override val baseUrl = map.host.let {
|
||||
if (it.endsWith("/")) it.dropLast(1) else it
|
||||
}
|
||||
|
||||
override val lang = map.lang.toLowerCase()
|
||||
|
||||
override val supportsLatest = map.latestupdates != null
|
||||
|
||||
override val client = when (map.client) {
|
||||
"cloudflare" -> network.cloudflareClient
|
||||
else -> network.client
|
||||
}
|
||||
|
||||
override val id = map.id.let {
|
||||
(it as? Int ?: (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff).toLong()
|
||||
}
|
||||
|
||||
// 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(url, headers, map.popular.createForm())
|
||||
else -> GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(map.popular.manga_css).map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.text()
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
popularNextPage = map.popular.next_url_css?.let { selector ->
|
||||
document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
|
||||
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(url, headers, map.search.createForm())
|
||||
else -> GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(map.search.manga_css).map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.text()
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
searchNextPage = map.search.next_url_css?.let { selector ->
|
||||
document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
|
||||
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(url, headers, map.latestupdates.createForm())
|
||||
else -> GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(map.latestupdates!!.manga_css).map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.text()
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
popularNextPage = map.latestupdates.next_url_css?.let { selector ->
|
||||
document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
manga.author = author?.process(document, pool)
|
||||
manga.artist = artist?.process(document, pool)
|
||||
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) ?: SManga.UNKNOWN
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
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 = SChapter.create()
|
||||
element.select(title).first().let {
|
||||
chapter.name = it.text()
|
||||
chapter.setUrlWithoutDomain(it.attr("href"))
|
||||
}
|
||||
val dateElement = element.select(date?.select).first()
|
||||
chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
|
||||
chapters.add(chapter)
|
||||
}
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
with(map.pages) {
|
||||
// Capture a list of values where page urls will be resolved.
|
||||
val capturedPages = if (pages_regex != null)
|
||||
pages_regex!!.toRegex().findAll(body).map { it.value }.toList()
|
||||
else if (pages_css != null)
|
||||
document.select(pages_css).map { it.attrOrText(pages_attr!!) }
|
||||
else
|
||||
null
|
||||
|
||||
// For each captured value, obtain the url and create a new page.
|
||||
capturedPages?.forEach { value ->
|
||||
// If the captured value isn't an url, we have to use replaces with the chapter url.
|
||||
val pageUrl = if (replace != null && replacement != null)
|
||||
url.replace(replace!!.toRegex(), replacement!!.replace("\$value", value))
|
||||
else
|
||||
value
|
||||
|
||||
pages.add(Page(pages.size, pageUrl))
|
||||
}
|
||||
|
||||
// Capture a list of images.
|
||||
val capturedImages = if (image_regex != null)
|
||||
image_regex!!.toRegex().findAll(body).map { it.groups[1]?.value }.toList()
|
||||
else if (image_css != null)
|
||||
document.select(image_css).map { it.absUrl(image_attr) }
|
||||
else
|
||||
null
|
||||
|
||||
// Assign the image url to each page
|
||||
capturedImages?.forEachIndexed { i, url ->
|
||||
val page = pages.getOrElse(i) { Page(i, "").apply { pages.add(this) } }
|
||||
page.imageUrl = url
|
||||
}
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
val body = response.body().string()
|
||||
val url = response.request().url().toString()
|
||||
|
||||
with(map.pages) {
|
||||
return if (image_regex != null)
|
||||
image_regex!!.toRegex().find(body)!!.groups[1]!!.value
|
||||
else if (image_css != null)
|
||||
Jsoup.parse(body, url).select(image_css).first().absUrl(image_attr)
|
||||
else
|
||||
throw Exception("image_regex and image_css are null")
|
||||
}
|
||||
}
|
||||
}
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.attrOrText
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class YamlOnlineSource(mappings: Map<*, *>) : OnlineSource() {
|
||||
|
||||
val map = YamlSourceNode(mappings)
|
||||
|
||||
override val name: String
|
||||
get() = map.name
|
||||
|
||||
override val baseUrl = map.host.let {
|
||||
if (it.endsWith("/")) it.dropLast(1) else it
|
||||
}
|
||||
|
||||
override val lang = map.lang.toLowerCase()
|
||||
|
||||
override val supportsLatest = map.latestupdates != null
|
||||
|
||||
override val client = when (map.client) {
|
||||
"cloudflare" -> network.cloudflareClient
|
||||
else -> network.client
|
||||
}
|
||||
|
||||
override val id = map.id.let {
|
||||
(it as? Int ?: (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff).toLong()
|
||||
}
|
||||
|
||||
// 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(url, headers, map.popular.createForm())
|
||||
else -> GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(map.popular.manga_css).map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.text()
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
popularNextPage = map.popular.next_url_css?.let { selector ->
|
||||
document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
|
||||
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(url, headers, map.search.createForm())
|
||||
else -> GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(map.search.manga_css).map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.text()
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
searchNextPage = map.search.next_url_css?.let { selector ->
|
||||
document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
|
||||
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(url, headers, map.latestupdates.createForm())
|
||||
else -> GET(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(map.latestupdates!!.manga_css).map { element ->
|
||||
SManga.create().apply {
|
||||
title = element.text()
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
popularNextPage = map.latestupdates.next_url_css?.let { selector ->
|
||||
document.select(selector).first()?.absUrl("href")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
manga.author = author?.process(document, pool)
|
||||
manga.artist = artist?.process(document, pool)
|
||||
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) ?: SManga.UNKNOWN
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
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 = SChapter.create()
|
||||
element.select(title).first().let {
|
||||
chapter.name = it.text()
|
||||
chapter.setUrlWithoutDomain(it.attr("href"))
|
||||
}
|
||||
val dateElement = element.select(date?.select).first()
|
||||
chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
|
||||
chapters.add(chapter)
|
||||
}
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
with(map.pages) {
|
||||
// Capture a list of values where page urls will be resolved.
|
||||
val capturedPages = if (pages_regex != null)
|
||||
pages_regex!!.toRegex().findAll(body).map { it.value }.toList()
|
||||
else if (pages_css != null)
|
||||
document.select(pages_css).map { it.attrOrText(pages_attr!!) }
|
||||
else
|
||||
null
|
||||
|
||||
// For each captured value, obtain the url and create a new page.
|
||||
capturedPages?.forEach { value ->
|
||||
// If the captured value isn't an url, we have to use replaces with the chapter url.
|
||||
val pageUrl = if (replace != null && replacement != null)
|
||||
url.replace(replace!!.toRegex(), replacement!!.replace("\$value", value))
|
||||
else
|
||||
value
|
||||
|
||||
pages.add(Page(pages.size, pageUrl))
|
||||
}
|
||||
|
||||
// Capture a list of images.
|
||||
val capturedImages = if (image_regex != null)
|
||||
image_regex!!.toRegex().findAll(body).map { it.groups[1]?.value }.toList()
|
||||
else if (image_css != null)
|
||||
document.select(image_css).map { it.absUrl(image_attr) }
|
||||
else
|
||||
null
|
||||
|
||||
// Assign the image url to each page
|
||||
capturedImages?.forEachIndexed { i, url ->
|
||||
val page = pages.getOrElse(i) { Page(i, "").apply { pages.add(this) } }
|
||||
page.imageUrl = url
|
||||
}
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
val body = response.body().string()
|
||||
val url = response.request().url().toString()
|
||||
|
||||
with(map.pages) {
|
||||
return if (image_regex != null)
|
||||
image_regex!!.toRegex().find(body)!!.groups[1]!!.value
|
||||
else if (image_css != null)
|
||||
Jsoup.parse(body, url).select(image_css).first().absUrl(image_attr)
|
||||
else
|
||||
throw Exception("image_regex and image_css are null")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,234 +1,234 @@
|
||||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package eu.kanade.tachiyomi.data.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.RequestBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
private fun toMap(map: Any?) = map as? Map<String, Any?>
|
||||
|
||||
class YamlSourceNode(uncheckedMap: Map<*, *>) {
|
||||
|
||||
val map = toMap(uncheckedMap)!!
|
||||
|
||||
val id: Any by map
|
||||
|
||||
val name: String by map
|
||||
|
||||
val host: String by map
|
||||
|
||||
val lang: String by map
|
||||
|
||||
val client: String?
|
||||
get() = map["client"] as? String
|
||||
|
||||
val popular = PopularNode(toMap(map["popular"])!!)
|
||||
|
||||
val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) }
|
||||
|
||||
val search = SearchNode(toMap(map["search"])!!)
|
||||
|
||||
val manga = MangaNode(toMap(map["manga"])!!)
|
||||
|
||||
val chapters = ChaptersNode(toMap(map["chapters"])!!)
|
||||
|
||||
val pages = PagesNode(toMap(map["pages"])!!)
|
||||
}
|
||||
|
||||
interface RequestableNode {
|
||||
|
||||
val map: Map<String, Any?>
|
||||
|
||||
val url: String
|
||||
get() = map["url"] as String
|
||||
|
||||
val method: String?
|
||||
get() = map["method"] as? String
|
||||
|
||||
val payload: Map<String, String>?
|
||||
get() = map["payload"] as? Map<String, String>
|
||||
|
||||
fun createForm(): RequestBody {
|
||||
return FormBody.Builder().apply {
|
||||
payload?.let {
|
||||
for ((key, value) in it) {
|
||||
add(key, value)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PopularNode(override val map: Map<String, Any?>): RequestableNode {
|
||||
|
||||
val manga_css: String by map
|
||||
|
||||
val next_url_css: String?
|
||||
get() = map["next_url_css"] as? String
|
||||
|
||||
}
|
||||
|
||||
|
||||
class LatestUpdatesNode(override val map: Map<String, Any?>): RequestableNode {
|
||||
|
||||
val manga_css: String by map
|
||||
|
||||
val next_url_css: String?
|
||||
get() = map["next_url_css"] as? String
|
||||
|
||||
}
|
||||
|
||||
|
||||
class SearchNode(override val map: Map<String, Any?>): RequestableNode {
|
||||
|
||||
val manga_css: String by map
|
||||
|
||||
val next_url_css: String?
|
||||
get() = map["next_url_css"] as? String
|
||||
}
|
||||
|
||||
class MangaNode(private val map: Map<String, Any?>) {
|
||||
|
||||
val parts = CacheNode(toMap(map["parts"]) ?: emptyMap())
|
||||
|
||||
val artist = toMap(map["artist"])?.let { SelectableNode(it) }
|
||||
|
||||
val author = toMap(map["author"])?.let { SelectableNode(it) }
|
||||
|
||||
val summary = toMap(map["summary"])?.let { SelectableNode(it) }
|
||||
|
||||
val status = toMap(map["status"])?.let { StatusNode(it) }
|
||||
|
||||
val genres = toMap(map["genres"])?.let { SelectableNode(it) }
|
||||
|
||||
val cover = toMap(map["cover"])?.let { CoverNode(it) }
|
||||
|
||||
}
|
||||
|
||||
class ChaptersNode(private val map: Map<String, Any?>) {
|
||||
|
||||
val chapter_css: String by map
|
||||
|
||||
val title: String by map
|
||||
|
||||
val date = toMap(toMap(map["date"]))?.let { DateNode(it) }
|
||||
}
|
||||
|
||||
class CacheNode(private val map: Map<String, Any?>) {
|
||||
|
||||
fun get(document: Document) = map.mapValues { document.select(it.value as String).first() }
|
||||
}
|
||||
|
||||
open class SelectableNode(private val map: Map<String, Any?>) {
|
||||
|
||||
val select: String by map
|
||||
|
||||
val from: String?
|
||||
get() = map["from"] as? String
|
||||
|
||||
open val attr: String?
|
||||
get() = map["attr"] as? String
|
||||
|
||||
val capture: String?
|
||||
get() = map["capture"] as? String
|
||||
|
||||
fun process(document: Element, cache: Map<String, Element>): String {
|
||||
val parent = from?.let { cache[it] } ?: document
|
||||
val node = parent.select(select).first()
|
||||
var text = attr?.let { node.attr(it) } ?: node.text()
|
||||
capture?.let {
|
||||
text = Regex(it).find(text)?.groupValues?.get(1) ?: text
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
||||
|
||||
val complete: String?
|
||||
get() = map["complete"] as? String
|
||||
|
||||
val ongoing: String?
|
||||
get() = map["ongoing"] as? String
|
||||
|
||||
val licensed: String?
|
||||
get() = map["licensed"] as? String
|
||||
|
||||
fun getStatus(document: Element, cache: Map<String, Element>): Int {
|
||||
val text = process(document, cache)
|
||||
complete?.let {
|
||||
if (text.contains(it)) return SManga.COMPLETED
|
||||
}
|
||||
ongoing?.let {
|
||||
if (text.contains(it)) return SManga.ONGOING
|
||||
}
|
||||
licensed?.let {
|
||||
if (text.contains(it)) return SManga.LICENSED
|
||||
}
|
||||
return SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
class CoverNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
||||
|
||||
override val attr: String?
|
||||
get() = map["attr"] as? String ?: "src"
|
||||
}
|
||||
|
||||
class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
||||
|
||||
val format: String by map
|
||||
|
||||
fun getDate(document: Element, cache: Map<String, Element>, formatter: SimpleDateFormat): Date {
|
||||
val text = process(document, cache)
|
||||
try {
|
||||
return formatter.parse(text)
|
||||
} catch (exception: ParseException) {}
|
||||
|
||||
for (i in 0..7) {
|
||||
(map["day$i"] as? List<String>)?.let {
|
||||
it.find { it.toRegex().containsMatchIn(text) }?.let {
|
||||
return Calendar.getInstance().apply { add(Calendar.DATE, -i) }.time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Date(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PagesNode(private val map: Map<String, Any?>) {
|
||||
|
||||
val pages_regex: String?
|
||||
get() = map["pages_regex"] as? String
|
||||
|
||||
val pages_css: String?
|
||||
get() = map["pages_css"] as? String
|
||||
|
||||
val pages_attr: String?
|
||||
get() = map["pages_attr"] as? String ?: "value"
|
||||
|
||||
val replace: String?
|
||||
get() = map["url_replace"] as? String
|
||||
|
||||
val replacement: String?
|
||||
get() = map["url_replacement"] as? String
|
||||
|
||||
val image_regex: String?
|
||||
get() = map["image_regex"] as? String
|
||||
|
||||
val image_css: String?
|
||||
get() = map["image_css"] as? String
|
||||
|
||||
val image_attr: String
|
||||
get() = map["image_attr"] as? String ?: "src"
|
||||
|
||||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.RequestBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
private fun toMap(map: Any?) = map as? Map<String, Any?>
|
||||
|
||||
class YamlSourceNode(uncheckedMap: Map<*, *>) {
|
||||
|
||||
val map = toMap(uncheckedMap)!!
|
||||
|
||||
val id: Any by map
|
||||
|
||||
val name: String by map
|
||||
|
||||
val host: String by map
|
||||
|
||||
val lang: String by map
|
||||
|
||||
val client: String?
|
||||
get() = map["client"] as? String
|
||||
|
||||
val popular = PopularNode(toMap(map["popular"])!!)
|
||||
|
||||
val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) }
|
||||
|
||||
val search = SearchNode(toMap(map["search"])!!)
|
||||
|
||||
val manga = MangaNode(toMap(map["manga"])!!)
|
||||
|
||||
val chapters = ChaptersNode(toMap(map["chapters"])!!)
|
||||
|
||||
val pages = PagesNode(toMap(map["pages"])!!)
|
||||
}
|
||||
|
||||
interface RequestableNode {
|
||||
|
||||
val map: Map<String, Any?>
|
||||
|
||||
val url: String
|
||||
get() = map["url"] as String
|
||||
|
||||
val method: String?
|
||||
get() = map["method"] as? String
|
||||
|
||||
val payload: Map<String, String>?
|
||||
get() = map["payload"] as? Map<String, String>
|
||||
|
||||
fun createForm(): RequestBody {
|
||||
return FormBody.Builder().apply {
|
||||
payload?.let {
|
||||
for ((key, value) in it) {
|
||||
add(key, value)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PopularNode(override val map: Map<String, Any?>): RequestableNode {
|
||||
|
||||
val manga_css: String by map
|
||||
|
||||
val next_url_css: String?
|
||||
get() = map["next_url_css"] as? String
|
||||
|
||||
}
|
||||
|
||||
|
||||
class LatestUpdatesNode(override val map: Map<String, Any?>): RequestableNode {
|
||||
|
||||
val manga_css: String by map
|
||||
|
||||
val next_url_css: String?
|
||||
get() = map["next_url_css"] as? String
|
||||
|
||||
}
|
||||
|
||||
|
||||
class SearchNode(override val map: Map<String, Any?>): RequestableNode {
|
||||
|
||||
val manga_css: String by map
|
||||
|
||||
val next_url_css: String?
|
||||
get() = map["next_url_css"] as? String
|
||||
}
|
||||
|
||||
class MangaNode(private val map: Map<String, Any?>) {
|
||||
|
||||
val parts = CacheNode(toMap(map["parts"]) ?: emptyMap())
|
||||
|
||||
val artist = toMap(map["artist"])?.let { SelectableNode(it) }
|
||||
|
||||
val author = toMap(map["author"])?.let { SelectableNode(it) }
|
||||
|
||||
val summary = toMap(map["summary"])?.let { SelectableNode(it) }
|
||||
|
||||
val status = toMap(map["status"])?.let { StatusNode(it) }
|
||||
|
||||
val genres = toMap(map["genres"])?.let { SelectableNode(it) }
|
||||
|
||||
val cover = toMap(map["cover"])?.let { CoverNode(it) }
|
||||
|
||||
}
|
||||
|
||||
class ChaptersNode(private val map: Map<String, Any?>) {
|
||||
|
||||
val chapter_css: String by map
|
||||
|
||||
val title: String by map
|
||||
|
||||
val date = toMap(toMap(map["date"]))?.let { DateNode(it) }
|
||||
}
|
||||
|
||||
class CacheNode(private val map: Map<String, Any?>) {
|
||||
|
||||
fun get(document: Document) = map.mapValues { document.select(it.value as String).first() }
|
||||
}
|
||||
|
||||
open class SelectableNode(private val map: Map<String, Any?>) {
|
||||
|
||||
val select: String by map
|
||||
|
||||
val from: String?
|
||||
get() = map["from"] as? String
|
||||
|
||||
open val attr: String?
|
||||
get() = map["attr"] as? String
|
||||
|
||||
val capture: String?
|
||||
get() = map["capture"] as? String
|
||||
|
||||
fun process(document: Element, cache: Map<String, Element>): String {
|
||||
val parent = from?.let { cache[it] } ?: document
|
||||
val node = parent.select(select).first()
|
||||
var text = attr?.let { node.attr(it) } ?: node.text()
|
||||
capture?.let {
|
||||
text = Regex(it).find(text)?.groupValues?.get(1) ?: text
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
||||
|
||||
val complete: String?
|
||||
get() = map["complete"] as? String
|
||||
|
||||
val ongoing: String?
|
||||
get() = map["ongoing"] as? String
|
||||
|
||||
val licensed: String?
|
||||
get() = map["licensed"] as? String
|
||||
|
||||
fun getStatus(document: Element, cache: Map<String, Element>): Int {
|
||||
val text = process(document, cache)
|
||||
complete?.let {
|
||||
if (text.contains(it)) return SManga.COMPLETED
|
||||
}
|
||||
ongoing?.let {
|
||||
if (text.contains(it)) return SManga.ONGOING
|
||||
}
|
||||
licensed?.let {
|
||||
if (text.contains(it)) return SManga.LICENSED
|
||||
}
|
||||
return SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
class CoverNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
||||
|
||||
override val attr: String?
|
||||
get() = map["attr"] as? String ?: "src"
|
||||
}
|
||||
|
||||
class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
|
||||
|
||||
val format: String by map
|
||||
|
||||
fun getDate(document: Element, cache: Map<String, Element>, formatter: SimpleDateFormat): Date {
|
||||
val text = process(document, cache)
|
||||
try {
|
||||
return formatter.parse(text)
|
||||
} catch (exception: ParseException) {}
|
||||
|
||||
for (i in 0..7) {
|
||||
(map["day$i"] as? List<String>)?.let {
|
||||
it.find { it.toRegex().containsMatchIn(text) }?.let {
|
||||
return Calendar.getInstance().apply { add(Calendar.DATE, -i) }.time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Date(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PagesNode(private val map: Map<String, Any?>) {
|
||||
|
||||
val pages_regex: String?
|
||||
get() = map["pages_regex"] as? String
|
||||
|
||||
val pages_css: String?
|
||||
get() = map["pages_css"] as? String
|
||||
|
||||
val pages_attr: String?
|
||||
get() = map["pages_attr"] as? String ?: "value"
|
||||
|
||||
val replace: String?
|
||||
get() = map["url_replace"] as? String
|
||||
|
||||
val replacement: String?
|
||||
get() = map["url_replacement"] as? String
|
||||
|
||||
val image_regex: String?
|
||||
get() = map["image_regex"] as? String
|
||||
|
||||
val image_css: String?
|
||||
get() = map["image_css"] as? String
|
||||
|
||||
val image_attr: String
|
||||
get() = map["image_attr"] as? String ?: "src"
|
||||
|
||||
}
|
@ -1,366 +1,366 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
import android.text.Html
|
||||
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.*
|
||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import java.net.URI
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Batoto : ParsedOnlineSource(), LoginSource {
|
||||
|
||||
override val id: Long = 1
|
||||
|
||||
override val name = "Batoto"
|
||||
|
||||
override val baseUrl = "http://bato.to"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*")
|
||||
|
||||
private val dateFields = HashMap<String, Int>().apply {
|
||||
put("second", Calendar.SECOND)
|
||||
put("minute", Calendar.MINUTE)
|
||||
put("hour", Calendar.HOUR)
|
||||
put("day", Calendar.DATE)
|
||||
put("week", Calendar.WEEK_OF_YEAR)
|
||||
put("month", Calendar.MONTH)
|
||||
put("year", Calendar.YEAR)
|
||||
}
|
||||
|
||||
private val staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE)
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Cookie", "lang_option=English")
|
||||
|
||||
private val pageHeaders = super.headersBuilder()
|
||||
.add("Referer", "http://bato.to/reader")
|
||||
.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/search_ajax?order_cond=views&order=desc&p=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/search_ajax?order_cond=update&order=desc&p=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "tr:has(a)"
|
||||
|
||||
override fun latestUpdatesSelector() = "tr:has(a)"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a[href^=http://bato.to]").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text().trim()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "#show_more_row"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
|
||||
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
|
||||
var genres = ""
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> if (!filter.isIgnored()) {
|
||||
url.addQueryParameter("completed", if (filter.isExcluded()) "i" else "c")
|
||||
}
|
||||
is GenreList -> {
|
||||
filter.state.forEach { filter ->
|
||||
when (filter) {
|
||||
is Genre -> if (!filter.isIgnored()) {
|
||||
genres += (if (filter.isExcluded()) ";e" else ";i") + filter.id
|
||||
}
|
||||
is SelectField -> {
|
||||
val sel = filter.values[filter.state].value
|
||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is TextField -> {
|
||||
if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
|
||||
}
|
||||
is SelectField -> {
|
||||
val sel = filter.values[filter.state].value
|
||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||
}
|
||||
is Flag -> {
|
||||
val sel = if (filter.state) filter.valTrue else filter.valFalse
|
||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||
}
|
||||
is OrderBy -> {
|
||||
url.addQueryParameter("order_cond", arrayOf("title", "author", "artist", "rating", "views", "update")[filter.state!!.index])
|
||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "asc" else "desc")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
|
||||
url.addQueryParameter("p", page.toString())
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val mangaId = manga.url.substringAfterLast("r")
|
||||
return GET("$baseUrl/comic_pop?id=$mangaId", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val tbody = document.select("tbody").first()
|
||||
val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = artistElement.selectText("td:eq(1)")
|
||||
manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author
|
||||
manga.description = tbody.selectText("tr:contains(Description:) > td:eq(1)")
|
||||
manga.thumbnail_url = document.select("img[src^=http://img.bato.to/forums/uploads/]").first()?.attr("src")
|
||||
manga.status = parseStatus(document.selectText("tr:contains(Status:) > td:eq(1)"))
|
||||
manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?) = when (status) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Complete" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val body = response.body().string()
|
||||
val matcher = staffNotice.matcher(body)
|
||||
if (matcher.find()) {
|
||||
@Suppress("DEPRECATION")
|
||||
val notice = Html.fromHtml(matcher.group(1)).toString().trim()
|
||||
throw Exception(notice)
|
||||
}
|
||||
|
||||
val document = response.asJsoup(body)
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a[href^=http://bato.to/reader").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
||||
parseDateFromElement(it)
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseDateFromElement(dateElement: Element): Long {
|
||||
val dateAsString = dateElement.text()
|
||||
|
||||
var date: Date
|
||||
try {
|
||||
date = SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString)
|
||||
} catch (e: ParseException) {
|
||||
val m = datePattern.matcher(dateAsString)
|
||||
|
||||
if (m.matches()) {
|
||||
val number = m.group(1)
|
||||
val amount = if (number.contains("A")) 1 else Integer.parseInt(m.group(1))
|
||||
val unit = m.group(2)
|
||||
|
||||
date = Calendar.getInstance().apply {
|
||||
add(dateFields[unit]!!, -amount)
|
||||
}.time
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
return date.time
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val id = chapter.url.substringAfterLast("#")
|
||||
return GET("$baseUrl/areader?id=$id&p=1", pageHeaders)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
val selectElement = document.select("#page_select").first()
|
||||
if (selectElement != null) {
|
||||
for ((i, element) in selectElement.select("option").withIndex()) {
|
||||
pages.add(Page(i, element.attr("value")))
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
} else {
|
||||
// For webtoons in one page
|
||||
for ((i, element) in document.select("div > img").withIndex()) {
|
||||
pages.add(Page(i, "", element.attr("src")))
|
||||
}
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page): Request {
|
||||
val pageUrl = page.url
|
||||
val start = pageUrl.indexOf("#") + 1
|
||||
val end = pageUrl.indexOf("_", start)
|
||||
val id = pageUrl.substring(start, end)
|
||||
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end + 1)}", pageHeaders)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String {
|
||||
return document.select("#comic_page").first().attr("src")
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String) =
|
||||
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers))
|
||||
.asObservable()
|
||||
.flatMap { doLogin(it, username, password) }
|
||||
.map { isAuthenticationSuccessful(it) }
|
||||
|
||||
private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
|
||||
val doc = response.asJsoup()
|
||||
val form = doc.select("#login").first()
|
||||
val url = form.attr("action")
|
||||
val authKey = form.select("input[name=auth_key]").first()
|
||||
|
||||
val payload = FormBody.Builder().apply {
|
||||
add(authKey.attr("name"), authKey.attr("value"))
|
||||
add("ips_username", username)
|
||||
add("ips_password", password)
|
||||
add("invisible", "1")
|
||||
add("rememberMe", "1")
|
||||
}.build()
|
||||
|
||||
return client.newCall(POST(url, headers, payload)).asObservable()
|
||||
}
|
||||
|
||||
override fun isAuthenticationSuccessful(response: Response) =
|
||||
response.priorResponse() != null && response.priorResponse().code() == 302
|
||||
|
||||
override fun isLogged(): Boolean {
|
||||
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
if (!isLogged()) {
|
||||
val username = preferences.sourceUsername(this)
|
||||
val password = preferences.sourcePassword(this)
|
||||
|
||||
if (username.isNullOrEmpty() || password.isNullOrEmpty()) {
|
||||
return Observable.error(Exception("User not logged"))
|
||||
} else {
|
||||
return login(username, password).flatMap { super.fetchChapterList(manga) }
|
||||
}
|
||||
|
||||
} else {
|
||||
return super.fetchChapterList(manga)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ListValue(val name: String, val value: String) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class SelectField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.Select<ListValue>(name, values, state)
|
||||
private class Flag(name: String, val key: String, val valTrue: String, val valFalse: String) : Filter.CheckBox(name)
|
||||
private class GenreList(genres: List<Filter<*>>) : Filter.Group<Filter<*>>("Genres", genres)
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Title", "Author", "Artist", "Rating", "Views", "Last Update"),
|
||||
Filter.Sort.Selection(4, false))
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "artist_name"),
|
||||
SelectField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
|
||||
Status(),
|
||||
Flag("Exclude mature", "mature", "m", ""),
|
||||
OrderBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
|
||||
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Genre("${el.textContent.trim()}", ${id})`
|
||||
// }).join(',\n')
|
||||
// on https://bato.to/search
|
||||
private fun getGenreList() = listOf(
|
||||
SelectField("Inclusion mode", "genre_cond", arrayOf(ListValue("And (all selected genres)", "and"), ListValue("Or (any selected genres) ", "or"))),
|
||||
Genre("4-Koma", 40),
|
||||
Genre("Action", 1),
|
||||
Genre("Adventure", 2),
|
||||
Genre("Award Winning", 39),
|
||||
Genre("Comedy", 3),
|
||||
Genre("Cooking", 41),
|
||||
Genre("Doujinshi", 9),
|
||||
Genre("Drama", 10),
|
||||
Genre("Ecchi", 12),
|
||||
Genre("Fantasy", 13),
|
||||
Genre("Gender Bender", 15),
|
||||
Genre("Harem", 17),
|
||||
Genre("Historical", 20),
|
||||
Genre("Horror", 22),
|
||||
Genre("Josei", 34),
|
||||
Genre("Martial Arts", 27),
|
||||
Genre("Mecha", 30),
|
||||
Genre("Medical", 42),
|
||||
Genre("Music", 37),
|
||||
Genre("Mystery", 4),
|
||||
Genre("Oneshot", 38),
|
||||
Genre("Psychological", 5),
|
||||
Genre("Romance", 6),
|
||||
Genre("School Life", 7),
|
||||
Genre("Sci-fi", 8),
|
||||
Genre("Seinen", 32),
|
||||
Genre("Shoujo", 35),
|
||||
Genre("Shoujo Ai", 16),
|
||||
Genre("Shounen", 33),
|
||||
Genre("Shounen Ai", 19),
|
||||
Genre("Slice of Life", 21),
|
||||
Genre("Smut", 23),
|
||||
Genre("Sports", 25),
|
||||
Genre("Supernatural", 26),
|
||||
Genre("Tragedy", 28),
|
||||
Genre("Webtoon", 36),
|
||||
Genre("Yaoi", 29),
|
||||
Genre("Yuri", 31),
|
||||
Genre("[no chapters]", 44)
|
||||
)
|
||||
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import android.text.Html
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.selectText
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import java.net.URI
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Batoto : ParsedOnlineSource(), LoginSource {
|
||||
|
||||
override val id: Long = 1
|
||||
|
||||
override val name = "Batoto"
|
||||
|
||||
override val baseUrl = "http://bato.to"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val datePattern = Pattern.compile("(\\d+|A|An)\\s+(.*?)s? ago.*")
|
||||
|
||||
private val dateFields = HashMap<String, Int>().apply {
|
||||
put("second", Calendar.SECOND)
|
||||
put("minute", Calendar.MINUTE)
|
||||
put("hour", Calendar.HOUR)
|
||||
put("day", Calendar.DATE)
|
||||
put("week", Calendar.WEEK_OF_YEAR)
|
||||
put("month", Calendar.MONTH)
|
||||
put("year", Calendar.YEAR)
|
||||
}
|
||||
|
||||
private val staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE)
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Cookie", "lang_option=English")
|
||||
|
||||
private val pageHeaders = super.headersBuilder()
|
||||
.add("Referer", "http://bato.to/reader")
|
||||
.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/search_ajax?order_cond=views&order=desc&p=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/search_ajax?order_cond=update&order=desc&p=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "tr:has(a)"
|
||||
|
||||
override fun latestUpdatesSelector() = "tr:has(a)"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a[href^=http://bato.to]").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text().trim()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "#show_more_row"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "#show_more_row"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search_ajax").newBuilder()
|
||||
if (!query.isEmpty()) url.addQueryParameter("name", query).addQueryParameter("name_cond", "c")
|
||||
var genres = ""
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> if (!filter.isIgnored()) {
|
||||
url.addQueryParameter("completed", if (filter.isExcluded()) "i" else "c")
|
||||
}
|
||||
is GenreList -> {
|
||||
filter.state.forEach { filter ->
|
||||
when (filter) {
|
||||
is Genre -> if (!filter.isIgnored()) {
|
||||
genres += (if (filter.isExcluded()) ";e" else ";i") + filter.id
|
||||
}
|
||||
is SelectField -> {
|
||||
val sel = filter.values[filter.state].value
|
||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is TextField -> {
|
||||
if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
|
||||
}
|
||||
is SelectField -> {
|
||||
val sel = filter.values[filter.state].value
|
||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||
}
|
||||
is Flag -> {
|
||||
val sel = if (filter.state) filter.valTrue else filter.valFalse
|
||||
if (!sel.isEmpty()) url.addQueryParameter(filter.key, sel)
|
||||
}
|
||||
is OrderBy -> {
|
||||
url.addQueryParameter("order_cond", arrayOf("title", "author", "artist", "rating", "views", "update")[filter.state!!.index])
|
||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "asc" else "desc")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!genres.isEmpty()) url.addQueryParameter("genres", genres)
|
||||
url.addQueryParameter("p", page.toString())
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val mangaId = manga.url.substringAfterLast("r")
|
||||
return GET("$baseUrl/comic_pop?id=$mangaId", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val tbody = document.select("tbody").first()
|
||||
val artistElement = tbody.select("tr:contains(Author/Artist:)").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = artistElement.selectText("td:eq(1)")
|
||||
manga.artist = artistElement.selectText("td:eq(2)") ?: manga.author
|
||||
manga.description = tbody.selectText("tr:contains(Description:) > td:eq(1)")
|
||||
manga.thumbnail_url = document.select("img[src^=http://img.bato.to/forums/uploads/]").first()?.attr("src")
|
||||
manga.status = parseStatus(document.selectText("tr:contains(Status:) > td:eq(1)"))
|
||||
manga.genre = tbody.select("tr:contains(Genres:) img").map { it.attr("alt") }.joinToString(", ")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?) = when (status) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Complete" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val body = response.body().string()
|
||||
val matcher = staffNotice.matcher(body)
|
||||
if (matcher.find()) {
|
||||
@Suppress("DEPRECATION")
|
||||
val notice = Html.fromHtml(matcher.group(1)).toString().trim()
|
||||
throw Exception(notice)
|
||||
}
|
||||
|
||||
val document = response.asJsoup(body)
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a[href^=http://bato.to/reader").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("td").getOrNull(4)?.let {
|
||||
parseDateFromElement(it)
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseDateFromElement(dateElement: Element): Long {
|
||||
val dateAsString = dateElement.text()
|
||||
|
||||
var date: Date
|
||||
try {
|
||||
date = SimpleDateFormat("dd MMMMM yyyy - hh:mm a", Locale.ENGLISH).parse(dateAsString)
|
||||
} catch (e: ParseException) {
|
||||
val m = datePattern.matcher(dateAsString)
|
||||
|
||||
if (m.matches()) {
|
||||
val number = m.group(1)
|
||||
val amount = if (number.contains("A")) 1 else Integer.parseInt(m.group(1))
|
||||
val unit = m.group(2)
|
||||
|
||||
date = Calendar.getInstance().apply {
|
||||
add(dateFields[unit]!!, -amount)
|
||||
}.time
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
return date.time
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val id = chapter.url.substringAfterLast("#")
|
||||
return GET("$baseUrl/areader?id=$id&p=1", pageHeaders)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
val selectElement = document.select("#page_select").first()
|
||||
if (selectElement != null) {
|
||||
for ((i, element) in selectElement.select("option").withIndex()) {
|
||||
pages.add(Page(i, element.attr("value")))
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
} else {
|
||||
// For webtoons in one page
|
||||
for ((i, element) in document.select("div > img").withIndex()) {
|
||||
pages.add(Page(i, "", element.attr("src")))
|
||||
}
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page): Request {
|
||||
val pageUrl = page.url
|
||||
val start = pageUrl.indexOf("#") + 1
|
||||
val end = pageUrl.indexOf("_", start)
|
||||
val id = pageUrl.substring(start, end)
|
||||
return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end + 1)}", pageHeaders)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String {
|
||||
return document.select("#comic_page").first().attr("src")
|
||||
}
|
||||
|
||||
override fun login(username: String, password: String) =
|
||||
client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers))
|
||||
.asObservable()
|
||||
.flatMap { doLogin(it, username, password) }
|
||||
.map { isAuthenticationSuccessful(it) }
|
||||
|
||||
private fun doLogin(response: Response, username: String, password: String): Observable<Response> {
|
||||
val doc = response.asJsoup()
|
||||
val form = doc.select("#login").first()
|
||||
val url = form.attr("action")
|
||||
val authKey = form.select("input[name=auth_key]").first()
|
||||
|
||||
val payload = FormBody.Builder().apply {
|
||||
add(authKey.attr("name"), authKey.attr("value"))
|
||||
add("ips_username", username)
|
||||
add("ips_password", password)
|
||||
add("invisible", "1")
|
||||
add("rememberMe", "1")
|
||||
}.build()
|
||||
|
||||
return client.newCall(POST(url, headers, payload)).asObservable()
|
||||
}
|
||||
|
||||
override fun isAuthenticationSuccessful(response: Response) =
|
||||
response.priorResponse() != null && response.priorResponse().code() == 302
|
||||
|
||||
override fun isLogged(): Boolean {
|
||||
return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" }
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
if (!isLogged()) {
|
||||
val username = preferences.sourceUsername(this)
|
||||
val password = preferences.sourcePassword(this)
|
||||
|
||||
if (username.isNullOrEmpty() || password.isNullOrEmpty()) {
|
||||
return Observable.error(Exception("User not logged"))
|
||||
} else {
|
||||
return login(username, password).flatMap { super.fetchChapterList(manga) }
|
||||
}
|
||||
|
||||
} else {
|
||||
return super.fetchChapterList(manga)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ListValue(val name: String, val value: String) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class SelectField(name: String, val key: String, values: Array<ListValue>, state: Int = 0) : Filter.Select<ListValue>(name, values, state)
|
||||
private class Flag(name: String, val key: String, val valTrue: String, val valFalse: String) : Filter.CheckBox(name)
|
||||
private class GenreList(genres: List<Filter<*>>) : Filter.Group<Filter<*>>("Genres", genres)
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Title", "Author", "Artist", "Rating", "Views", "Last Update"),
|
||||
Filter.Sort.Selection(4, false))
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "artist_name"),
|
||||
SelectField("Type", "type", arrayOf(ListValue("Any", ""), ListValue("Manga (Jp)", "jp"), ListValue("Manhwa (Kr)", "kr"), ListValue("Manhua (Cn)", "cn"), ListValue("Artbook", "ar"), ListValue("Other", "ot"))),
|
||||
Status(),
|
||||
Flag("Exclude mature", "mature", "m", ""),
|
||||
OrderBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => {
|
||||
// const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Genre("${el.textContent.trim()}", ${id})`
|
||||
// }).join(',\n')
|
||||
// on https://bato.to/search
|
||||
private fun getGenreList() = listOf(
|
||||
SelectField("Inclusion mode", "genre_cond", arrayOf(ListValue("And (all selected genres)", "and"), ListValue("Or (any selected genres) ", "or"))),
|
||||
Genre("4-Koma", 40),
|
||||
Genre("Action", 1),
|
||||
Genre("Adventure", 2),
|
||||
Genre("Award Winning", 39),
|
||||
Genre("Comedy", 3),
|
||||
Genre("Cooking", 41),
|
||||
Genre("Doujinshi", 9),
|
||||
Genre("Drama", 10),
|
||||
Genre("Ecchi", 12),
|
||||
Genre("Fantasy", 13),
|
||||
Genre("Gender Bender", 15),
|
||||
Genre("Harem", 17),
|
||||
Genre("Historical", 20),
|
||||
Genre("Horror", 22),
|
||||
Genre("Josei", 34),
|
||||
Genre("Martial Arts", 27),
|
||||
Genre("Mecha", 30),
|
||||
Genre("Medical", 42),
|
||||
Genre("Music", 37),
|
||||
Genre("Mystery", 4),
|
||||
Genre("Oneshot", 38),
|
||||
Genre("Psychological", 5),
|
||||
Genre("Romance", 6),
|
||||
Genre("School Life", 7),
|
||||
Genre("Sci-fi", 8),
|
||||
Genre("Seinen", 32),
|
||||
Genre("Shoujo", 35),
|
||||
Genre("Shoujo Ai", 16),
|
||||
Genre("Shounen", 33),
|
||||
Genre("Shounen Ai", 19),
|
||||
Genre("Slice of Life", 21),
|
||||
Genre("Smut", 23),
|
||||
Genre("Sports", 25),
|
||||
Genre("Supernatural", 26),
|
||||
Genre("Tragedy", 28),
|
||||
Genre("Webtoon", 36),
|
||||
Genre("Yaoi", 29),
|
||||
Genre("Yuri", 31),
|
||||
Genre("[no chapters]", 44)
|
||||
)
|
||||
|
||||
}
|
@ -1,197 +1,197 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.source.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Kissmanga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 4
|
||||
|
||||
override val name = "Kissmanga"
|
||||
|
||||
override val baseUrl = "http://kissmanga.com"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
override fun popularMangaSelector() = "table.listing tr:gt(1)"
|
||||
|
||||
override fun latestUpdatesSelector() = "table.listing tr:gt(1)"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/MangaList/MostPopular?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("http://kissmanga.com/MangaList/LatestUpdate?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("td a:eq(0)").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = "ul.pager > li > a:contains(Next)"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val form = FormBody.Builder().apply {
|
||||
add("mangaName", query)
|
||||
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is Author -> add("authorArtist", filter.state)
|
||||
is Status -> add("status", arrayOf("", "Completed", "Ongoing")[filter.state])
|
||||
is GenreList -> filter.state.forEach { genre -> add("genres", genre.state.toString()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return POST("$baseUrl/AdvanceSearch", headers, form.build())
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.barContent").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
|
||||
manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
|
||||
manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text()
|
||||
manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "table.listing tr:gt(1)"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||
SimpleDateFormat("MM/dd/yyyy").parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
//language=RegExp
|
||||
val p = Pattern.compile("""lstImages.push\("(.+?)"""")
|
||||
val m = p.matcher(response.body().string())
|
||||
|
||||
var i = 0
|
||||
while (m.find()) {
|
||||
pages.add(Page(i++, "", m.group(1)))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page) = GET(page.url)
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Author : Filter.Text("Author")
|
||||
private class Genre(name: String) : Filter.TriState(name)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Author(),
|
||||
Status(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
|
||||
// on http://kissmanga.com/AdvanceSearch
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("4-Koma"),
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Comic"),
|
||||
Genre("Cooking"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Lolicon"),
|
||||
Genre("Manga"),
|
||||
Genre("Manhua"),
|
||||
Genre("Manhwa"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Medical"),
|
||||
Genre("Music"),
|
||||
Genre("Mystery"),
|
||||
Genre("One shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shotacon"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Webtoon"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
)
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Kissmanga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 4
|
||||
|
||||
override val name = "Kissmanga"
|
||||
|
||||
override val baseUrl = "http://kissmanga.com"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
override fun popularMangaSelector() = "table.listing tr:gt(1)"
|
||||
|
||||
override fun latestUpdatesSelector() = "table.listing tr:gt(1)"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/MangaList/MostPopular?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("http://kissmanga.com/MangaList/LatestUpdate?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("td a:eq(0)").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "li > a:contains(› Next)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = "ul.pager > li > a:contains(Next)"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val form = FormBody.Builder().apply {
|
||||
add("mangaName", query)
|
||||
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is Author -> add("authorArtist", filter.state)
|
||||
is Status -> add("status", arrayOf("", "Completed", "Ongoing")[filter.state])
|
||||
is GenreList -> filter.state.forEach { genre -> add("genres", genre.state.toString()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return POST("$baseUrl/AdvanceSearch", headers, form.build())
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.barContent").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text()
|
||||
manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text()
|
||||
manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text()
|
||||
manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "table.listing tr:gt(1)"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||
SimpleDateFormat("MM/dd/yyyy").parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) = POST(baseUrl + chapter.url, headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
//language=RegExp
|
||||
val p = Pattern.compile("""lstImages.push\("(.+?)"""")
|
||||
val m = p.matcher(response.body().string())
|
||||
|
||||
var i = 0
|
||||
while (m.find()) {
|
||||
pages.add(Page(i++, "", m.group(1)))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlRequest(page: Page) = GET(page.url)
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Author : Filter.Text("Author")
|
||||
private class Genre(name: String) : Filter.TriState(name)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Author(),
|
||||
Status(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// $("select[name=\"genres\"]").map((i,el) => `Genre("${$(el).next().text().trim()}", ${i})`).get().join(',\n')
|
||||
// on http://kissmanga.com/AdvanceSearch
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("4-Koma"),
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Comic"),
|
||||
Genre("Cooking"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Lolicon"),
|
||||
Genre("Manga"),
|
||||
Genre("Manhua"),
|
||||
Genre("Manhwa"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Medical"),
|
||||
Genre("Music"),
|
||||
Genre("Mystery"),
|
||||
Genre("One shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shotacon"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Webtoon"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
)
|
||||
}
|
@ -1,223 +1,223 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
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 Mangafox : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 3
|
||||
|
||||
override val name = "Mangafox"
|
||||
|
||||
override val baseUrl = "http://mangafox.me"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val pageStr = if (page != 1) "$page.htm" else ""
|
||||
return GET("$baseUrl/directory/$pageStr", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val pageStr = if (page != 1) "$page.htm" else ""
|
||||
return GET("$baseUrl/directory/$pageStr?latest")
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.title").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> url.addQueryParameter(filter.id, filter.state.toString())
|
||||
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
|
||||
is OrderBy -> {
|
||||
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||
}
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("page", page.toString())
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.title").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div#title").first()
|
||||
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
|
||||
val sideInfoElement = document.select("#series_info").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = rowElement.select("td:eq(1)").first()?.text()
|
||||
manga.artist = rowElement.select("td:eq(2)").first()?.text()
|
||||
manga.genre = rowElement.select("td:eq(3)").first()?.text()
|
||||
manga.description = infoElement.select("p.summary").first()?.text()
|
||||
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div#chapters li div"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a.tips").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return if ("Today" in date || " ago" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else if ("Yesterday" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -1)
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
try {
|
||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val url = document.baseUri().substringBeforeLast('/')
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("select.m").first()?.select("option:not([value=0])")?.forEach {
|
||||
pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String {
|
||||
val url = document.getElementById("image").attr("src")
|
||||
return if ("compressed?token=" !in url) {
|
||||
url
|
||||
} else {
|
||||
"http://mangafox.me/media/logo.png"
|
||||
}
|
||||
}
|
||||
|
||||
private class Status(val id: String = "is_completed") : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Artist", "artist"),
|
||||
Type(),
|
||||
Status(),
|
||||
OrderBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
|
||||
// on http://mangafox.me/search.php
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("One Shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Webtoons"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
)
|
||||
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.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 Mangafox : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 3
|
||||
|
||||
override val name = "Mangafox"
|
||||
|
||||
override val baseUrl = "http://mangafox.me"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val pageStr = if (page != 1) "$page.htm" else ""
|
||||
return GET("$baseUrl/directory/$pageStr", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val pageStr = if (page != 1) "$page.htm" else ""
|
||||
return GET("$baseUrl/directory/$pageStr?latest")
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.title").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> url.addQueryParameter(filter.id, filter.state.toString())
|
||||
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
|
||||
is OrderBy -> {
|
||||
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||
}
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("page", page.toString())
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div#mangalist > ul.list > li"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.title").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div#title").first()
|
||||
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
|
||||
val sideInfoElement = document.select("#series_info").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = rowElement.select("td:eq(1)").first()?.text()
|
||||
manga.artist = rowElement.select("td:eq(2)").first()?.text()
|
||||
manga.genre = rowElement.select("td:eq(3)").first()?.text()
|
||||
manga.description = infoElement.select("p.summary").first()?.text()
|
||||
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div#chapters li div"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a.tips").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return if ("Today" in date || " ago" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else if ("Yesterday" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -1)
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
try {
|
||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val url = document.baseUri().substringBeforeLast('/')
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("select.m").first()?.select("option:not([value=0])")?.forEach {
|
||||
pages.add(Page(pages.size, "$url/${it.attr("value")}.html"))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String {
|
||||
val url = document.getElementById("image").attr("src")
|
||||
return if ("compressed?token=" !in url) {
|
||||
url
|
||||
} else {
|
||||
"http://mangafox.me/media/logo.png"
|
||||
}
|
||||
}
|
||||
|
||||
private class Status(val id: String = "is_completed") : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Artist", "artist"),
|
||||
Type(),
|
||||
Status(),
|
||||
OrderBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// $('select.genres').map((i,el)=>`Genre("${$(el).next().text().trim()}", "${$(el).attr('name')}")`).get().join(',\n')
|
||||
// on http://mangafox.me/search.php
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("One Shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Webtoons"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
)
|
||||
|
||||
}
|
@ -1,220 +1,220 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
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 : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 2
|
||||
|
||||
override val name = "Mangahere"
|
||||
|
||||
override val baseUrl = "http://www.mangahere.co"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/directory/$page.htm?views.za", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/directory/$page.htm?last_chapter_time.za", headers)
|
||||
}
|
||||
|
||||
private fun mangaFromElement(query: String, element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select(query).first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return mangaFromElement("div.title > a", element)
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
||||
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is Type -> url.addQueryParameter("direction", arrayOf("", "rl", "lr")[filter.state])
|
||||
is OrderBy -> {
|
||||
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||
}
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("page", page.toString())
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return mangaFromElement("a.manga_info", element)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select(".manga_detail_top").first()
|
||||
val infoElement = detailElement.select(".detail_topText").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text()
|
||||
manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text()
|
||||
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
|
||||
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
|
||||
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val parentEl = element.select("span.left").first()
|
||||
|
||||
val urlElement = parentEl.select("a").first()
|
||||
|
||||
var volume = parentEl.select("span.mr6")?.first()?.text()?.trim() ?: ""
|
||||
if (volume.length > 0) {
|
||||
volume = " - " + volume
|
||||
}
|
||||
|
||||
var title = parentEl?.textNodes()?.last()?.text()?.trim() ?: ""
|
||||
if (title.length > 0) {
|
||||
title = " - " + title
|
||||
}
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text() + volume + title
|
||||
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return if ("Today" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else if ("Yesterday" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -1)
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
try {
|
||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga (read from right to left)", "Korean Manhwa (read from left to right)"))
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Artist", "artist"),
|
||||
Type(),
|
||||
Status(),
|
||||
OrderBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Genre("${el.nextSibling.nextSibling.textContent.trim()}", "${el.getAttribute('name')}")`).join(',\n')
|
||||
// http://www.mangahere.co/advsearch.htm
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("One Shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
)
|
||||
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.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 : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 2
|
||||
|
||||
override val name = "Mangahere"
|
||||
|
||||
override val baseUrl = "http://www.mangahere.co"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = "div.directory_list > ul > li"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/directory/$page.htm?views.za", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/directory/$page.htm?last_chapter_time.za", headers)
|
||||
}
|
||||
|
||||
private fun mangaFromElement(query: String, element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select(query).first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = if (it.hasAttr("title")) it.attr("title") else if (it.hasAttr("rel")) it.attr("rel") else it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return mangaFromElement("div.title > a", element)
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search.php?name_method=cw&author_method=cw&artist_method=cw&advopts=1").newBuilder().addQueryParameter("name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> url.addQueryParameter("is_completed", arrayOf("", "1", "0")[filter.state])
|
||||
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is Type -> url.addQueryParameter("direction", arrayOf("", "rl", "lr")[filter.state])
|
||||
is OrderBy -> {
|
||||
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
|
||||
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
|
||||
}
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("page", page.toString())
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return mangaFromElement("a.manga_info", element)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select(".manga_detail_top").first()
|
||||
val infoElement = detailElement.select(".detail_topText").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text()
|
||||
manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text()
|
||||
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
|
||||
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
|
||||
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = detailElement.select("img.img").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".detail_list > ul:not([class]) > li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val parentEl = element.select("span.left").first()
|
||||
|
||||
val urlElement = parentEl.select("a").first()
|
||||
|
||||
var volume = parentEl.select("span.mr6")?.first()?.text()?.trim() ?: ""
|
||||
if (volume.length > 0) {
|
||||
volume = " - " + volume
|
||||
}
|
||||
|
||||
var title = parentEl?.textNodes()?.last()?.text()?.trim() ?: ""
|
||||
if (title.length > 0) {
|
||||
title = " - " + title
|
||||
}
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text() + volume + title
|
||||
chapter.date_upload = element.select("span.right").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return if ("Today" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else if ("Yesterday" in date) {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, -1)
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
} else {
|
||||
try {
|
||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src")
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: String = "genres[$name]") : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class Type : Filter.Select<String>("Type", arrayOf("Any", "Japanese Manga (read from right to left)", "Korean Manhwa (read from left to right)"))
|
||||
private class OrderBy : Filter.Sort("Order by",
|
||||
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
|
||||
Filter.Sort.Selection(2, false))
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Artist", "artist"),
|
||||
Type(),
|
||||
Status(),
|
||||
OrderBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("select[id^='genres'")].map((el,i) => `Genre("${el.nextSibling.nextSibling.textContent.trim()}", "${el.getAttribute('name')}")`).join(',\n')
|
||||
// http://www.mangahere.co/advsearch.htm
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("One Shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
)
|
||||
|
||||
}
|
@ -1,243 +1,243 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.source.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Mangasee : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 9
|
||||
|
||||
override val name = "Mangasee"
|
||||
|
||||
override val baseUrl = "http://mangaseeonline.net"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val recentUpdatesPattern = Pattern.compile("(.*?)\\s(\\d+\\.?\\d*)\\s?(Completed)?")
|
||||
|
||||
private val indexPattern = Pattern.compile("-index-(.*?)-")
|
||||
|
||||
override fun popularMangaSelector() = "div.requested > div.row"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending")
|
||||
return POST(requestUrl, headers, body.build())
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.resultLink").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun searchMangaSelector() = "div.requested > div.row"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
|
||||
if (!query.isEmpty()) url.addQueryParameter("keyword", query)
|
||||
val genres = mutableListOf<String>()
|
||||
val genresNo = mutableListOf<String>()
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is Sort -> {
|
||||
if (filter.state?.index != 0)
|
||||
url.addQueryParameter("sortBy", if (filter.state?.index == 1) "dateUpdated" else "popularity")
|
||||
if (filter.state?.ascending != true)
|
||||
url.addQueryParameter("sortOrder", "descending")
|
||||
}
|
||||
is SelectField -> if (filter.state != 0) url.addQueryParameter(filter.key, filter.values[filter.state])
|
||||
is TextField -> if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
when (genre.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
|
||||
Filter.TriState.STATE_EXCLUDE -> genresNo.add(genre.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (genres.isNotEmpty()) url.addQueryParameter("genre", genres.joinToString(","))
|
||||
if (genresNo.isNotEmpty()) url.addQueryParameter("genreNo", genresNo.joinToString(","))
|
||||
|
||||
val (body, requestUrl) = convertQueryToPost(page, url.toString())
|
||||
return POST(requestUrl, headers, body.build())
|
||||
}
|
||||
|
||||
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
||||
val url = HttpUrl.parse(url)
|
||||
val body = FormBody.Builder().add("page", page.toString())
|
||||
for (i in 0..url.querySize() - 1) {
|
||||
body.add(url.queryParameterName(i), url.queryParameterValue(i))
|
||||
}
|
||||
val requestUrl = url.scheme() + "://" + url.host() + url.encodedPath()
|
||||
return Pair(body, requestUrl)
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.resultLink").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select("div.well > div.row").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text()
|
||||
manga.genre = detailElement.select("span.details > div.row > div:has(b:contains(Genre(s))) > a").map { it.text() }.joinToString()
|
||||
manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
|
||||
manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing (Scan)") -> SManga.ONGOING
|
||||
status.contains("Complete (Scan)") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapter-list > a"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: ""
|
||||
chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(dateAsString: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val fullUrl = document.baseUri()
|
||||
val url = fullUrl.substringBeforeLast('/')
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
val series = document.select("input.IndexName").first().attr("value")
|
||||
val chapter = document.select("span.CurChapter").first().text()
|
||||
var index = ""
|
||||
|
||||
val m = indexPattern.matcher(fullUrl)
|
||||
if (m.find()) {
|
||||
val indexNumber = m.group(1)
|
||||
index = "-index-$indexNumber"
|
||||
}
|
||||
|
||||
document.select("div.ContainerNav").first().select("select.PageSelect > option").forEach {
|
||||
pages.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html"))
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun latestUpdatesSelector(): String = "a.latestSeries"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = "http://mangaseeonline.net/home/latest.request.php"
|
||||
val (body, requestUrl) = convertQueryToPost(page, url)
|
||||
return POST(requestUrl, headers, body.build())
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.latestSeries").first().let {
|
||||
val chapterUrl = it.attr("href")
|
||||
val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
|
||||
val indexOfLastPath = chapterUrl.lastIndexOf("/")
|
||||
val mangaUrl = chapterUrl.substring(indexOfLastPath, indexOfMangaUrl)
|
||||
val defaultText = it.select("p.clamp2").text()
|
||||
val m = recentUpdatesPattern.matcher(defaultText)
|
||||
val title = if (m.matches()) m.group(1) else defaultText
|
||||
manga.setUrlWithoutDomain("/manga" + mangaUrl)
|
||||
manga.title = title
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Filter.Sort.Selection(2, false))
|
||||
private class Genre(name: String) : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class SelectField(name: String, val key: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Years", "year"),
|
||||
TextField("Author", "author"),
|
||||
SelectField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
|
||||
SelectField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
|
||||
SelectField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
|
||||
Sort(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||
// http://mangasee.co/advanced-search/
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Hentai"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Lolicon"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shotacon"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
)
|
||||
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Mangasee : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 9
|
||||
|
||||
override val name = "Mangasee"
|
||||
|
||||
override val baseUrl = "http://mangaseeonline.net"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val recentUpdatesPattern = Pattern.compile("(.*?)\\s(\\d+\\.?\\d*)\\s?(Completed)?")
|
||||
|
||||
private val indexPattern = Pattern.compile("-index-(.*?)-")
|
||||
|
||||
override fun popularMangaSelector() = "div.requested > div.row"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending")
|
||||
return POST(requestUrl, headers, body.build())
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.resultLink").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun searchMangaSelector() = "div.requested > div.row"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/search/request.php").newBuilder()
|
||||
if (!query.isEmpty()) url.addQueryParameter("keyword", query)
|
||||
val genres = mutableListOf<String>()
|
||||
val genresNo = mutableListOf<String>()
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is Sort -> {
|
||||
if (filter.state?.index != 0)
|
||||
url.addQueryParameter("sortBy", if (filter.state?.index == 1) "dateUpdated" else "popularity")
|
||||
if (filter.state?.ascending != true)
|
||||
url.addQueryParameter("sortOrder", "descending")
|
||||
}
|
||||
is SelectField -> if (filter.state != 0) url.addQueryParameter(filter.key, filter.values[filter.state])
|
||||
is TextField -> if (!filter.state.isEmpty()) url.addQueryParameter(filter.key, filter.state)
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
when (genre.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
|
||||
Filter.TriState.STATE_EXCLUDE -> genresNo.add(genre.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (genres.isNotEmpty()) url.addQueryParameter("genre", genres.joinToString(","))
|
||||
if (genresNo.isNotEmpty()) url.addQueryParameter("genreNo", genresNo.joinToString(","))
|
||||
|
||||
val (body, requestUrl) = convertQueryToPost(page, url.toString())
|
||||
return POST(requestUrl, headers, body.build())
|
||||
}
|
||||
|
||||
private fun convertQueryToPost(page: Int, url: String): Pair<FormBody.Builder, String> {
|
||||
val url = HttpUrl.parse(url)
|
||||
val body = FormBody.Builder().add("page", page.toString())
|
||||
for (i in 0..url.querySize() - 1) {
|
||||
body.add(url.queryParameterName(i), url.queryParameterValue(i))
|
||||
}
|
||||
val requestUrl = url.scheme() + "://" + url.host() + url.encodedPath()
|
||||
return Pair(body, requestUrl)
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.resultLink").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select("div.well > div.row").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text()
|
||||
manga.genre = detailElement.select("span.details > div.row > div:has(b:contains(Genre(s))) > a").map { it.text() }.joinToString()
|
||||
manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
|
||||
manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing (Scan)") -> SManga.ONGOING
|
||||
status.contains("Complete (Scan)") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapter-list > a"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = element.select("span.chapterLabel").first().text()?.let { it } ?: ""
|
||||
chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(dateAsString: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(dateAsString).time
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val fullUrl = document.baseUri()
|
||||
val url = fullUrl.substringBeforeLast('/')
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
val series = document.select("input.IndexName").first().attr("value")
|
||||
val chapter = document.select("span.CurChapter").first().text()
|
||||
var index = ""
|
||||
|
||||
val m = indexPattern.matcher(fullUrl)
|
||||
if (m.find()) {
|
||||
val indexNumber = m.group(1)
|
||||
index = "-index-$indexNumber"
|
||||
}
|
||||
|
||||
document.select("div.ContainerNav").first().select("select.PageSelect > option").forEach {
|
||||
pages.add(Page(pages.size, "$url/$series-chapter-$chapter$index-page-${pages.size + 1}.html"))
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = document.select("img.CurImage").attr("src")
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "button.requestMore"
|
||||
|
||||
override fun latestUpdatesSelector(): String = "a.latestSeries"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = "http://mangaseeonline.net/home/latest.request.php"
|
||||
val (body, requestUrl) = convertQueryToPost(page, url)
|
||||
return POST(requestUrl, headers, body.build())
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.latestSeries").first().let {
|
||||
val chapterUrl = it.attr("href")
|
||||
val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
|
||||
val indexOfLastPath = chapterUrl.lastIndexOf("/")
|
||||
val mangaUrl = chapterUrl.substring(indexOfLastPath, indexOfMangaUrl)
|
||||
val defaultText = it.select("p.clamp2").text()
|
||||
val m = recentUpdatesPattern.matcher(defaultText)
|
||||
val title = if (m.matches()) m.group(1) else defaultText
|
||||
manga.setUrlWithoutDomain("/manga" + mangaUrl)
|
||||
manga.title = title
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Filter.Sort.Selection(2, false))
|
||||
private class Genre(name: String) : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class SelectField(name: String, val key: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Years", "year"),
|
||||
TextField("Author", "author"),
|
||||
SelectField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
|
||||
SelectField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
|
||||
SelectField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
|
||||
Sort(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||
// http://mangasee.co/advanced-search/
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Hentai"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Lolicon"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shotacon"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri")
|
||||
)
|
||||
|
||||
}
|
@ -1,219 +1,219 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.POST
|
||||
import eu.kanade.tachiyomi.data.source.model.*
|
||||
import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.util.*
|
||||
|
||||
class Readmangatoday : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 8
|
||||
|
||||
override val name = "ReadMangaToday"
|
||||
|
||||
override val baseUrl = "http://www.readmanga.today"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient get() = network.cloudflareClient
|
||||
|
||||
/**
|
||||
* Search only returns data with this set
|
||||
*/
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/hot-manga/$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/latest-releases/$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.hot-manga > div.style-grid > div.box"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("div.title > h2 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val builder = okhttp3.FormBody.Builder()
|
||||
builder.add("manga-name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is TextField -> builder.add(filter.key, filter.state)
|
||||
is Type -> builder.add("type", arrayOf("all", "japanese", "korean", "chinese")[filter.state])
|
||||
is Status -> builder.add("status", arrayOf("both", "completed", "ongoing")[filter.state])
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
when (genre.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> builder.add("include[]", genre.id.toString())
|
||||
Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", genre.id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return POST("$baseUrl/service/advanced_search", headers, builder.build())
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.style-list > div.box"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("div.title > h2 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select("div.movie-meta").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = document.select("ul.cast-list li.director > ul a").first()?.text()
|
||||
manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text()
|
||||
manga.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text()
|
||||
manga.description = detailElement.select("li.movie-detail").first()?.text()
|
||||
manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "ul.chp_lst > li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.select("span.val").text()
|
||||
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
val dateWords: List<String> = date.split(" ")
|
||||
|
||||
if (dateWords.size == 3) {
|
||||
val timeAgo = Integer.parseInt(dateWords[0])
|
||||
val date: Calendar = Calendar.getInstance()
|
||||
|
||||
if (dateWords[1].contains("Minute")) {
|
||||
date.add(Calendar.MINUTE, -timeAgo)
|
||||
} else if (dateWords[1].contains("Hour")) {
|
||||
date.add(Calendar.HOUR_OF_DAY, -timeAgo)
|
||||
} else if (dateWords[1].contains("Day")) {
|
||||
date.add(Calendar.DAY_OF_YEAR, -timeAgo)
|
||||
} else if (dateWords[1].contains("Week")) {
|
||||
date.add(Calendar.WEEK_OF_YEAR, -timeAgo)
|
||||
} else if (dateWords[1].contains("Month")) {
|
||||
date.add(Calendar.MONTH, -timeAgo)
|
||||
} else if (dateWords[1].contains("Year")) {
|
||||
date.add(Calendar.YEAR, -timeAgo)
|
||||
}
|
||||
|
||||
return date.timeInMillis
|
||||
}
|
||||
|
||||
return 0L
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option").forEach {
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class Type : Filter.Select<String>("Type", arrayOf("All", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author-name"),
|
||||
TextField("Artist", "artist-name"),
|
||||
Type(),
|
||||
Status(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("ul.manga-cat span")].map(el => `Genre("${el.nextSibling.textContent.trim()}", ${el.getAttribute('data-id')})`).join(',\n')
|
||||
// http://www.readmanga.today/advanced-search
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action", 2),
|
||||
Genre("Adventure", 4),
|
||||
Genre("Comedy", 5),
|
||||
Genre("Doujinshi", 6),
|
||||
Genre("Drama", 7),
|
||||
Genre("Ecchi", 8),
|
||||
Genre("Fantasy", 9),
|
||||
Genre("Gender Bender", 10),
|
||||
Genre("Harem", 11),
|
||||
Genre("Historical", 12),
|
||||
Genre("Horror", 13),
|
||||
Genre("Josei", 14),
|
||||
Genre("Lolicon", 15),
|
||||
Genre("Martial Arts", 16),
|
||||
Genre("Mature", 17),
|
||||
Genre("Mecha", 18),
|
||||
Genre("Mystery", 19),
|
||||
Genre("One shot", 20),
|
||||
Genre("Psychological", 21),
|
||||
Genre("Romance", 22),
|
||||
Genre("School Life", 23),
|
||||
Genre("Sci-fi", 24),
|
||||
Genre("Seinen", 25),
|
||||
Genre("Shotacon", 26),
|
||||
Genre("Shoujo", 27),
|
||||
Genre("Shoujo Ai", 28),
|
||||
Genre("Shounen", 29),
|
||||
Genre("Shounen Ai", 30),
|
||||
Genre("Slice of Life", 31),
|
||||
Genre("Smut", 32),
|
||||
Genre("Sports", 33),
|
||||
Genre("Supernatural", 34),
|
||||
Genre("Tragedy", 35),
|
||||
Genre("Yaoi", 36),
|
||||
Genre("Yuri", 37)
|
||||
)
|
||||
package eu.kanade.tachiyomi.source.online.english
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.util.*
|
||||
|
||||
class Readmangatoday : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 8
|
||||
|
||||
override val name = "ReadMangaToday"
|
||||
|
||||
override val baseUrl = "http://www.readmanga.today"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient get() = network.cloudflareClient
|
||||
|
||||
/**
|
||||
* Search only returns data with this set
|
||||
*/
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/hot-manga/$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/latest-releases/$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.hot-manga > div.style-list > div.box"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.hot-manga > div.style-grid > div.box"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("div.title > h2 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val builder = okhttp3.FormBody.Builder()
|
||||
builder.add("manga-name", query)
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is TextField -> builder.add(filter.key, filter.state)
|
||||
is Type -> builder.add("type", arrayOf("all", "japanese", "korean", "chinese")[filter.state])
|
||||
is Status -> builder.add("status", arrayOf("both", "completed", "ongoing")[filter.state])
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
when (genre.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> builder.add("include[]", genre.id.toString())
|
||||
Filter.TriState.STATE_EXCLUDE -> builder.add("exclude[]", genre.id.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return POST("$baseUrl/service/advanced_search", headers, builder.build())
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.style-list > div.box"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("div.title > h2 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "div.next-page > a.next"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select("div.movie-meta").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = document.select("ul.cast-list li.director > ul a").first()?.text()
|
||||
manga.artist = document.select("ul.cast-list li:not(.director) > ul a").first()?.text()
|
||||
manga.genre = detailElement.select("dl.dl-horizontal > dd:eq(5)").first()?.text()
|
||||
manga.description = detailElement.select("li.movie-detail").first()?.text()
|
||||
manga.status = detailElement.select("dl.dl-horizontal > dd:eq(3)").first()?.text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = detailElement.select("img.img-responsive").first()?.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "ul.chp_lst > li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.select("span.val").text()
|
||||
chapter.date_upload = element.select("span.dte").first()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
val dateWords: List<String> = date.split(" ")
|
||||
|
||||
if (dateWords.size == 3) {
|
||||
val timeAgo = Integer.parseInt(dateWords[0])
|
||||
val date: Calendar = Calendar.getInstance()
|
||||
|
||||
if (dateWords[1].contains("Minute")) {
|
||||
date.add(Calendar.MINUTE, -timeAgo)
|
||||
} else if (dateWords[1].contains("Hour")) {
|
||||
date.add(Calendar.HOUR_OF_DAY, -timeAgo)
|
||||
} else if (dateWords[1].contains("Day")) {
|
||||
date.add(Calendar.DAY_OF_YEAR, -timeAgo)
|
||||
} else if (dateWords[1].contains("Week")) {
|
||||
date.add(Calendar.WEEK_OF_YEAR, -timeAgo)
|
||||
} else if (dateWords[1].contains("Month")) {
|
||||
date.add(Calendar.MONTH, -timeAgo)
|
||||
} else if (dateWords[1].contains("Year")) {
|
||||
date.add(Calendar.YEAR, -timeAgo)
|
||||
}
|
||||
|
||||
return date.timeInMillis
|
||||
}
|
||||
|
||||
return 0L
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("ul.list-switcher-2 > li > select.jump-menu").first().getElementsByTag("option").forEach {
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
}
|
||||
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
|
||||
|
||||
private class Status : Filter.TriState("Completed")
|
||||
private class Genre(name: String, val id: Int) : Filter.TriState(name)
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class Type : Filter.Select<String>("Type", arrayOf("All", "Japanese Manga", "Korean Manhwa", "Chinese Manhua"))
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author-name"),
|
||||
TextField("Artist", "artist-name"),
|
||||
Type(),
|
||||
Status(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("ul.manga-cat span")].map(el => `Genre("${el.nextSibling.textContent.trim()}", ${el.getAttribute('data-id')})`).join(',\n')
|
||||
// http://www.readmanga.today/advanced-search
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action", 2),
|
||||
Genre("Adventure", 4),
|
||||
Genre("Comedy", 5),
|
||||
Genre("Doujinshi", 6),
|
||||
Genre("Drama", 7),
|
||||
Genre("Ecchi", 8),
|
||||
Genre("Fantasy", 9),
|
||||
Genre("Gender Bender", 10),
|
||||
Genre("Harem", 11),
|
||||
Genre("Historical", 12),
|
||||
Genre("Horror", 13),
|
||||
Genre("Josei", 14),
|
||||
Genre("Lolicon", 15),
|
||||
Genre("Martial Arts", 16),
|
||||
Genre("Mature", 17),
|
||||
Genre("Mecha", 18),
|
||||
Genre("Mystery", 19),
|
||||
Genre("One shot", 20),
|
||||
Genre("Psychological", 21),
|
||||
Genre("Romance", 22),
|
||||
Genre("School Life", 23),
|
||||
Genre("Sci-fi", 24),
|
||||
Genre("Seinen", 25),
|
||||
Genre("Shotacon", 26),
|
||||
Genre("Shoujo", 27),
|
||||
Genre("Shoujo Ai", 28),
|
||||
Genre("Shounen", 29),
|
||||
Genre("Shounen Ai", 30),
|
||||
Genre("Slice of Life", 31),
|
||||
Genre("Smut", 32),
|
||||
Genre("Sports", 33),
|
||||
Genre("Supernatural", 34),
|
||||
Genre("Tragedy", 35),
|
||||
Genre("Yaoi", 36),
|
||||
Genre("Yuri", 37)
|
||||
)
|
||||
}
|
@ -1,122 +1,122 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.german
|
||||
|
||||
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 okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class WieManga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 10
|
||||
|
||||
override val name = "Wie Manga!"
|
||||
|
||||
override val baseUrl = "http://www.wiemanga.com"
|
||||
|
||||
override val lang = "de"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = ".booklist td > div"
|
||||
|
||||
override fun latestUpdatesSelector() = ".booklist td > div"
|
||||
|
||||
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): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = null
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = null
|
||||
|
||||
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): 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): 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", "")
|
||||
manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
|
||||
|
||||
if (manga.author == "RSS")
|
||||
manga.author = null
|
||||
|
||||
if (manga.artist == "RSS")
|
||||
manga.artist = null
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
|
||||
|
||||
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(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
document.select("select#page").first().select("option").forEach {
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
|
||||
|
||||
package eu.kanade.tachiyomi.source.online.german
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class WieManga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 10
|
||||
|
||||
override val name = "Wie Manga!"
|
||||
|
||||
override val baseUrl = "http://www.wiemanga.com"
|
||||
|
||||
override val lang = "de"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = ".booklist td > div"
|
||||
|
||||
override fun latestUpdatesSelector() = ".booklist td > div"
|
||||
|
||||
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): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = null
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = null
|
||||
|
||||
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): 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): 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", "")
|
||||
manga.thumbnail_url = imageElement.select("img").first()?.attr("src")
|
||||
|
||||
if (manga.author == "RSS")
|
||||
manga.author = null
|
||||
|
||||
if (manga.artist == "RSS")
|
||||
manga.artist = null
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".chapterlist tr:not(:first-child)"
|
||||
|
||||
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(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
document.select("select#page").first().select("option").forEach {
|
||||
pages.add(Page(pages.size, it.attr("value")))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = document.select("img#comicpic").first().attr("src")
|
||||
|
||||
}
|
@ -1,230 +1,230 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.russian
|
||||
|
||||
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 : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 7
|
||||
|
||||
override val name = "Mangachan"
|
||||
|
||||
override val baseUrl = "http://mangachan.me"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if (query.isNotEmpty()) {
|
||||
"$baseUrl/?do=search&subaction=search&story=$query"
|
||||
} else {
|
||||
val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
|
||||
if (filt.isNotEmpty()) {
|
||||
var genres = ""
|
||||
filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
|
||||
"$baseUrl/tags/${genres.dropLast(1)}"
|
||||
} else {
|
||||
"$baseUrl/?do=search&subaction=search&story=$query"
|
||||
}
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/newestch?page=$page")
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.content_row"
|
||||
|
||||
override fun latestUpdatesSelector() = "ul.area_rightNews li"
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("h2 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a:nth-child(1)").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = "a:contains(Далее)"
|
||||
|
||||
private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
|
||||
// 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): SManga {
|
||||
val infoElement = document.select("table.mangatitle").first()
|
||||
val descElement = document.select("div#description").first()
|
||||
val imgElement = document.select("img#cover").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
|
||||
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
|
||||
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
|
||||
manga.description = descElement.textNodes().first().text()
|
||||
manga.thumbnail_url = baseUrl + imgElement.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("перевод завершен") -> return SManga.COMPLETED
|
||||
element.contains("перевод продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("div.date").first()?.text()?.let {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body().string()
|
||||
val beginIndex = html.indexOf("fullimg\":[") + 10
|
||||
val endIndex = html.indexOf(",]", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
||||
val pageUrls = trimmedHtml.split(',')
|
||||
|
||||
return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||
|
||||
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
|
||||
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
|
||||
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
|
||||
* on http://mangachan.me/
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("18 плюс"),
|
||||
Genre("bdsm"),
|
||||
Genre("арт"),
|
||||
Genre("биография"),
|
||||
Genre("боевик"),
|
||||
Genre("боевые искусства"),
|
||||
Genre("вампиры"),
|
||||
Genre("веб"),
|
||||
Genre("гарем"),
|
||||
Genre("гендерная интрига"),
|
||||
Genre("героическое фэнтези"),
|
||||
Genre("детектив"),
|
||||
Genre("дзёсэй"),
|
||||
Genre("додзинси"),
|
||||
Genre("драма"),
|
||||
Genre("игра"),
|
||||
Genre("инцест"),
|
||||
Genre("искусство"),
|
||||
Genre("история"),
|
||||
Genre("киберпанк"),
|
||||
Genre("кодомо"),
|
||||
Genre("комедия"),
|
||||
Genre("литРПГ"),
|
||||
Genre("магия"),
|
||||
Genre("махо-сёдзё"),
|
||||
Genre("меха"),
|
||||
Genre("мистика"),
|
||||
Genre("музыка"),
|
||||
Genre("научная фантастика"),
|
||||
Genre("повседневность"),
|
||||
Genre("постапокалиптика"),
|
||||
Genre("приключения"),
|
||||
Genre("психология"),
|
||||
Genre("романтика"),
|
||||
Genre("самурайский боевик"),
|
||||
Genre("сборник"),
|
||||
Genre("сверхъестественное"),
|
||||
Genre("сказка"),
|
||||
Genre("спорт"),
|
||||
Genre("супергерои"),
|
||||
Genre("сэйнэн"),
|
||||
Genre("сёдзё"),
|
||||
Genre("сёдзё-ай"),
|
||||
Genre("сёнэн"),
|
||||
Genre("сёнэн-ай"),
|
||||
Genre("тентакли"),
|
||||
Genre("трагедия"),
|
||||
Genre("триллер"),
|
||||
Genre("ужасы"),
|
||||
Genre("фантастика"),
|
||||
Genre("фурри"),
|
||||
Genre("фэнтези"),
|
||||
Genre("школа"),
|
||||
Genre("эротика"),
|
||||
Genre("юри"),
|
||||
Genre("яой"),
|
||||
Genre("ёнкома")
|
||||
)
|
||||
package eu.kanade.tachiyomi.source.online.russian
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.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 : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 7
|
||||
|
||||
override val name = "Mangachan"
|
||||
|
||||
override val baseUrl = "http://mangachan.me"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if (query.isNotEmpty()) {
|
||||
"$baseUrl/?do=search&subaction=search&story=$query"
|
||||
} else {
|
||||
val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
|
||||
if (filt.isNotEmpty()) {
|
||||
var genres = ""
|
||||
filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
|
||||
"$baseUrl/tags/${genres.dropLast(1)}"
|
||||
} else {
|
||||
"$baseUrl/?do=search&subaction=search&story=$query"
|
||||
}
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/newestch?page=$page")
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.content_row"
|
||||
|
||||
override fun latestUpdatesSelector() = "ul.area_rightNews li"
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("h2 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a:nth-child(1)").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = "a:contains(Далее)"
|
||||
|
||||
private fun searchGenresNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||
searchMangaFromElement(element)
|
||||
}
|
||||
|
||||
// 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): SManga {
|
||||
val infoElement = document.select("table.mangatitle").first()
|
||||
val descElement = document.select("div#description").first()
|
||||
val imgElement = document.select("img#cover").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("tr:eq(2) > td:eq(1)").text()
|
||||
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
|
||||
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
|
||||
manga.description = descElement.textNodes().first().text()
|
||||
manga.thumbnail_url = baseUrl + imgElement.attr("src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("перевод завершен") -> return SManga.COMPLETED
|
||||
element.contains("перевод продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = element.select("div.date").first()?.text()?.let {
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body().string()
|
||||
val beginIndex = html.indexOf("fullimg\":[") + 10
|
||||
val endIndex = html.indexOf(",]", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex).replace("\"", "")
|
||||
val pageUrls = trimmedHtml.split(',')
|
||||
|
||||
return pageUrls.mapIndexed { i, url -> Page(i, "", url) }
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
|
||||
|
||||
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
|
||||
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
|
||||
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
|
||||
* on http://mangachan.me/
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("18 плюс"),
|
||||
Genre("bdsm"),
|
||||
Genre("арт"),
|
||||
Genre("биография"),
|
||||
Genre("боевик"),
|
||||
Genre("боевые искусства"),
|
||||
Genre("вампиры"),
|
||||
Genre("веб"),
|
||||
Genre("гарем"),
|
||||
Genre("гендерная интрига"),
|
||||
Genre("героическое фэнтези"),
|
||||
Genre("детектив"),
|
||||
Genre("дзёсэй"),
|
||||
Genre("додзинси"),
|
||||
Genre("драма"),
|
||||
Genre("игра"),
|
||||
Genre("инцест"),
|
||||
Genre("искусство"),
|
||||
Genre("история"),
|
||||
Genre("киберпанк"),
|
||||
Genre("кодомо"),
|
||||
Genre("комедия"),
|
||||
Genre("литРПГ"),
|
||||
Genre("магия"),
|
||||
Genre("махо-сёдзё"),
|
||||
Genre("меха"),
|
||||
Genre("мистика"),
|
||||
Genre("музыка"),
|
||||
Genre("научная фантастика"),
|
||||
Genre("повседневность"),
|
||||
Genre("постапокалиптика"),
|
||||
Genre("приключения"),
|
||||
Genre("психология"),
|
||||
Genre("романтика"),
|
||||
Genre("самурайский боевик"),
|
||||
Genre("сборник"),
|
||||
Genre("сверхъестественное"),
|
||||
Genre("сказка"),
|
||||
Genre("спорт"),
|
||||
Genre("супергерои"),
|
||||
Genre("сэйнэн"),
|
||||
Genre("сёдзё"),
|
||||
Genre("сёдзё-ай"),
|
||||
Genre("сёнэн"),
|
||||
Genre("сёнэн-ай"),
|
||||
Genre("тентакли"),
|
||||
Genre("трагедия"),
|
||||
Genre("триллер"),
|
||||
Genre("ужасы"),
|
||||
Genre("фантастика"),
|
||||
Genre("фурри"),
|
||||
Genre("фэнтези"),
|
||||
Genre("школа"),
|
||||
Genre("эротика"),
|
||||
Genre("юри"),
|
||||
Genre("яой"),
|
||||
Genre("ёнкома")
|
||||
)
|
||||
}
|
@ -1,184 +1,184 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.russian
|
||||
|
||||
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
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Mintmanga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 6
|
||||
|
||||
override val name = "Mintmanga"
|
||||
|
||||
override val baseUrl = "http://mintmanga.com"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.desc"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.desc"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("h3 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
|
||||
return GET("$baseUrl/search?q=$query&$genres", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.leftContent").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("span.elem_author").first()?.text()
|
||||
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
|
||||
manga.description = infoElement.select("div.manga-description").text()
|
||||
manga.status = parseStatus(infoElement.html())
|
||||
manga.thumbnail_url = infoElement.select("img").attr("data-full")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
|
||||
chapter.name = urlElement.text().replace(" новое", "")
|
||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
chapter.chapter_number = -2f
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body().string()
|
||||
val beginIndex = html.indexOf("rm_h.init( [")
|
||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||
|
||||
val p = Pattern.compile("'.+?','.+?',\".+?\"")
|
||||
val m = p.matcher(trimmedHtml)
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
var i = 0
|
||||
while (m.find()) {
|
||||
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
||||
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class Genre(name: String, val id: String) : Filter.TriState(name)
|
||||
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
* on http://mintmanga.com/search
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("арт", "el_2220"),
|
||||
Genre("бара", "el_1353"),
|
||||
Genre("боевик", "el_1346"),
|
||||
Genre("боевые искусства", "el_1334"),
|
||||
Genre("вампиры", "el_1339"),
|
||||
Genre("гарем", "el_1333"),
|
||||
Genre("гендерная интрига", "el_1347"),
|
||||
Genre("героическое фэнтези", "el_1337"),
|
||||
Genre("детектив", "el_1343"),
|
||||
Genre("дзёсэй", "el_1349"),
|
||||
Genre("додзинси", "el_1332"),
|
||||
Genre("драма", "el_1310"),
|
||||
Genre("игра", "el_5229"),
|
||||
Genre("история", "el_1311"),
|
||||
Genre("киберпанк", "el_1351"),
|
||||
Genre("комедия", "el_1328"),
|
||||
Genre("меха", "el_1318"),
|
||||
Genre("мистика", "el_1324"),
|
||||
Genre("научная фантастика", "el_1325"),
|
||||
Genre("повседневность", "el_1327"),
|
||||
Genre("постапокалиптика", "el_1342"),
|
||||
Genre("приключения", "el_1322"),
|
||||
Genre("психология", "el_1335"),
|
||||
Genre("романтика", "el_1313"),
|
||||
Genre("самурайский боевик", "el_1316"),
|
||||
Genre("сверхъестественное", "el_1350"),
|
||||
Genre("сёдзё", "el_1314"),
|
||||
Genre("сёдзё-ай", "el_1320"),
|
||||
Genre("сёнэн", "el_1326"),
|
||||
Genre("сёнэн-ай", "el_1330"),
|
||||
Genre("спорт", "el_1321"),
|
||||
Genre("сэйнэн", "el_1329"),
|
||||
Genre("трагедия", "el_1344"),
|
||||
Genre("триллер", "el_1341"),
|
||||
Genre("ужасы", "el_1317"),
|
||||
Genre("фантастика", "el_1331"),
|
||||
Genre("фэнтези", "el_1323"),
|
||||
Genre("школа", "el_1319"),
|
||||
Genre("эротика", "el_1340"),
|
||||
Genre("этти", "el_1354"),
|
||||
Genre("юри", "el_1315"),
|
||||
Genre("яой", "el_1336")
|
||||
)
|
||||
package eu.kanade.tachiyomi.source.online.russian
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Mintmanga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 6
|
||||
|
||||
override val name = "Mintmanga"
|
||||
|
||||
override val baseUrl = "http://mintmanga.com"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.desc"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.desc"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("h3 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
|
||||
return GET("$baseUrl/search?q=$query&$genres", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.leftContent").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("span.elem_author").first()?.text()
|
||||
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
|
||||
manga.description = infoElement.select("div.manga-description").text()
|
||||
manga.status = parseStatus(infoElement.html())
|
||||
manga.thumbnail_url = infoElement.select("img").attr("data-full")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
|
||||
chapter.name = urlElement.text().replace(" новое", "")
|
||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
chapter.chapter_number = -2f
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body().string()
|
||||
val beginIndex = html.indexOf("rm_h.init( [")
|
||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||
|
||||
val p = Pattern.compile("'.+?','.+?',\".+?\"")
|
||||
val m = p.matcher(trimmedHtml)
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
var i = 0
|
||||
while (m.find()) {
|
||||
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
||||
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class Genre(name: String, val id: String) : Filter.TriState(name)
|
||||
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
* on http://mintmanga.com/search
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("арт", "el_2220"),
|
||||
Genre("бара", "el_1353"),
|
||||
Genre("боевик", "el_1346"),
|
||||
Genre("боевые искусства", "el_1334"),
|
||||
Genre("вампиры", "el_1339"),
|
||||
Genre("гарем", "el_1333"),
|
||||
Genre("гендерная интрига", "el_1347"),
|
||||
Genre("героическое фэнтези", "el_1337"),
|
||||
Genre("детектив", "el_1343"),
|
||||
Genre("дзёсэй", "el_1349"),
|
||||
Genre("додзинси", "el_1332"),
|
||||
Genre("драма", "el_1310"),
|
||||
Genre("игра", "el_5229"),
|
||||
Genre("история", "el_1311"),
|
||||
Genre("киберпанк", "el_1351"),
|
||||
Genre("комедия", "el_1328"),
|
||||
Genre("меха", "el_1318"),
|
||||
Genre("мистика", "el_1324"),
|
||||
Genre("научная фантастика", "el_1325"),
|
||||
Genre("повседневность", "el_1327"),
|
||||
Genre("постапокалиптика", "el_1342"),
|
||||
Genre("приключения", "el_1322"),
|
||||
Genre("психология", "el_1335"),
|
||||
Genre("романтика", "el_1313"),
|
||||
Genre("самурайский боевик", "el_1316"),
|
||||
Genre("сверхъестественное", "el_1350"),
|
||||
Genre("сёдзё", "el_1314"),
|
||||
Genre("сёдзё-ай", "el_1320"),
|
||||
Genre("сёнэн", "el_1326"),
|
||||
Genre("сёнэн-ай", "el_1330"),
|
||||
Genre("спорт", "el_1321"),
|
||||
Genre("сэйнэн", "el_1329"),
|
||||
Genre("трагедия", "el_1344"),
|
||||
Genre("триллер", "el_1341"),
|
||||
Genre("ужасы", "el_1317"),
|
||||
Genre("фантастика", "el_1331"),
|
||||
Genre("фэнтези", "el_1323"),
|
||||
Genre("школа", "el_1319"),
|
||||
Genre("эротика", "el_1340"),
|
||||
Genre("этти", "el_1354"),
|
||||
Genre("юри", "el_1315"),
|
||||
Genre("яой", "el_1336")
|
||||
)
|
||||
}
|
@ -1,183 +1,183 @@
|
||||
package eu.kanade.tachiyomi.data.source.online.russian
|
||||
|
||||
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
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Readmanga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 5
|
||||
|
||||
override val name = "Readmanga"
|
||||
|
||||
override val baseUrl = "http://readmanga.me"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = "div.desc"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.desc"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("h3 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
|
||||
return GET("$baseUrl/search?q=$query&$genres", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.leftContent").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("span.elem_author").first()?.text()
|
||||
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
|
||||
manga.description = infoElement.select("div.manga-description").text()
|
||||
manga.status = parseStatus(infoElement.html())
|
||||
manga.thumbnail_url = infoElement.select("img").attr("data-full")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
|
||||
chapter.name = urlElement.text().replace(" новое", "")
|
||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
chapter.chapter_number = -2f
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body().string()
|
||||
val beginIndex = html.indexOf("rm_h.init( [")
|
||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||
|
||||
val p = Pattern.compile("'.+?','.+?',\".+?\"")
|
||||
val m = p.matcher(trimmedHtml)
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
var i = 0
|
||||
while (m.find()) {
|
||||
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
||||
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class Genre(name: String, val id: String) : Filter.TriState(name)
|
||||
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
* on http://readmanga.me/search
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("арт", "el_5685"),
|
||||
Genre("боевик", "el_2155"),
|
||||
Genre("боевые искусства", "el_2143"),
|
||||
Genre("вампиры", "el_2148"),
|
||||
Genre("гарем", "el_2142"),
|
||||
Genre("гендерная интрига", "el_2156"),
|
||||
Genre("героическое фэнтези", "el_2146"),
|
||||
Genre("детектив", "el_2152"),
|
||||
Genre("дзёсэй", "el_2158"),
|
||||
Genre("додзинси", "el_2141"),
|
||||
Genre("драма", "el_2118"),
|
||||
Genre("игра", "el_2154"),
|
||||
Genre("история", "el_2119"),
|
||||
Genre("киберпанк", "el_8032"),
|
||||
Genre("кодомо", "el_2137"),
|
||||
Genre("комедия", "el_2136"),
|
||||
Genre("махо-сёдзё", "el_2147"),
|
||||
Genre("меха", "el_2126"),
|
||||
Genre("мистика", "el_2132"),
|
||||
Genre("научная фантастика", "el_2133"),
|
||||
Genre("повседневность", "el_2135"),
|
||||
Genre("постапокалиптика", "el_2151"),
|
||||
Genre("приключения", "el_2130"),
|
||||
Genre("психология", "el_2144"),
|
||||
Genre("романтика", "el_2121"),
|
||||
Genre("самурайский боевик", "el_2124"),
|
||||
Genre("сверхъестественное", "el_2159"),
|
||||
Genre("сёдзё", "el_2122"),
|
||||
Genre("сёдзё-ай", "el_2128"),
|
||||
Genre("сёнэн", "el_2134"),
|
||||
Genre("сёнэн-ай", "el_2139"),
|
||||
Genre("спорт", "el_2129"),
|
||||
Genre("сэйнэн", "el_2138"),
|
||||
Genre("трагедия", "el_2153"),
|
||||
Genre("триллер", "el_2150"),
|
||||
Genre("ужасы", "el_2125"),
|
||||
Genre("фантастика", "el_2140"),
|
||||
Genre("фэнтези", "el_2131"),
|
||||
Genre("школа", "el_2127"),
|
||||
Genre("этти", "el_2149"),
|
||||
Genre("юри", "el_2123")
|
||||
)
|
||||
package eu.kanade.tachiyomi.source.online.russian
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.ParsedOnlineSource
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class Readmanga : ParsedOnlineSource() {
|
||||
|
||||
override val id: Long = 5
|
||||
|
||||
override val name = "Readmanga"
|
||||
|
||||
override val baseUrl = "http://readmanga.me"
|
||||
|
||||
override val lang = "ru"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = "div.desc"
|
||||
|
||||
override fun latestUpdatesSelector() = "div.desc"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("h3 > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "a.nextLink"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
|
||||
return GET("$baseUrl/search?q=$query&$genres", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
// max 200 results
|
||||
override fun searchMangaNextPageSelector() = null
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.leftContent").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = infoElement.select("span.elem_author").first()?.text()
|
||||
manga.genre = infoElement.select("span.elem_genre").text().replace(" ,", ",")
|
||||
manga.description = infoElement.select("div.manga-description").text()
|
||||
manga.status = parseStatus(infoElement.html())
|
||||
manga.thumbnail_url = infoElement.select("img").attr("data-full")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(element: String): Int {
|
||||
when {
|
||||
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
|
||||
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
|
||||
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
|
||||
else -> return SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapters-link tbody tr"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href") + "?mature=1")
|
||||
chapter.name = urlElement.text().replace(" новое", "")
|
||||
chapter.date_upload = element.select("td:eq(1)").first()?.text()?.let {
|
||||
SimpleDateFormat("dd/MM/yy", Locale.US).parse(it).time
|
||||
} ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
chapter.chapter_number = -2f
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val html = response.body().string()
|
||||
val beginIndex = html.indexOf("rm_h.init( [")
|
||||
val endIndex = html.indexOf("], 0, false);", beginIndex)
|
||||
val trimmedHtml = html.substring(beginIndex, endIndex)
|
||||
|
||||
val p = Pattern.compile("'.+?','.+?',\".+?\"")
|
||||
val m = p.matcher(trimmedHtml)
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
var i = 0
|
||||
while (m.find()) {
|
||||
val urlParts = m.group().replace("[\"\']+".toRegex(), "").split(',')
|
||||
pages.add(Page(i++, "", urlParts[1] + urlParts[0] + urlParts[2]))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
throw Exception("Not used")
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
private class Genre(name: String, val id: String) : Filter.TriState(name)
|
||||
|
||||
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
|
||||
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
|
||||
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
|
||||
* on http://readmanga.me/search
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
Genre("арт", "el_5685"),
|
||||
Genre("боевик", "el_2155"),
|
||||
Genre("боевые искусства", "el_2143"),
|
||||
Genre("вампиры", "el_2148"),
|
||||
Genre("гарем", "el_2142"),
|
||||
Genre("гендерная интрига", "el_2156"),
|
||||
Genre("героическое фэнтези", "el_2146"),
|
||||
Genre("детектив", "el_2152"),
|
||||
Genre("дзёсэй", "el_2158"),
|
||||
Genre("додзинси", "el_2141"),
|
||||
Genre("драма", "el_2118"),
|
||||
Genre("игра", "el_2154"),
|
||||
Genre("история", "el_2119"),
|
||||
Genre("киберпанк", "el_8032"),
|
||||
Genre("кодомо", "el_2137"),
|
||||
Genre("комедия", "el_2136"),
|
||||
Genre("махо-сёдзё", "el_2147"),
|
||||
Genre("меха", "el_2126"),
|
||||
Genre("мистика", "el_2132"),
|
||||
Genre("научная фантастика", "el_2133"),
|
||||
Genre("повседневность", "el_2135"),
|
||||
Genre("постапокалиптика", "el_2151"),
|
||||
Genre("приключения", "el_2130"),
|
||||
Genre("психология", "el_2144"),
|
||||
Genre("романтика", "el_2121"),
|
||||
Genre("самурайский боевик", "el_2124"),
|
||||
Genre("сверхъестественное", "el_2159"),
|
||||
Genre("сёдзё", "el_2122"),
|
||||
Genre("сёдзё-ай", "el_2128"),
|
||||
Genre("сёнэн", "el_2134"),
|
||||
Genre("сёнэн-ай", "el_2139"),
|
||||
Genre("спорт", "el_2129"),
|
||||
Genre("сэйнэн", "el_2138"),
|
||||
Genre("трагедия", "el_2153"),
|
||||
Genre("триллер", "el_2150"),
|
||||
Genre("ужасы", "el_2125"),
|
||||
Genre("фантастика", "el_2140"),
|
||||
Genre("фэнтези", "el_2131"),
|
||||
Genre("школа", "el_2127"),
|
||||
Genre("этти", "el_2149"),
|
||||
Genre("юри", "el_2123")
|
||||
)
|
||||
}
|
@ -15,8 +15,8 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
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.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||
|
@ -1,8 +1,8 @@
|
||||
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.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
|
@ -8,13 +8,13 @@ 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.Filter
|
||||
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.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.filter.*
|
||||
import rx.Observable
|
||||
|
@ -1,8 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.data.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
|
@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<CheckboxItem.Holder>() {
|
||||
|
||||
|
@ -10,7 +10,7 @@ import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.davidea.viewholders.ExpandableViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.setVectorCompat
|
||||
|
||||
class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<GroupItem.Holder, ISectionable<*, *>>() {
|
||||
|
@ -9,7 +9,7 @@ import android.widget.TextView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Holder>() {
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue.filter
|
||||
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISectionable<TriStateItem.Holder, GroupItem> {
|
||||
|
||||
|
@ -10,7 +10,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
||||
|
||||
open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<SelectItem.Holder>() {
|
||||
|
@ -8,7 +8,7 @@ import android.view.ViewGroup
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<SeparatorItem.Holder>() {
|
||||
|
||||
|
@ -7,7 +7,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.setVectorCompat
|
||||
|
||||
class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
|
||||
|
@ -10,7 +10,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
|
||||
class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) {
|
||||
|
@ -9,7 +9,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
||||
|
||||
open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
|
||||
|
@ -9,7 +9,7 @@ import android.widget.CheckedTextView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.data.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.util.dpToPx
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.R as TR
|
||||
|
@ -7,7 +7,7 @@ import android.view.MenuItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
|
@ -1,7 +1,7 @@
|
||||
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.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
|
@ -1,8 +1,8 @@
|
||||
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.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||
|
||||
|
@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
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.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.combineLatest
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
|
@ -9,8 +9,8 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||
|
@ -14,9 +14,9 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
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.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
|
@ -4,8 +4,8 @@ import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
|
@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
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.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.source.online.fetchImageFromCacheThenNet
|
||||
import eu.kanade.tachiyomi.source.online.fetchPageListFromCacheThenNet
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
|
@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
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.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
||||
class ReaderChapter(c: Chapter) : Chapter by c {
|
||||
|
||||
|
@ -13,9 +13,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
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.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackUpdateService
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.base
|
||||
|
||||
import com.davemorrissey.labs.subscaleview.decoder.*
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
|
||||
|
@ -4,7 +4,7 @@ import android.net.Uri
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import kotlinx.android.synthetic.main.page_decode_error.view.*
|
||||
|
||||
|
@ -10,7 +10,7 @@ import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader
|
||||
|
@ -6,7 +6,7 @@ import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
|
||||
|
@ -4,7 +4,7 @@ import android.support.v4.view.PagerAdapter
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||
|
||||
|
@ -4,7 +4,7 @@ import android.support.v7.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
|
||||
/**
|
||||
|
@ -8,7 +8,7 @@ import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
|
@ -5,7 +5,7 @@ import android.support.v7.widget.RecyclerView
|
||||
import android.view.*
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
|
||||
import eu.kanade.tachiyomi.widget.PreCachingLayoutManager
|
||||
|
@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
|
@ -4,7 +4,7 @@ import android.view.ViewGroup
|
||||
import eu.davidea.flexibleadapter4.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
|
@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import rx.Observable
|
||||
|
@ -7,8 +7,8 @@ import android.support.v7.preference.XpPreferenceFragment
|
||||
import android.view.View
|
||||
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.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginCheckBoxPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
||||
import eu.kanade.tachiyomi.widget.preference.SwitchPreferenceCategory
|
||||
|
@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.util
|
||||
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 eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
|
@ -6,8 +6,8 @@ import android.support.v7.preference.PreferenceViewHolder
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.setVectorCompat
|
||||
import kotlinx.android.synthetic.main.pref_item_source.view.*
|
||||
|
@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.widget.preference
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.pref_account_login.view.*
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
|
@ -8,9 +8,9 @@ import eu.kanade.tachiyomi.BuildConfig
|
||||
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 eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.online.OnlineSource
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
Loading…
Reference in New Issue
Block a user