mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 22:37:56 +01:00 
			
		
		
		
	Move source and network outside data
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -1,80 +0,0 @@ | ||||
| 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() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| 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 | ||||
|  | ||||
| } | ||||
| @@ -1,70 +0,0 @@ | ||||
| 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) | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| 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) | ||||
|     } | ||||
| } | ||||
| @@ -1,75 +0,0 @@ | ||||
| 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() | ||||
|  | ||||
| } | ||||
| @@ -1,5 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.network | ||||
|  | ||||
| interface ProgressListener { | ||||
|     fun update(bytesRead: Long, contentLength: Long, done: Boolean) | ||||
| } | ||||
| @@ -1,40 +0,0 @@ | ||||
| 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 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| 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() | ||||
| } | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -1,46 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.source | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.source.model.FilterList | ||||
| import eu.kanade.tachiyomi.data.source.model.MangasPage | ||||
| import rx.Observable | ||||
|  | ||||
| interface CatalogueSource : Source { | ||||
|  | ||||
|     /** | ||||
|      * An ISO 639-1 compliant language code (two letters in lower case). | ||||
|      */ | ||||
|     val lang: String | ||||
|  | ||||
|     /** | ||||
|      * Whether the source has support for latest updates. | ||||
|      */ | ||||
|     val supportsLatest: Boolean | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable containing a page with a list of manga. | ||||
|      * | ||||
|      * @param page the page number to retrieve. | ||||
|      */ | ||||
|     fun fetchPopularManga(page: Int): Observable<MangasPage> | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable containing a page with a list of manga. | ||||
|      * | ||||
|      * @param page the page number to retrieve. | ||||
|      * @param query the search query. | ||||
|      * @param filters the list of filters to apply. | ||||
|      */ | ||||
|     fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable containing a page with a list of latest manga updates. | ||||
|      * | ||||
|      * @param page the page number to retrieve. | ||||
|      */ | ||||
|     fun fetchLatestUpdates(page: Int): Observable<MangasPage> | ||||
|  | ||||
|     /** | ||||
|      * Returns the list of filters for the source. | ||||
|      */ | ||||
|     fun getFilterList(): FilterList | ||||
| } | ||||
| @@ -1,44 +0,0 @@ | ||||
| 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>> | ||||
|  | ||||
| } | ||||
| @@ -1,159 +0,0 @@ | ||||
| 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 | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,40 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.source.model | ||||
|  | ||||
| sealed class Filter<T>(val name: String, var state: T) { | ||||
|     open class Header(name: String) : Filter<Any>(name, 0) | ||||
|     open class Separator(name: String = "") : Filter<Any>(name, 0) | ||||
|     abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) | ||||
|     abstract class Text(name: String, state: String = "") : Filter<String>(name, state) | ||||
|     abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state) | ||||
|     abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) { | ||||
|         fun isIgnored() = state == STATE_IGNORE | ||||
|         fun isIncluded() = state == STATE_INCLUDE | ||||
|         fun isExcluded() = state == STATE_EXCLUDE | ||||
|  | ||||
|         companion object { | ||||
|             const val STATE_IGNORE = 0 | ||||
|             const val STATE_INCLUDE = 1 | ||||
|             const val STATE_EXCLUDE = 2 | ||||
|         } | ||||
|     } | ||||
|     abstract class Group<V>(name: String, state: List<V>): Filter<List<V>>(name, state) | ||||
|  | ||||
|     abstract class Sort(name: String, val values: Array<String>, state: Selection? = null) | ||||
|         : Filter<Sort.Selection?>(name, state) { | ||||
|         data class Selection(val index: Int, val ascending: Boolean) | ||||
|     } | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
|         if (this === other) return true | ||||
|         if (other !is Filter<*>) return false | ||||
|  | ||||
|         return name == other.name && state == other.state | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|         var result = name.hashCode() | ||||
|         result = 31 * result + (state?.hashCode() ?: 0) | ||||
|         return result | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,7 +0,0 @@ | ||||
| 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()) | ||||
|  | ||||
| } | ||||
| @@ -1,3 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.source.model | ||||
|  | ||||
| data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean) | ||||
| @@ -1,47 +0,0 @@ | ||||
| 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 | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.source.model | ||||
|  | ||||
| import java.io.Serializable | ||||
|  | ||||
| interface SChapter : Serializable { | ||||
|  | ||||
|     var url: String | ||||
|  | ||||
|     var name: String | ||||
|  | ||||
|     var date_upload: Long | ||||
|  | ||||
|     var chapter_number: Float | ||||
|  | ||||
|     fun copyFrom(other: SChapter) { | ||||
|         name = other.name | ||||
|         url = other.url | ||||
|         date_upload = other.date_upload | ||||
|         chapter_number = other.chapter_number | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         fun create(): SChapter { | ||||
|             return SChapterImpl() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,13 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.source.model | ||||
|  | ||||
| class SChapterImpl : SChapter { | ||||
|  | ||||
|     override lateinit var url: String | ||||
|  | ||||
|     override lateinit var name: String | ||||
|  | ||||
|     override var date_upload: Long = 0 | ||||
|  | ||||
|     override var chapter_number: Float = -1f | ||||
|  | ||||
| } | ||||
| @@ -1,58 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.source.model | ||||
|  | ||||
| import java.io.Serializable | ||||
|  | ||||
| interface SManga : Serializable { | ||||
|  | ||||
|     var url: String | ||||
|  | ||||
|     var title: String | ||||
|  | ||||
|     var artist: String? | ||||
|  | ||||
|     var author: String? | ||||
|  | ||||
|     var description: String? | ||||
|  | ||||
|     var genre: String? | ||||
|  | ||||
|     var status: Int | ||||
|  | ||||
|     var thumbnail_url: String? | ||||
|  | ||||
|     var initialized: Boolean | ||||
|  | ||||
|     fun copyFrom(other: SManga) { | ||||
|         if (other.author != null) | ||||
|             author = other.author | ||||
|  | ||||
|         if (other.artist != null) | ||||
|             artist = other.artist | ||||
|  | ||||
|         if (other.description != null) | ||||
|             description = other.description | ||||
|  | ||||
|         if (other.genre != null) | ||||
|             genre = other.genre | ||||
|  | ||||
|         if (other.thumbnail_url != null) | ||||
|             thumbnail_url = other.thumbnail_url | ||||
|  | ||||
|         status = other.status | ||||
|  | ||||
|         if (!initialized) | ||||
|             initialized = other.initialized | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         const val UNKNOWN = 0 | ||||
|         const val ONGOING = 1 | ||||
|         const val COMPLETED = 2 | ||||
|         const val LICENSED = 3 | ||||
|  | ||||
|         fun create(): SManga { | ||||
|             return SMangaImpl() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.source.model | ||||
|  | ||||
| class SMangaImpl : SManga { | ||||
|  | ||||
|     override lateinit var url: String | ||||
|  | ||||
|     override lateinit var title: String | ||||
|  | ||||
|     override var artist: String? = null | ||||
|  | ||||
|     override var author: String? = null | ||||
|  | ||||
|     override var description: String? = null | ||||
|  | ||||
|     override var genre: String? = null | ||||
|  | ||||
|     override var status: Int = 0 | ||||
|  | ||||
|     override var thumbnail_url: String? = null | ||||
|  | ||||
|     override var initialized: Boolean = false | ||||
|  | ||||
| } | ||||
| @@ -1,15 +0,0 @@ | ||||
| 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 | ||||
|  | ||||
| } | ||||
| @@ -1,361 +0,0 @@ | ||||
| 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() | ||||
| } | ||||
| @@ -1,98 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.source.online | ||||
|  | ||||
| import android.net.Uri | ||||
| import eu.kanade.tachiyomi.data.cache.ChapterCache | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.source.model.Page | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
|  | ||||
| // TODO: this should be handled with a different approach. | ||||
|  | ||||
| /** | ||||
|  * Chapter cache. | ||||
|  */ | ||||
| private val chapterCache: ChapterCache by injectLazy() | ||||
|  | ||||
| /** | ||||
|  * Returns an observable with the page list for a chapter. It tries to return the page list from | ||||
|  * the local cache, otherwise fallbacks to network. | ||||
|  * | ||||
|  * @param chapter the chapter whose page list has to be fetched. | ||||
|  */ | ||||
| fun OnlineSource.fetchPageListFromCacheThenNet(chapter: Chapter): Observable<List<Page>> { | ||||
|     return chapterCache | ||||
|             .getPageListFromCache(chapter) | ||||
|             .onErrorResumeNext { fetchPageList(chapter) } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns an observable of the page with the downloaded image. | ||||
|  * | ||||
|  * @param page the page whose source image has to be downloaded. | ||||
|  */ | ||||
| fun OnlineSource.fetchImageFromCacheThenNet(page: Page): Observable<Page> { | ||||
|     return if (page.imageUrl.isNullOrEmpty()) | ||||
|         getImageUrl(page).flatMap { getCachedImage(it) } | ||||
|     else | ||||
|         getCachedImage(page) | ||||
| } | ||||
|  | ||||
| fun OnlineSource.getImageUrl(page: Page): Observable<Page> { | ||||
|     page.status = Page.LOAD_PAGE | ||||
|     return fetchImageUrl(page) | ||||
|             .doOnError { page.status = Page.ERROR } | ||||
|             .onErrorReturn { null } | ||||
|             .doOnNext { page.imageUrl = it } | ||||
|             .map { page } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns an observable of the page that gets the image from the chapter or fallbacks to | ||||
|  * network and copies it to the cache calling [cacheImage]. | ||||
|  * | ||||
|  * @param page the page. | ||||
|  */ | ||||
| fun OnlineSource.getCachedImage(page: Page): Observable<Page> { | ||||
|     val imageUrl = page.imageUrl ?: return Observable.just(page) | ||||
|  | ||||
|     return Observable.just(page) | ||||
|             .flatMap { | ||||
|                 if (!chapterCache.isImageInCache(imageUrl)) { | ||||
|                     cacheImage(page) | ||||
|                 } else { | ||||
|                     Observable.just(page) | ||||
|                 } | ||||
|             } | ||||
|             .doOnNext { | ||||
|                 page.uri = Uri.fromFile(chapterCache.getImageFile(imageUrl)) | ||||
|                 page.status = Page.READY | ||||
|             } | ||||
|             .doOnError { page.status = Page.ERROR } | ||||
|             .onErrorReturn { page } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns an observable of the page that downloads the image to [ChapterCache]. | ||||
|  * | ||||
|  * @param page the page. | ||||
|  */ | ||||
| private fun OnlineSource.cacheImage(page: Page): Observable<Page> { | ||||
|     page.status = Page.DOWNLOAD_IMAGE | ||||
|     return fetchImage(page) | ||||
|             .doOnNext { chapterCache.putImageToCache(page.imageUrl!!, it) } | ||||
|             .map { page } | ||||
| } | ||||
|  | ||||
| fun OnlineSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> { | ||||
|     return Observable.from(pages) | ||||
|             .filter { !it.imageUrl.isNullOrEmpty() } | ||||
|             .mergeWith(fetchRemainingImageUrlsFromPageList(pages)) | ||||
| } | ||||
|  | ||||
| fun OnlineSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> { | ||||
|     return Observable.from(pages) | ||||
|             .filter { it.imageUrl.isNullOrEmpty() } | ||||
|             .concatMap { getImageUrl(it) } | ||||
| } | ||||
| @@ -1,200 +0,0 @@ | ||||
| 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 | ||||
| } | ||||
| @@ -1,232 +0,0 @@ | ||||
| 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") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,234 +0,0 @@ | ||||
| @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" | ||||
|  | ||||
| } | ||||
| @@ -1,366 +0,0 @@ | ||||
| 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) | ||||
|     ) | ||||
|  | ||||
| } | ||||
| @@ -1,197 +0,0 @@ | ||||
| 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") | ||||
|     ) | ||||
| } | ||||
| @@ -1,223 +0,0 @@ | ||||
| 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") | ||||
|     ) | ||||
|  | ||||
| } | ||||
| @@ -1,220 +0,0 @@ | ||||
| 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") | ||||
|     ) | ||||
|  | ||||
| } | ||||
| @@ -1,243 +0,0 @@ | ||||
| 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") | ||||
|     ) | ||||
|  | ||||
| } | ||||
| @@ -1,219 +0,0 @@ | ||||
| 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) | ||||
|     ) | ||||
| } | ||||
| @@ -1,122 +0,0 @@ | ||||
| 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") | ||||
|  | ||||
| } | ||||
| @@ -1,230 +0,0 @@ | ||||
| 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("ёнкома") | ||||
|     ) | ||||
| } | ||||
| @@ -1,184 +0,0 @@ | ||||
| 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") | ||||
|     ) | ||||
| } | ||||
| @@ -1,183 +0,0 @@ | ||||
| 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") | ||||
|     ) | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user