mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-02 10:24:35 -05:00
Compare commits
14 Commits
v0.6.2
...
token-auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53c3ac5676 | ||
|
|
a26b8ecca0 | ||
|
|
5a32ccfa7a | ||
|
|
f51818b157 | ||
|
|
31a624db51 | ||
|
|
f045b18762 | ||
|
|
f5006cac7d | ||
|
|
152b193ad5 | ||
|
|
a27af0b642 | ||
|
|
44ffed3f7c | ||
|
|
fa035ad9be | ||
|
|
186ace4343 | ||
|
|
8fb1a0bb1f | ||
|
|
05513bf8b9 |
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,3 +1,45 @@
|
||||
# Server: v0.6.3 + WebUI: r942
|
||||
## TL;DR
|
||||
- Changes in Server
|
||||
- Support for array search filter changes list
|
||||
- Support for Tachiyomi extensions lib 1.3
|
||||
- Changes in WebUI
|
||||
- Better search filter support
|
||||
- Fluid manga grid
|
||||
- Library comfortable grid
|
||||
- Sources view layouts
|
||||
- Various other changes...
|
||||
|
||||
## Tachidesk-Server Changelog
|
||||
- (r1074) v0.6.2 (by @AriaMoradi)
|
||||
- (r1075) support array filter changes ([#304](https://github.com/Suwayomi/Tachidesk-Server/pull/304) by @AriaMoradi)
|
||||
- (r1076) fix filterlist bugs ([#306](https://github.com/Suwayomi/Tachidesk-Server/pull/306) by @AriaMoradi)
|
||||
- (r1077) Update README.md ([#305](https://github.com/Suwayomi/Tachidesk-Server/pull/305) by @mahor1221)
|
||||
- (r1078) fix meta update changing all keys ([#314](https://github.com/Suwayomi/Tachidesk-Server/pull/314) by @AriaMoradi)
|
||||
- (r1079) add support for tachiyomi extensions Lib 1.3 ([#316](https://github.com/Suwayomi/Tachidesk-Server/pull/316) by @AriaMoradi)
|
||||
- (r1080) Fix sources list of one source throws an exception ([#308](https://github.com/Suwayomi/Tachidesk-Server/pull/308) by @Syer10)
|
||||
- (r1081) Improve source handling, fix errors with uninitialized mangas in broken sources ([#319](https://github.com/Suwayomi/Tachidesk-Server/pull/319) by @Syer10)
|
||||
- (r1082) Add thumbnail support for stub sources ([#320](https://github.com/Suwayomi/Tachidesk-Server/pull/320) by @Syer10)
|
||||
- (r1083) update description for Tachidesk-Sorayomi ([#326](https://github.com/Suwayomi/Tachidesk-Server/pull/326) by @DattatreyaReddy)
|
||||
- (r1084) Add last bit of code needed for Extensions Lib 1.3 ([#330](https://github.com/Suwayomi/Tachidesk-Server/pull/330) by @Syer10)
|
||||
- (r1085) Add QuickJS, replaces Duktape for Extensions Lib 1.3 ([#331](https://github.com/Suwayomi/Tachidesk-Server/pull/331) by @Syer10)
|
||||
- (r1086) fix auth not actually blocking requests ([#333](https://github.com/Suwayomi/Tachidesk-Server/pull/333) by @AriaMoradi)
|
||||
|
||||
## Tachidesk-WebUI Changelog
|
||||
- (r930) Source filter scroll fix (array of filters on submit [#149](https://github.com/Suwayomi/Tachidesk-WebUI/pull/149) by @Robonau)
|
||||
- (r931) fix manga badges setting menu that turns the update/download badges on and off ([#150](https://github.com/Suwayomi/Tachidesk-WebUI/pull/150) by @Robonau)
|
||||
- (r932) move sorts to copy tachiyomi ([#151](https://github.com/Suwayomi/Tachidesk-WebUI/pull/151) by @Robonau)
|
||||
- (r933) add comfortable grid option ([#152](https://github.com/Suwayomi/Tachidesk-WebUI/pull/152) by @Robonau)
|
||||
- (r934) source layouts ([#153](https://github.com/Suwayomi/Tachidesk-WebUI/pull/153) by @Robonau)
|
||||
- (r935) List layout ([#154](https://github.com/Suwayomi/Tachidesk-WebUI/pull/154) by @Robonau)
|
||||
- (r936) in library badge to manga in sources ([#156](https://github.com/Suwayomi/Tachidesk-WebUI/pull/156) by @Robonau)
|
||||
- (r937) mass search ([#157](https://github.com/Suwayomi/Tachidesk-WebUI/pull/157) by @Robonau)
|
||||
- (r938) 18+ tag on source/extension cards ([#160](https://github.com/Suwayomi/Tachidesk-WebUI/pull/160) by @Robonau)
|
||||
- (r939) fix search source click ([#164](https://github.com/Suwayomi/Tachidesk-WebUI/pull/164) by @Robonau)
|
||||
- (r940) items per row setting ([#165](https://github.com/Suwayomi/Tachidesk-WebUI/pull/165) by @Robonau)
|
||||
- (r941) fix the grid width thing ([#169](https://github.com/Suwayomi/Tachidesk-WebUI/pull/169) by @Robonau)
|
||||
- (r942) unified library options ([#168](https://github.com/Suwayomi/Tachidesk-WebUI/pull/168) by @infix)
|
||||
|
||||
# Server: v0.6.2 + WebUI: r929
|
||||
## TL;DR
|
||||
- Changes in WebUI
|
||||
|
||||
25
README.md
25
README.md
@@ -49,7 +49,7 @@ Here's a list of known clients/user interfaces for Tachidesk-Server:
|
||||
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/ElectronJS front-end that Tachidesk-Server is traditionally shipped with. Usually gets new features faster.
|
||||
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Tachidesk-Server. Currently the most advanced.
|
||||
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development.
|
||||
- [Tachidesk-Flutter](https://github.com/Suwayomi/Tachidesk-Flutter): A Flutter front-end for Desktop(Linux, windows, etc.), in early stage of development.
|
||||
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android. UI and UX similar to Tachiyomi.
|
||||
##### Inctive/Abandoned Cients
|
||||
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stage of development.
|
||||
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client, in super early stage of development.
|
||||
@@ -80,33 +80,40 @@ If a bundle for your operating system or cpu architecture is not provided then r
|
||||
**Node:** Linux launcher scripts are named a bit differently but work the same.
|
||||
|
||||
### Windows
|
||||
Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
|
||||
Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-Server-preview/releases).
|
||||
|
||||
Unzip the downloaded file and double click on one of the launcher scripts.
|
||||
|
||||
### macOS
|
||||
Download the latest `macOS-x64`(older macOS systems) or `macOS-arm64`(Apple M1) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
|
||||
Download the latest `macOS-x64`(older macOS systems) or `macOS-arm64`(Apple M1) release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-Server-preview/releases).
|
||||
|
||||
Unzip the downloaded file and double click on one of the launcher scripts.
|
||||
|
||||
### GNU/Linux
|
||||
Download the latest `linux-x64`(x86_64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
|
||||
Download the latest `linux-x64`(x86_64) release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-Server-preview/releases).
|
||||
|
||||
`tar xvf` the downloaded file and double click on one of the launcher scripts or run them using the terminal.
|
||||
|
||||
## Other methods of getting Tachidesk
|
||||
### Arch Linux
|
||||
You can install Tachidesk from the AUR
|
||||
You can install Tachidesk from the AUR:
|
||||
```
|
||||
yay -S tachidesk
|
||||
```
|
||||
|
||||
### Ubuntu-based distributions
|
||||
More information can be found on the [PPA's page](https://launchpad.net/~suwayomi/+archive/ubuntu/tachidesk).
|
||||
### Debian/Ubuntu
|
||||
Download the latest deb package from the release section or Install from the MPR
|
||||
```
|
||||
sudo add-apt-repository ppa:suwayomi/tachidesk
|
||||
git clone https://mpr.makedeb.org/tachidesk-server.git
|
||||
cd tachidesk-server
|
||||
makedeb -si
|
||||
```
|
||||
|
||||
### Ubuntu
|
||||
```
|
||||
sudo add-apt-repository ppa:suwayomi/tachidesk-server
|
||||
sudo apt update
|
||||
sudo apt install tachidesk
|
||||
sudo apt install tachidesk-server
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
@@ -12,9 +12,9 @@ const val kotlinVersion = "1.6.10"
|
||||
const val MainClass = "suwayomi.tachidesk.MainKt"
|
||||
|
||||
// should be bumped with each stable release
|
||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.2"
|
||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.6.3"
|
||||
|
||||
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r929"
|
||||
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r942"
|
||||
|
||||
// counts commits on the master branch
|
||||
val tachideskRevision = runCatching {
|
||||
|
||||
@@ -44,6 +44,7 @@ dependencies {
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("org.jsoup:jsoup:1.14.3")
|
||||
implementation("app.cash.quickjs:quickjs-jvm:0.9.2")
|
||||
|
||||
// Sort
|
||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package eu.kanade.tachiyomi;
|
||||
|
||||
public class BuildConfig {
|
||||
/** should be something like 74 */
|
||||
public static final int VERSION_CODE = Integer.parseInt(suwayomi.tachidesk.server.BuildConfig.REVISION.substring(1));
|
||||
|
||||
/** should be something like "0.13.1" */
|
||||
public static final String VERSION_NAME = suwayomi.tachidesk.server.BuildConfig.VERSION.substring(1);
|
||||
}
|
||||
11
server/src/main/kotlin/eu/kanade/tachiyomi/AppInfo.kt
Normal file
11
server/src/main/kotlin/eu/kanade/tachiyomi/AppInfo.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
/**
|
||||
* Used by extensions.
|
||||
*
|
||||
* @since extension-lib 1.3
|
||||
*/
|
||||
object AppInfo {
|
||||
fun getVersionCode() = BuildConfig.VERSION_CODE
|
||||
fun getVersionName() = BuildConfig.VERSION_NAME
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package eu.kanade.tachiyomi;
|
||||
|
||||
public class BuildConfig {
|
||||
public static final int VERSION_CODE = -1;
|
||||
public static final String VERSION_NAME = "stub";
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.os.SystemClock
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* An OkHttp interceptor that handles rate limiting.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* permits = 5, period = 1, unit = seconds => 5 requests per second
|
||||
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
|
||||
*
|
||||
* @since extension-lib 1.3
|
||||
*
|
||||
* @param permits {Int} Number of requests allowed within a period of units.
|
||||
* @param period {Long} The limiting duration. Defaults to 1.
|
||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||
*/
|
||||
fun OkHttpClient.Builder.rateLimit(
|
||||
permits: Int,
|
||||
period: Long = 1,
|
||||
unit: TimeUnit = TimeUnit.SECONDS,
|
||||
) = addInterceptor(RateLimitInterceptor(permits, period, unit))
|
||||
|
||||
private class RateLimitInterceptor(
|
||||
private val permits: Int,
|
||||
period: Long,
|
||||
unit: TimeUnit,
|
||||
) : Interceptor {
|
||||
|
||||
private val requestQueue = ArrayList<Long>(permits)
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val waitTime = if (requestQueue.size < permits) {
|
||||
0
|
||||
} else {
|
||||
val oldestReq = requestQueue[0]
|
||||
val newestReq = requestQueue[permits - 1]
|
||||
|
||||
if (newestReq - oldestReq > rateLimitMillis) {
|
||||
0
|
||||
} else {
|
||||
oldestReq + rateLimitMillis - now // Remaining time
|
||||
}
|
||||
}
|
||||
|
||||
if (requestQueue.size == permits) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
if (waitTime > 0) {
|
||||
requestQueue.add(now + waitTime)
|
||||
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
|
||||
} else {
|
||||
requestQueue.add(now)
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import android.os.SystemClock
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* An OkHttp interceptor that handles given url host's rate limiting.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
|
||||
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
|
||||
*
|
||||
* @since extension-lib 1.3
|
||||
*
|
||||
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
||||
* @param permits {Int} Number of requests allowed within a period of units.
|
||||
* @param period {Long} The limiting duration. Defaults to 1.
|
||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||
*/
|
||||
fun OkHttpClient.Builder.rateLimitHost(
|
||||
httpUrl: HttpUrl,
|
||||
permits: Int,
|
||||
period: Long = 1,
|
||||
unit: TimeUnit = TimeUnit.SECONDS,
|
||||
) = addInterceptor(SpecificHostRateLimitInterceptor(httpUrl, permits, period, unit))
|
||||
|
||||
class SpecificHostRateLimitInterceptor(
|
||||
httpUrl: HttpUrl,
|
||||
private val permits: Int,
|
||||
period: Long,
|
||||
unit: TimeUnit,
|
||||
) : Interceptor {
|
||||
|
||||
private val requestQueue = ArrayList<Long>(permits)
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
private val host = httpUrl.host
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (chain.request().url.host != host) {
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
synchronized(requestQueue) {
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val waitTime = if (requestQueue.size < permits) {
|
||||
0
|
||||
} else {
|
||||
val oldestReq = requestQueue[0]
|
||||
val newestReq = requestQueue[permits - 1]
|
||||
|
||||
if (newestReq - oldestReq > rateLimitMillis) {
|
||||
0
|
||||
} else {
|
||||
oldestReq + rateLimitMillis - now // Remaining time
|
||||
}
|
||||
}
|
||||
|
||||
if (requestQueue.size == permits) {
|
||||
requestQueue.removeAt(0)
|
||||
}
|
||||
if (waitTime > 0) {
|
||||
requestQueue.add(now + waitTime)
|
||||
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
|
||||
} else {
|
||||
requestQueue.add(now)
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
/**
|
||||
* A source that explicitly doesn't require traffic considerations.
|
||||
*
|
||||
* This typically applies for self-hosted sources.
|
||||
*/
|
||||
interface UnmeteredSource
|
||||
@@ -55,6 +55,9 @@ interface SManga : Serializable {
|
||||
const val ONGOING = 1
|
||||
const val COMPLETED = 2
|
||||
const val LICENSED = 3
|
||||
const val PUBLISHING_FINISHED = 4
|
||||
const val CANCELLED = 5
|
||||
const val ON_HIATUS = 6
|
||||
|
||||
fun create(): SManga {
|
||||
return SMangaImpl()
|
||||
|
||||
@@ -45,7 +45,7 @@ object MangaAPI {
|
||||
post("{sourceId}/preferences", SourceController::setPreference)
|
||||
|
||||
get("{sourceId}/filters", SourceController::getFilters)
|
||||
post("{sourceId}/filters", SourceController::setFilter)
|
||||
post("{sourceId}/filters", SourceController::setFilters)
|
||||
|
||||
get("{sourceId}/search", SourceController::searchSingle)
|
||||
// get("all/search", SourceController::searchGlobal) // TODO
|
||||
|
||||
@@ -8,6 +8,11 @@ package suwayomi.tachidesk.manga.controller
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import io.javalin.http.Context
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import suwayomi.tachidesk.manga.impl.MangaList
|
||||
import suwayomi.tachidesk.manga.impl.Search
|
||||
import suwayomi.tachidesk.manga.impl.Search.FilterChange
|
||||
@@ -24,7 +29,7 @@ object SourceController {
|
||||
/** fetch source with id `sourceId` */
|
||||
fun retrieve(ctx: Context) {
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(Source.getSource(sourceId))
|
||||
ctx.json(Source.getSource(sourceId)!!)
|
||||
}
|
||||
|
||||
/** popular mangas from source with id `sourceId` */
|
||||
@@ -69,10 +74,16 @@ object SourceController {
|
||||
ctx.json(Search.getFilterList(sourceId, reset))
|
||||
}
|
||||
|
||||
/** set one filter of source with id `sourceId` */
|
||||
fun setFilter(ctx: Context) {
|
||||
private val json by DI.global.instance<Json>()
|
||||
|
||||
/** change filters of source with id `sourceId` */
|
||||
fun setFilters(ctx: Context) {
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val filterChange = ctx.bodyAsClass(FilterChange::class.java)
|
||||
val filterChange = try {
|
||||
json.decodeFromString<List<FilterChange>>(ctx.body())
|
||||
} catch (e: Exception) {
|
||||
listOf(json.decodeFromString<FilterChange>(ctx.body()))
|
||||
}
|
||||
|
||||
ctx.json(Search.setFilter(sourceId, filterChange))
|
||||
}
|
||||
|
||||
@@ -201,8 +201,10 @@ object Chapter {
|
||||
val chapterId =
|
||||
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
|
||||
.first()[ChapterTable.id].value
|
||||
val meta =
|
||||
transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } }.firstOrNull()
|
||||
val meta = transaction {
|
||||
ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
|
||||
}.firstOrNull()
|
||||
|
||||
if (meta == null) {
|
||||
ChapterMetaTable.insert {
|
||||
it[ChapterMetaTable.key] = key
|
||||
@@ -210,7 +212,7 @@ object Chapter {
|
||||
it[ChapterMetaTable.ref] = chapterId
|
||||
}
|
||||
} else {
|
||||
ChapterMetaTable.update {
|
||||
ChapterMetaTable.update({ (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }) {
|
||||
it[ChapterMetaTable.value] = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ package suwayomi.tachidesk.manga.impl
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
@@ -23,7 +25,9 @@ import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSource
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
@@ -34,6 +38,7 @@ import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -50,30 +55,10 @@ object Manga {
|
||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
|
||||
return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].toString(),
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
proxyThumbnailUrl(mangaId),
|
||||
|
||||
true,
|
||||
|
||||
mangaEntry[MangaTable.artist],
|
||||
mangaEntry[MangaTable.author],
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre].toGenreList(),
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
mangaEntry[MangaTable.inLibrary],
|
||||
mangaEntry[MangaTable.inLibraryAt],
|
||||
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||
getMangaMetaMap(mangaId),
|
||||
mangaEntry[MangaTable.realUrl],
|
||||
false
|
||||
)
|
||||
getMangaDataClass(mangaId, mangaEntry)
|
||||
} else { // initialize manga
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
||||
?: return getMangaDataClass(mangaId, mangaEntry)
|
||||
val sManga = SManga.create().apply {
|
||||
url = mangaEntry[MangaTable.url]
|
||||
title = mangaEntry[MangaTable.title]
|
||||
@@ -135,6 +120,29 @@ object Manga {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMangaDataClass(mangaId: Int, mangaEntry: ResultRow) = MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].toString(),
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
proxyThumbnailUrl(mangaId),
|
||||
|
||||
true,
|
||||
|
||||
mangaEntry[MangaTable.artist],
|
||||
mangaEntry[MangaTable.author],
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre].toGenreList(),
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
mangaEntry[MangaTable.inLibrary],
|
||||
mangaEntry[MangaTable.inLibraryAt],
|
||||
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||
getMangaMetaMap(mangaId),
|
||||
mangaEntry[MangaTable.realUrl],
|
||||
false
|
||||
)
|
||||
|
||||
fun getMangaMetaMap(manga: Int): Map<String, String> {
|
||||
return transaction {
|
||||
MangaMetaTable.select { MangaMetaTable.ref eq manga }
|
||||
@@ -146,8 +154,10 @@ object Manga {
|
||||
transaction {
|
||||
val manga = MangaTable.select { MangaTable.id eq mangaId }
|
||||
.first()[MangaTable.id]
|
||||
val meta =
|
||||
transaction { MangaMetaTable.select { (MangaMetaTable.ref eq manga) and (MangaMetaTable.key eq key) } }.firstOrNull()
|
||||
val meta = transaction {
|
||||
MangaMetaTable.select { (MangaMetaTable.ref eq manga) and (MangaMetaTable.key eq key) }
|
||||
}.firstOrNull()
|
||||
|
||||
if (meta == null) {
|
||||
MangaMetaTable.insert {
|
||||
it[MangaMetaTable.key] = key
|
||||
@@ -155,7 +165,7 @@ object Manga {
|
||||
it[MangaMetaTable.ref] = manga
|
||||
}
|
||||
} else {
|
||||
MangaMetaTable.update {
|
||||
MangaMetaTable.update({ (MangaMetaTable.ref eq manga) and (MangaMetaTable.key eq key) }) {
|
||||
it[MangaMetaTable.value] = value
|
||||
}
|
||||
}
|
||||
@@ -163,6 +173,7 @@ object Manga {
|
||||
}
|
||||
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
suspend fun getMangaThumbnail(mangaId: Int, useCache: Boolean): Pair<InputStream, String> {
|
||||
val saveDir = applicationDirs.thumbnailsRoot
|
||||
val fileName = mangaId.toString()
|
||||
@@ -176,10 +187,12 @@ object Manga {
|
||||
?: if (!mangaEntry[MangaTable.initialized]) {
|
||||
// initialize then try again
|
||||
getManga(mangaId)
|
||||
transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }[MangaTable.thumbnail_url]!!
|
||||
transaction {
|
||||
MangaTable.select { MangaTable.id eq mangaId }.first()
|
||||
}[MangaTable.thumbnail_url]!!
|
||||
} else {
|
||||
// source provides no thumbnail url for this manga
|
||||
throw NullPointerException()
|
||||
throw NullPointerException("No thumbnail found")
|
||||
}
|
||||
|
||||
source.client.newCall(
|
||||
@@ -199,6 +212,13 @@ object Manga {
|
||||
?: "image/jpeg"
|
||||
imageFile.inputStream() to contentType
|
||||
}
|
||||
is StubSource -> getImageResponse(saveDir, fileName, useCache) {
|
||||
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||
?: throw NullPointerException("No thumbnail found")
|
||||
network.client.newCall(
|
||||
GET(thumbnailUrl)
|
||||
).await()
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unknown source")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import io.javalin.plugin.json.JsonMapper
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
@@ -80,37 +81,42 @@ object Search {
|
||||
val filter: Filter<*>,
|
||||
)
|
||||
|
||||
fun setFilter(sourceId: Long, change: FilterChange) {
|
||||
fun setFilter(sourceId: Long, changes: List<FilterChange>) {
|
||||
val source = getCatalogueSourceOrStub(sourceId)
|
||||
val filterList = getFilterListOf(source, false)
|
||||
|
||||
when (val filter = filterList[change.position]) {
|
||||
is Filter.Header -> {
|
||||
// NOOP
|
||||
}
|
||||
is Filter.Separator -> {
|
||||
// NOOP
|
||||
}
|
||||
is Filter.Select<*> -> filter.state = change.state.toInt()
|
||||
is Filter.Text -> filter.state = change.state
|
||||
is Filter.CheckBox -> filter.state = change.state.toBooleanStrict()
|
||||
is Filter.TriState -> filter.state = change.state.toInt()
|
||||
is Filter.Group<*> -> {
|
||||
val groupChange = jsonMapper.fromJsonString(change.state, FilterChange::class.java)
|
||||
changes.forEach { change ->
|
||||
when (val filter = filterList[change.position]) {
|
||||
is Filter.Header -> {
|
||||
// NOOP
|
||||
}
|
||||
is Filter.Separator -> {
|
||||
// NOOP
|
||||
}
|
||||
is Filter.Select<*> -> filter.state = change.state.toInt()
|
||||
is Filter.Text -> filter.state = change.state
|
||||
is Filter.CheckBox -> filter.state = change.state.toBooleanStrict()
|
||||
is Filter.TriState -> filter.state = change.state.toInt()
|
||||
is Filter.Group<*> -> {
|
||||
val groupChange = jsonMapper.fromJsonString(change.state, FilterChange::class.java)
|
||||
|
||||
when (val groupFilter = filter.state[groupChange.position]) {
|
||||
is Filter.CheckBox -> groupFilter.state = groupChange.state.toBooleanStrict()
|
||||
is Filter.TriState -> groupFilter.state = groupChange.state.toInt()
|
||||
is Filter.Text -> groupFilter.state = groupChange.state
|
||||
is Filter.Select<*> -> groupFilter.state = groupChange.state.toInt()
|
||||
when (val groupFilter = filter.state[groupChange.position]) {
|
||||
is Filter.CheckBox -> groupFilter.state = groupChange.state.toBooleanStrict()
|
||||
is Filter.TriState -> groupFilter.state = groupChange.state.toInt()
|
||||
is Filter.Text -> groupFilter.state = groupChange.state
|
||||
is Filter.Select<*> -> groupFilter.state = groupChange.state.toInt()
|
||||
}
|
||||
}
|
||||
is Filter.Sort -> {
|
||||
filter.state = jsonMapper.fromJsonString(change.state, Filter.Sort.Selection::class.java)
|
||||
}
|
||||
}
|
||||
is Filter.Sort -> filter.state = jsonMapper.fromJsonString(change.state, Filter.Sort.Selection::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val jsonMapper by DI.global.instance<JsonMapper>()
|
||||
|
||||
@Serializable
|
||||
data class FilterChange(
|
||||
val position: Int,
|
||||
val state: String
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
||||
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
||||
@@ -36,8 +36,8 @@ object Source {
|
||||
|
||||
fun getSourceList(): List<SourceDataClass> {
|
||||
return transaction {
|
||||
SourceTable.selectAll().map {
|
||||
val catalogueSource = getCatalogueSourceOrStub(it[SourceTable.id].value)
|
||||
SourceTable.selectAll().mapNotNull {
|
||||
val catalogueSource = getCatalogueSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
|
||||
val sourceExtension = ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()
|
||||
|
||||
SourceDataClass(
|
||||
@@ -54,27 +54,23 @@ object Source {
|
||||
}
|
||||
}
|
||||
|
||||
fun getSource(sourceId: Long): SourceDataClass { // all the data extracted fresh form the source instance
|
||||
fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance
|
||||
return transaction {
|
||||
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
|
||||
val catalogueSource = source?.let { getCatalogueSource(sourceId) }
|
||||
val extension = source?.let {
|
||||
ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()
|
||||
}
|
||||
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull() ?: return@transaction null
|
||||
val catalogueSource = getCatalogueSourceOrNull(sourceId) ?: return@transaction null
|
||||
val extension = ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()
|
||||
|
||||
SourceDataClass(
|
||||
sourceId.toString(),
|
||||
source?.get(SourceTable.name),
|
||||
source?.get(SourceTable.lang),
|
||||
source?.let {
|
||||
getExtensionIconUrl(
|
||||
extension!![ExtensionTable.apkName]
|
||||
)
|
||||
},
|
||||
catalogueSource?.supportsLatest,
|
||||
catalogueSource?.let { it is ConfigurableSource },
|
||||
source?.get(SourceTable.isNsfw),
|
||||
catalogueSource?.toString()
|
||||
source[SourceTable.name],
|
||||
source[SourceTable.lang],
|
||||
getExtensionIconUrl(
|
||||
extension[ExtensionTable.apkName]
|
||||
),
|
||||
catalogueSource.supportsLatest,
|
||||
catalogueSource is ConfigurableSource,
|
||||
source[SourceTable.isNsfw],
|
||||
catalogueSource.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ object PackageTools {
|
||||
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||
const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||
const val LIB_VERSION_MIN = 1.2
|
||||
const val LIB_VERSION_MAX = 1.2
|
||||
const val LIB_VERSION_MAX = 1.3
|
||||
|
||||
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" // inorichi's key
|
||||
private const val unofficialSignature = "64feb21075ba97ebc9cc981243645b331595c111cef1b0d084236a0403b00581" // ArMor's key
|
||||
|
||||
@@ -26,7 +26,7 @@ object GetCatalogueSource {
|
||||
private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>()
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
|
||||
fun getCatalogueSource(sourceId: Long): CatalogueSource? {
|
||||
private fun getCatalogueSource(sourceId: Long): CatalogueSource? {
|
||||
val cachedResult: CatalogueSource? = sourceCache[sourceId]
|
||||
if (cachedResult != null) {
|
||||
return cachedResult
|
||||
@@ -56,8 +56,12 @@ object GetCatalogueSource {
|
||||
return sourceCache[sourceId]!!
|
||||
}
|
||||
|
||||
fun getCatalogueSourceOrNull(sourceId: Long): CatalogueSource? {
|
||||
return runCatching { getCatalogueSource(sourceId) }.getOrNull()
|
||||
}
|
||||
|
||||
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource {
|
||||
return getCatalogueSource(sourceId) ?: StubSource(sourceId)
|
||||
return getCatalogueSourceOrNull(sourceId) ?: StubSource(sourceId)
|
||||
}
|
||||
|
||||
fun registerCatalogueSource(sourcePair: Pair<Long, CatalogueSource>) {
|
||||
|
||||
@@ -11,19 +11,19 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
|
||||
data class SourceDataClass(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val lang: String?,
|
||||
val iconUrl: String?,
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val iconUrl: String,
|
||||
|
||||
/** The Source provides a latest listing */
|
||||
val supportsLatest: Boolean?,
|
||||
val supportsLatest: Boolean,
|
||||
|
||||
/** The Source implements [ConfigurableSource] */
|
||||
val isConfigurable: Boolean?,
|
||||
val isConfigurable: Boolean,
|
||||
|
||||
/** The Source class has a @Nsfw annotation */
|
||||
val isNsfw: Boolean?,
|
||||
val isNsfw: Boolean,
|
||||
|
||||
/** A nicer version of [name] */
|
||||
val displayName: String?,
|
||||
val displayName: String,
|
||||
)
|
||||
|
||||
@@ -66,7 +66,10 @@ enum class MangaStatus(val value: Int) {
|
||||
UNKNOWN(0),
|
||||
ONGOING(1),
|
||||
COMPLETED(2),
|
||||
LICENSED(3);
|
||||
LICENSED(3),
|
||||
PUBLISHING_FINISHED(4),
|
||||
CANCELLED(5),
|
||||
ON_HIATUS(6);
|
||||
|
||||
companion object {
|
||||
fun valueOf(value: Int): MangaStatus = values().find { it.value == value } ?: UNKNOWN
|
||||
|
||||
@@ -54,6 +54,22 @@ object JavalinSetup {
|
||||
}
|
||||
|
||||
config.enableCorsForAllOrigins()
|
||||
|
||||
config.accessManager { handler, ctx, _ ->
|
||||
fun basicAuthCredentialsValid(): Boolean {
|
||||
val (username, password) = ctx.basicAuthCredentials()
|
||||
return username == serverConfig.basicAuthUsername && password == serverConfig.basicAuthPassword
|
||||
}
|
||||
|
||||
if (serverConfig.authType != "none") {
|
||||
if (serverConfig.authType == "basicAuth" && !(ctx.basicAuthCredentialsExist() && basicAuthCredentialsValid())) {
|
||||
ctx.header("WWW-Authenticate", "Basic")
|
||||
ctx.status(401).json("Unauthorized")
|
||||
}
|
||||
} else {
|
||||
handler.handle(ctx)
|
||||
}
|
||||
}
|
||||
}.events { event ->
|
||||
event.serverStarted {
|
||||
if (serverConfig.initialOpenInBrowserEnabled) {
|
||||
@@ -83,18 +99,6 @@ object JavalinSetup {
|
||||
ctx.result(e.message ?: "Internal Server Error")
|
||||
}
|
||||
|
||||
app.before { ctx ->
|
||||
fun credentialsValid(): Boolean {
|
||||
val (username, password) = ctx.basicAuthCredentials()
|
||||
return username == serverConfig.basicAuthUsername && password == serverConfig.basicAuthPassword
|
||||
}
|
||||
|
||||
if (serverConfig.basicAuthEnabled && !(ctx.basicAuthCredentialsExist() && credentialsValid())) {
|
||||
ctx.header("WWW-Authenticate", "Basic")
|
||||
ctx.status(401).json("Unauthorized")
|
||||
}
|
||||
}
|
||||
|
||||
app.routes {
|
||||
path("api/v1/") {
|
||||
GlobalAPI.defineEndpoints()
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.typesafe.config.Config
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule
|
||||
import xyz.nulldev.ts.config.debugLogsEnabled
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
private const val MODULE_NAME = "server"
|
||||
class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPropertyOverridableConfigModule(config, moduleName) {
|
||||
@@ -34,6 +35,15 @@ class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPro
|
||||
val electronPath: String by overridableConfig
|
||||
|
||||
// Authentication
|
||||
val authType: String by object {
|
||||
operator fun <R> getValue(thisRef: R, property: KProperty<*>): String {
|
||||
val propValue: String = overridableConfig.getValue(thisRef, property)
|
||||
if (basicAuthEnabled) {
|
||||
return "basicAuth"
|
||||
}
|
||||
return propValue
|
||||
}
|
||||
}
|
||||
val basicAuthEnabled: Boolean by overridableConfig
|
||||
val basicAuthUsername: String by overridableConfig
|
||||
val basicAuthPassword: String by overridableConfig
|
||||
|
||||
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import io.javalin.plugin.json.JavalinJackson
|
||||
import io.javalin.plugin.json.JsonMapper
|
||||
import kotlinx.serialization.json.Json
|
||||
import mu.KotlinLogging
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.bind
|
||||
@@ -59,6 +60,7 @@ fun applicationSetup() {
|
||||
bind<ApplicationDirs>() with singleton { applicationDirs }
|
||||
bind<IUpdater>() with singleton { Updater() }
|
||||
bind<JsonMapper>() with singleton { JavalinJackson() }
|
||||
bind<Json>() with singleton { Json { ignoreUnknownKeys = true } }
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ server.webUIInterface = "browser" # "browser" or "electron"
|
||||
server.electronPath = ""
|
||||
|
||||
# Authentication
|
||||
server.basicAuthEnabled = false
|
||||
server.authType = "none" # "none" or "basicAuth" or "token"
|
||||
server.basicAuthEnabled = false # This is deprecated, use server.authType
|
||||
server.basicAuthUsername = ""
|
||||
server.basicAuthPassword = ""
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||
import suwayomi.tachidesk.server.applicationSetup
|
||||
import suwayomi.tachidesk.test.BASE_PATH
|
||||
@@ -72,7 +72,7 @@ class TestExtensionCompatibility {
|
||||
}
|
||||
}
|
||||
}
|
||||
sources = getSourceList().map { getCatalogueSource(it.id.toLong())!! as HttpSource }
|
||||
sources = getSourceList().map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
|
||||
}
|
||||
setLoggingEnabled(true)
|
||||
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
||||
|
||||
Reference in New Issue
Block a user