mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-09 05:44:33 -05:00
[skip ci] Formatting
This commit is contained in:
@@ -32,7 +32,9 @@ import uy.kohesive.injekt.api.addSingleton
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
class AppModule(
|
||||
val app: Application,
|
||||
) : InjektModule {
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@ class MemoryCookieJar : CookieJar {
|
||||
}
|
||||
}
|
||||
|
||||
class WrappedCookie private constructor(val cookie: Cookie) {
|
||||
class WrappedCookie private constructor(
|
||||
val cookie: Cookie,
|
||||
) {
|
||||
fun unwrap() = cookie
|
||||
|
||||
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
|
||||
|
||||
@@ -30,7 +30,9 @@ import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NetworkHelper(context: Context) {
|
||||
class NetworkHelper(
|
||||
context: Context,
|
||||
) {
|
||||
// private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
// private val cacheDir = File(context.cacheDir, "network_cache")
|
||||
@@ -53,9 +55,7 @@ class NetworkHelper(context: Context) {
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
)
|
||||
|
||||
fun defaultUserAgentProvider(): String {
|
||||
return userAgent.value
|
||||
}
|
||||
fun defaultUserAgentProvider(): String = userAgent.value
|
||||
|
||||
init {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@@ -63,14 +63,14 @@ class NetworkHelper(context: Context) {
|
||||
.drop(1)
|
||||
.onEach {
|
||||
GetCatalogueSource.unregisterAllCatalogueSources() // need to reset the headers
|
||||
}
|
||||
.launchIn(GlobalScope)
|
||||
}.launchIn(GlobalScope)
|
||||
}
|
||||
|
||||
private val baseClientBuilder: OkHttpClient.Builder
|
||||
get() {
|
||||
val builder =
|
||||
OkHttpClient.Builder()
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.cookieJar(PersistentCookieJar(cookieStore))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
@@ -80,8 +80,7 @@ class NetworkHelper(context: Context) {
|
||||
directory = File.createTempFile("tachidesk_network_cache", null),
|
||||
maxSize = 5L * 1024 * 1024, // 5 MiB
|
||||
),
|
||||
)
|
||||
.addInterceptor(UncaughtExceptionInterceptor())
|
||||
).addInterceptor(UncaughtExceptionInterceptor())
|
||||
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
|
||||
.addNetworkInterceptor(IgnoreGzipInterceptor())
|
||||
.addNetworkInterceptor(BrotliInterceptor)
|
||||
|
||||
@@ -50,9 +50,7 @@ fun Call.asObservable(): Observable<Response> {
|
||||
// call.cancel()
|
||||
}
|
||||
|
||||
override fun isUnsubscribed(): Boolean {
|
||||
return call.isCanceled()
|
||||
}
|
||||
override fun isUnsubscribed(): Boolean = call.isCanceled()
|
||||
}
|
||||
|
||||
subscriber.add(requestArbiter)
|
||||
@@ -60,15 +58,14 @@ fun Call.asObservable(): Observable<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable()
|
||||
fun Call.asObservableSuccess(): Observable<Response> =
|
||||
asObservable()
|
||||
.doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -135,29 +132,28 @@ fun OkHttpClient.newCachelessCallWithProgress(
|
||||
.cache(null)
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse.newBuilder()
|
||||
originalResponse
|
||||
.newBuilder()
|
||||
.body(ProgressResponseBody(originalResponse.body, listener))
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}.build()
|
||||
|
||||
return progressClient.newCall(request)
|
||||
}
|
||||
|
||||
context(Json)
|
||||
inline fun <reified T> Response.parseAs(): T {
|
||||
return decodeFromJsonResponse(serializer(), this)
|
||||
}
|
||||
inline fun <reified T> Response.parseAs(): T = decodeFromJsonResponse(serializer(), this)
|
||||
|
||||
context(Json)
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun <T> decodeFromJsonResponse(
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
response: Response,
|
||||
): T {
|
||||
return response.body.source().use {
|
||||
): T =
|
||||
response.body.source().use {
|
||||
decodeFromBufferedSource(deserializer, it)
|
||||
}
|
||||
}
|
||||
|
||||
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")
|
||||
class HttpException(
|
||||
val code: Int,
|
||||
) : IllegalStateException("HTTP error $code")
|
||||
|
||||
@@ -5,7 +5,9 @@ import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
// from TachiWeb-Server
|
||||
class PersistentCookieJar(private val store: PersistentCookieStore) : CookieJar {
|
||||
class PersistentCookieJar(
|
||||
private val store: PersistentCookieStore,
|
||||
) : CookieJar {
|
||||
override fun saveFromResponse(
|
||||
url: HttpUrl,
|
||||
cookies: List<Cookie>,
|
||||
@@ -13,7 +15,5 @@ class PersistentCookieJar(private val store: PersistentCookieStore) : CookieJar
|
||||
store.addAll(url, cookies)
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return store.get(url)
|
||||
}
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> = store.get(url)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
// from TachiWeb-Server
|
||||
class PersistentCookieStore(context: Context) : CookieStore {
|
||||
class PersistentCookieStore(
|
||||
context: Context,
|
||||
) : CookieStore {
|
||||
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
||||
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
||||
|
||||
@@ -23,7 +25,8 @@ class PersistentCookieStore(context: Context) : CookieStore {
|
||||
|
||||
init {
|
||||
val domains =
|
||||
prefs.all.keys.map { it.substringBeforeLast(".") }
|
||||
prefs.all.keys
|
||||
.map { it.substringBeforeLast(".") }
|
||||
.toSet()
|
||||
domains.forEach { domain ->
|
||||
val cookies = prefs.getStringSet(domain, emptySet())
|
||||
@@ -31,7 +34,8 @@ class PersistentCookieStore(context: Context) : CookieStore {
|
||||
try {
|
||||
val url = "http://$domain".toHttpUrlOrNull() ?: return@forEach
|
||||
val nonExpiredCookies =
|
||||
cookies.mapNotNull { Cookie.parse(url, it) }
|
||||
cookies
|
||||
.mapNotNull { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
cookieMap[domain] = nonExpiredCookies
|
||||
} catch (e: Exception) {
|
||||
@@ -63,14 +67,13 @@ class PersistentCookieStore(context: Context) : CookieStore {
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeAll(): Boolean {
|
||||
return lock.withLock {
|
||||
override fun removeAll(): Boolean =
|
||||
lock.withLock {
|
||||
val wasNotEmpty = cookieMap.isEmpty()
|
||||
prefs.edit().clear().apply()
|
||||
cookieMap.clear()
|
||||
wasNotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(uri: URI) {
|
||||
val url = uri.toURL()
|
||||
@@ -87,9 +90,7 @@ class PersistentCookieStore(context: Context) : CookieStore {
|
||||
}
|
||||
}
|
||||
|
||||
fun get(url: HttpUrl): List<Cookie> {
|
||||
return get(url.host)
|
||||
}
|
||||
fun get(url: HttpUrl): List<Cookie> = get(url.host)
|
||||
|
||||
override fun add(
|
||||
uri: URI?,
|
||||
@@ -105,19 +106,17 @@ class PersistentCookieStore(context: Context) : CookieStore {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCookies(): List<HttpCookie> {
|
||||
return cookieMap.values.flatMap {
|
||||
override fun getCookies(): List<HttpCookie> =
|
||||
cookieMap.values.flatMap {
|
||||
it.map {
|
||||
it.toHttpCookie()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getURIs(): List<URI> {
|
||||
return cookieMap.keys().toList().map {
|
||||
override fun getURIs(): List<URI> =
|
||||
cookieMap.keys().toList().map {
|
||||
URI("http://$it")
|
||||
}
|
||||
}
|
||||
|
||||
override fun remove(
|
||||
uri: URI?,
|
||||
@@ -145,9 +144,7 @@ class PersistentCookieStore(context: Context) : CookieStore {
|
||||
}
|
||||
}
|
||||
|
||||
private fun get(url: String): List<Cookie> {
|
||||
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||
}
|
||||
private fun get(url: String): List<Cookie> = cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||
|
||||
private fun saveToDisk(url: URL) {
|
||||
// Get cookies to be stored in disk
|
||||
@@ -165,7 +162,8 @@ class PersistentCookieStore(context: Context) : CookieStore {
|
||||
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt
|
||||
|
||||
private fun HttpCookie.toCookie(uri: URI) =
|
||||
Cookie.Builder()
|
||||
Cookie
|
||||
.Builder()
|
||||
.name(name)
|
||||
.value(value)
|
||||
.domain(uri.toURL().host)
|
||||
@@ -176,22 +174,19 @@ class PersistentCookieStore(context: Context) : CookieStore {
|
||||
} else {
|
||||
it.expiresAt(Long.MAX_VALUE)
|
||||
}
|
||||
}
|
||||
.let {
|
||||
}.let {
|
||||
if (secure) {
|
||||
it.secure()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.let {
|
||||
}.let {
|
||||
if (isHttpOnly) {
|
||||
it.httpOnly()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}.build()
|
||||
|
||||
private fun Cookie.toHttpCookie(): HttpCookie {
|
||||
val it = this
|
||||
|
||||
@@ -9,22 +9,19 @@ import okio.Source
|
||||
import okio.buffer
|
||||
import java.io.IOException
|
||||
|
||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
||||
class ProgressResponseBody(
|
||||
private val responseBody: ResponseBody,
|
||||
private val progressListener: ProgressListener,
|
||||
) : ResponseBody() {
|
||||
private val bufferedSource: BufferedSource by lazy {
|
||||
source(responseBody.source()).buffer()
|
||||
}
|
||||
|
||||
override fun contentType(): MediaType? {
|
||||
return responseBody.contentType()
|
||||
}
|
||||
override fun contentType(): MediaType? = responseBody.contentType()
|
||||
|
||||
override fun contentLength(): Long {
|
||||
return responseBody.contentLength()
|
||||
}
|
||||
override fun contentLength(): Long = responseBody.contentLength()
|
||||
|
||||
override fun source(): BufferedSource {
|
||||
return bufferedSource
|
||||
}
|
||||
override fun source(): BufferedSource = bufferedSource
|
||||
|
||||
private fun source(source: Source): Source {
|
||||
return object : ForwardingSource(source) {
|
||||
|
||||
@@ -18,13 +18,13 @@ fun GET(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
): Request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* @since extensions-lib 1.4
|
||||
@@ -33,52 +33,52 @@ fun GET(
|
||||
url: HttpUrl,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
): Request =
|
||||
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()
|
||||
): Request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.post(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun PUT(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
body: RequestBody = DEFAULT_BODY,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
): Request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.put(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun DELETE(
|
||||
url: String,
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
body: RequestBody = DEFAULT_BODY,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
): Request =
|
||||
Request
|
||||
.Builder()
|
||||
.url(url)
|
||||
.delete(body)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -117,12 +117,12 @@ object CFClearance {
|
||||
serverConfig.flareSolverrTimeout
|
||||
.map { timeoutInt ->
|
||||
val timeout = timeoutInt.seconds
|
||||
network.client.newBuilder()
|
||||
network.client
|
||||
.newBuilder()
|
||||
.callTimeout(timeout.plus(10.seconds).toJavaDuration())
|
||||
.readTimeout(timeout.plus(5.seconds).toJavaDuration())
|
||||
.build()
|
||||
}
|
||||
.stateIn(GlobalScope, SharingStarted.Eagerly, network.client)
|
||||
}.stateIn(GlobalScope, SharingStarted.Eagerly, network.client)
|
||||
}
|
||||
private val json: Json by injectLazy()
|
||||
private val jsonMediaType = "application/json".toMediaType()
|
||||
@@ -190,26 +190,29 @@ object CFClearance {
|
||||
|
||||
return with(json) {
|
||||
mutex.withLock {
|
||||
client.value.newCall(
|
||||
POST(
|
||||
url = serverConfig.flareSolverrUrl.value.removeSuffix("/") + "/v1",
|
||||
body =
|
||||
Json.encodeToString(
|
||||
FlareSolverRequest(
|
||||
"request.get",
|
||||
originalRequest.url.toString(),
|
||||
session = serverConfig.flareSolverrSessionName.value,
|
||||
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
||||
cookies =
|
||||
network.cookieStore.get(originalRequest.url).map {
|
||||
FlareSolverCookie(it.name, it.value)
|
||||
},
|
||||
returnOnlyCookies = onlyCookies,
|
||||
maxTimeout = timeout.inWholeMilliseconds.toInt(),
|
||||
),
|
||||
).toRequestBody(jsonMediaType),
|
||||
),
|
||||
).awaitSuccess().parseAs<FlareSolverResponse>()
|
||||
client.value
|
||||
.newCall(
|
||||
POST(
|
||||
url = serverConfig.flareSolverrUrl.value.removeSuffix("/") + "/v1",
|
||||
body =
|
||||
Json
|
||||
.encodeToString(
|
||||
FlareSolverRequest(
|
||||
"request.get",
|
||||
originalRequest.url.toString(),
|
||||
session = serverConfig.flareSolverrSessionName.value,
|
||||
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
|
||||
cookies =
|
||||
network.cookieStore.get(originalRequest.url).map {
|
||||
FlareSolverCookie(it.name, it.value)
|
||||
},
|
||||
returnOnlyCookies = onlyCookies,
|
||||
maxTimeout = timeout.inWholeMilliseconds.toInt(),
|
||||
),
|
||||
).toRequestBody(jsonMediaType),
|
||||
),
|
||||
).awaitSuccess()
|
||||
.parseAs<FlareSolverResponse>()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +227,8 @@ object CFClearance {
|
||||
val cookies =
|
||||
flareSolverResponse.solution.cookies
|
||||
.map { cookie ->
|
||||
Cookie.Builder()
|
||||
Cookie
|
||||
.Builder()
|
||||
.name(cookie.name)
|
||||
.value(cookie.value)
|
||||
.domain(cookie.domain.removePrefix("."))
|
||||
@@ -233,13 +237,12 @@ object CFClearance {
|
||||
if (cookie.httpOnly != null && cookie.httpOnly) it.httpOnly()
|
||||
if (cookie.secure != null && cookie.secure) it.secure()
|
||||
if (!cookie.path.isNullOrEmpty()) it.path(cookie.path)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
.groupBy { it.domain }
|
||||
}.build()
|
||||
}.groupBy { it.domain }
|
||||
.flatMap { (domain, cookies) ->
|
||||
network.cookieStore.addAll(
|
||||
HttpUrl.Builder()
|
||||
HttpUrl
|
||||
.Builder()
|
||||
.scheme("http")
|
||||
.host(domain.removePrefix("."))
|
||||
.build(),
|
||||
@@ -254,7 +257,8 @@ object CFClearance {
|
||||
"${it.name}=${it.value}"
|
||||
}
|
||||
logger.trace { "Final cookies\n$finalCookies" }
|
||||
return originalRequest.newBuilder()
|
||||
return originalRequest
|
||||
.newBuilder()
|
||||
.header("Cookie", finalCookies)
|
||||
.header("User-Agent", flareSolverResponse.solution.userAgent)
|
||||
.build()
|
||||
|
||||
@@ -13,8 +13,8 @@ import java.io.IOException
|
||||
* See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-interceptor/
|
||||
*/
|
||||
class UncaughtExceptionInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
return try {
|
||||
override fun intercept(chain: Interceptor.Chain): Response =
|
||||
try {
|
||||
chain.proceed(chain.request())
|
||||
} catch (e: Exception) {
|
||||
if (e is IOException) {
|
||||
@@ -23,5 +23,4 @@ class UncaughtExceptionInterceptor : Interceptor {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.network.interceptor
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class UserAgentInterceptor(private val userAgentProvider: () -> String) : Interceptor {
|
||||
class UserAgentInterceptor(
|
||||
private val userAgentProvider: () -> String,
|
||||
) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
|
||||
@@ -23,9 +23,7 @@ interface CatalogueSource : Source {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getPopularManga(page: Int): MangasPage {
|
||||
return fetchPopularManga(page).awaitSingle()
|
||||
}
|
||||
suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle()
|
||||
|
||||
/**
|
||||
* Get a page with a list of manga.
|
||||
@@ -40,9 +38,7 @@ interface CatalogueSource : Source {
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): MangasPage {
|
||||
return fetchSearchManga(page, query, filters).awaitSingle()
|
||||
}
|
||||
): MangasPage = fetchSearchManga(page, query, filters).awaitSingle()
|
||||
|
||||
/**
|
||||
* Get a page with a list of latest manga updates.
|
||||
@@ -51,9 +47,7 @@ interface CatalogueSource : Source {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getLatestUpdates(page: Int): MangasPage {
|
||||
return fetchLatestUpdates(page).awaitSingle()
|
||||
}
|
||||
suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle()
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
|
||||
@@ -31,9 +31,7 @@ interface Source {
|
||||
* @return the updated manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
return fetchMangaDetails(manga).awaitSingle()
|
||||
}
|
||||
suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
|
||||
|
||||
/**
|
||||
* Get all the available chapters for a manga.
|
||||
@@ -43,9 +41,7 @@ interface Source {
|
||||
* @return the chapters for the manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
return fetchChapterList(manga).awaitSingle()
|
||||
}
|
||||
suspend fun getChapterList(manga: SManga): List<SChapter> = fetchChapterList(manga).awaitSingle()
|
||||
|
||||
/**
|
||||
* Get the list of pages a chapter has. Pages should be returned
|
||||
@@ -56,9 +52,7 @@ interface Source {
|
||||
* @return the pages for the chapter.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
return fetchPageList(chapter).awaitSingle()
|
||||
}
|
||||
suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
|
||||
@@ -59,7 +59,8 @@ import com.github.junrar.Archive as JunrarArchive
|
||||
class LocalSource(
|
||||
private val fileSystem: LocalSourceFileSystem,
|
||||
private val coverManager: LocalCoverManager,
|
||||
) : CatalogueSource, UnmeteredSource {
|
||||
) : CatalogueSource,
|
||||
UnmeteredSource {
|
||||
private val json: Json by injectLazy()
|
||||
private val xml: XML by injectLazy()
|
||||
|
||||
@@ -93,7 +94,8 @@ class LocalSource(
|
||||
// Filter out files that are hidden and is not a folder
|
||||
.filter { it.isDirectory && !it.name.startsWith('.') }
|
||||
.distinctBy { it.name }
|
||||
.filter { // Filter by query or last modified
|
||||
.filter {
|
||||
// Filter by query or last modified
|
||||
if (lastModifiedLimit == 0L) {
|
||||
it.name.contains(query, ignoreCase = true)
|
||||
} else {
|
||||
@@ -134,7 +136,8 @@ class LocalSource(
|
||||
url = mangaDir.name
|
||||
|
||||
// Try to find the cover
|
||||
coverManager.find(mangaDir.name)
|
||||
coverManager
|
||||
.find(mangaDir.name)
|
||||
?.takeIf(File::exists)
|
||||
?.let { thumbnail_url = it.absolutePath }
|
||||
}
|
||||
@@ -238,7 +241,7 @@ class LocalSource(
|
||||
for (chapter in chapterArchives) {
|
||||
when (Format.valueOf(chapter)) {
|
||||
is Format.Zip -> {
|
||||
ZipFile(chapter).use { zip: ZipFile ->
|
||||
ZipFile.builder().setFile(chapter).get().use { zip: ZipFile ->
|
||||
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
||||
zip.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||
return copyComicInfoFile(stream, folderPath)
|
||||
@@ -264,13 +267,12 @@ class LocalSource(
|
||||
private fun copyComicInfoFile(
|
||||
comicInfoFileStream: InputStream,
|
||||
folderPath: String?,
|
||||
): File {
|
||||
return File("$folderPath/$COMIC_INFO_FILE").apply {
|
||||
): File =
|
||||
File("$folderPath/$COMIC_INFO_FILE").apply {
|
||||
outputStream().use { outputStream ->
|
||||
comicInfoFileStream.use { it.copyTo(outputStream) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalXmlUtilApi::class)
|
||||
private fun setMangaDetailsFromComicInfoFile(
|
||||
@@ -286,8 +288,9 @@ class LocalSource(
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
return fileSystem.getFilesInMangaDirectory(manga.url)
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> =
|
||||
fileSystem
|
||||
.getFilesInMangaDirectory(manga.url)
|
||||
// Only keep supported formats
|
||||
.filter { it.isDirectory || Archive.isSupported(it) }
|
||||
.map { chapterFile ->
|
||||
@@ -312,22 +315,21 @@ class LocalSource(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sortedWith { c1, c2 ->
|
||||
}.sortedWith { c1, c2 ->
|
||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}.toList()
|
||||
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(OrderBy.Popular())
|
||||
|
||||
// TODO Fix Memory Leak
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
return when (val format = getFormat(chapter)) {
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> =
|
||||
when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
format.file.listFiles().orEmpty()
|
||||
format.file
|
||||
.listFiles()
|
||||
.orEmpty()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name, it::inputStream) }
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { index, page ->
|
||||
@@ -359,11 +361,11 @@ class LocalSource(
|
||||
pages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getFormat(chapter: SChapter): Format {
|
||||
try {
|
||||
return fileSystem.getBaseDirectories()
|
||||
return fileSystem
|
||||
.getBaseDirectories()
|
||||
.map { dir -> File(dir, chapter.url) }
|
||||
.find { it.exists() }
|
||||
?.let(Format.Companion::valueOf)
|
||||
@@ -378,21 +380,23 @@ class LocalSource(
|
||||
private fun updateCover(
|
||||
chapter: SChapter,
|
||||
manga: SManga,
|
||||
): File? {
|
||||
return try {
|
||||
): File? =
|
||||
try {
|
||||
when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
val entry =
|
||||
format.file.listFiles()
|
||||
format.file
|
||||
.listFiles()
|
||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
|
||||
entry?.let { coverManager.update(manga, it.inputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
ZipFile(format.file).use { zip ->
|
||||
ZipFile.builder().setFile(format.file).get().use { zip ->
|
||||
val entry =
|
||||
zip.entries.toList()
|
||||
zip.entries
|
||||
.toList()
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
|
||||
@@ -412,7 +416,8 @@ class LocalSource(
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file).use { epub ->
|
||||
val entry =
|
||||
epub.getImagesFromPages()
|
||||
epub
|
||||
.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
|
||||
@@ -424,7 +429,6 @@ class LocalSource(
|
||||
logger.error(e) { "Error updating cover for ${manga.title}" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
|
||||
@@ -2,12 +2,14 @@ package eu.kanade.tachiyomi.source.local.filter
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
sealed class OrderBy(selection: Selection) : Filter.Sort(
|
||||
"Order by",
|
||||
arrayOf("Title", "Date"),
|
||||
selection,
|
||||
) {
|
||||
class Popular() : OrderBy(Selection(0, true))
|
||||
sealed class OrderBy(
|
||||
selection: Selection,
|
||||
) : Filter.Sort(
|
||||
"Order by",
|
||||
arrayOf("Title", "Date"),
|
||||
selection,
|
||||
) {
|
||||
class Popular : OrderBy(Selection(0, true))
|
||||
|
||||
class Latest() : OrderBy(Selection(1, false))
|
||||
class Latest : OrderBy(Selection(1, false))
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||
class LocalCoverManager(
|
||||
private val fileSystem: LocalSourceFileSystem,
|
||||
) {
|
||||
fun find(mangaUrl: String): File? {
|
||||
return fileSystem.getFilesInMangaDirectory(mangaUrl)
|
||||
fun find(mangaUrl: String): File? =
|
||||
fileSystem
|
||||
.getFilesInMangaDirectory(mangaUrl)
|
||||
// Get all file whose names start with 'cover'
|
||||
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
|
||||
// Get the first actual image
|
||||
.firstOrNull {
|
||||
ImageUtil.isImage(it.name) { it.inputStream() }
|
||||
}
|
||||
}
|
||||
|
||||
fun update(
|
||||
manga: SManga,
|
||||
|
||||
@@ -3,13 +3,21 @@ package eu.kanade.tachiyomi.source.local.io
|
||||
import java.io.File
|
||||
|
||||
sealed interface Format {
|
||||
data class Directory(val file: File) : Format
|
||||
data class Directory(
|
||||
val file: File,
|
||||
) : Format
|
||||
|
||||
data class Zip(val file: File) : Format
|
||||
data class Zip(
|
||||
val file: File,
|
||||
) : Format
|
||||
|
||||
data class Rar(val file: File) : Format
|
||||
data class Rar(
|
||||
val file: File,
|
||||
) : Format
|
||||
|
||||
data class Epub(val file: File) : Format
|
||||
data class Epub(
|
||||
val file: File,
|
||||
) : Format
|
||||
|
||||
class UnknownFormatException : Exception()
|
||||
|
||||
|
||||
@@ -6,27 +6,22 @@ import java.io.File
|
||||
class LocalSourceFileSystem(
|
||||
private val applicationDirs: ApplicationDirs,
|
||||
) {
|
||||
fun getBaseDirectories(): Sequence<File> {
|
||||
return sequenceOf(File(applicationDirs.localMangaRoot))
|
||||
}
|
||||
fun getBaseDirectories(): Sequence<File> = sequenceOf(File(applicationDirs.localMangaRoot))
|
||||
|
||||
fun getFilesInBaseDirectories(): Sequence<File> {
|
||||
return getBaseDirectories()
|
||||
fun getFilesInBaseDirectories(): Sequence<File> =
|
||||
getBaseDirectories()
|
||||
// Get all the files inside all baseDir
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
|
||||
fun getMangaDirectory(name: String): File? {
|
||||
return getFilesInBaseDirectories()
|
||||
fun getMangaDirectory(name: String): File? =
|
||||
getFilesInBaseDirectories()
|
||||
// Get the first mangaDir or null
|
||||
.firstOrNull { it.isDirectory && it.name == name }
|
||||
}
|
||||
|
||||
fun getFilesInMangaDirectory(name: String): Sequence<File> {
|
||||
return getFilesInBaseDirectories()
|
||||
fun getFilesInMangaDirectory(name: String): Sequence<File> =
|
||||
getFilesInBaseDirectories()
|
||||
// Filter out ones that are not related to the manga and is not a directory
|
||||
.filter { it.isDirectory && it.name == name }
|
||||
// Get all the files inside the filtered folders
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,20 @@ import java.io.File
|
||||
/**
|
||||
* Loader used to load a chapter from a .epub file.
|
||||
*/
|
||||
class EpubPageLoader(file: File) : PageLoader {
|
||||
class EpubPageLoader(
|
||||
file: File,
|
||||
) : PageLoader {
|
||||
private val epub = EpubFile(file)
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return epub.getImagesFromPages()
|
||||
override suspend fun getPages(): List<ReaderPage> =
|
||||
epub
|
||||
.getImagesFromPages()
|
||||
.mapIndexed { i, path ->
|
||||
val streamFn = { epub.getInputStream(epub.getEntry(path)!!) }
|
||||
ReaderPage(i).apply {
|
||||
stream = streamFn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
epub.close()
|
||||
|
||||
@@ -12,20 +12,21 @@ import java.io.PipedOutputStream
|
||||
/**
|
||||
* Loader used to load a chapter from a .rar or .cbr file.
|
||||
*/
|
||||
class RarPageLoader(file: File) : PageLoader {
|
||||
class RarPageLoader(
|
||||
file: File,
|
||||
) : PageLoader {
|
||||
private val rar = Archive(file)
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return rar.fileHeaders.asSequence()
|
||||
override suspend fun getPages(): List<ReaderPage> =
|
||||
rar.fileHeaders
|
||||
.asSequence()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.mapIndexed { i, header ->
|
||||
ReaderPage(i).apply {
|
||||
stream = { getStream(rar, header) }
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}.toList()
|
||||
|
||||
override fun recycle() {
|
||||
rar.close()
|
||||
|
||||
@@ -8,20 +8,21 @@ import java.io.File
|
||||
/**
|
||||
* Loader used to load a chapter from a .zip or .cbz file.
|
||||
*/
|
||||
class ZipPageLoader(file: File) : PageLoader {
|
||||
private val zip = ZipFile(file)
|
||||
class ZipPageLoader(
|
||||
file: File,
|
||||
) : PageLoader {
|
||||
private val zip = ZipFile.builder().setFile(file).get()
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
return zip.entries.asSequence()
|
||||
override suspend fun getPages(): List<ReaderPage> =
|
||||
zip.entries
|
||||
.asSequence()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { i, entry ->
|
||||
ReaderPage(i).apply {
|
||||
stream = { zip.getInputStream(entry) }
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}.toList()
|
||||
|
||||
override fun recycle() {
|
||||
zip.close()
|
||||
|
||||
@@ -17,8 +17,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
comicInfo.genre?.value,
|
||||
comicInfo.tags?.value,
|
||||
comicInfo.categories?.value,
|
||||
)
|
||||
.distinct()
|
||||
).distinct()
|
||||
.joinToString(", ") { it.trim() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { genre = it }
|
||||
@@ -29,8 +28,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
comicInfo.colorist?.value,
|
||||
comicInfo.letterer?.value,
|
||||
comicInfo.coverArtist?.value,
|
||||
)
|
||||
.flatMap { it.split(", ") }
|
||||
).flatMap { it.split(", ") }
|
||||
.distinct()
|
||||
.joinToString(", ") { it.trim() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
@@ -202,14 +200,12 @@ enum class ComicInfoPublishingStatus(
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun toComicInfoValue(value: Long): String {
|
||||
return entries.firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue
|
||||
fun toComicInfoValue(value: Long): String =
|
||||
entries.firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue
|
||||
?: UNKNOWN.comicInfoValue
|
||||
}
|
||||
|
||||
fun toSMangaValue(value: String?): Int {
|
||||
return entries.firstOrNull { it.comicInfoValue == value }?.sMangaModelValue
|
||||
fun toSMangaValue(value: String?): Int =
|
||||
entries.firstOrNull { it.comicInfoValue == value }?.sMangaModelValue
|
||||
?: UNKNOWN.sMangaModelValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,40 @@ package eu.kanade.tachiyomi.source.model
|
||||
|
||||
// The class is originally sealed, Tachidesk adds new subclasses for serialization
|
||||
// sealed class Filter<T>(val name: String, var state: T) {
|
||||
open class Filter<T>(val name: String, var state: T) {
|
||||
open class Header(name: String) : Filter<Any>(name, 0)
|
||||
open 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)
|
||||
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 Select<V>(
|
||||
name: String,
|
||||
val values: Array<V>,
|
||||
state: Int = 0,
|
||||
) : Filter<Int>(name, state) {
|
||||
val displayValues get() = values.map { it.toString() }
|
||||
}
|
||||
|
||||
abstract class Text(name: String, state: String = "") : Filter<String>(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 CheckBox(
|
||||
name: String,
|
||||
state: Boolean = false,
|
||||
) : Filter<Boolean>(name, state)
|
||||
|
||||
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(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
|
||||
@@ -29,11 +49,20 @@ open class Filter<T>(val name: String, var state: T) {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Group<V>(name: String, state: List<V>) : Filter<List<V>>(name, state)
|
||||
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)
|
||||
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 {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
|
||||
data class FilterList(
|
||||
val list: List<Filter<*>>,
|
||||
) : List<Filter<*>> by list {
|
||||
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
|
||||
data class MangasPage(
|
||||
val mangas: List<SManga>,
|
||||
val hasNextPage: Boolean,
|
||||
)
|
||||
|
||||
@@ -24,8 +24,6 @@ interface SChapter : Serializable {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(): SChapter {
|
||||
return SChapterImpl()
|
||||
}
|
||||
fun create(): SChapter = SChapterImpl()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,7 @@ interface SManga : Serializable {
|
||||
const val CANCELLED = 5
|
||||
const val ON_HIATUS = 6
|
||||
|
||||
fun create(): SManga {
|
||||
return SMangaImpl()
|
||||
}
|
||||
fun create(): SManga = SMangaImpl()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,9 +66,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
open val client: OkHttpClient
|
||||
get() = network.client
|
||||
|
||||
private fun generateId(): Long {
|
||||
return generateId("${name.lowercase()}/$lang/$versionId")
|
||||
}
|
||||
private fun generateId(): Long = generateId("${name.lowercase()}/$lang/$versionId")
|
||||
|
||||
/**
|
||||
* Generates a unique ID for the source based on the provided [name], [lang] and
|
||||
@@ -121,13 +119,13 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> =
|
||||
client
|
||||
.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
popularMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the popular manga given the page.
|
||||
@@ -156,20 +154,19 @@ abstract class HttpSource : CatalogueSource {
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> {
|
||||
return Observable.defer {
|
||||
try {
|
||||
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
// RxJava doesn't handle Errors, which tends to happen during global searches
|
||||
// if an old extension using non-existent classes is still around
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
.map { response ->
|
||||
): Observable<MangasPage> =
|
||||
Observable
|
||||
.defer {
|
||||
try {
|
||||
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
// RxJava doesn't handle Errors, which tends to happen during global searches
|
||||
// if an old extension using non-existent classes is still around
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for the search manga given the page.
|
||||
@@ -197,13 +194,13 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(latestUpdatesRequest(page))
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
|
||||
client
|
||||
.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
latestUpdatesParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for latest manga given the page.
|
||||
@@ -227,18 +224,16 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @return the updated manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
return fetchMangaDetails(manga).awaitSingle()
|
||||
}
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||
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
|
||||
@@ -246,9 +241,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
*/
|
||||
open fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
open fun mangaDetailsRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the details of a manga.
|
||||
@@ -275,9 +268,10 @@ abstract class HttpSource : CatalogueSource {
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return if (manga.status != SManga.LICENSED) {
|
||||
client.newCall(chapterListRequest(manga))
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
||||
if (manga.status != SManga.LICENSED) {
|
||||
client
|
||||
.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
chapterListParse(response)
|
||||
@@ -285,7 +279,6 @@ abstract class HttpSource : CatalogueSource {
|
||||
} else {
|
||||
Observable.error(LicensedMangaChaptersException())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for updating the chapter list. Override only if it's needed to override
|
||||
@@ -293,9 +286,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
*/
|
||||
protected open fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(baseUrl + manga.url, headers)
|
||||
}
|
||||
protected open fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
@@ -312,18 +303,16 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @return the pages for the chapter.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
return fetchPageList(chapter).awaitSingle()
|
||||
}
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
||||
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
|
||||
@@ -331,9 +320,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
protected open fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(baseUrl + chapter.url, headers)
|
||||
}
|
||||
protected open fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + chapter.url, headers)
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of pages.
|
||||
@@ -350,16 +337,14 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
open suspend fun getImageUrl(page: Page): String {
|
||||
return fetchImageUrl(page).awaitSingle()
|
||||
}
|
||||
open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle()
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
|
||||
open fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return client.newCall(imageUrlRequest(page))
|
||||
open fun fetchImageUrl(page: Page): Observable<String> =
|
||||
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
|
||||
@@ -367,9 +352,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
protected open fun imageUrlRequest(page: Page): Request {
|
||||
return GET(page.url, headers)
|
||||
}
|
||||
protected open fun imageUrlRequest(page: Page): Request = GET(page.url, headers)
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns the absolute url to the source image.
|
||||
@@ -385,10 +368,10 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
open suspend fun getImage(page: Page): Response {
|
||||
return client.newCachelessCallWithProgress(imageRequest(page), page)
|
||||
open suspend fun getImage(page: Page): Response =
|
||||
client
|
||||
.newCachelessCallWithProgress(imageRequest(page), page)
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request for getting the source image. Override only if it's needed to override
|
||||
@@ -396,9 +379,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
protected open fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers)
|
||||
}
|
||||
protected open fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headers)
|
||||
|
||||
/**
|
||||
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
|
||||
@@ -425,8 +406,8 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param orig the full url.
|
||||
*/
|
||||
private fun getUrlWithoutDomain(orig: String): String {
|
||||
return try {
|
||||
private fun getUrlWithoutDomain(orig: String): String =
|
||||
try {
|
||||
val uri = URI(orig.replace(" ", "%20"))
|
||||
var out = uri.path
|
||||
if (uri.query != null) {
|
||||
@@ -439,7 +420,6 @@ abstract class HttpSource : CatalogueSource {
|
||||
} catch (e: URISyntaxException) {
|
||||
orig
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url of the provided manga
|
||||
@@ -448,9 +428,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param manga the manga
|
||||
* @return url of the manga
|
||||
*/
|
||||
open fun getMangaUrl(manga: SManga): String {
|
||||
return mangaDetailsRequest(manga).url.toString()
|
||||
}
|
||||
open fun getMangaUrl(manga: SManga): String = mangaDetailsRequest(manga).url.toString()
|
||||
|
||||
/**
|
||||
* Returns the url of the provided chapter
|
||||
@@ -459,9 +437,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
* @param chapter the chapter
|
||||
* @return url of the chapter
|
||||
*/
|
||||
open fun getChapterUrl(chapter: SChapter): String {
|
||||
return pageListRequest(chapter).url.toString()
|
||||
}
|
||||
open fun getChapterUrl(chapter: SChapter): String = pageListRequest(chapter).url.toString()
|
||||
|
||||
/**
|
||||
* Called before inserting a new chapter into database. Use it if you need to override chapter
|
||||
|
||||
@@ -138,9 +138,7 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return mangaDetailsParse(response.asJsoup())
|
||||
}
|
||||
override fun mangaDetailsParse(response: Response): SManga = mangaDetailsParse(response.asJsoup())
|
||||
|
||||
/**
|
||||
* Returns the details of the manga from the given [document].
|
||||
@@ -176,9 +174,7 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
return pageListParse(response.asJsoup())
|
||||
}
|
||||
override fun pageListParse(response: Response): List<Page> = pageListParse(response.asJsoup())
|
||||
|
||||
/**
|
||||
* Returns a page list from the given document.
|
||||
@@ -192,9 +188,7 @@ abstract class ParsedHttpSource : HttpSource() {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
return imageUrlParse(response.asJsoup())
|
||||
}
|
||||
override fun imageUrlParse(response: Response): String = imageUrlParse(response.asJsoup())
|
||||
|
||||
/**
|
||||
* Returns the absolute url to the source image from the document.
|
||||
|
||||
@@ -8,25 +8,17 @@ import org.jsoup.nodes.Element
|
||||
fun Element.selectText(
|
||||
css: String,
|
||||
defaultValue: String? = null,
|
||||
): String? {
|
||||
return select(css).first()?.text() ?: defaultValue
|
||||
}
|
||||
): String? = select(css).first()?.text() ?: defaultValue
|
||||
|
||||
fun Element.selectInt(
|
||||
css: String,
|
||||
defaultValue: Int = 0,
|
||||
): Int {
|
||||
return select(css).first()?.text()?.toInt() ?: defaultValue
|
||||
}
|
||||
): Int = select(css).first()?.text()?.toInt() ?: defaultValue
|
||||
|
||||
fun Element.attrOrText(css: String): String {
|
||||
return if (css != "text") attr(css) else text()
|
||||
}
|
||||
fun Element.attrOrText(css: String): String = if (css != "text") attr(css) else text()
|
||||
|
||||
/**
|
||||
* Returns a Jsoup document for this response.
|
||||
* @param html the body of the response. Use only if the body was read before calling this method.
|
||||
*/
|
||||
fun Response.asJsoup(html: String? = null): Document {
|
||||
return Jsoup.parse(html ?: body.string(), request.url.toString())
|
||||
}
|
||||
fun Response.asJsoup(html: String? = null): Document = Jsoup.parse(html ?: body.string(), request.url.toString())
|
||||
|
||||
@@ -68,15 +68,14 @@ object ChapterRecognition {
|
||||
* @param match result of regex
|
||||
* @return chapter number if found else null
|
||||
*/
|
||||
private fun getChapterNumberFromMatch(match: MatchResult): Double {
|
||||
return match.let {
|
||||
private fun getChapterNumberFromMatch(match: MatchResult): Double =
|
||||
match.let {
|
||||
val initial = it.groups[1]?.value?.toDouble()!!
|
||||
val subChapterDecimal = it.groups[2]?.value
|
||||
val subChapterAlpha = it.groups[3]?.value
|
||||
val addition = checkForDecimal(subChapterDecimal, subChapterAlpha)
|
||||
initial.plus(addition)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for decimal in received strings
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package eu.kanade.tachiyomi.util.chapter
|
||||
|
||||
object ChapterSanitizer {
|
||||
fun String.sanitize(title: String): String {
|
||||
return trim()
|
||||
fun String.sanitize(title: String): String =
|
||||
trim()
|
||||
.removePrefix(title)
|
||||
.trim(*CHAPTER_TRIM_CHARS)
|
||||
}
|
||||
|
||||
private val CHAPTER_TRIM_CHARS =
|
||||
arrayOf(
|
||||
|
||||
@@ -5,29 +5,35 @@ import java.security.MessageDigest
|
||||
object Hash {
|
||||
private val chars =
|
||||
charArrayOf(
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
)
|
||||
|
||||
private val MD5 get() = MessageDigest.getInstance("MD5")
|
||||
|
||||
private val SHA256 get() = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
fun sha256(bytes: ByteArray): String {
|
||||
return encodeHex(SHA256.digest(bytes))
|
||||
}
|
||||
fun sha256(bytes: ByteArray): String = encodeHex(SHA256.digest(bytes))
|
||||
|
||||
fun sha256(string: String): String {
|
||||
return sha256(string.toByteArray())
|
||||
}
|
||||
fun sha256(string: String): String = sha256(string.toByteArray())
|
||||
|
||||
fun md5(bytes: ByteArray): String {
|
||||
return encodeHex(MD5.digest(bytes))
|
||||
}
|
||||
fun md5(bytes: ByteArray): String = encodeHex(MD5.digest(bytes))
|
||||
|
||||
fun md5(string: String): String {
|
||||
return md5(string.toByteArray())
|
||||
}
|
||||
fun md5(string: String): String = md5(string.toByteArray())
|
||||
|
||||
private fun encodeHex(data: ByteArray): String {
|
||||
val l = data.size
|
||||
|
||||
@@ -10,13 +10,12 @@ import kotlin.math.floor
|
||||
fun String.chop(
|
||||
count: Int,
|
||||
replacement: String = "…",
|
||||
): String {
|
||||
return if (length > count) {
|
||||
): String =
|
||||
if (length > count) {
|
||||
take(count - replacement.length) + replacement
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the given string to have at most [count] characters using [replacement] near the center.
|
||||
@@ -46,9 +45,7 @@ fun String.compareToCaseInsensitiveNaturalOrder(other: String): Int {
|
||||
/**
|
||||
* Returns the size of the string as the number of bytes.
|
||||
*/
|
||||
fun String.byteSize(): Int {
|
||||
return toByteArray(Charsets.UTF_8).size
|
||||
}
|
||||
fun String.byteSize(): Int = toByteArray(Charsets.UTF_8).size
|
||||
|
||||
/**
|
||||
* Returns a string containing the first [n] bytes from this string, or the entire string if this
|
||||
|
||||
@@ -11,11 +11,13 @@ import java.io.InputStream
|
||||
/**
|
||||
* Wrapper over ZipFile to load files in epub format.
|
||||
*/
|
||||
class EpubFile(file: File) : Closeable {
|
||||
class EpubFile(
|
||||
file: File,
|
||||
) : Closeable {
|
||||
/**
|
||||
* Zip file of this epub.
|
||||
*/
|
||||
private val zip = ZipFile(file)
|
||||
private val zip = ZipFile.builder().setFile(file).get()
|
||||
|
||||
/**
|
||||
* Path separator used by this epub.
|
||||
@@ -32,16 +34,12 @@ class EpubFile(file: File) : Closeable {
|
||||
/**
|
||||
* Returns an input stream for reading the contents of the specified zip file entry.
|
||||
*/
|
||||
fun getInputStream(entry: ZipArchiveEntry): InputStream {
|
||||
return zip.getInputStream(entry)
|
||||
}
|
||||
fun getInputStream(entry: ZipArchiveEntry): InputStream = zip.getInputStream(entry)
|
||||
|
||||
/**
|
||||
* Returns the zip file entry for the specified name, or null if not found.
|
||||
*/
|
||||
fun getEntry(name: String): ZipArchiveEntry? {
|
||||
return zip.getEntry(name)
|
||||
}
|
||||
fun getEntry(name: String): ZipArchiveEntry? = zip.getEntry(name)
|
||||
|
||||
/**
|
||||
* Returns the path of all the images found in the epub file.
|
||||
@@ -81,7 +79,8 @@ class EpubFile(file: File) : Closeable {
|
||||
*/
|
||||
fun getPagesFromDocument(document: Document): List<String> {
|
||||
val pages =
|
||||
document.select("manifest > item")
|
||||
document
|
||||
.select("manifest > item")
|
||||
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
|
||||
.associateBy { it.attr("id") }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user