Kissmanga loading through Cloudflare. A lot of refactoring was needed

This commit is contained in:
len
2016-05-10 15:09:44 +02:00
parent 8da11dbdb9
commit 6e8a41f898
42 changed files with 753 additions and 524 deletions

View File

@@ -1,11 +1,6 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.animation.GlideAnimation
import com.bumptech.glide.request.target.SimpleTarget
import eu.kanade.tachiyomi.util.DiskUtils
import java.io.File
import java.io.IOException
@@ -27,80 +22,19 @@ class CoverCache(private val context: Context) {
*/
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
/**
* Download the cover with Glide and save the file.
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
* @param onReady function to call when the image is ready
*/
fun save(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)? = null) {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return
// Download the cover with Glide and save the file.
val url = GlideUrl(thumbnailUrl, headers)
Glide.with(context)
.load(url)
.downloadOnly(object : SimpleTarget<File>() {
override fun onResourceReady(resource: File, anim: GlideAnimation<in File>) {
try {
// Copy the cover from Glide's cache to local cache.
copyToCache(thumbnailUrl!!, resource)
onReady?.invoke(resource)
} catch (e: IOException) {
// Do nothing.
}
}
})
}
/**
* Save or load the image from cache
* @param thumbnailUrl the thumbnail url.
* @param headers headers included in Glide request.
* @param onReady function to call when the image is ready
*/
fun saveOrLoadFromCache(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)?) {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return
// If file exist load it otherwise save it.
val localCover = getCoverFromCache(thumbnailUrl!!)
if (localCover.exists()) {
onReady?.invoke(localCover)
} else {
save(thumbnailUrl, headers, onReady)
}
}
/**
* Returns the cover from cache.
*
* @param thumbnailUrl the thumbnail url.
* @return cover image.
*/
private fun getCoverFromCache(thumbnailUrl: String): File {
fun getCoverFile(thumbnailUrl: String): File {
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
}
/**
* Copy the given file to this cache.
* @param thumbnailUrl url of thumbnail.
* @param sourceFile the source file of the cover image.
* @throws IOException if there's any error.
*/
@Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, sourceFile: File) {
// Get destination file.
val destFile = getCoverFromCache(thumbnailUrl)
sourceFile.copyTo(destFile, overwrite = true)
}
/**
* Copy the given stream to this cache.
*
* @param thumbnailUrl url of the thumbnail.
* @param inputStream the stream to copy.
* @throws IOException if there's any error.
@@ -108,13 +42,14 @@ class CoverCache(private val context: Context) {
@Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
// Get destination file.
val destFile = getCoverFromCache(thumbnailUrl)
val destFile = getCoverFile(thumbnailUrl)
destFile.outputStream().use { inputStream.copyTo(it) }
}
/**
* Delete the cover file from the cache.
*
* @param thumbnailUrl the thumbnail url.
* @return status of deletion.
*/
@@ -124,7 +59,7 @@ class CoverCache(private val context: Context) {
return false
// Remove file.
val file = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
val file = getCoverFile(thumbnailUrl!!)
return file.exists() && file.delete()
}

View File

@@ -1,22 +0,0 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.module.GlideModule
/**
* Class used to update Glide module settings
*/
class CoverGlideModule : GlideModule {
override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
}
override fun registerComponents(context: Context, glide: Glide) {
// Nothing to see here!
}
}

View File

@@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
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.App
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.NetworkHelper
import java.io.InputStream
import javax.inject.Inject
/**
* Class used to update Glide module settings
*/
class AppGlideModule : GlideModule {
@Inject lateinit var networkHelper: NetworkHelper
override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
}
override fun registerComponents(context: Context, glide: Glide) {
App.get(context).component.inject(this)
glide.register(GlideUrl::class.java, InputStream::class.java,
OkHttpUrlLoader.Factory(networkHelper.defaultClient))
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
}
}

View File

@@ -0,0 +1,67 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
/**
* A [DataFetcher] for loading a cover of a manga depending on its favorite status.
* If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it
* fallbacks to network and copies it to the cache.
* If the manga is not favorite, it tries to delete the cover from our cache and always fallback
* to network for fetching.
*
* @param networkFetcher the network fetcher for this cover.
* @param file the file where this cover should be. It may exists or not.
* @param manga the manga of the cover to load.
*/
class MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
private val file: File,
private val manga: Manga)
: DataFetcher<InputStream> {
@Throws(Exception::class)
override fun loadData(priority: Priority): InputStream? {
if (manga.favorite) {
if (!file.exists()) {
file.parentFile.mkdirs()
networkFetcher.loadData(priority)?.let {
it.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
return FileInputStream(file)
} else {
if (file.exists()) {
file.delete()
}
return networkFetcher.loadData(priority)
}
}
/**
* Returns the id for this manga's cover.
*
* Appending the file's modified date to the url, we can force Glide to skip its memory and disk
* lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when
* the file has changed. If the file doesn't exist it will append a 0.
*/
override fun getId(): String {
return manga.thumbnail_url + file.lastModified()
}
override fun cancel() {
networkFetcher.cancel()
}
override fun cleanup() {
networkFetcher.cleanup()
}
}

View File

@@ -0,0 +1,118 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.*
import com.bumptech.glide.load.model.stream.StreamModelLoader
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.SourceManager
import java.io.File
import java.io.InputStream
import javax.inject.Inject
/**
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
* Coupled with [MangaDataFetcher], this class allows to implement the following flow:
*
* - Check in RAM LRU.
* - Check in disk LRU.
* - Check in this module.
* - Fetch from the network connection.
*
* @param context the application context.
*/
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
/**
* Cover cache where persistent covers are stored.
*/
@Inject lateinit var coverCache: CoverCache
/**
* Source manager.
*/
@Inject lateinit var sourceManager: SourceManager
/**
* Base network loader.
*/
private val baseLoader = Glide.buildModelLoader(GlideUrl::class.java,
InputStream::class.java, context)
/**
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
* and the file where it should be stored in case the manga is a favorite.
*/
private val modelCache = ModelCache<String, Pair<GlideUrl, File>>(100)
/**
* Map where request headers are stored for a source.
*/
private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
init {
App.get(context).component.inject(this)
}
/**
* Factory class for creating [MangaModelLoader] instances.
*/
class Factory : ModelLoaderFactory<Manga, InputStream> {
override fun build(context: Context, factories: GenericLoaderFactory)
= MangaModelLoader(context)
override fun teardown() {}
}
/**
* Returns a [MangaDataFetcher] for the given manga or null if the url is empty.
*
* @param manga the model.
* @param width the width of the view where the resource will be loaded.
* @param height the height of the view where the resource will be loaded.
*/
override fun getResourceFetcher(manga: Manga,
width: Int,
height: Int): DataFetcher<InputStream>? {
// Check thumbnail is not null or empty
val url = manga.thumbnail_url
if (url.isNullOrEmpty()) {
return null
}
// Obtain the request url and the file for this url from the LRU cache, or calculate it
// and add them to the cache.
val (glideUrl, file) = modelCache.get(url, width, height) ?:
Pair(GlideUrl(url, getHeaders(manga)), coverCache.getCoverFile(url)).apply {
modelCache.put(url, width, height, this)
}
// Get the network fetcher for this request url.
val networkFetcher = baseLoader.getResourceFetcher(glideUrl, width, height)
// Return an instance of our fetcher providing the needed elements.
return MangaDataFetcher(networkFetcher, file, manga)
}
/**
* Returns the request headers for a source copying its OkHttp headers and caching them.
*
* @param manga the model.
*/
fun getHeaders(manga: Manga): LazyHeaders {
return cachedHeaders.getOrPut(manga.source) {
val source = sourceManager.get(manga.source)!!
LazyHeaders.Builder().apply {
for ((key, value) in source.requestHeaders.toMultimap()) {
addHeader(key, value[0])
}
}.build()
}
}
}

View File

@@ -102,7 +102,7 @@ class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(cont
// MAL doesn't support score with decimals
fun getList(): Observable<List<MangaSync>> {
return networkService.requestBody(get(getListUrl(username), headers), true)
return networkService.requestBody(get(getListUrl(username), headers), networkService.forceCacheClient)
.map { Jsoup.parse(it) }
.flatMap { Observable.from(it.select("manga")) }
.map {

View File

@@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.data.network
import android.net.Uri
import com.squareup.duktape.Duktape
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
object CloudflareScraper {
//language=RegExp
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var t,r,a,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+)"""")
fun request(chain: Interceptor.Chain, cookies: PersistentCookieStore): Response {
val response = chain.proceed(chain.request())
// Check if we already solved a challenge
if (response.code() != 502 &&
cookies.get(response.request().url()).find { it.name() == "cf_clearance" } != null) {
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 5 seconds before resolving the challenge
Thread.sleep(5000)
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", "")
// Duktape can only return strings, so the result has to be converted to string first
val result = duktape.evaluate("$js.toString()").toInt()
val answer = "${result + domain.length}"
val url = Uri.parse("http://$domain/cdn-cgi/l/chk_jschl").buildUpon()
.appendQueryParameter("jschl_vc", challenge)
.appendQueryParameter("pass", pass)
.appendQueryParameter("jschl_answer", answer)
.toString()
val referer = originalRequest.url().toString()
return get(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
} finally {
duktape.close()
}
}
}

View File

@@ -1,12 +1,12 @@
package eu.kanade.tachiyomi.data.network
import android.content.Context
import okhttp3.*
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import java.io.File
import java.net.CookieManager
import java.net.CookiePolicy
import java.net.CookieStore
class NetworkHelper(context: Context) {
@@ -14,43 +14,41 @@ class NetworkHelper(context: Context) {
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
private val cookieManager = CookieManager().apply {
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
}
private val cookieManager = PersistentCookieJar(context)
private val forceCacheInterceptor = { chain: Interceptor.Chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=" + 600)
.build()
}
private val client = OkHttpClient.Builder()
.cookieJar(JavaNetCookieJar(cookieManager))
val defaultClient = OkHttpClient.Builder()
.cookieJar(cookieManager)
.cache(Cache(cacheDir, cacheSize))
.build()
private val forceCacheClient = client.newBuilder()
.addNetworkInterceptor(forceCacheInterceptor)
val forceCacheClient = defaultClient.newBuilder()
.addNetworkInterceptor({ chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=" + 600)
.build()
})
.build()
val cookies: CookieStore
get() = cookieManager.cookieStore
val cloudflareClient = defaultClient.newBuilder()
.addInterceptor { CloudflareScraper.request(it, cookies) }
.build()
val cookies: PersistentCookieStore
get() = cookieManager.store
@JvmOverloads
fun request(request: Request, forceCache: Boolean = false): Observable<Response> {
fun request(request: Request, client: OkHttpClient = defaultClient): Observable<Response> {
return Observable.fromCallable {
val c = if (forceCache) forceCacheClient else client
c.newCall(request).execute().apply { body().close() }
client.newCall(request).execute().apply { body().close() }
}
}
@JvmOverloads
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
fun requestBody(request: Request, client: OkHttpClient = defaultClient): Observable<String> {
return Observable.fromCallable {
val c = if (forceCache) forceCacheClient else client
c.newCall(request).execute().body().string()
client.newCall(request).execute().body().string()
}
}
@@ -59,7 +57,7 @@ class NetworkHelper(context: Context) {
}
fun requestBodyProgressBlocking(request: Request, listener: ProgressListener): Response {
val progressClient = client.newBuilder()
val progressClient = defaultClient.newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
@@ -72,5 +70,4 @@ class NetworkHelper(context: Context) {
return progressClient.newCall(request).execute()
}
}

View File

@@ -0,0 +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)
}
}

View File

@@ -0,0 +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() }
}
fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
}

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.source.base
import android.content.Context
import com.bumptech.glide.load.model.LazyHeaders
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.Chapter
@@ -11,6 +10,7 @@ import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
@@ -27,12 +27,13 @@ abstract class Source(context: Context) : BaseSource() {
val requestHeaders by lazy { headersBuilder().build() }
val glideHeaders by lazy { glideHeadersBuilder().build() }
init {
App.get(context).component.inject(this)
}
open val networkClient: OkHttpClient
get() = networkService.defaultClient
override fun isLoginRequired(): Boolean {
return false
}
@@ -75,7 +76,7 @@ abstract class Source(context: Context) : BaseSource() {
// Get the most popular mangas from the source
open fun pullPopularMangasFromNetwork(page: MangasPage): Observable<MangasPage> {
return networkService.requestBody(popularMangaRequest(page), true)
return networkService.requestBody(popularMangaRequest(page), networkClient)
.map { Jsoup.parse(it) }
.doOnNext { doc -> page.mangas = parsePopularMangasFromHtml(doc) }
.doOnNext { doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page) }
@@ -84,7 +85,7 @@ abstract class Source(context: Context) : BaseSource() {
// Get mangas from the source with a query
open fun searchMangasFromNetwork(page: MangasPage, query: String): Observable<MangasPage> {
return networkService.requestBody(searchMangaRequest(page, query), true)
return networkService.requestBody(searchMangaRequest(page, query), networkClient)
.map { Jsoup.parse(it) }
.doOnNext { doc -> page.mangas = parseSearchFromHtml(doc) }
.doOnNext { doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query) }
@@ -93,13 +94,13 @@ abstract class Source(context: Context) : BaseSource() {
// Get manga details from the source
open fun pullMangaFromNetwork(mangaUrl: String): Observable<Manga> {
return networkService.requestBody(mangaDetailsRequest(mangaUrl))
return networkService.requestBody(mangaDetailsRequest(mangaUrl), networkClient)
.flatMap { Observable.just(parseHtmlToManga(mangaUrl, it)) }
}
// Get chapter list of a manga from the source
open fun pullChaptersFromNetwork(mangaUrl: String): Observable<List<Chapter>> {
return networkService.requestBody(chapterListRequest(mangaUrl))
return networkService.requestBody(chapterListRequest(mangaUrl), networkClient)
.flatMap { unparsedHtml ->
val chapters = parseHtmlToChapters(unparsedHtml)
if (!chapters.isEmpty())
@@ -116,7 +117,7 @@ abstract class Source(context: Context) : BaseSource() {
}
open fun pullPageListFromNetwork(chapterUrl: String): Observable<List<Page>> {
return networkService.requestBody(pageListRequest(chapterUrl))
return networkService.requestBody(pageListRequest(chapterUrl), networkClient)
.flatMap { unparsedHtml ->
val pages = convertToPages(parseHtmlToPageUrls(unparsedHtml))
if (!pages.isEmpty())
@@ -141,7 +142,7 @@ abstract class Source(context: Context) : BaseSource() {
open fun getImageUrlFromPage(page: Page): Observable<Page> {
page.status = Page.LOAD_PAGE
return networkService.requestBody(imageUrlRequest(page))
return networkService.requestBody(imageUrlRequest(page), networkClient)
.flatMap { unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)) }
.onErrorResumeNext { e ->
page.status = Page.ERROR
@@ -224,13 +225,4 @@ abstract class Source(context: Context) : BaseSource() {
}
protected open fun glideHeadersBuilder(): LazyHeaders.Builder {
val builder = LazyHeaders.Builder()
for ((key, value) in requestHeaders.toMultimap()) {
builder.addHeader(key, value[0])
}
return builder
}
}

View File

@@ -10,7 +10,6 @@ import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
@@ -34,6 +33,7 @@ import eu.kanade.tachiyomi.data.source.base.LoginSource;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.Cookie;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Request;
@@ -358,8 +358,8 @@ public class Batoto extends LoginSource {
@Override
public boolean isLogged() {
try {
for ( HttpCookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL)) ) {
if (cookie.getName().equals("pass_hash"))
for (Cookie cookie : getNetworkService().getCookies().get(new URI(BASE_URL))) {
if (cookie.name().equals("pass_hash"))
return true;
}

View File

@@ -1,234 +0,0 @@
package eu.kanade.tachiyomi.data.source.online.english;
import android.content.Context;
import android.net.Uri;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.network.ReqKt;
import eu.kanade.tachiyomi.data.source.Language;
import eu.kanade.tachiyomi.data.source.LanguageKt;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Request;
public class Kissmanga extends Source {
public static final String NAME = "Kissmanga";
public static final String HOST = "kissmanga.com";
public static final String IP = "93.174.95.110";
public static final String BASE_URL = "http://" + IP;
public static final String POPULAR_MANGAS_URL = BASE_URL + "/MangaList/MostPopular?page=%s";
public static final String SEARCH_URL = BASE_URL + "/AdvanceSearch";
public Kissmanga(Context context) {
super(context);
}
@Override
protected Headers.Builder headersBuilder() {
Headers.Builder builder = super.headersBuilder();
builder.add("Host", HOST);
return builder;
}
@Override
public String getName() {
return NAME;
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
public Language getLang() {
return LanguageKt.getEN();
}
@Override
protected String getInitialPopularMangasUrl() {
return String.format(POPULAR_MANGAS_URL, 1);
}
@Override
protected String getInitialSearchUrl(String query) {
return SEARCH_URL;
}
@Override
protected Request searchMangaRequest(MangasPage page, String query) {
if (page.page == 1) {
page.url = getInitialSearchUrl(query);
}
FormBody.Builder form = new FormBody.Builder();
form.add("authorArtist", "");
form.add("mangaName", query);
form.add("status", "");
form.add("genres", "");
return ReqKt.post(page.url, getRequestHeaders(), form.build());
}
@Override
protected Request pageListRequest(String chapterUrl) {
return ReqKt.post(getBaseUrl() + chapterUrl, getRequestHeaders());
}
@Override
protected Request imageRequest(Page page) {
return ReqKt.get(page.getImageUrl());
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
List<Manga> mangaList = new ArrayList<>();
for (Element currentHtmlBlock : parsedHtml.select("table.listing tr:gt(1)")) {
Manga manga = constructPopularMangaFromHtml(currentHtmlBlock);
mangaList.add(manga);
}
return mangaList;
}
private Manga constructPopularMangaFromHtml(Element htmlBlock) {
Manga manga = new Manga();
manga.source = getId();
Element urlElement = Parser.element(htmlBlock, "td a:eq(0)");
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"));
manga.title = urlElement.text();
}
return manga;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
String path = Parser.href(parsedHtml, "li > a:contains( Next)");
return path != null ? BASE_URL + path : null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return parsePopularMangasFromHtml(parsedHtml);
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
Element infoElement = parsedDocument.select("div.barContent").first();
Manga manga = Manga.create(mangaUrl);
manga.title = Parser.text(infoElement, "a.bigChar");
manga.author = Parser.text(infoElement, "p:has(span:contains(Author:)) > a");
manga.genre = Parser.allText(infoElement, "p:has(span:contains(Genres:)) > *:gt(0)");
manga.description = Parser.allText(infoElement, "p:has(span:contains(Summary:)) ~ p");
manga.status = parseStatus(Parser.text(infoElement, "p:has(span:contains(Status:))"));
String thumbnail = Parser.src(parsedDocument, ".rightBox:eq(0) img");
if (thumbnail != null) {
manga.thumbnail_url = Uri.parse(thumbnail).buildUpon().authority(IP).toString();
}
manga.initialized = true;
return manga;
}
private int parseStatus(String status) {
if (status.contains("Ongoing")) {
return Manga.ONGOING;
}
if (status.contains("Completed")) {
return Manga.COMPLETED;
}
return Manga.UNKNOWN;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>();
for (Element chapterElement : parsedDocument.select("table.listing tr:gt(1)")) {
Chapter chapter = constructChapterFromHtmlBlock(chapterElement);
chapterList.add(chapter);
}
return chapterList;
}
private Chapter constructChapterFromHtmlBlock(Element chapterElement) {
Chapter chapter = Chapter.create();
Element urlElement = Parser.element(chapterElement, "a");
String date = Parser.text(chapterElement, "td:eq(1)");
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href"));
chapter.name = urlElement.text();
}
if (date != null) {
try {
chapter.date_upload = new SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH).parse(date).getTime();
} catch (ParseException e) { /* Ignore */ }
}
return chapter;
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
Document parsedDocument = Jsoup.parse(unparsedHtml);
List<String> pageUrlList = new ArrayList<>();
int numImages = parsedDocument.select("#divImage img").size();
for (int i = 0; i < numImages; i++) {
pageUrlList.add("");
}
return pageUrlList;
}
@Override
protected List<Page> parseFirstPage(List<? extends Page> pages, String unparsedHtml) {
Pattern p = Pattern.compile("lstImages.push\\(\"(.+?)\"");
Matcher m = p.matcher(unparsedHtml);
int i = 0;
while (m.find()) {
pages.get(i++).setImageUrl(m.group(1));
}
return (List<Page>) pages;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
return null;
}
}

View File

@@ -0,0 +1,200 @@
package eu.kanade.tachiyomi.data.source.online.english
import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.network.post
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.model.MangasPage
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.Parser
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Pattern
class Kissmanga(context: Context) : Source(context) {
override fun getName() = NAME
override fun getBaseUrl() = BASE_URL
override fun getLang() = EN
override val networkClient: OkHttpClient
get() = networkService.cloudflareClient
override fun getInitialPopularMangasUrl(): String {
return String.format(POPULAR_MANGAS_URL, 1)
}
override fun getInitialSearchUrl(query: String): String {
return SEARCH_URL
}
override fun searchMangaRequest(page: MangasPage, query: String): Request {
if (page.page == 1) {
page.url = getInitialSearchUrl(query)
}
val form = FormBody.Builder()
form.add("authorArtist", "")
form.add("mangaName", query)
form.add("status", "")
form.add("genres", "")
return post(page.url, requestHeaders, form.build())
}
override fun pageListRequest(chapterUrl: String): Request {
return post(baseUrl + chapterUrl, requestHeaders)
}
override fun imageRequest(page: Page): Request {
return get(page.imageUrl)
}
override fun parsePopularMangasFromHtml(parsedHtml: Document): List<Manga> {
val mangaList = ArrayList<Manga>()
for (currentHtmlBlock in parsedHtml.select("table.listing tr:gt(1)")) {
val manga = constructPopularMangaFromHtml(currentHtmlBlock)
mangaList.add(manga)
}
return mangaList
}
private fun constructPopularMangaFromHtml(htmlBlock: Element): Manga {
val manga = Manga()
manga.source = id
val urlElement = Parser.element(htmlBlock, "td a:eq(0)")
if (urlElement != null) {
manga.setUrl(urlElement.attr("href"))
manga.title = urlElement.text()
}
return manga
}
override fun parseNextPopularMangasUrl(parsedHtml: Document, page: MangasPage): String? {
val path = Parser.href(parsedHtml, "li > a:contains( Next)")
return if (path != null) BASE_URL + path else null
}
override fun parseSearchFromHtml(parsedHtml: Document): List<Manga> {
return parsePopularMangasFromHtml(parsedHtml)
}
override fun parseNextSearchUrl(parsedHtml: Document, page: MangasPage, query: String): String? {
return null
}
override fun parseHtmlToManga(mangaUrl: String, unparsedHtml: String): Manga {
val parsedDocument = Jsoup.parse(unparsedHtml)
val infoElement = parsedDocument.select("div.barContent").first()
val manga = Manga.create(mangaUrl)
manga.title = Parser.text(infoElement, "a.bigChar")
manga.author = Parser.text(infoElement, "p:has(span:contains(Author:)) > a")
manga.genre = Parser.allText(infoElement, "p:has(span:contains(Genres:)) > *:gt(0)")
manga.description = Parser.allText(infoElement, "p:has(span:contains(Summary:)) ~ p")
manga.status = parseStatus(Parser.text(infoElement, "p:has(span:contains(Status:))")!!)
val thumbnail = Parser.src(parsedDocument, ".rightBox:eq(0) img")
if (thumbnail != null) {
manga.thumbnail_url = thumbnail
}
manga.initialized = true
return manga
}
private fun parseStatus(status: String): Int {
if (status.contains("Ongoing")) {
return Manga.ONGOING
}
if (status.contains("Completed")) {
return Manga.COMPLETED
}
return Manga.UNKNOWN
}
override fun parseHtmlToChapters(unparsedHtml: String): List<Chapter> {
val parsedDocument = Jsoup.parse(unparsedHtml)
val chapterList = ArrayList<Chapter>()
for (chapterElement in parsedDocument.select("table.listing tr:gt(1)")) {
val chapter = constructChapterFromHtmlBlock(chapterElement)
chapterList.add(chapter)
}
return chapterList
}
private fun constructChapterFromHtmlBlock(chapterElement: Element): Chapter {
val chapter = Chapter.create()
val urlElement = Parser.element(chapterElement, "a")
val date = Parser.text(chapterElement, "td:eq(1)")
if (urlElement != null) {
chapter.setUrl(urlElement.attr("href"))
chapter.name = urlElement.text()
}
if (date != null) {
try {
chapter.date_upload = SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH).parse(date).time
} catch (e: ParseException) { /* Ignore */
}
}
return chapter
}
override fun parseHtmlToPageUrls(unparsedHtml: String): List<String> {
val parsedDocument = Jsoup.parse(unparsedHtml)
val pageUrlList = ArrayList<String>()
val numImages = parsedDocument.select("#divImage img").size
for (i in 0..numImages - 1) {
pageUrlList.add("")
}
return pageUrlList
}
override fun parseFirstPage(pages: List<Page>, unparsedHtml: String): List<Page> {
val p = Pattern.compile("lstImages.push\\(\"(.+?)\"")
val m = p.matcher(unparsedHtml)
var i = 0
while (m.find()) {
pages[i++].imageUrl = m.group(1)
}
return pages
}
override fun parseHtmlToImageUrl(unparsedHtml: String): String? {
return null
}
companion object {
val NAME = "Kissmanga"
val BASE_URL = "http://kissmanga.com"
val POPULAR_MANGAS_URL = BASE_URL + "/MangaList/MostPopular?page=%s"
val SEARCH_URL = BASE_URL + "/AdvanceSearch"
}
}

View File

@@ -100,7 +100,7 @@ public class ReadMangaToday extends Source {
@Override
public Observable<MangasPage> searchMangasFromNetwork(final MangasPage page, String query) {
return networkService
.requestBody(searchMangaRequest(page, query), true)
.requestBody(searchMangaRequest(page, query), networkService.getDefaultClient())
.doOnNext(new Action1<String>() {
@Override
public void call(String doc) {