Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 771030b911 | |||
| 8d5744a2cf | |||
| a58aab9004 | |||
| 61bd32f7f0 | |||
| 63a444bd81 | |||
| 8f28c3b74b | |||
| d766206343 | |||
| 172f83f5b3 | |||
| 9e308025c3 | |||
| aaa6a16778 | |||
| 2a21da2210 |
@@ -49,14 +49,17 @@ This project has two components:
|
|||||||
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
||||||
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
|
||||||
|
|
||||||
## Credit
|
## Credit
|
||||||
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
||||||
|
|
||||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
||||||
|
|
||||||
Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this project.
|
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
You can obtain a copy of the license from http://www.apache.org/licenses/LICENSE-2.0
|
Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this project.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ plugins {
|
|||||||
id("org.jmailen.kotlinter") version "3.3.0"
|
id("org.jmailen.kotlinter") version "3.3.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
val TachideskVersion = "v0.1.3"
|
val TachideskVersion = "v0.1.5"
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -79,7 +79,8 @@ dependencies {
|
|||||||
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
|
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
|
||||||
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
||||||
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
||||||
implementation ("org.xerial:sqlite-jdbc:3.30.1")
|
implementation ("com.h2database:h2:1.4.199")
|
||||||
|
|
||||||
|
|
||||||
// AndroidCompat
|
// AndroidCompat
|
||||||
implementation(project(":AndroidCompat"))
|
implementation(project(":AndroidCompat"))
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
class MemoryCookieJar : CookieJar {
|
||||||
|
private val cache = mutableSetOf<WrappedCookie>()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
|
val cookiesToRemove = mutableSetOf<WrappedCookie>()
|
||||||
|
val validCookies = mutableSetOf<WrappedCookie>()
|
||||||
|
|
||||||
|
cache.forEach { cookie ->
|
||||||
|
if (cookie.isExpired()) {
|
||||||
|
cookiesToRemove.add(cookie)
|
||||||
|
} else if (cookie.matches(url)) {
|
||||||
|
validCookies.add(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.removeAll(cookiesToRemove)
|
||||||
|
|
||||||
|
return validCookies.toList().map(WrappedCookie::unwrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
|
val cookiesToAdd = cookies.map { WrappedCookie.wrap(it) }
|
||||||
|
|
||||||
|
cache.removeAll(cookiesToAdd)
|
||||||
|
cache.addAll(cookiesToAdd)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun clear() {
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WrappedCookie private constructor(val cookie: Cookie) {
|
||||||
|
fun unwrap() = cookie
|
||||||
|
|
||||||
|
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
|
||||||
|
|
||||||
|
fun matches(url: HttpUrl) = cookie.matches(url)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is WrappedCookie) return false
|
||||||
|
|
||||||
|
return other.cookie.name == cookie.name &&
|
||||||
|
other.cookie.domain == cookie.domain &&
|
||||||
|
other.cookie.path == cookie.path &&
|
||||||
|
other.cookie.secure == cookie.secure &&
|
||||||
|
other.cookie.hostOnly == cookie.hostOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var hash = 17
|
||||||
|
hash = 31 * hash + cookie.name.hashCode()
|
||||||
|
hash = 31 * hash + cookie.domain.hashCode()
|
||||||
|
hash = 31 * hash + cookie.path.hashCode()
|
||||||
|
hash = 31 * hash + if (cookie.secure) 0 else 1
|
||||||
|
hash = 31 * hash + if (cookie.hostOnly) 0 else 1
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun wrap(cookie: Cookie) = WrappedCookie(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,14 +19,15 @@ class NetworkHelper(context: Context) {
|
|||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
// val cookieManager = AndroidCookieJar()
|
val cookieManager = MemoryCookieJar()
|
||||||
|
|
||||||
val client by lazy {
|
val client by lazy {
|
||||||
val builder = OkHttpClient.Builder()
|
val builder = OkHttpClient.Builder()
|
||||||
// .cookieJar(cookieManager)
|
.cookieJar(cookieManager)
|
||||||
// .cache(Cache(cacheDir, cacheSize))
|
// .cache(Cache(cacheDir, cacheSize))
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(5, TimeUnit.MINUTES)
|
||||||
|
.writeTimeout(5, TimeUnit.MINUTES)
|
||||||
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
|
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
|
||||||
|
|
||||||
// .addInterceptor(UserAgentInterceptor())
|
// .addInterceptor(UserAgentInterceptor())
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun unsubscribe() {
|
override fun unsubscribe() {
|
||||||
call.cancel()
|
// call.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isUnsubscribed(): Boolean {
|
override fun isUnsubscribed(): Boolean {
|
||||||
@@ -80,17 +80,18 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
fun Call.asObservableSuccess(): Observable<Response> {
|
fun Call.asObservableSuccess(): Observable<Response> {
|
||||||
return asObservable().doOnNext { response ->
|
return asObservable()
|
||||||
if (!response.isSuccessful) {
|
.doOnNext { response ->
|
||||||
response.close()
|
if (!response.isSuccessful) {
|
||||||
throw Exception("HTTP error ${response.code}")
|
response.close()
|
||||||
|
throw Exception("HTTP error ${response.code}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
// fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
// val progressClient = newBuilder()
|
// val progressClient = newBuilder()
|
||||||
// .cache(null)
|
// .cache(nasObservableSuccessull)
|
||||||
// .addNetworkInterceptor { chain ->
|
// .addNetworkInterceptor { chain ->
|
||||||
// val originalResponse = chain.proceed(chain.request())
|
// val originalResponse = chain.proceed(chain.request())
|
||||||
// originalResponse.newBuilder()
|
// originalResponse.newBuilder()
|
||||||
@@ -104,7 +105,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
|||||||
|
|
||||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
val progressClient = newBuilder()
|
val progressClient = newBuilder()
|
||||||
.cache(null)
|
// .cache(null)
|
||||||
// .addNetworkInterceptor { chain ->
|
// .addNetworkInterceptor { chain ->
|
||||||
// val originalResponse = chain.proceed(chain.request())
|
// val originalResponse = chain.proceed(chain.request())
|
||||||
// originalResponse.newBuilder()
|
// originalResponse.newBuilder()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Network service.
|
* Network service.
|
||||||
*/
|
*/
|
||||||
protected val network: NetworkHelper by injectLazy()
|
val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
// /**
|
// /**
|
||||||
// * Preferences that a source may need.
|
// * Preferences that a source may need.
|
||||||
@@ -311,7 +311,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param page the chapter whose page list has to be fetched
|
* @param page the chapter whose page list has to be fetched
|
||||||
*/
|
*/
|
||||||
protected open fun imageRequest(page: Page): Request {
|
open fun imageRequest(page: Page): Request {
|
||||||
return GET(page.imageUrl!!, headers)
|
return GET(page.imageUrl!!, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ object Config {
|
|||||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||||
val extensionsRoot = "$dataRoot/extensions"
|
val extensionsRoot = "$dataRoot/extensions"
|
||||||
val thumbnailsRoot = "$dataRoot/thumbnails"
|
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||||
|
val mangaRoot = "$dataRoot/manga"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ package ir.armor.tachidesk
|
|||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import io.javalin.Javalin
|
import io.javalin.Javalin
|
||||||
import ir.armor.tachidesk.util.applicationSetup
|
import ir.armor.tachidesk.util.applicationSetup
|
||||||
|
import ir.armor.tachidesk.util.getChapter
|
||||||
import ir.armor.tachidesk.util.getChapterList
|
import ir.armor.tachidesk.util.getChapterList
|
||||||
import ir.armor.tachidesk.util.getExtensionList
|
import ir.armor.tachidesk.util.getExtensionList
|
||||||
import ir.armor.tachidesk.util.getManga
|
import ir.armor.tachidesk.util.getManga
|
||||||
import ir.armor.tachidesk.util.getMangaList
|
import ir.armor.tachidesk.util.getMangaList
|
||||||
import ir.armor.tachidesk.util.getMangaUpdateQueueThread
|
import ir.armor.tachidesk.util.getPageImage
|
||||||
import ir.armor.tachidesk.util.getPages
|
|
||||||
import ir.armor.tachidesk.util.getSource
|
import ir.armor.tachidesk.util.getSource
|
||||||
import ir.armor.tachidesk.util.getSourceList
|
import ir.armor.tachidesk.util.getSourceList
|
||||||
import ir.armor.tachidesk.util.getThumbnail
|
import ir.armor.tachidesk.util.getThumbnail
|
||||||
@@ -57,7 +57,7 @@ class Main {
|
|||||||
// start app
|
// start app
|
||||||
androidCompat.startApp(App())
|
androidCompat.startApp(App())
|
||||||
|
|
||||||
Thread(getMangaUpdateQueueThread).start()
|
// Thread(getMangaUpdateQueueThread).start()
|
||||||
|
|
||||||
val app = Javalin.create { config ->
|
val app = Javalin.create { config ->
|
||||||
try {
|
try {
|
||||||
@@ -119,6 +119,14 @@ class Main {
|
|||||||
ctx.json(getManga(mangaId))
|
ctx.json(getManga(mangaId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val result = getThumbnail(mangaId)
|
||||||
|
|
||||||
|
ctx.result(result.first)
|
||||||
|
ctx.header("content-type", result.second)
|
||||||
|
}
|
||||||
|
|
||||||
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(getChapterList(mangaId))
|
ctx.json(getChapterList(mangaId))
|
||||||
@@ -127,13 +135,14 @@ class Main {
|
|||||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
||||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(getPages(chapterId, mangaId))
|
ctx.json(getChapter(chapterId, mangaId))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
|
||||||
|
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
println("got request for: $mangaId")
|
val index = ctx.pathParam("index").toInt()
|
||||||
val result = getThumbnail(mangaId)
|
val result = getPageImage(mangaId, chapterId, index)
|
||||||
|
|
||||||
ctx.result(result.first)
|
ctx.result(result.first)
|
||||||
ctx.header("content-type", result.second)
|
ctx.header("content-type", result.second)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ir.armor.tachidesk.Config
|
|||||||
import ir.armor.tachidesk.database.table.ChapterTable
|
import ir.armor.tachidesk.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
import ir.armor.tachidesk.database.table.ExtensionsTable
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.PageTable
|
||||||
import ir.armor.tachidesk.database.table.SourceTable
|
import ir.armor.tachidesk.database.table.SourceTable
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
@@ -15,18 +16,21 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
|||||||
|
|
||||||
object DBMangaer {
|
object DBMangaer {
|
||||||
val db by lazy {
|
val db by lazy {
|
||||||
Database.connect("jdbc:sqlite:${Config.dataRoot}/database.db", "org.sqlite.JDBC")
|
Database.connect("jdbc:h2:${Config.dataRoot}/database", "org.h2.Driver")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun makeDataBaseTables() {
|
fun makeDataBaseTables() {
|
||||||
// mention db object to connect
|
// mention db object to connect
|
||||||
DBMangaer.db
|
DBMangaer.db
|
||||||
|
// val db = DBMangaer.db
|
||||||
|
// db.useNestedTransactions = true
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
SchemaUtils.create(ExtensionsTable)
|
SchemaUtils.create(ExtensionsTable)
|
||||||
SchemaUtils.create(SourceTable)
|
SchemaUtils.create(SourceTable)
|
||||||
SchemaUtils.create(MangaTable)
|
SchemaUtils.create(MangaTable)
|
||||||
SchemaUtils.create(ChapterTable)
|
SchemaUtils.create(ChapterTable)
|
||||||
|
SchemaUtils.create(PageTable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ data class ChapterDataClass(
|
|||||||
val chapter_number: Float,
|
val chapter_number: Float,
|
||||||
val scanlator: String?,
|
val scanlator: String?,
|
||||||
val mangaId: Int,
|
val mangaId: Int,
|
||||||
|
val pageCount: Int? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
object ChapterTable : IntIdTable() {
|
object ChapterTable : IntIdTable() {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
object ExtensionsTable : IntIdTable() {
|
object ExtensionsTable : IntIdTable() {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
|
object PageTable : IntIdTable() {
|
||||||
|
val index = integer("index")
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val imageUrl = varchar("imageUrl", 2048).nullable()
|
||||||
|
|
||||||
|
val chapter = reference("chapter", ChapterTable)
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.id.IdTable
|
import org.jetbrains.exposed.dao.id.IdTable
|
||||||
|
|
||||||
object SourceTable : IdTable<Long>() {
|
object SourceTable : IdTable<Long>() {
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ package ir.armor.tachidesk.util
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import ir.armor.tachidesk.database.dataclass.ChapterDataClass
|
import ir.armor.tachidesk.database.dataclass.ChapterDataClass
|
||||||
import ir.armor.tachidesk.database.dataclass.PageDataClass
|
|
||||||
import ir.armor.tachidesk.database.table.ChapterTable
|
import ir.armor.tachidesk.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.PageTable
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
@@ -57,14 +57,14 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPages(chapterId: Int, mangaId: Int): Pair<ChapterDataClass, List<PageDataClass>> {
|
fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
||||||
return transaction {
|
return transaction {
|
||||||
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
||||||
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
||||||
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||||
|
|
||||||
val pagesList = source.fetchPageList(
|
val pageList = source.fetchPageList(
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
url = chapterEntry[ChapterTable.url]
|
url = chapterEntry[ChapterTable.url]
|
||||||
name = chapterEntry[ChapterTable.name]
|
name = chapterEntry[ChapterTable.name]
|
||||||
@@ -78,22 +78,24 @@ fun getPages(chapterId: Int, mangaId: Int): Pair<ChapterDataClass, List<PageData
|
|||||||
chapterEntry[ChapterTable.date_upload],
|
chapterEntry[ChapterTable.date_upload],
|
||||||
chapterEntry[ChapterTable.chapter_number],
|
chapterEntry[ChapterTable.chapter_number],
|
||||||
chapterEntry[ChapterTable.scanlator],
|
chapterEntry[ChapterTable.scanlator],
|
||||||
mangaId
|
mangaId,
|
||||||
|
pageList.count()
|
||||||
)
|
)
|
||||||
|
|
||||||
val pages = pagesList.map {
|
pageList.forEach { page ->
|
||||||
PageDataClass(
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||||
it.index,
|
if (pageEntry == null) {
|
||||||
getTrueImageUrl(it, source)
|
transaction {
|
||||||
)
|
PageTable.insert {
|
||||||
|
it[index] = page.index
|
||||||
|
it[url] = page.url
|
||||||
|
it[imageUrl] = page.imageUrl
|
||||||
|
it[this.chapter] = chapterId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return@transaction Pair(chapter, pages)
|
return@transaction chapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
|
||||||
return if (page.imageUrl == null) {
|
|
||||||
source.fetchImageUrl(page).toBlocking().first()!!
|
|
||||||
} else page.imageUrl!!
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
package ir.armor.tachidesk.util
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
||||||
fun writeStream(fileStream: InputStream, path: String) {
|
fun writeStream(fileStream: InputStream, path: String) {
|
||||||
Files.newOutputStream(Paths.get(path)).use { os ->
|
Files.newOutputStream(Paths.get(path)).use { os ->
|
||||||
val buffer = ByteArray(1024)
|
val buffer = ByteArray(128 * 1024)
|
||||||
var len: Int
|
var len: Int
|
||||||
while (fileStream.read(buffer).also { len = it } > 0) {
|
while (fileStream.read(buffer).also { len = it } > 0) {
|
||||||
os.write(buffer, 0, len)
|
os.write(buffer, 0, len)
|
||||||
@@ -28,3 +36,17 @@ fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the given source to an output stream and closes both resources.
|
||||||
|
*
|
||||||
|
* @param stream the stream where the source is copied.
|
||||||
|
*/
|
||||||
|
fun BufferedSource.saveTo(stream: OutputStream) {
|
||||||
|
use { input ->
|
||||||
|
stream.sink().buffer().use {
|
||||||
|
it.writeAll(input)
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,158 +14,109 @@ import org.jetbrains.exposed.sql.select
|
|||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.concurrent.ArrayBlockingQueue
|
|
||||||
|
|
||||||
val getMangaUpdateQueue = ArrayBlockingQueue<Pair<Int, SManga?>>(1000)
|
|
||||||
@Volatile
|
|
||||||
var getMangaCount = 0
|
|
||||||
|
|
||||||
val getMangaUpdateQueueThread = Runnable {
|
|
||||||
while (true) {
|
|
||||||
val p = getMangaUpdateQueue.take()
|
|
||||||
println("took ${p.first}")
|
|
||||||
while (getMangaCount > 0) {
|
|
||||||
println("count is $getMangaCount")
|
|
||||||
Thread.sleep(1000)
|
|
||||||
}
|
|
||||||
val mangaId = p.first
|
|
||||||
println("working on $mangaId")
|
|
||||||
val fetchedManga = p.second!!
|
|
||||||
try {
|
|
||||||
transaction {
|
|
||||||
println("transaction start $mangaId")
|
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
|
||||||
|
|
||||||
it[MangaTable.initialized] = true
|
|
||||||
|
|
||||||
it[MangaTable.artist] = fetchedManga.artist
|
|
||||||
it[MangaTable.author] = fetchedManga.author
|
|
||||||
it[MangaTable.description] = fetchedManga.description
|
|
||||||
it[MangaTable.genre] = fetchedManga.genre
|
|
||||||
it[MangaTable.status] = fetchedManga.status
|
|
||||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
|
||||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
|
||||||
}
|
|
||||||
println("transaction end $mangaId")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||||
synchronized(getMangaCount) {
|
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
getMangaCount++
|
|
||||||
}
|
return if (mangaEntry[MangaTable.initialized]) {
|
||||||
return try {
|
MangaDataClass(
|
||||||
|
mangaId,
|
||||||
|
mangaEntry[MangaTable.sourceReference].value,
|
||||||
|
|
||||||
|
mangaEntry[MangaTable.url],
|
||||||
|
mangaEntry[MangaTable.title],
|
||||||
|
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
||||||
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
mangaEntry[MangaTable.artist],
|
||||||
|
mangaEntry[MangaTable.author],
|
||||||
|
mangaEntry[MangaTable.description],
|
||||||
|
mangaEntry[MangaTable.genre],
|
||||||
|
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||||
|
)
|
||||||
|
} else { // initialize manga
|
||||||
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||||
|
val fetchedManga = source.fetchMangaDetails(
|
||||||
|
SManga.create().apply {
|
||||||
|
url = mangaEntry[MangaTable.url]
|
||||||
|
title = mangaEntry[MangaTable.title]
|
||||||
|
}
|
||||||
|
).toBlocking().first()
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
|
||||||
return@transaction if (mangaEntry[MangaTable.initialized]) {
|
it[MangaTable.initialized] = true
|
||||||
println("${mangaEntry[MangaTable.title]} is initialized")
|
|
||||||
println("${mangaEntry[MangaTable.thumbnail_url]}")
|
|
||||||
MangaDataClass(
|
|
||||||
mangaId,
|
|
||||||
mangaEntry[MangaTable.sourceReference].value,
|
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
it[MangaTable.artist] = fetchedManga.artist
|
||||||
mangaEntry[MangaTable.title],
|
it[MangaTable.author] = fetchedManga.author
|
||||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
it[MangaTable.description] = fetchedManga.description
|
||||||
|
it[MangaTable.genre] = fetchedManga.genre
|
||||||
true,
|
it[MangaTable.status] = fetchedManga.status
|
||||||
|
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
||||||
mangaEntry[MangaTable.artist],
|
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||||
mangaEntry[MangaTable.author],
|
|
||||||
mangaEntry[MangaTable.description],
|
|
||||||
mangaEntry[MangaTable.genre],
|
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
|
||||||
)
|
|
||||||
} else { // initialize manga
|
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
|
||||||
val fetchedManga = source.fetchMangaDetails(
|
|
||||||
SManga.create().apply {
|
|
||||||
url = mangaEntry[MangaTable.url]
|
|
||||||
title = mangaEntry[MangaTable.title]
|
|
||||||
}
|
|
||||||
).toBlocking().first()
|
|
||||||
|
|
||||||
// update database
|
|
||||||
// TODO: sqlite gets fucked here
|
|
||||||
println("putting $mangaId")
|
|
||||||
getMangaUpdateQueue.put(Pair(mangaId, fetchedManga))
|
|
||||||
|
|
||||||
// mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
|
||||||
val newThumbnail =
|
|
||||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty()) {
|
|
||||||
fetchedManga.thumbnail_url
|
|
||||||
} else mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
|
|
||||||
MangaDataClass(
|
|
||||||
mangaId,
|
|
||||||
mangaEntry[MangaTable.sourceReference].value,
|
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
|
||||||
mangaEntry[MangaTable.title],
|
|
||||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
|
|
||||||
|
|
||||||
true,
|
|
||||||
|
|
||||||
fetchedManga.artist,
|
|
||||||
fetchedManga.author,
|
|
||||||
fetchedManga.description,
|
|
||||||
fetchedManga.genre,
|
|
||||||
MangaStatus.valueOf(fetchedManga.status).name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
synchronized(getMangaCount) {
|
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
getMangaCount--
|
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
|
||||||
}
|
|
||||||
|
MangaDataClass(
|
||||||
|
mangaId,
|
||||||
|
mangaEntry[MangaTable.sourceReference].value,
|
||||||
|
|
||||||
|
mangaEntry[MangaTable.url],
|
||||||
|
mangaEntry[MangaTable.title],
|
||||||
|
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
|
||||||
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
fetchedManga.artist,
|
||||||
|
fetchedManga.author,
|
||||||
|
fetchedManga.description,
|
||||||
|
fetchedManga.genre,
|
||||||
|
MangaStatus.valueOf(fetchedManga.status).name,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||||
return transaction {
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
var filePath = Config.thumbnailsRoot + "/$mangaId"
|
var filePath = "${Config.thumbnailsRoot}/$mangaId."
|
||||||
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
|
||||||
|
|
||||||
val potentialCache = findFileNameStartingWith(Config.thumbnailsRoot, mangaId.toString())
|
val potentialCache = findFileNameStartingWith(Config.thumbnailsRoot, mangaId.toString())
|
||||||
if (potentialCache != null) {
|
if (potentialCache != null) {
|
||||||
println("using cached thumbnail file")
|
println("using cached thumbnail file")
|
||||||
return@transaction Pair(
|
return Pair(
|
||||||
pathToInputStream(potentialCache),
|
pathToInputStream(potentialCache),
|
||||||
"image/${potentialCache.substringAfter("$mangaId.")}"
|
"image/${potentialCache.substringAfter(filePath)}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
||||||
println("getting source for $mangaId")
|
println("getting source for $mangaId")
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||||
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
||||||
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
||||||
}
|
}
|
||||||
println(thumbnailUrl)
|
println(thumbnailUrl)
|
||||||
val response = source.client.newCall(
|
val response = source.client.newCall(
|
||||||
GET(thumbnailUrl, source.headers)
|
GET(thumbnailUrl, source.headers)
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
println(response.code)
|
if (response.code == 200) {
|
||||||
|
val contentType = response.headers["content-type"]!!
|
||||||
|
filePath += contentType.substringAfter("image/")
|
||||||
|
|
||||||
if (response.code == 200) {
|
writeStream(response.body!!.byteStream(), filePath)
|
||||||
val contentType = response.headers["content-type"]!!
|
|
||||||
filePath += "." + contentType.substringAfter("image/")
|
|
||||||
|
|
||||||
writeStream(response.body!!.byteStream(), filePath)
|
return Pair(
|
||||||
|
pathToInputStream(filePath),
|
||||||
return@transaction Pair(
|
contentType
|
||||||
pathToInputStream(filePath),
|
)
|
||||||
contentType
|
} else {
|
||||||
)
|
throw Exception("request error! ${response.code}")
|
||||||
} else {
|
|
||||||
throw Exception("request error! ${response.code}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
class MangaDexHelper(private val mangaDexSource: HttpSource) {
|
||||||
|
|
||||||
|
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
|
||||||
|
|
||||||
|
private fun clientBuilder(
|
||||||
|
r18Toggle: Int,
|
||||||
|
okHttpClient: OkHttpClient = mangaDexSource.network.client
|
||||||
|
): OkHttpClient = okHttpClient.newBuilder()
|
||||||
|
.addNetworkInterceptor { chain ->
|
||||||
|
val originalCookies = chain.request().header("Cookie") ?: ""
|
||||||
|
val newReq = chain
|
||||||
|
.request()
|
||||||
|
.newBuilder()
|
||||||
|
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
|
||||||
|
.build()
|
||||||
|
chain.proceed(newReq)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
private fun cookiesHeader(r18Toggle: Int): String {
|
||||||
|
val cookies = mutableMapOf<String, String>()
|
||||||
|
cookies["mangadex_h_toggle"] = r18Toggle.toString()
|
||||||
|
return buildCookies(cookies)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildCookies(cookies: Map<String, String>) =
|
||||||
|
cookies.entries.joinToString(separator = "; ", postfix = ";") {
|
||||||
|
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun isLogged(): Boolean {
|
||||||
|
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
|
||||||
|
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
|
||||||
|
val formBody = FormBody.Builder()
|
||||||
|
.add("login_username", username)
|
||||||
|
.add("login_password", password)
|
||||||
|
.add("no_js", "1")
|
||||||
|
.add("remember_me", "1")
|
||||||
|
|
||||||
|
twoFactorCode.let {
|
||||||
|
formBody.add("two_factor", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = clientBuilder().newCall(
|
||||||
|
POST(
|
||||||
|
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
|
||||||
|
mangaDexSource.headers,
|
||||||
|
formBody.build()
|
||||||
|
)
|
||||||
|
).execute()
|
||||||
|
return response.body!!.string().isEmpty()
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// fun logout(): Boolean {
|
||||||
|
// return withContext(Dispatchers.IO) {
|
||||||
|
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
|
||||||
|
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
|
||||||
|
// val listOfDexCookies = network.cookieManager.get(httpUrl)
|
||||||
|
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
|
||||||
|
// val token = cookie?.value
|
||||||
|
// if (token.isNullOrEmpty()) {
|
||||||
|
// return@withContext true
|
||||||
|
// }
|
||||||
|
// val result = clientBuilder().newCall(
|
||||||
|
// POSTWithCookie(
|
||||||
|
// "$baseUrl/ajax/actions.ajax.php?function=logout",
|
||||||
|
// REMEMBER_ME,
|
||||||
|
// token,
|
||||||
|
// headers
|
||||||
|
// )
|
||||||
|
// ).execute()
|
||||||
|
// val resultStr = result.body!!.string()
|
||||||
|
// if (resultStr.contains("success", true)) {
|
||||||
|
// network.cookieManager.remove(httpUrl)
|
||||||
|
// return@withContext true
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import ir.armor.tachidesk.Config
|
||||||
|
import ir.armor.tachidesk.database.table.ChapterTable
|
||||||
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.PageTable
|
||||||
|
import ir.armor.tachidesk.database.table.SourceTable
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||||
|
if (page.imageUrl == null) {
|
||||||
|
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!!
|
||||||
|
}
|
||||||
|
return page.imageUrl!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||||
|
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||||
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
|
||||||
|
|
||||||
|
val tachiPage = Page(
|
||||||
|
pageEntry[PageTable.index],
|
||||||
|
pageEntry[PageTable.url],
|
||||||
|
pageEntry[PageTable.imageUrl]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pageEntry[PageTable.imageUrl] == null) {
|
||||||
|
transaction {
|
||||||
|
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
|
||||||
|
it[imageUrl] = getTrueImageUrl(tachiPage, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val saveDir = getChapterDir(mangaId, chapterId)
|
||||||
|
File(saveDir).mkdirs()
|
||||||
|
var filePath = "$saveDir/$index."
|
||||||
|
|
||||||
|
val potentialCache = findFileNameStartingWith(saveDir, index.toString())
|
||||||
|
if (potentialCache != null) {
|
||||||
|
println("using cached page file for $index")
|
||||||
|
return Pair(
|
||||||
|
pathToInputStream(potentialCache),
|
||||||
|
"image/${potentialCache.substringAfter("$filePath")}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = source.fetchImage(tachiPage).toBlocking().first()
|
||||||
|
|
||||||
|
if (response.code == 200) {
|
||||||
|
val contentType = response.headers["content-type"]!!
|
||||||
|
filePath += contentType.substringAfter("image/")
|
||||||
|
|
||||||
|
Files.newOutputStream(Paths.get(filePath)).use { os ->
|
||||||
|
|
||||||
|
response.body!!.source().saveTo(os)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeStream(response.body!!.source(), filePath)
|
||||||
|
|
||||||
|
return Pair(
|
||||||
|
pathToInputStream(filePath),
|
||||||
|
contentType
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw Exception("request error! ${response.code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
|
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
||||||
|
val source = getHttpSource(sourceId)
|
||||||
|
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
||||||
|
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||||
|
|
||||||
|
val chapterDir = when {
|
||||||
|
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||||
|
else -> chapterEntry[ChapterTable.name]
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaTitle = mangaEntry[MangaTable.title]
|
||||||
|
val sourceName = source.toString()
|
||||||
|
|
||||||
|
val mangaDir = "${Config.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
||||||
|
// make sure dirs exist
|
||||||
|
File(mangaDir).mkdirs()
|
||||||
|
return mangaDir
|
||||||
|
}
|
||||||
@@ -14,44 +14,36 @@ const style = {
|
|||||||
backgroundColor: '#343a40',
|
backgroundColor: '#343a40',
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
interface IPage {
|
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
||||||
index: number
|
|
||||||
imageUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IData {
|
|
||||||
first: IChapter
|
|
||||||
second: IPage[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Reader() {
|
export default function Reader() {
|
||||||
const { setTitle } = useContext(NavBarTitle);
|
const { setTitle } = useContext(NavBarTitle);
|
||||||
|
|
||||||
const [pages, setPages] = useState<IPage[]>([]);
|
const [pageCount, setPageCount] = useState<number>(-1);
|
||||||
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
|
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data:IData) => {
|
.then((data:IChapter) => {
|
||||||
setTitle(data.first.name);
|
setTitle(data.name);
|
||||||
setPages(data.second);
|
setPageCount(data.pageCount);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
pages.sort((a, b) => (a.index - b.index));
|
if (pageCount === -1) {
|
||||||
|
return (
|
||||||
let mapped;
|
<div style={style}>
|
||||||
if (pages.length === 0) {
|
<h3>wait</h3>
|
||||||
mapped = <h3>wait</h3>;
|
|
||||||
} else {
|
|
||||||
mapped = pages.map(({ imageUrl }) => (
|
|
||||||
<div style={{ margin: '0 auto' }}>
|
|
||||||
<img src={imageUrl} alt="f" style={{ maxWidth: '100%' }} />
|
|
||||||
</div>
|
</div>
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapped = range(pageCount).map((index) => (
|
||||||
|
<div style={{ margin: '0 auto' }}>
|
||||||
|
<img src={`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="f" style={{ maxWidth: '100%' }} />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
{mapped}
|
{mapped}
|
||||||
|
|||||||
Vendored
+1
@@ -34,4 +34,5 @@ interface IChapter {
|
|||||||
chapter_number: number
|
chapter_number: number
|
||||||
scanlator: String
|
scanlator: String
|
||||||
mangaId: number
|
mangaId: number
|
||||||
|
pageCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user