Compare commits

..

25 Commits
v0.7.0 ... jep

Author SHA1 Message Date
Aria Moradi
e4a3dad4e8 trying to bundle the changes 2023-06-05 01:45:22 +03:30
Aria Moradi
6934d344f0 better paths 2023-04-25 10:57:54 +03:30
Aria Moradi
62ee91ff0e fix python path 2023-04-25 10:47:21 +03:30
Aria Moradi
f37d7c841b become jep-less 2023-04-25 01:36:37 +03:30
Aria Moradi
86aaf28046 better initialization 2023-04-24 19:16:21 +03:30
Aria Moradi
34f658e5f2 remove DriverJar 2023-04-24 18:55:18 +03:30
Aria Moradi
597022f24a remove unused line 2023-04-24 18:29:10 +03:30
Aria Moradi
0458a80c17 remove unused line 2023-04-24 18:28:25 +03:30
Aria Moradi
cbefe1125d migrate webview solution to Jep 2023-04-24 18:26:04 +03:30
schroda
cde5dc5bfa Update "dex2jar" to v60 (#538) 2023-04-10 11:44:22 +03:30
Aria Moradi
f2a650ba02 fix typo 2023-03-27 21:27:10 +03:30
Aria Moradi
871c28b1ea cleanup notes 2023-03-27 21:26:37 +03:30
schroda
d3aa32147a Add logic to only update specific categories (#520)
Makes it possible to only update specific categories.

In case a manga is in an excluded category it will be excluded even if it is also in an included category.
2023-03-27 20:15:46 +03:30
schroda
9a50f2e408 Notify clients even if no manga gets updated (#531)
In case no manga gets updated and no update job was running before, the client would never receive an info about its update request
2023-03-27 17:11:19 +03:30
schroda
dcde4947e8 Emit update to clients after adding all mangas to the queue (#521)
Emitting updates before all the mangas were added to the queue could lead to e.g. wrong progress calculation.
2023-03-25 22:08:42 +03:30
schroda
5b61bdc3a8 add size field to Category data class (#519)
Makes it possible to display the size of a category to the user
2023-03-25 22:07:50 +03:30
schroda
ec1d65f4c3 update library grouped by source (#511)
* Update mangas grouped by source

* Limit parallel update requests
2023-03-10 11:03:09 +03:30
akabhirav
a0081dec07 fix manga unread and download count (#509) 2023-02-23 22:04:03 +03:30
akabhirav
783787e514 Send last read chapter in Mangas in Category API (#507)
* Send last read chapter with manga

* optimize query

* introduce new field for better performance
2023-02-21 02:47:45 +03:30
akabhirav
ac99dd55a2 Fix random page sent when manga is downloaded (#508) 2023-02-21 02:40:38 +03:30
Mitchell Syer
c56f984952 Fix SharedPreferences.Editor.clear and SharedPreferences.Editor.remove (#505)
* Fix SharedPreferences.Editor.clear and SharedPreferences.Editor.remove

* UNCHECKED_CAST

* Support removing Set<String>

* Typo

* Remove unneeded OptIn
2023-02-19 07:54:24 +03:30
Aria Moradi
9269ca726e It's not us, I swear ;;; 2023-02-16 10:57:26 +03:30
DattatreyaReddy Panta
eca3205dcf Update winget.yml (#500) 2023-02-14 15:02:41 +03:30
akabhirav
13f5486d0b Fix CBZ download bug for newly added mangas in Library (#499) 2023-02-13 19:17:14 +03:30
Aria Moradi
d4e71274f9 update changelog 2023-02-12 23:33:06 +03:30
36 changed files with 549 additions and 919 deletions

View File

@@ -1,12 +1,14 @@
name: Publish to WinGet
on:
release:
types: [released]
workflow_run:
workflows: ["CI Publish"]
types:
- completed
jobs:
publish:
runs-on: windows-latest # action can only be run on windows
steps:
- uses: vedantmgoyal2009/winget-releaser@v1
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: Suwayomi.Tachidesk-Server
installers-regex: '.*x64.msi$'

View File

@@ -9,7 +9,6 @@ package xyz.nulldev.androidcompat.io.sharedprefs
import android.content.SharedPreferences
import com.russhwolf.settings.ExperimentalSettingsApi
import com.russhwolf.settings.ExperimentalSettingsImplementation
import com.russhwolf.settings.PreferencesSettings
import com.russhwolf.settings.serialization.decodeValue
import com.russhwolf.settings.serialization.decodeValueOrNull
@@ -21,7 +20,7 @@ import kotlinx.serialization.builtins.serializer
import java.util.prefs.PreferenceChangeListener
import java.util.prefs.Preferences
@OptIn(ExperimentalSettingsImplementation::class, ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
@OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
class JavaSharedPreferences(key: String) : SharedPreferences {
private val javaPreferences = Preferences.userRoot().node("suwayomi/tachidesk/$key")
private val preferences = PreferencesSettings(javaPreferences)
@@ -77,13 +76,19 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
}
class Editor(private val preferences: PreferencesSettings) : SharedPreferences.Editor {
val itemsToAdd = mutableMapOf<String, Any>()
private val actions = mutableListOf<Action>()
private sealed class Action {
data class Add(val key: String, val value: Any) : Action()
data class Remove(val key: String) : Action()
object Clear : Action()
}
override fun putString(key: String, value: String?): SharedPreferences.Editor {
if (value != null) {
itemsToAdd[key] = value
actions += Action.Add(key, value)
} else {
remove(key)
actions += Action.Remove(key)
}
return this
}
@@ -93,40 +98,40 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
values: MutableSet<String>?
): SharedPreferences.Editor {
if (values != null) {
itemsToAdd[key] = values
actions += Action.Add(key, values)
} else {
remove(key)
actions += Action.Remove(key)
}
return this
}
override fun putInt(key: String, value: Int): SharedPreferences.Editor {
itemsToAdd[key] = value
actions += Action.Add(key, value)
return this
}
override fun putLong(key: String, value: Long): SharedPreferences.Editor {
itemsToAdd[key] = value
actions += Action.Add(key, value)
return this
}
override fun putFloat(key: String, value: Float): SharedPreferences.Editor {
itemsToAdd[key] = value
actions += Action.Add(key, value)
return this
}
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
itemsToAdd[key] = value
actions += Action.Add(key, value)
return this
}
override fun remove(key: String): SharedPreferences.Editor {
itemsToAdd.remove(key)
actions += Action.Remove(key)
return this
}
override fun clear(): SharedPreferences.Editor {
itemsToAdd.clear()
actions.add(Action.Clear)
return this
}
@@ -140,16 +145,33 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
}
private fun addToPreferences() {
itemsToAdd.forEach { (key, value) ->
actions.forEach {
@Suppress("UNCHECKED_CAST")
when (value) {
is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), key, value as Set<String>)
is String -> preferences.putString(key, value)
is Int -> preferences.putInt(key, value)
is Long -> preferences.putLong(key, value)
is Float -> preferences.putFloat(key, value)
is Double -> preferences.putDouble(key, value)
is Boolean -> preferences.putBoolean(key, value)
when (it) {
is Action.Add -> when (val value = it.value) {
is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), it.key, value as Set<String>)
is String -> preferences.putString(it.key, value)
is Int -> preferences.putInt(it.key, value)
is Long -> preferences.putLong(it.key, value)
is Float -> preferences.putFloat(it.key, value)
is Double -> preferences.putDouble(it.key, value)
is Boolean -> preferences.putBoolean(it.key, value)
}
is Action.Remove -> {
preferences.remove(it.key)
/**
* Set<String> are stored like
* key.0 = value1
* key.1 = value2
* key.size = 2
*/
preferences.keys.forEach { key ->
if (key.startsWith(it.key + ".")) {
preferences.remove(key)
}
}
}
Action.Clear -> preferences.clear()
}
}
}

View File

@@ -1,3 +1,76 @@
# Server: v0.7.0 + WebUI: r983
## TL;DR
- CBZ downloads support
- Webview implementation based on Microsoft playwright, disabled for this release
- Fixed compatibility with some chinese extensions
- Support for Tachiyomi extensions lib 1.4
- WebUI changes:
- Uhh, idk, find out yourself...
## Tachidesk-Server Changelog
- (r1159) v0.6.6 (by @AriaMoradi)
- (r1160) add Chagelog TL;DR (by @AriaMoradi)
- (r1161) fix Changelog typos (by @AriaMoradi)
- (r1162) WebView based cloudflare interceptor ([#456](https://github.com/Suwayomi/Tachidesk-Server/pull/456) by @AriaMoradi)
- (r1163) update issue mod (by @AriaMoradi)
- (r1164) better description (by @AriaMoradi)
- (r1165) fix regex (by @AriaMoradi)
- (r1166) get default User Agent from WebView ([#457](https://github.com/Suwayomi/Tachidesk-Server/pull/457) by @AriaMoradi)
- (r1167) implementation of android.graphics.BitmapFactory ([#460](https://github.com/Suwayomi/Tachidesk-Server/pull/460) by @animeavi)
- (r1168) Basic android.graphics Rect and Canvas implementation ([#461](https://github.com/Suwayomi/Tachidesk-Server/pull/461) by @animeavi)
- (r1169) Get Playwright working ([#462](https://github.com/Suwayomi/Tachidesk-Server/pull/462) by @Syer10)
- (r1170) disable deb release (by @AriaMoradi)
- (r1171) Fix debian release ([#463](https://github.com/Suwayomi/Tachidesk-Server/pull/463) by @mahor1221)
- (r1172) Add better manga thumbnail handling ([#465](https://github.com/Suwayomi/Tachidesk-Server/pull/465) by @Syer10)
- (r1173) Use extension list fallback if extensions fail to fetch ([#469](https://github.com/Suwayomi/Tachidesk-Server/pull/469) by @Syer10)
- (r1174) fix when playwright fails on providing a UA (by @AriaMoradi)
- (r1175) Update CategoryMetaTable.kt (by @AriaMoradi)
- (r1176) fix CategoryMetaTable reference to CategoryTable ([#473](https://github.com/Suwayomi/Tachidesk-Server/pull/473) by @AriaMoradi)
- (r1177) remove possibly misleading sentence (by @AriaMoradi)
- (r1178) Clarify and Update (by @AriaMoradi)
- (r1179) Clarify and Update (by @AriaMoradi)
- (r1180) link to Tachiyomi section (by @AriaMoradi)
- (r1181) fix typo (by @AriaMoradi)
- (r1182) Improve Gradle Configuration ([#478](https://github.com/Suwayomi/Tachidesk-Server/pull/478) by @Syer10)
- (r1183) Improve Playwright handling ([#479](https://github.com/Suwayomi/Tachidesk-Server/pull/479) by @Syer10)
- (r1184) fix ambiguous reference issue on JDK 13+ (by @AriaMoradi)
- (r1185) update gradle version (by @AriaMoradi)
- (r1186) upgrade dorkbox stuff (by @AriaMoradi)
- (r1187) Fixe Dex2Jar and dorkbox dependency issues ([#487](https://github.com/Suwayomi/Tachidesk-Server/pull/487) by @akabhirav)
- (r1188) Fix logging and update system try ([#488](https://github.com/Suwayomi/Tachidesk-Server/pull/488) by @Syer10)
- (r1189) add support for Extensions Lib 1.4 ([#496](https://github.com/Suwayomi/Tachidesk-Server/pull/496) by @Syer10)
- (r1190) disable playwright for v0.6.7 (by @AriaMoradi)
- (r1191) Decouple Cache and Download behaviour ([#493](https://github.com/Suwayomi/Tachidesk-Server/pull/493) by @akabhirav)
- (r1192) rethink image cache ([#498](https://github.com/Suwayomi/Tachidesk-Server/pull/498) by @AriaMoradi)
- (r1193) fix Page index issues for some providers ([#491](https://github.com/Suwayomi/Tachidesk-Server/pull/491) by @akabhirav)
- (r1194) Download as CBZ ([#490](https://github.com/Suwayomi/Tachidesk-Server/pull/490) by @akabhirav)
- (r1195) re-order config options (by @AriaMoradi)
- (r1196) stop using depricated API (by @AriaMoradi)
## Tachidesk-WebUI Changelog
- (r964) Created a GridLayout enum and updated all locations to use it. ([#208](https://github.com/Suwayomi/Tachidesk-WebUI/pull/208) by @infix)
- (r965) fix library update progress rendering ([#210](https://github.com/Suwayomi/Tachidesk-WebUI/pull/210) by @schroda)
- (r966) Save reader settings per manga in Meta ([#216](https://github.com/Suwayomi/Tachidesk-WebUI/pull/216) by @schroda)
- (r967) make default reader settings changeable ([#217](https://github.com/Suwayomi/Tachidesk-WebUI/pull/217) by @schroda)
- (r968) [#211] Refresh Library after a update ([#212](https://github.com/Suwayomi/Tachidesk-WebUI/pull/212) by @schroda)
- (r969) add logic for metadata migration ([#218](https://github.com/Suwayomi/Tachidesk-WebUI/pull/218) by @schroda)
- (r970) set browser tab title ([#220](https://github.com/Suwayomi/Tachidesk-WebUI/pull/220) by @schroda)
- (r971) Add tooltip containing full manga title to title of manga ([#221](https://github.com/Suwayomi/Tachidesk-WebUI/pull/221) by @schroda)
- (r972) show more detailed upload dates for today and yesterday ([#222](https://github.com/Suwayomi/Tachidesk-WebUI/pull/222) by @schroda)
- (r973) add GitHub action on pushing to run lint ([#224](https://github.com/Suwayomi/Tachidesk-WebUI/pull/224) by @schroda)
- (r974) Ignore filters while searching ([#226](https://github.com/Suwayomi/Tachidesk-WebUI/pull/226) by @schroda)
- (r975) force absolute import path ([#223](https://github.com/Suwayomi/Tachidesk-WebUI/pull/223) by @schroda)
- (r976) add prettier for auto formatting ([#231](https://github.com/Suwayomi/Tachidesk-WebUI/pull/231) by @schroda)
- (r977) Fix import path ([#228](https://github.com/Suwayomi/Tachidesk-WebUI/pull/228) by @schroda)
- (r978) increase prettier line length to 120 ([#233](https://github.com/Suwayomi/Tachidesk-WebUI/pull/233) by @schroda)
- (r979) Add chapter page dropdown ([#230](https://github.com/Suwayomi/Tachidesk-WebUI/pull/230) by @schroda)
- (r980) add chapter dropdown to reader nav bar ([#229](https://github.com/Suwayomi/Tachidesk-WebUI/pull/229) by @schroda)
- (r981) Fix lint error ([#235](https://github.com/Suwayomi/Tachidesk-WebUI/pull/235) by @schroda)
- (r982) Fix reader nav bar scroll to page ([#236](https://github.com/Suwayomi/Tachidesk-WebUI/pull/236) by @schroda)
- (r964) Created a GridLayout enum and updated all locations to use it. ([#208](https://github.com/Suwayomi/Tachidesk-WebUI/pull/208) by @infix)
# Server: v0.6.6 + WebUI: r963
## TL;DR
- Batch actions for chapters

View File

@@ -2,9 +2,10 @@
## Where should I start?
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
**Note 1:** Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#tachidesk-server and #tachidesk-webui channels) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
**Note 2:** Your pull request will be squashed into a single commit.
### Important notes
- Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#tachidesk-server and #tachidesk-webui channels) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
- Your pull request will be squashed into a single commit.
- We hate big pull requests, make them as small as possible, change one meaningful thing. Spam pull requests, we don't mind.
### Project goals and vision
- Porting Tachiyomi and covering it's features

View File

@@ -177,3 +177,7 @@ Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this pro
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 http://mozilla.org/MPL/2.0/.
## Disclaimer
The developer of this application does not have any affiliation with the content providers available.

View File

@@ -12,7 +12,7 @@ const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.7.0"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r983"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r1045"
// counts commits on the master branch
val tachideskRevision = runCatching {

View File

@@ -6,11 +6,10 @@ okhttp = "5.0.0-alpha.11" # Major version is locked by Tachiyomi extensions
javalin = "4.6.6" # Javalin 5.0.0+ requires Java 11
jackson = "2.13.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "0.40.1"
dex2jar = "v59"
dex2jar = "v60"
rhino = "1.7.14"
settings = "1.0.0-RC"
twelvemonkeys = "3.9.4"
playwright = "1.28.0"
[libraries]
# Kotlin
@@ -95,9 +94,6 @@ appdirs = "net.harawata:appdirs:1.2.1"
zip4j = "net.lingala.zip4j:zip4j:2.11.2"
junrar = "com.github.junrar:junrar:7.5.3"
# CloudflareInterceptor
playwright = { module = "com.microsoft.playwright:playwright", version.ref = "playwright" }
# AES/CBC/PKCS7Padding Cypher provider
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.72"

View File

@@ -26,8 +26,6 @@ main() {
set -- "${POSITIONAL_ARGS[@]}"
OS="$1"
PLAYWRIGHT_VERSION="$(cat gradle/libs.versions.toml | grep -oP "playwright = \"\K([0-9\.]*)(?=\")")"
PLAYWRIGHT_REVISION="$(curl --silent "https://raw.githubusercontent.com/microsoft/playwright/v$PLAYWRIGHT_VERSION/packages/playwright-core/browsers.json" 2>&1 | grep -ozP "\"name\": \"chromium\",\n *\"revision\": \"\K[0-9]*")"
JAR="$(ls server/build/*.jar | tail -n1)"
RELEASE_NAME="$(echo "${JAR%.*}" | xargs basename)-$OS"
RELEASE_VERSION="$(tmp="${JAR%-*}"; echo "${tmp##*-}" | tr -d v)"
@@ -127,8 +125,9 @@ main() {
ELECTRON_URL="https://github.com/electron/electron/releases/download/$electron_version/$ELECTRON"
download_jre_and_electron
PLAYWRIGHT_PLATFORM="win64"
setup_playwright
PYTHON=Winpython64-3.10.9.0dot.exe
PYTHON_URL=https://github.com/winpython/winpython/releases/download/5.3.20221233/Winpython64-3.10.9.0dot.exe
setup_undetected_chromedriver_and_python
RELEASE="$RELEASE_NAME.zip"
make_windows_bundle
@@ -285,9 +284,23 @@ make_windows_package() {
"$RELEASE_NAME/jre.wxs" "$RELEASE_NAME/electron.wxs" -o "$RELEASE"
}
setup_playwright() {
mkdir "$RELEASE_NAME/bin"
curl -L "https://playwright.azureedge.net/builds/chromium/$PLAYWRIGHT_REVISION/chromium-$PLAYWRIGHT_PLATFORM.zip" -o "$RELEASE_NAME/bin/chromium.zip"
setup_python() {
mkdir "$RELEASE_NAME/"
curl -L "$PYTHON_URL" -o "$PYTHON"
7z x $PYTHON
mv WPy64-31090/python-3.10.9.amd64 "$RELEASE_NAME/python"
}
setup_undetected_chromedriver() {
curl -L "https://github.com/Suwayomi/undetected-chromedriver/archive/refs/heads/master.zip" -o undetected-chromedriver-master.zip
unzip undetected-chromedriver-master.zip
mv undetected-chromedriver-master "$RELEASE_NAME/undetected-chromedriver"
}
setup_undetected_chromedriver_and_python() {
setup_python
setup_undetected_chromedriver
}
# Error handler

View File

@@ -50,9 +50,6 @@ dependencies {
implementation(libs.zip4j)
implementation(libs.junrar)
// CloudflareInterceptor
implementation(libs.playwright)
// AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
implementation(libs.bouncycastle)

View File

@@ -1,212 +0,0 @@
/*
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package suwayomi.tachidesk.server.util;
import com.microsoft.playwright.impl.driver.Driver;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Copy of <a href="https://github.com/microsoft/playwright-java/blob/8c0231b0f739656e8a86bc58fca9ee778ddc571b/driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java">DriverJar</a>
* with support for pre-installing chromium and only supports chromium playwright
*/
public class DriverJar extends Driver {
private static final String PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD";
private static final String SELENIUM_REMOTE_URL = "SELENIUM_REMOTE_URL";
static final String PLAYWRIGHT_NODEJS_PATH = "PLAYWRIGHT_NODEJS_PATH";
private final Path driverTempDir;
private Path preinstalledNodePath;
public DriverJar() throws IOException {
// Allow specifying custom path for the driver installation
// See https://github.com/microsoft/playwright-java/issues/728
String alternativeTmpdir = System.getProperty("playwright.driver.tmpdir");
String prefix = "playwright-java-";
driverTempDir = alternativeTmpdir == null
? Files.createTempDirectory(prefix)
: Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix);
driverTempDir.toFile().deleteOnExit();
String nodePath = System.getProperty("playwright.nodejs.path");
if (nodePath != null) {
preinstalledNodePath = Paths.get(nodePath);
if (!Files.exists(preinstalledNodePath)) {
throw new RuntimeException("Invalid Node.js path specified: " + nodePath);
}
}
logMessage("created DriverJar: " + driverTempDir);
}
@Override
protected void initialize(Boolean installBrowsers) throws Exception {
if (preinstalledNodePath == null && env.containsKey(PLAYWRIGHT_NODEJS_PATH)) {
preinstalledNodePath = Paths.get(env.get(PLAYWRIGHT_NODEJS_PATH));
if (!Files.exists(preinstalledNodePath)) {
throw new RuntimeException("Invalid Node.js path specified: " + preinstalledNodePath);
}
} else if (preinstalledNodePath != null) {
// Pass the env variable to the driver process.
env.put(PLAYWRIGHT_NODEJS_PATH, preinstalledNodePath.toString());
}
extractDriverToTempDir();
logMessage("extracted driver from jar to " + driverPath());
if (installBrowsers)
installBrowsers(env);
}
private void installBrowsers(Map<String, String> env) throws IOException, InterruptedException {
String skip = env.get(PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD);
if (skip == null) {
skip = System.getenv(PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD);
}
if (skip != null && !"0".equals(skip) && !"false".equals(skip)) {
System.out.println("Skipping browsers download because `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` env variable is set");
return;
}
if (env.get(SELENIUM_REMOTE_URL) != null || System.getenv(SELENIUM_REMOTE_URL) != null) {
logMessage("Skipping browsers download because `SELENIUM_REMOTE_URL` env variable is set");
return;
}
Chromium.preinstall(platformDir());
Path driver = driverPath();
if (!Files.exists(driver)) {
throw new RuntimeException("Failed to find driver: " + driver);
}
ProcessBuilder pb = createProcessBuilder();
pb.command().add("install");
pb.command().add("chromium");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
Process p = pb.start();
boolean result = p.waitFor(10, TimeUnit.MINUTES);
if (!result) {
p.destroy();
throw new RuntimeException("Timed out waiting for browsers to install");
}
if (p.exitValue() != 0) {
throw new RuntimeException("Failed to install browsers, exit code: " + p.exitValue());
}
}
private static boolean isExecutable(Path filePath) {
String name = filePath.getFileName().toString();
return name.endsWith(".sh") || name.endsWith(".exe") || !name.contains(".");
}
private FileSystem initFileSystem(URI uri) throws IOException {
try {
return FileSystems.newFileSystem(uri, Collections.emptyMap());
} catch (FileSystemAlreadyExistsException e) {
return null;
}
}
public static URI getDriverResourceURI() throws URISyntaxException {
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
return classloader.getResource("driver/" + platformDir()).toURI();
}
void extractDriverToTempDir() throws URISyntaxException, IOException {
URI originalUri = getDriverResourceURI();
URI uri = maybeExtractNestedJar(originalUri);
// Create zip filesystem if loading from jar.
try (FileSystem fileSystem = "jar".equals(uri.getScheme()) ? initFileSystem(uri) : null) {
Path srcRoot = Paths.get(uri);
// jar file system's .relativize gives wrong results when used with
// spring-boot-maven-plugin, convert to the default filesystem to
// have predictable results.
// See https://github.com/microsoft/playwright-java/issues/306
Path srcRootDefaultFs = Paths.get(srcRoot.toString());
Files.walk(srcRoot).forEach(fromPath -> {
if (preinstalledNodePath != null) {
String fileName = fromPath.getFileName().toString();
if ("node.exe".equals(fileName) || "node".equals(fileName)) {
return;
}
}
Path relative = srcRootDefaultFs.relativize(Paths.get(fromPath.toString()));
Path toPath = driverTempDir.resolve(relative.toString());
try {
if (Files.isDirectory(fromPath)) {
Files.createDirectories(toPath);
} else {
Files.copy(fromPath, toPath);
if (isExecutable(toPath)) {
toPath.toFile().setExecutable(true, true);
}
}
toPath.toFile().deleteOnExit();
} catch (IOException e) {
throw new RuntimeException("Failed to extract driver from " + uri + ", full uri: " + originalUri, e);
}
});
}
}
private URI maybeExtractNestedJar(final URI uri) throws URISyntaxException {
if (!"jar".equals(uri.getScheme())) {
return uri;
}
final String JAR_URL_SEPARATOR = "!/";
String[] parts = uri.toString().split("!/");
if (parts.length != 3) {
return uri;
}
String innerJar = String.join(JAR_URL_SEPARATOR, parts[0], parts[1]);
URI jarUri = new URI(innerJar);
try (FileSystem fs = FileSystems.newFileSystem(jarUri, Collections.emptyMap())) {
Path fromPath = Paths.get(jarUri);
Path toPath = driverTempDir.resolve(fromPath.getFileName().toString());
Files.copy(fromPath, toPath);
toPath.toFile().deleteOnExit();
return new URI("jar:" + toPath.toUri() + JAR_URL_SEPARATOR + parts[2]);
} catch (IOException e) {
throw new RuntimeException("Failed to extract driver's nested .jar from " + jarUri + "; full uri: " + uri, e);
}
}
private static String platformDir() {
String name = System.getProperty("os.name").toLowerCase();
String arch = System.getProperty("os.arch").toLowerCase();
if (name.contains("windows")) {
return "win32_x64";
}
if (name.contains("linux")) {
if (arch.equals("aarch64")) {
return "linux-arm64";
} else {
return "linux";
}
}
if (name.contains("mac os x")) {
return "mac";
}
throw new RuntimeException("Unexpected os.name value: " + name);
}
@Override
protected Path driverDir() {
return driverTempDir;
}
}

View File

@@ -1,24 +1,29 @@
package eu.kanade.tachiyomi.network.interceptor
import com.microsoft.playwright.Browser
import com.microsoft.playwright.BrowserType.LaunchOptions
import com.microsoft.playwright.Page
import com.microsoft.playwright.Playwright
import com.microsoft.playwright.PlaywrightException
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.interceptor.CFClearance.resolveWithWebView
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypasser.resolveWithWebView
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import mu.KotlinLogging
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import suwayomi.tachidesk.server.ServerConfig
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy
import java.io.BufferedReader
import java.io.Closeable
import java.io.File
import java.io.IOException
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import java.io.InputStreamReader
import java.io.PrintWriter
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
class CloudflareInterceptor : Interceptor {
private val logger = KotlinLogging.logger {}
@@ -38,10 +43,12 @@ class CloudflareInterceptor : Interceptor {
return originalResponse
}
throw IOException("playwrite is diabled for v0.6.7")
logger.debug { "Cloudflare anti-bot is on, CloudflareInterceptor is kicking in..." }
if (!serverConfig.webviewEnabled) {
throw CloudflareBypassException("Webview is disabled, enable it in server config")
}
return try {
originalResponse.close()
network.cookies.remove(originalRequest.url.toUri())
@@ -63,43 +70,43 @@ class CloudflareInterceptor : Interceptor {
}
}
/*
* This class is ported from https://github.com/vvanglro/cf-clearance
* The original code is licensed under Apache 2.0
*/
object CFClearance {
object CloudflareBypasser {
private val logger = KotlinLogging.logger {}
private val network: NetworkHelper by injectLazy()
init {
// Fix the default DriverJar issue by providing our own implementation
// ref: https://github.com/microsoft/playwright-java/issues/1138
System.setProperty("playwright.driver.impl", "suwayomi.tachidesk.server.util.DriverJar")
}
fun resolveWithWebView(originalRequest: Request): Request {
val url = originalRequest.url.toString()
logger.debug { "resolveWithWebView($url)" }
val cookies = Playwright.create().use { playwright ->
playwright.chromium().launch(
LaunchOptions()
.setHeadless(false)
.apply {
if (serverConfig.socksProxyEnabled) {
setProxy("socks5://${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
}
}
).use { browser ->
val userAgent = originalRequest.header("User-Agent")
if (userAgent != null) {
browser.newContext(Browser.NewContextOptions().setUserAgent(userAgent)).use { browserContext ->
browserContext.newPage().use { getCookies(it, url) }
}
} else {
browser.newPage().use { getCookies(it, url) }
val cookies = PythonInterpreter.create().use { py ->
try {
py.exec("import undetected_chromedriver as uc")
py.exec("options = uc.ChromeOptions()")
if (serverConfig.socksProxyEnabled) {
val proxy = "socks5://${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}"
py.exec("options.add_argument('--proxy-server=$proxy')")
}
// py.exec("driver = uc.Chrome(options=options)")
py.exec("driver = uc.Chrome(options=options, driver_executable_path='${py.chromedriverPath.replace("\\","\\\\")}', version_main=111)")
// TODO: handle custom userAgent
// val userAgent = originalRequest.header("User-Agent")
// if (userAgent != null) {
// browser.newContext(Browser.NewContextOptions().setUserAgent(userAgent)).use { browserContext ->
// browserContext.newPage().use { getCookies(it, url) }
// }
// } else {
// browser.newPage().use { getCookies(it, url) }
// }
py.exec("driver.get('$url')")
getCookies(py)
} finally {
py.exec("driver.quit()")
}
}
@@ -139,44 +146,64 @@ object CFClearance {
fun getWebViewUserAgent(): String {
return try {
throw PlaywrightException("playwrite is diabled for v0.6.7")
Playwright.create().use { playwright ->
playwright.chromium().launch(
LaunchOptions()
.setHeadless(true)
).use { browser ->
browser.newPage().use { page ->
val userAgent = page.evaluate("() => {return navigator.userAgent}") as String
logger.debug { "WebView User-Agent is $userAgent" }
return userAgent
}
}
if (!serverConfig.webviewEnabled) {
throw CloudflareBypassException("Webview is disabled, enable it in server config")
}
} catch (e: PlaywrightException) {
// Playwright might fail on headless environments like docker
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
PythonInterpreter.create().use { py ->
py.exec("import undetected_chromedriver as uc")
py.exec("options = uc.ChromeOptions()")
py.exec("options.add_argument('--headless')")
py.exec("options.add_argument('--disable-gpu')")
// py.exec("driver = uc.Chrome(options=options)")
py.exec("driver = uc.Chrome(options=options, driver_executable_path='${py.chromedriverPath.replace("\\","\\\\")}', version_main=111)")
py.exec("userAgent = driver.execute_script('return navigator.userAgent')")
val userAgent = py.getValue("userAgent")
py.exec("driver.quit()")
userAgent
}
} catch (e: Exception) {
// Webview might fail on headless environments like docker
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
}
}
private fun getCookies(page: Page, url: String): List<Cookie> {
applyStealthInitScripts(page)
page.navigate(url)
val challengeResolved = waitForChallengeResolve(page)
@Serializable
data class PythonSeleniumCookie(
val domain: String,
val expiry: Long?,
val httpOnly: Boolean,
val name: String,
val path: String,
val sameSite: String,
val secure: Boolean,
val value: String
)
private val json by DI.global.instance<Json>()
private fun getCookies(py: PythonInterpreter): List<Cookie> {
val challengeResolved = waitForChallengeResolve(py)
return if (challengeResolved) {
val cookies = page.context().cookies()
py.exec("import json")
py.exec("cookies = json.dumps(driver.get_cookies())")
val cookiesJson = py.getValue("cookies")
val cookies = json.decodeFromString<List<PythonSeleniumCookie>>(cookiesJson)
logger.debug {
val userAgent = page.evaluate("() => {return navigator.userAgent}")
"Playwright User-Agent is $userAgent"
py.exec("userAgent = driver.execute_script('return navigator.userAgent')")
val userAgent = py.getValue("userAgent")
"Webview User-Agent is $userAgent"
}
// Convert PlayWright cookies to OkHttp cookies
// Convert Webview cookies to OkHttp cookies
cookies.map {
Cookie.Builder()
.domain(it.domain.removePrefix("."))
.expiresAt(it.expires?.times(1000)?.toLong() ?: Long.MAX_VALUE)
.expiresAt(it.expiry?.times(1000) ?: Long.MAX_VALUE)
.name(it.name)
.path(it.path)
.value(it.value).apply {
@@ -185,39 +212,18 @@ object CFClearance {
}.build()
}
} else {
logger.debug { "Cloudflare challenge failed to resolve" }
throw CloudflareBypassException()
throw CloudflareBypassException("Cloudflare challenge failed to resolve")
}
}
// ref: https://github.com/vvanglro/cf-clearance/blob/44124a8f06d8d0ecf2bf558a027082ff88dab435/cf_clearance/stealth.py#L18
private val stealthInitScripts by lazy {
arrayOf(
ServerConfig::class.java.getResource("/cloudflare-js/canvas.fingerprinting.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/chrome.global.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/emulate.touch.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/navigator.permissions.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/navigator.webdriver.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/chrome.runtime.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/chrome.plugin.js")!!.readText()
)
}
// ref: https://github.com/vvanglro/cf-clearance/blob/44124a8f06d8d0ecf2bf558a027082ff88dab435/cf_clearance/stealth.py#L76
private fun applyStealthInitScripts(page: Page) {
for (script in stealthInitScripts) {
page.addInitScript(script)
}
}
// ref: https://github.com/vvanglro/cf-clearance/blob/44124a8f06d8d0ecf2bf558a027082ff88dab435/cf_clearance/retry.py#L21
private fun waitForChallengeResolve(page: Page): Boolean {
// sometimes the user has to solve the captcha challenge manually, potentially wait a long time
private fun waitForChallengeResolve(py: PythonInterpreter): Boolean {
// sometimes the user has to solve the captcha challenge manually and multiple times, potentially wait a long time
val timeoutSeconds = 120
repeat(timeoutSeconds) {
page.waitForTimeout(1.seconds.toDouble(DurationUnit.MILLISECONDS))
TimeUnit.SECONDS.sleep(1)
val success = try {
page.querySelector("#challenge-form") == null
py.exec("r = driver.execute_script('return document.querySelector(\"#challenge-form\") == null')")
py.getValue("r").lowercase().toBoolean()
} catch (e: Exception) {
logger.debug(e) { "query Error" }
false
@@ -226,6 +232,113 @@ object CFClearance {
}
return false
}
private class CloudflareBypassException : Exception()
}
private class CloudflareBypassException(message: String?) : Exception(message)
class PythonInterpreter
private constructor(private val process: Process, val chromedriverPath: String) : Closeable {
private val stdin = process.outputStream
private val stdout = process.inputStream
private val stderr = process.errorStream
private val stdinWriter = PrintWriter(stdin)
private val stdoutReader = BufferedReader(InputStreamReader(stdout))
private val stderrReader = BufferedReader(InputStreamReader(stderr))
private fun rawExec(command: String) {
stdinWriter.println(command)
stdinWriter.flush()
}
val BUFF_SIZE = 102400
fun exec(command: String) {
logger.debug { "Python Command: $command" }
rawExec(command)
makeSureExecDone()
}
private val commandOutputs = mutableListOf<String>()
fun makeSureExecDone() {
val makeSureString = "PYTHON_IS_READY"
rawExec("print('$makeSureString')")
var line: String?
do {
line = stdoutReader.readLine()
if (line != makeSureString) {
commandOutputs.add(line)
}
} while (line != makeSureString)
val pyError = buildString {
while (stderrReader.ready())
append(stderr.read().toChar())
}
if (pyError.isNotEmpty()) {
println("Python STDERR: $pyError")
}
}
fun getValue(variableName: String): String {
exec("print($variableName)")
return commandOutputs.last()
}
private fun flushStdoutReader() {
var line: String?
while (stdoutReader.ready()) {
val line = stdoutReader.readLine()
}
}
fun destroy() {
stdinWriter.close()
stdoutReader.close()
stderr.close()
process.destroy()
}
override fun close() {
destroy()
}
companion object {
private val logger = KotlinLogging.logger {}
fun create(pythonPath: String, workingDir: String, pythonStartupFile: String, chromedriverPath: String): PythonInterpreter {
val processBuilder = ProcessBuilder()
.command(pythonPath, "-i", "-q")
processBuilder.directory(File(workingDir))
val environment = processBuilder.environment()
environment["PYTHONSTARTUP"] = pythonStartupFile
val process = processBuilder.start()
return PythonInterpreter(process, chromedriverPath)
}
fun create(): PythonInterpreter {
val uc = Paths.get(serverConfig.undetectedChromePath).toAbsolutePath().toString()
logger.debug { "absolute path to undetected-chromedriver: $uc" }
val (pythonPath, chromedriverPath) = if (System.getProperty("os.name").startsWith("Windows")) {
arrayOf(
"$uc\\venv\\Scripts\\python.exe",
"$uc\\chromedriver.exe"
)
} else {
arrayOf(
"$uc/venv/bin/python",
"$uc/chromedriver"
)
}
return create(
pythonPath,
uc,
"$uc/console.py",
chromedriverPath
)
}
}
}

View File

@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.CFClearance.getWebViewUserAgent
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypasser.getWebViewUserAgent
import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList

View File

@@ -61,14 +61,15 @@ object CategoryController {
pathParam<Int>("categoryId"),
formParam<String?>("name"),
formParam<Boolean?>("default"),
formParam<Int?>("includeInUpdate"),
documentWith = {
withOperation {
summary("Category modify")
description("Modify a category")
}
},
behaviorOf = { ctx, categoryId, name, isDefault ->
Category.updateCategory(categoryId, name, isDefault)
behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate ->
Category.updateCategory(categoryId, name, isDefault, includeInUpdate)
ctx.status(200)
},
withResults = {

View File

@@ -1,5 +1,12 @@
package suwayomi.tachidesk.manga.controller
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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.UpdateStrategy
import io.javalin.http.HttpCode
import io.javalin.websocket.WsConfig
@@ -13,23 +20,13 @@ import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.manga.impl.update.UpdateStatus
import suwayomi.tachidesk.manga.impl.update.UpdaterSocket
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
import suwayomi.tachidesk.manga.model.dataclass.*
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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/. */
object UpdateController {
private val logger = KotlinLogging.logger { }
@@ -93,14 +90,33 @@ object UpdateController {
if (clear) {
updater.reset()
}
categories
val includeInUpdateStatusToCategoryMap = categories.groupBy { it.includeInUpdate }
val excludedCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.EXCLUDE].orEmpty()
val includedCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.INCLUDE].orEmpty()
val unsetCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.UNSET].orEmpty()
val categoriesToUpdate = includedCategories.ifEmpty { unsetCategories }
logger.debug { "Updating categories: '${categoriesToUpdate.joinToString("', '") { it.name }}'" }
val categoriesToUpdateMangas = categoriesToUpdate
.flatMap { CategoryManga.getCategoryMangaList(it.id) }
.distinctBy { it.id }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title))
val mangasToCategoriesMap = CategoryManga.getMangasCategories(categoriesToUpdateMangas.map { it.id })
val mangasToUpdate = categoriesToUpdateMangas
.filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE }
.forEach { manga ->
updater.addMangaToQueue(manga)
}
.filter { !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } }
// In case no manga gets updated and no update job was running before, the client would never receive an info about its update request
if (mangasToUpdate.isEmpty()) {
UpdaterSocket.notifyAllClients(UpdateStatus())
return
}
updater.addMangasToQueue(
mangasToUpdate
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, MangaDataClass::title))
)
}
fun categoryUpdateWS(ws: WsConfig) {

View File

@@ -18,8 +18,8 @@ import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.CategoryManga.removeMangaFromCategory
import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
@@ -50,11 +50,12 @@ object Category {
}
}
fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?) {
fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?, includeInUpdate: Int?) {
transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) {
if (name != null && !name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)) it[CategoryTable.name] = name
if (isDefault != null) it[CategoryTable.isDefault] = isDefault
if (includeInUpdate != null) it[CategoryTable.includeInUpdate] = includeInUpdate
}
}
}
@@ -98,12 +99,14 @@ object Category {
const val DEFAULT_CATEGORY_ID = 0
const val DEFAULT_CATEGORY_NAME = "Default"
private fun addDefaultIfNecessary(categories: List<CategoryDataClass>): List<CategoryDataClass> =
if (MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.isNotEmpty()) {
listOf(CategoryDataClass(DEFAULT_CATEGORY_ID, 0, DEFAULT_CATEGORY_NAME, true)) + categories
private fun addDefaultIfNecessary(categories: List<CategoryDataClass>): List<CategoryDataClass> {
val defaultCategorySize = MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.count().toInt()
return if (defaultCategorySize > 0) {
listOf(CategoryDataClass(DEFAULT_CATEGORY_ID, 0, DEFAULT_CATEGORY_NAME, true, defaultCategorySize, IncludeInUpdate.UNSET)) + categories
} else {
categories
}
}
fun getCategoryList(): List<CategoryDataClass> {
return transaction {
@@ -123,6 +126,14 @@ object Category {
}
}
fun getCategorySize(categoryId: Int): Int {
return transaction {
CategoryMangaTable.select {
CategoryMangaTable.category eq categoryId
}.count().toInt()
}
}
fun getCategoryMetaMap(categoryId: Int): Map<String, String> {
return transaction {
CategoryMetaTable.select { CategoryMetaTable.ref eq categoryId }

View File

@@ -10,10 +10,13 @@ package suwayomi.tachidesk.manga.impl
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.alias
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.count
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.leftJoin
import org.jetbrains.exposed.sql.max
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
@@ -61,46 +64,45 @@ object CategoryManga {
* list of mangas that belong to a category
*/
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
val unreadExpression = wrapAsExpression<Long>(
ChapterTable
.slice(ChapterTable.id.count())
.select { (MangaTable.id eq ChapterTable.manga) and (ChapterTable.isRead eq false) }
// Select the required columns from the MangaTable and add the aggregate functions to compute unread, download, and chapter counts
val unreadCount = wrapAsExpression<Long>(
ChapterTable.slice(ChapterTable.id.count()).select((ChapterTable.isRead eq false) and (ChapterTable.manga eq MangaTable.id))
)
val downloadExpression = wrapAsExpression<Long>(
ChapterTable
.slice(ChapterTable.id.count())
.select { (MangaTable.id eq ChapterTable.manga) and (ChapterTable.isDownloaded eq true) }
)
val chapterCountExpression = wrapAsExpression<Long>(
ChapterTable
.slice(ChapterTable.id.count())
.select { (MangaTable.id eq ChapterTable.manga) }
val downloadedCount = wrapAsExpression<Long>(
ChapterTable.slice(ChapterTable.id.count()).select((ChapterTable.isDownloaded eq true) and (ChapterTable.manga eq MangaTable.id))
)
val selectedColumns = MangaTable.columns + unreadExpression + downloadExpression + chapterCountExpression
val chapterCount = ChapterTable.id.count().alias("chapter_count")
val lastReadAt = ChapterTable.lastReadAt.max().alias("last_read_at")
val selectedColumns = MangaTable.columns + unreadCount + downloadedCount + chapterCount + lastReadAt
val transform: (ResultRow) -> MangaDataClass = {
// Map the data from the result row to the MangaDataClass
val dataClass = MangaTable.toDataClass(it)
dataClass.unreadCount = it[unreadExpression]
dataClass.downloadCount = it[downloadExpression]
dataClass.chapterCount = it[chapterCountExpression]
dataClass.lastReadAt = it[lastReadAt]
dataClass.unreadCount = it[unreadCount]
dataClass.downloadCount = it[downloadedCount]
dataClass.chapterCount = it[chapterCount]
dataClass
}
if (categoryId == DEFAULT_CATEGORY_ID) {
return transaction {
MangaTable
.slice(selectedColumns)
.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }
.map(transform)
}
}
return transaction {
CategoryMangaTable.innerJoin(MangaTable)
.slice(selectedColumns)
.select { (MangaTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) }
.map(transform)
// Fetch data from the MangaTable and join with the CategoryMangaTable, if a category is specified
val query = if (categoryId == DEFAULT_CATEGORY_ID) {
MangaTable
.leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga })
.slice(columns = selectedColumns)
.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }
} else {
MangaTable
.innerJoin(CategoryMangaTable)
.leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga })
.slice(columns = selectedColumns)
.select { (MangaTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) }
}
// Join with the ChapterTable to fetch the last read chapter for each manga
query.groupBy(*MangaTable.columns.toTypedArray()).map(transform)
}
}
@@ -114,4 +116,20 @@ object CategoryManga {
}
}
}
fun getMangasCategories(mangaIDs: List<Int>): Map<Int, List<CategoryDataClass>> {
return buildMap {
transaction {
CategoryMangaTable.innerJoin(CategoryTable)
.select { CategoryMangaTable.manga inList mangaIDs }
.groupBy { it[CategoryMangaTable.manga] }
.forEach {
val mangaId = it.key.value
val categories = it.value
set(mangaId, categories.map { category -> CategoryTable.toDataClass(category) })
}
}
}
}
}

View File

@@ -33,12 +33,12 @@ class ArchiveProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(ma
val chapterFolder = File(chapterDir)
if (outputFile.exists()) handleExistingCbzFile(outputFile, chapterFolder)
FolderProvider(mangaId, chapterId).download(download, scope, step)
withContext(Dispatchers.IO) {
outputFile.createNewFile()
}
FolderProvider(mangaId, chapterId).download(download, scope, step)
ZipOutputStream(outputFile.outputStream()).use { zipOut ->
if (chapterFolder.isDirectory) {
chapterFolder.listFiles()?.sortedBy { it.name }?.forEach {

View File

@@ -24,7 +24,7 @@ class FolderProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(man
val chapterDir = getChapterDownloadPath(mangaId, chapterId)
val folder = File(chapterDir)
folder.mkdirs()
val file = folder.listFiles()?.get(index)
val file = folder.listFiles()?.sortedBy { it.name }?.get(index)
val fileType = file!!.name.substringAfterLast(".")
return Pair(FileInputStream(file).buffered(), "image/$fileType")
}

View File

@@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.StateFlow
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
interface IUpdater {
fun addMangaToQueue(manga: MangaDataClass)
fun addMangasToQueue(mangas: List<MangaDataClass>)
val status: StateFlow<UpdateStatus>
fun reset()
}

View File

@@ -14,9 +14,12 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import mu.KotlinLogging
import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.server.serverConfig
import java.util.concurrent.ConcurrentHashMap
class Updater : IUpdater {
@@ -27,18 +30,29 @@ class Updater : IUpdater {
override val status = _status.asStateFlow()
private val tracker = ConcurrentHashMap<Int, UpdateJob>()
private var updateChannel = createUpdateChannel()
private val updateChannels = ConcurrentHashMap<String, Channel<UpdateJob>>()
private val semaphore = Semaphore(serverConfig.maxParallelUpdateRequests)
private fun getOrCreateUpdateChannelFor(source: String): Channel<UpdateJob> {
return updateChannels.getOrPut(source) {
logger.debug { "getOrCreateUpdateChannelFor: created channel for $source - channels: ${updateChannels.size + 1}" }
createUpdateChannel()
}
}
private fun createUpdateChannel(): Channel<UpdateJob> {
val channel = Channel<UpdateJob>(Channel.UNLIMITED)
channel.consumeAsFlow()
.onEach { job ->
_status.value = UpdateStatus(
process(job),
tracker.any { (_, job) ->
job.status == JobStatus.PENDING || job.status == JobStatus.RUNNING
}
)
semaphore.withPermit {
_status.value = UpdateStatus(
process(job),
tracker.any { (_, job) ->
job.status == JobStatus.PENDING || job.status == JobStatus.RUNNING
}
)
}
}
.catch { logger.error(it) { "Error during updates" } }
.launchIn(scope)
@@ -49,7 +63,7 @@ class Updater : IUpdater {
tracker[job.manga.id] = job.copy(status = JobStatus.RUNNING)
_status.update { UpdateStatus(tracker.values.toList(), true) }
tracker[job.manga.id] = try {
logger.info { "Updating ${job.manga.title}" }
logger.info { "Updating \"${job.manga.title}\" (source: ${job.manga.sourceId})" }
Chapter.getChapterList(job.manga.id, true)
job.copy(status = JobStatus.COMPLETE)
} catch (e: Exception) {
@@ -60,19 +74,24 @@ class Updater : IUpdater {
return tracker.values.toList()
}
override fun addMangaToQueue(manga: MangaDataClass) {
override fun addMangasToQueue(mangas: List<MangaDataClass>) {
mangas.forEach { tracker[it.id] = UpdateJob(it) }
_status.update { UpdateStatus(tracker.values.toList(), mangas.isNotEmpty()) }
mangas.forEach { addMangaToQueue(it) }
}
private fun addMangaToQueue(manga: MangaDataClass) {
val updateChannel = getOrCreateUpdateChannelFor(manga.sourceId)
scope.launch {
updateChannel.send(UpdateJob(manga))
}
tracker[manga.id] = UpdateJob(manga)
_status.update { UpdateStatus(tracker.values.toList(), true) }
}
override fun reset() {
scope.coroutineContext.cancelChildren()
tracker.clear()
_status.update { UpdateStatus() }
updateChannel.cancel()
updateChannel = createUpdateChannel()
updateChannels.forEach { (_, channel) -> channel.cancel() }
updateChannels.clear()
}
}

View File

@@ -1,5 +1,7 @@
package suwayomi.tachidesk.manga.model.dataclass
import com.fasterxml.jackson.annotation.JsonValue
/*
* Copyright (C) Contributors to the Suwayomi project
*
@@ -7,10 +9,20 @@ package suwayomi.tachidesk.manga.model.dataclass
* 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/. */
enum class IncludeInUpdate(@JsonValue val value: Int) {
EXCLUDE(0), INCLUDE(1), UNSET(-1);
companion object {
fun fromValue(value: Int) = IncludeInUpdate.values().find { it.value == value } ?: UNSET
}
}
data class CategoryDataClass(
val id: Int,
val order: Int,
val name: String,
val default: Boolean,
val size: Int,
val includeInUpdate: IncludeInUpdate,
val meta: Map<String, String> = emptyMap()
)

View File

@@ -45,6 +45,7 @@ data class MangaDataClass(
var unreadCount: Long? = null,
var downloadCount: Long? = null,
var chapterCount: Long? = null,
var lastReadAt: Long? = null,
var lastChapterRead: ChapterDataClass? = null,
val age: Long? = if (lastFetchedAt == null) 0 else Instant.now().epochSecond.minus(lastFetchedAt),

View File

@@ -11,11 +11,13 @@ import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
object CategoryTable : IntIdTable() {
val name = varchar("name", 64)
val order = integer("order").default(0)
val isDefault = bool("is_default").default(false)
val includeInUpdate = integer("include_in_update").default(IncludeInUpdate.UNSET.value)
}
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
@@ -23,5 +25,7 @@ fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
categoryEntry[order],
categoryEntry[name],
categoryEntry[isDefault],
Category.getCategorySize(categoryEntry[id].value),
IncludeInUpdate.fromValue(categoryEntry[includeInUpdate]),
Category.getCategoryMetaMap(categoryEntry[id].value)
)

View File

@@ -33,6 +33,9 @@ class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPro
val downloadAsCbz: Boolean by overridableConfig
val downloadsPath: String by overridableConfig
// updater
val maxParallelUpdateRequests: Int by overridableConfig
// Authentication
val basicAuthEnabled: Boolean by overridableConfig
val basicAuthUsername: String by overridableConfig
@@ -42,6 +45,10 @@ class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPro
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
val systemTrayEnabled: Boolean by overridableConfig
// Webview
val webviewEnabled: Boolean by overridableConfig
val undetectedChromePath: String by overridableConfig
companion object {
fun register(config: Config) = ServerConfig(config.getConfig(MODULE_NAME))
}

View File

@@ -0,0 +1,19 @@
package suwayomi.tachidesk.server.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 de.neonew.exposed.migrations.helpers.AddColumnMigration
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
@Suppress("ClassName", "unused")
class M0026_CategoryIncludeInUpdate : AddColumnMigration(
"Category",
"include_in_update",
"INT",
IncludeInUpdate.UNSET.value.toString()
)

View File

@@ -7,7 +7,6 @@ package suwayomi.tachidesk.server.util
* 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 kotlinx.serialization.json.Json
import mu.KotlinLogging
import net.lingala.zip4j.ZipFile
import org.kodein.di.DI
@@ -16,7 +15,6 @@ import org.kodein.di.instance
import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.BuildConfig
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
import java.net.HttpURLConnection
@@ -26,7 +24,6 @@ import java.security.MessageDigest
private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
private val json: Json by injectLazy()
private val tmpDir = System.getProperty("java.io.tmpdir")
private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }

View File

@@ -1,28 +0,0 @@
(function () {
const ORIGINAL_CANVAS = HTMLCanvasElement.prototype[name];
Object.defineProperty(HTMLCanvasElement.prototype, name, {
"value": function () {
var shift = {
'r': Math.floor(Math.random() * 10) - 5,
'g': Math.floor(Math.random() * 10) - 5,
'b': Math.floor(Math.random() * 10) - 5,
'a': Math.floor(Math.random() * 10) - 5
};
var width = this.width,
height = this.height,
context = this.getContext("2d");
var imageData = context.getImageData(0, 0, width, height);
for (var i = 0; i < height; i++) {
for (var j = 0; j < width; j++) {
var n = ((i * (width * 4)) + (j * 4));
imageData.data[n + 0] = imageData.data[n + 0] + shift.r;
imageData.data[n + 1] = imageData.data[n + 1] + shift.g;
imageData.data[n + 2] = imageData.data[n + 2] + shift.b;
imageData.data[n + 3] = imageData.data[n + 3] + shift.a;
}
}
context.putImageData(imageData, 0, 0);
return ORIGINAL_CANVAS.apply(this, arguments);
}
});
})(this);

View File

@@ -1,52 +0,0 @@
Object.defineProperty(window, 'chrome', {
value: new Proxy(window.chrome, {
has: (target, key) => true,
get: (target, key) => {
return {
app: {
isInstalled: false,
},
webstore: {
onInstallStageChanged: {},
onDownloadProgress: {},
},
runtime: {
PlatformOs: {
MAC: 'mac',
WIN: 'win',
ANDROID: 'android',
CROS: 'cros',
LINUX: 'linux',
OPENBSD: 'openbsd',
},
PlatformArch: {
ARM: 'arm',
X86_32: 'x86-32',
X86_64: 'x86-64',
},
PlatformNaclArch: {
ARM: 'arm',
X86_32: 'x86-32',
X86_64: 'x86-64',
},
RequestUpdateCheckStatus: {
THROTTLED: 'throttled',
NO_UPDATE: 'no_update',
UPDATE_AVAILABLE: 'update_available',
},
OnInstalledReason: {
INSTALL: 'install',
UPDATE: 'update',
CHROME_UPDATE: 'chrome_update',
SHARED_MODULE_UPDATE: 'shared_module_update',
},
OnRestartRequiredReason: {
APP_UPDATE: 'app_update',
OS_UPDATE: 'os_update',
PERIODIC: 'periodic',
},
},
}
}
})
});

View File

@@ -1,203 +0,0 @@
(function () {
const plugin0 = Object.create(Plugin.prototype);
const mimeType0 = Object.create(MimeType.prototype);
const mimeType1 = Object.create(MimeType.prototype);
Object.defineProperties(mimeType0, {
type: {
get: () => 'application/pdf',
},
suffixes: {
get: () => 'pdf',
},
});
Object.defineProperties(mimeType1, {
type: {
get: () => 'text/pdf',
},
suffixes: {
get: () => 'pdf',
},
});
Object.defineProperties(plugin0, {
name: {
get: () => 'Chrome PDF Viewer',
},
description: {
get: () => 'Portable Document Format',
},
0: {
get: () => {
return mimeType0;
},
},
1: {
get: () => {
return mimeType1;
},
},
length: {
get: () => 2,
},
filename: {
get: () => 'internal-pdf-viewer',
},
});
const plugin1 = Object.create(Plugin.prototype);
Object.defineProperties(plugin1, {
name: {
get: () => 'Chromium PDF Viewer',
},
description: {
get: () => 'Portable Document Format',
},
0: {
get: () => {
return mimeType0;
},
},
1: {
get: () => {
return mimeType1;
},
},
length: {
get: () => 2,
},
filename: {
get: () => 'internal-pdf-viewer',
},
});
const plugin2 = Object.create(Plugin.prototype);
Object.defineProperties(plugin2, {
name: {
get: () => 'Microsoft Edge PDF Viewer',
},
description: {
get: () => 'Portable Document Format',
},
0: {
get: () => {
return mimeType0;
},
},
1: {
get: () => {
return mimeType1;
},
},
length: {
get: () => 2,
},
filename: {
get: () => 'internal-pdf-viewer',
},
});
const plugin3 = Object.create(Plugin.prototype);
Object.defineProperties(plugin3, {
name: {
get: () => 'PDF Viewer',
},
description: {
get: () => 'Portable Document Format',
},
0: {
get: () => {
return mimeType0;
},
},
1: {
get: () => {
return mimeType1;
},
},
length: {
get: () => 2,
},
filename: {
get: () => 'internal-pdf-viewer',
},
});
const plugin4 = Object.create(Plugin.prototype);
Object.defineProperties(plugin4, {
name: {
get: () => 'WebKit built-in PDF',
},
description: {
get: () => 'Portable Document Format',
},
0: {
get: () => {
return mimeType0;
},
},
1: {
get: () => {
return mimeType1;
},
},
length: {
get: () => 2,
},
filename: {
get: () => 'internal-pdf-viewer',
},
});
const pluginArray = Object.create(PluginArray.prototype);
pluginArray['0'] = plugin0;
pluginArray['1'] = plugin1;
pluginArray['2'] = plugin2;
pluginArray['3'] = plugin3;
pluginArray['4'] = plugin4;
let refreshValue;
Object.defineProperties(pluginArray, {
length: {
get: () => 5,
},
item: {
value: (index) => {
if (index > 4294967295) {
index = index % 4294967296;
}
switch (index) {
case 0:
return plugin3;
case 1:
return plugin0;
case 2:
return plugin1;
case 3:
return plugin2;
case 4:
return plugin4;
default:
break;
}
},
},
refresh: {
get: () => {
return refreshValue;
},
set: (value) => {
refreshValue = value;
},
},
});
Object.defineProperty(Object.getPrototypeOf(navigator), 'plugins', {
get: () => {
return pluginArray;
},
});
})();

View File

@@ -1,170 +0,0 @@
(function () {
window.chrome = {};
window.chrome.app = {
InstallState: {
DISABLED: 'disabled',
INSTALLED: 'installed',
NOT_INSTALLED: 'not_installed',
},
RunningState: {
CANNOT_RUN: 'cannot_run',
READY_TO_RUN: 'ready_to_run',
RUNNING: 'running',
},
getDetails: () => {
'[native code]';
},
getIsInstalled: () => {
'[native code]';
},
installState: () => {
'[native code]';
},
get isInstalled() {
return false;
},
runningState: () => {
'[native code]';
},
};
window.chrome.runtime = {
OnInstalledReason: {
CHROME_UPDATE: 'chrome_update',
INSTALL: 'install',
SHARED_MODULE_UPDATE: 'shared_module_update',
UPDATE: 'update',
},
OnRestartRequiredReason: {
APP_UPDATE: 'app_update',
OS_UPDATE: 'os_update',
PERIODIC: 'periodic',
},
PlatformArch: {
ARM: 'arm',
ARM64: 'arm64',
MIPS: 'mips',
MIPS64: 'mips64',
X86_32: 'x86-32',
X86_64: 'x86-64',
},
PlatformNaclArch: {
ARM: 'arm',
MIPS: 'mips',
MIPS64: 'mips64',
X86_32: 'x86-32',
X86_64: 'x86-64',
},
PlatformOs: {
ANDROID: 'android',
CROS: 'cros',
FUCHSIA: 'fuchsia',
LINUX: 'linux',
MAC: 'mac',
OPENBSD: 'openbsd',
WIN: 'win',
},
RequestUpdateCheckStatus: {
NO_UPDATE: 'no_update',
THROTTLED: 'throttled',
UPDATE_AVAILABLE: 'update_available',
},
connect() {
'[native code]';
},
sendMessage() {
'[native code]';
},
id: undefined,
};
let startE = Date.now();
window.chrome.csi = function () {
'[native code]';
return {
startE: startE,
onloadT: startE + 281,
pageT: 3947.235,
tran: 15,
};
};
window.chrome.loadTimes = function () {
'[native code]';
return {
get requestTime() {
return startE / 1000;
},
get startLoadTime() {
return startE / 1000;
},
get commitLoadTime() {
return startE / 1000 + 0.324;
},
get finishDocumentLoadTime() {
return startE / 1000 + 0.498;
},
get finishLoadTime() {
return startE / 1000 + 0.534;
},
get firstPaintTime() {
return startE / 1000 + 0.437;
},
get firstPaintAfterLoadTime() {
return 0;
},
get navigationType() {
return 'Other';
},
get wasFetchedViaSpdy() {
return true;
},
get wasNpnNegotiated() {
return true;
},
get npnNegotiatedProtocol() {
return 'h3';
},
get wasAlternateProtocolAvailable() {
return false;
},
get connectionInfo() {
return 'h3';
},
};
};
})();
// Bypass OOPIF test
(function performance_memory() {
const jsHeapSizeLimitInt = 4294705152;
const total_js_heap_size = 35244183;
const used_js_heap_size = [
17632315, 17632315, 17632315, 17634847, 17636091, 17636751,
];
let counter = 0;
let MemoryInfoProto = Object.getPrototypeOf(performance.memory);
Object.defineProperties(MemoryInfoProto, {
jsHeapSizeLimit: {
get: () => {
return jsHeapSizeLimitInt;
},
},
totalJSHeapSize: {
get: () => {
return total_js_heap_size;
},
},
usedJSHeapSize: {
get: () => {
if (counter > 5) {
counter = 0;
}
return used_js_heap_size[counter++];
},
},
});
})();

View File

@@ -1,3 +0,0 @@
Object.defineProperty(navigator, 'maxTouchPoints', {
get: () => 1
});

View File

@@ -1,33 +0,0 @@
// https://github.com/microlinkhq/browserless/blob/master/packages/goto/src/evasions/navigator-permissions.js
if (!window.Notification) {
window.Notification = {
permission: 'denied'
}
}
const originalQuery = window.navigator.permissions.query
window.navigator.permissions.__proto__.query = parameters =>
parameters.name === 'notifications'
? Promise.resolve({state: window.Notification.permission})
: originalQuery(parameters)
const oldCall = Function.prototype.call
function call() {
return oldCall.apply(this, arguments)
}
Function.prototype.call = call
const nativeToStringFunctionString = Error.toString().replace(/Error/g, 'toString')
const oldToString = Function.prototype.toString
function functionToString() {
if (this === window.navigator.permissions.query) {
return 'function query() { [native code] }'
}
if (this === functionToString) {
return nativeToStringFunctionString
}
return oldCall.call(oldToString, this)
}
// eslint-disable-next-line
Function.prototype.toString = functionToString

View File

@@ -1,5 +0,0 @@
Object.defineProperty(Navigator.prototype, 'webdriver', {
get() {
return false;
},
});

View File

@@ -18,6 +18,9 @@ server.electronPath = ""
server.downloadAsCbz = false
server.downloadsPath = ""
# updater
server.maxParallelUpdateRequests = 10 # sets how many sources can be updated in parallel. updates are grouped by source and all mangas of a source are updated synchronously
# Authentication
server.basicAuthEnabled = false
server.basicAuthUsername = ""
@@ -26,3 +29,7 @@ server.basicAuthPassword = ""
# misc
server.debugLogsEnabled = false
server.systemTrayEnabled = true
# Webview
server.webviewEnabled = false
server.undetectedChromePath = ""

View File

@@ -13,8 +13,8 @@ class TestUpdater : IUpdater {
private val _status = MutableStateFlow(UpdateStatus())
override val status: StateFlow<UpdateStatus> = _status.asStateFlow()
override fun addMangaToQueue(manga: MangaDataClass) {
updateQueue.add(UpdateJob(manga))
override fun addMangasToQueue(mangas: List<MangaDataClass>) {
mangas.forEach { updateQueue.add(UpdateJob(it)) }
isRunning = true
updateStatus()
}

View File

@@ -10,6 +10,9 @@ server.socksProxyPort = ""
# downloader
server.downloadAsCbz = false
# updater
server.maxParallelUpdateRequests = 10
# misc
server.debugLogsEnabled = true
server.systemTrayEnabled = false