Compare commits

...

29 Commits

Author SHA1 Message Date
Syer10
ef067ef5b9 Release v2.2.2100 2026-05-08 16:48:27 -04:00
Constantin Piber
76686db6a1 [#1974] Uninstall extension completely on install failure (#1975)
* [#1974] Uninstall extension completely on install failure

* Add changelog entry

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2026-05-05 10:04:50 -04:00
lamaxama
6bc5046773 Fix java.lang.VerifyError (#1972)
* Fix EditText.java

* Update CHANGELOG.md
2026-05-05 10:04:13 -04:00
Constantin Piber
1a5cfd8f58 Implement setPixel & setPixels (#1971)
* Implement `setPixel` & `setPixels`

Closes #1970

* Update changelog
2026-05-05 10:03:56 -04:00
schroda
bf76962d23 [skip ci] Add github pr template (#1976) 2026-05-03 17:15:04 -04:00
schroda
a8acca6a38 [skip ci] Update client section in readme (#1977)
Makes it so that the client section is less likely to get outdated and therefore requires less maintenance

Provides only information about how the clients can be run.
The client repo itself is responsible for providing any other information.

Remove clients that have not had any commits in years
2026-05-03 17:14:43 -04:00
schroda
7891c627c1 [skip ci] Update Preview Changelog (#1969) 2026-05-03 13:47:57 -04:00
schroda
ee55145e45 [skip ci] Align changelog with webUI repo changelog (#1968) 2026-05-02 18:16:31 -04:00
schroda
5cda584568 Inject html base tag directly (#1967)
Using a script to inject the base tag is unnecessarily complex as well as it is introducing an issue where the initial requests will potentially fail, due to the base tag not being injected yet.

See https://github.com/Suwayomi/Suwayomi-WebUI/issues/1096, same issue applies when a subpath is set up which can't be fixed on the client side
2026-05-02 17:21:23 -04:00
AwkwardPeak7
031890deb6 extract apk icon (#1966) 2026-05-02 17:21:13 -04:00
ItsGlassPlus1
0f149c9b33 Add JXL container format support (#1951) 2026-05-02 17:21:01 -04:00
manti
41f22df16f Singleton Protobuf (#1961) 2026-05-02 17:20:52 -04:00
Shozikan
a11e5e623d [skip ci] Chore: Added Moku to README & Quick Grammar/Formatting Fixes (#1935)
* Chore: Added Moku to README & Quick Grammar/Formatting Fixes

* Chore: Updated README with Moku Desc Changes
2026-03-31 16:43:51 -04:00
renovate[bot]
489ffa1679 Update dependency io.github.oshai:kotlin-logging-jvm to v8 (#1913)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-20 20:09:45 -05:00
David Brochero
f977d181a8 fix: default body to empty string if not present for FlareSolverr POST requests (#1915)
* fix: convert `RequestBody` to `FormBody` in FlareSolverr `POST` requests

* linting

* ref: don't convert json to form

* remove unused import
2026-02-20 20:09:34 -05:00
David Brochero
2249d237dd fix: support for POST requests on CloudflareInterceptor (#1909)
* fix: support for POST requests

Works with Flaresolverr. Required for Kagane.

Byparr is not a drop-in replacement, it just ignores the `cmd`  and interprets everything as a GET request.

* Use encodeToString instead

* linting

* Use FormBody for encoding

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Add missing imports

* linting, again

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
2026-02-18 18:51:07 -05:00
renovate[bot]
c52457c80e Update dependency com.auth0:java-jwt to v4.5.1 (#1910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:50:22 -05:00
renovate[bot]
3904cbf789 Update plugin download to v5.7.0 (#1908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:50:10 -05:00
renovate[bot]
759ae9fca0 Update moko to v0.26.0 (#1907)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:49:57 -05:00
renovate[bot]
06954591c7 Update dependency org.postgresql:postgresql to v42.7.10 (#1904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:49:47 -05:00
renovate[bot]
bbdae74567 Update dependency net.lingala.zip4j:zip4j to v2.11.6 (#1902)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:49:27 -05:00
renovate[bot]
154e54d833 Update dependency ch.qos.logback:logback-classic to v1.5.32 (#1901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-18 18:49:21 -05:00
Weblate (bot)
f18e0f4a62 Translations update from Hosted Weblate (#1845)
* Weblate translations

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Micka149 <dr.mischutckin2017@yandex.ru>
Co-authored-by: Roland Vezsenyi <miscogd5yf2paqvxvc@farvoid.com>
Co-authored-by: Syer10 <Mitchellptbo@gmail.com>
Co-authored-by: TheRay82 <raycoc1382@gmail.com>
Co-authored-by: UnknownSkyrimPasserby <f7022961@opayq.com>
Co-authored-by: 圭紫 <kaceykoo@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ja/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/pl/
Translate-URL: https://hosted.weblate.org/projects/suwayomi/suwayomi-server/ru/
Translation: Suwayomi/Suwayomi-Server

* Deleted translation using Weblate (Hungarian)

---------

Co-authored-by: Micka149 <dr.mischutckin2017@yandex.ru>
Co-authored-by: Roland Vezsenyi <miscogd5yf2paqvxvc@farvoid.com>
Co-authored-by: Syer10 <Mitchellptbo@gmail.com>
Co-authored-by: TheRay82 <raycoc1382@gmail.com>
Co-authored-by: UnknownSkyrimPasserby <f7022961@opayq.com>
Co-authored-by: 圭紫 <kaceykoo@gmail.com>
2026-02-14 11:38:22 -05:00
Constantin Piber
2b19bc850d Introduce Rect.set (#1900)
* Introduce `Rect.set`

As used by Young Jump+

Also fixes the `Rect(Rect)` constructor to use the correct values

* Missing line
2026-02-14 11:36:28 -05:00
renovate[bot]
a0fb30a3ad Update jte to v3.2.3 (#1862)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 11:36:16 -05:00
renovate[bot]
e5387ff5f7 Update kotlin to v2.3.10 (#1896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 11:35:52 -05:00
renovate[bot]
123d8a2637 Update serialization to v1.10.0 (#1866)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 11:35:38 -05:00
renovate[bot]
6c72659bd8 Update dependency com.android.tools.build:apksig to v9 (#1859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 11:35:30 -05:00
Mitchell Syer
44d89506d4 [skip ci] Fix Wiki PR Check (#1897) 2026-02-08 15:23:40 -05:00
17 changed files with 587 additions and 1878 deletions

View File

@@ -42,7 +42,7 @@ body:
label: Suwayomi-Server version
description: You can find your Suwayomi-Server version in **More → About**.
placeholder: |
Example: "v2.1.1867"
Example: "v2.2.2100"
validations:
required: true

6
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,6 @@
<!--
Pull Request Checklist:
- Mention what the pull request does and the reasons behind the changes
- Mention all issues the pull request is closing
- Make sure to update the CHANGELOG accordingly if necessary based on the LAST stable release
-->

View File

@@ -7,7 +7,7 @@ on:
paths: [docs/**, .github/workflows/wiki.yml]
concurrency:
group: wiki
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
@@ -38,4 +38,4 @@ jobs:
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add .
git diff-index --quiet HEAD || git commit -m "action: wiki sync" && git push
git diff-index --quiet HEAD || git commit -m "action: wiki sync" && git push

View File

@@ -345,6 +345,57 @@ public final class Bitmap {
return image.getRGB(x, y);
}
/**
* <p>Write the specified {@link Color} into the bitmap (assuming it is
* mutable) at the x,y coordinate. The color must be a
* non-premultiplied ARGB value in the {@link ColorSpace.Named#SRGB sRGB}
* color space.</p>
*
* @param x The x coordinate of the pixel to replace (0...width-1)
* @param y The y coordinate of the pixel to replace (0...height-1)
* @param color The ARGB color to write into the bitmap
*
* @throws IllegalStateException if the bitmap is not mutable
* @throws IllegalArgumentException if x, y are outside of the bitmap's
* bounds.
*/
public void setPixel(int x, int y, @ColorInt int color) {
checkPixelAccess(x, y);
image.setRGB(x, y, color);
}
/**
* <p>Replace pixels in the bitmap with the colors in the array. Each element
* in the array is a packed int representing a non-premultiplied ARGB
* {@link Color} in the {@link ColorSpace.Named#SRGB sRGB} color space.</p>
*
* @param pixels The colors to write to the bitmap
* @param offset The index of the first color to read from pixels[]
* @param stride The number of colors in pixels[] to skip between rows.
* Normally this value will be the same as the width of
* the bitmap, but it can be larger (or negative).
* @param x The x coordinate of the first pixel to write to in
* the bitmap.
* @param y The y coordinate of the first pixel to write to in
* the bitmap.
* @param width The number of colors to copy from pixels[] per row
* @param height The number of rows to write to the bitmap
*
* @throws IllegalStateException if the bitmap is not mutable
* @throws IllegalArgumentException if x, y, width, height are outside of
* the bitmap's bounds.
* @throws ArrayIndexOutOfBoundsException if the pixels array is too small
* to receive the specified number of pixels.
*/
public void setPixels(@NonNull @ColorInt int[] pixels, int offset, int stride,
int x, int y, int width, int height) {
if (width == 0 || height == 0) {
return; // nothing to do
}
checkPixelsAccess(x, y, width, height, offset, stride, pixels);
image.setRGB(x, y, width, height, pixels, offset, stride);
}
public void eraseColor(int c) {
java.awt.Color color = Color.valueOf(c).toJavaColor();
Graphics2D graphics = image.createGraphics();

View File

@@ -37,13 +37,27 @@ public final class Rect {
this.right = 0;
this.bottom = 0;
} else {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = r.left;
this.top = r.top;
this.right = r.right;
this.bottom = r.bottom;
}
}
public void set(int left, int top, int right, int bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public void set(Rect r) {
this.left = r.left;
this.top = r.top;
this.right = r.right;
this.bottom = r.bottom;
}
public final int getWidth() {
return right - left;
}

View File

@@ -7,14 +7,26 @@ package android.widget;
* 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/. */
public class EditText {
public EditText(android.content.Context context) { throw new RuntimeException("Stub!"); }
public class EditText extends TextView {
public EditText(android.content.Context context) {
super(context);
throw new RuntimeException("Stub!");
}
public EditText(android.content.Context context, android.util.AttributeSet attrs) { throw new RuntimeException("Stub!"); }
public EditText(android.content.Context context, android.util.AttributeSet attrs) {
super(context);
throw new RuntimeException("Stub!");
}
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) { throw new RuntimeException("Stub!"); }
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr) {
super(context);
throw new RuntimeException("Stub!");
}
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) { throw new RuntimeException("Stub!"); }
public EditText(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
throw new RuntimeException("Stub!");
}
public boolean getFreezesText() { throw new RuntimeException("Stub!"); }

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,12 @@
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| ![CI](https://github.com/Suwayomi/Suwayomi-Server/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Suwayomi-Server.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Suwayomi-Server/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Suwayomi-Server-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Suwayomi-Server-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) |
## Table of Content
## Table of Contents
- [What is Suwayomi?](#what-is-suwayomi)
- [Features](#features)
- [Suwayomi client projects](#suwayomi-client-projects)
- [Actively Developed Clients](#actively-developed-clients)
- [Inactive Clients (functional but outdated)](#inactive-clients-functional-but-outdated)
- [Abandoned Clients (functionality unknown)](#abandoned-clients-functionality-unknown)
- [Integrated clients](#integrated-clients)
- [Other clients](#other-clients-potentially-inactive-or-abondend)
- [Downloading and Running the app](#downloading-and-running-the-app)
- [Using Operating System Specific Bundles](#using-operating-system-specific-bundles)
- [Windows](#windows)
@@ -38,7 +37,7 @@
# What is Suwayomi?
<img src="https://github.com/Suwayomi/Suwayomi-Server/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
A free and open source manga reader server that runs extensions built for [Mihon (Tachiyomi)](https://mihon.app/).
A free and open source manga reader server that runs extensions built for [Mihon (Tachiyomi)](https://mihon.app/).
Suwayomi is an independent Mihon (Tachiyomi) compatible software and is **not a Fork of** Mihon (Tachiyomi).
@@ -65,21 +64,24 @@ You can use Mihon (Tachiyomi) to access your Suwayomi-Server. For more info look
- Automated WebUI updates (supports the default WebUI and VUI)
- OPDS and OPDS-PSE support (endpoint: `/api/opds/v1.2`)
# Suwayomi client projects
# Suwayomi Client Projects
**You need a client/user interface app as a front-end for Suwayomi-Server, if you [Directly Download Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server/releases/latest) you'll get a bundled version of [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI) with it.**
Here's a list of known clients/user interfaces for Suwayomi-Server (checkout the respective GitHub repository for their features):
##### Actively Developed Clients
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): The web front-end that Suwayomi-Server ships with by default.
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): A Suwayomi-Server preview focused web frontend built with svelte
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): A Web front-end for Suwayomi-Server built with Vaadin.
##### Inactive Clients (functional but outdated)
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The native desktop front-end for Suwayomi-Server.
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): A Flutter front-end for Desktop(Linux, windows, etc.), Web and Android with a User Interface inspired by Mihon (Tachiyomi).
##### Abandoned Clients (functionality unknown)
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), feature support is basic.
- [Tachidesk-GTK](https://github.com/mahor1221/Tachidesk-GTK): A native Rust/GTK desktop client.
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js.
##### Integrated clients
These clients are built-in options, and the server can keep them automatically up-to-date.
- [Suwayomi-WebUI](https://github.com/Suwayomi/Suwayomi-WebUI): Web app, PWA
- [Suwayomi-VUI](https://github.com/Suwayomi/Suwayomi-VUI): Web app, PWA
##### Other clients (potentially inactive or abondend)
- [Tachidesk-VaadinUI](https://github.com/Suwayomi/Tachidesk-VaadinUI): Desktop app (windows, linux, mac); UI in the browser, manages its own suwayomi server instance
- [Moku](https://github.com/Youwes09/Moku): Desktop app (windows, linux, mac), requires access to a running server
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): Desktop app (windows, linux, mac); can manage its own suwayomi server instance
- [Tachidesk-Sorayomi](https://github.com/Suwayomi/Tachidesk-Sorayomi): Web app; Desktop app (windows, linux, mac); Android app; requires access to a running server
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): Android app; iOS app Desktop app (linux); requires access to a running server
# Downloading and Running the app
## Using Operating System Specific Bundles

View File

@@ -10,9 +10,9 @@ import java.io.BufferedReader
const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release
val getTachideskVersion = { "v2.1.${getCommitCount()}" }
val getTachideskVersion = { "v2.2.${getCommitCount()}" }
val webUIRevisionTag = "r2643"
val webUIRevisionTag = "r3136"
private val getCommitCount = {
runCatching {

View File

@@ -1,11 +1,11 @@
[versions]
kotlin = "2.3.0"
kotlin = "2.3.10"
coroutines = "1.10.2"
serialization = "1.9.0"
serialization = "1.10.0"
jvmTarget = "21"
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
javalin = "6.7.0"
jte = "3.2.1"
jte = "3.2.3"
jackson = "2.18.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "0.61.0"
dex2jar = "2.4.34"
@@ -16,7 +16,7 @@ graphqlkotlin = "8.8.1"
xmlserialization = "0.91.3"
ktlint = "1.8.0"
koin = "4.1.1"
moko = "0.25.2"
moko = "0.26.0"
[libraries]
# Kotlin
@@ -38,8 +38,8 @@ serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", v
# Logging
slf4japi = "org.slf4j:slf4j-api:2.0.17"
logback = "ch.qos.logback:logback-classic:1.5.28"
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:7.0.14"
logback = "ch.qos.logback:logback-classic:1.5.32"
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.01"
# OkHttp
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
@@ -68,7 +68,7 @@ exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "e
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
postgres = "org.postgresql:postgresql:42.7.9"
postgres = "org.postgresql:postgresql:42.7.10"
h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration
hikaricp = "com.zaxxer:HikariCP:7.0.2"
@@ -105,7 +105,7 @@ dex2jar-tools = { module = "de.femtopedia.dex2jar:dex-tools", version.ref = "dex
# APK
apk-parser = "net.dongliu:apk-parser:2.6.10"
apksig = "com.android.tools.build:apksig:8.13.2"
apksig = "com.android.tools.build:apksig:9.0.1"
# Xml
xmlpull = "xmlpull:xmlpull:1.1.3.4a"
@@ -113,7 +113,7 @@ xmlpull = "xmlpull:xmlpull:1.1.3.4a"
# Disk & File
appdirs = "ca.gosyer:kotlin-multiplatform-appdirs:2.0.0"
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
zip4j = "net.lingala.zip4j:zip4j:2.11.5"
zip4j = "net.lingala.zip4j:zip4j:2.11.6"
commonscompress = "org.apache.commons:commons-compress:1.28.0"
junrar = "com.github.junrar:junrar:7.5.7"
@@ -158,7 +158,7 @@ cronUtils = "com.cronutils:cron-utils:9.2.1"
kcef = "dev.datlag:kcef:2024.04.20.4"
# User
jwt = "com.auth0:java-jwt:4.5.0"
jwt = "com.auth0:java-jwt:4.5.1"
# lint - used for renovate to update ktlint version
ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
@@ -179,7 +179,7 @@ ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "14.0.1"}
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "6.0.7"}
# Download
download = { id = "de.undercouch.download", version = "5.6.0"}
download = { id = "de.undercouch.download", version = "5.7.0"}
# ShadowJar
shadowjar = { id = "com.gradleup.shadow", version = "8.3.9"}

View File

@@ -53,4 +53,11 @@
<string name="manga_status_licensed">正式版</string>
<string name="manga_status_publishing_finished">連載終了</string>
<string name="manga_status_cancelled">打ち切り</string>
<string name="opds_feeds_history_title">履歴</string>
<string name="opds_feeds_history_entry_content">最近読んだ章</string>
<string name="opds_feeds_all_series_in_library_title">すべてのマンガ</string>
<string name="opds_feeds_all_series_in_library_entry_content">ライブラリに保存されたマンガを閲覧</string>
<string name="opds_feeds_library_sources_title">ソース</string>
<string name="opds_feeds_library_sources_entry_content">ソース別にライブラリ内のマンガを閲覧</string>
<string name="opds_feeds_search_results_title">検索結果</string>
</resources>

View File

@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="opds_search_description">Wyszukiwanie mangi w katalogu</string>
<string name="opds_search_description">Wyszukaj serie w katalogu.</string>
<string name="manga_status_on_hiatus">Zawieszone</string>
<string name="opds_feeds_genre_specific_title">Gatunek: %1$s</string>
<string name="opds_feeds_chapter_details">%1$s | %2$s | Szczegóły</string>
<string name="opds_chapter_details_base">%1$s | %2$s</string>
<string name="opds_feeds_library_updates_title">Historia Aktualizacji Biblioteki</string>
<string name="opds_feeds_categories_entry_content">Przeglądaj mangi uporządkowane według kategorii</string>
<string name="opds_feeds_categories_entry_content">Przeglądaj serie uporządkowane według kategorii</string>
<string name="opds_chapter_status_downloaded">⬇️</string>
<string name="opds_linktitle_self_feed">Aktualny Kanał</string>
<string name="opds_chapter_status_unread"></string>
@@ -14,11 +14,11 @@
<string name="opds_feeds_manga_chapters">%1$s Rozdziały</string>
<string name="opds_search_shortname">Suwayomi Wyszukiwanie OPDS</string>
<string name="opds_feeds_root">Suwayomi Katalog OPDS</string>
<string name="opds_feeds_sources_title">Źródła</string>
<string name="opds_feeds_sources_title">Wszystkie Źródła</string>
<string name="opds_feeds_genres_title">Gatunki</string>
<string name="opds_feeds_status_title">Status</string>
<string name="opds_feeds_languages_title">Języki</string>
<string name="opds_feeds_languages_entry_content">Przeglądaj mangi według języka treści</string>
<string name="opds_feeds_languages_entry_content">Przeglądaj serie według języka treści</string>
<string name="opds_feeds_library_updates_entry_content">Ostatnio zaktualizowane rozdziały z biblioteki</string>
<string name="opds_feeds_category_specific_title">Kategoria: %1$s</string>
<string name="opds_feeds_status_specific_title">Status: %1$s</string>
@@ -32,8 +32,8 @@
<string name="opds_facet_sort_date_asc">Data rosnąco</string>
<string name="opds_facet_sort_date_desc">Data malejąco</string>
<string name="opds_facet_filter_all_chapters">Wszystkie Rozdziały</string>
<string name="opds_facet_filter_unread_only">Tylko Nieprzeczytane</string>
<string name="opds_facet_filter_read_only">Tylko Przeczytane</string>
<string name="opds_facet_filter_unread_only">Nieprzeczytane</string>
<string name="opds_facet_filter_read_only">Przeczytane</string>
<string name="opds_linktitle_view_chapter_details">Wyświetl Szczegóły Rozdziału i Pobierz Strony</string>
<string name="opds_linktitle_download_cbz">Pobierz CBZ</string>
<string name="opds_linktitle_chapter_cover">Okładka Rozdziału</string>
@@ -51,11 +51,29 @@
<string name="manga_status_publishing_finished">Publikacja Zakończona</string>
<string name="manga_status_cancelled">Anulowano</string>
<string name="opds_feeds_categories_title">Kategorie</string>
<string name="opds_feeds_genres_entry_content">Przeglądaj mangi według tagów gatunku</string>
<string name="opds_feeds_status_entry_content">Przeglądaj mangi według statusu publikacji</string>
<string name="opds_feeds_genres_entry_content">Przeglądaj serie według tagów gatunku</string>
<string name="opds_feeds_status_entry_content">Przeglądaj serie według statusu publikacji</string>
<string name="opds_feeds_source_specific_popular_title">Źródło: %1$s - Popularne</string>
<string name="opds_feeds_library_source_specific_title">Biblioteka - Źródło: %1$s</string>
<string name="opds_feeds_source_specific_latest_title">Źródło: %1$s - Ostatnie</string>
<string name="opds_feeds_search_results_title">Wyniki Wyszukiwania</string>
<string name="opds_feeds_history_title">Historia</string>
<string name="opds_feeds_explore_title">Odkrywaj</string>
<string name="opds_feeds_explore_entry_content">Odkryj nowe serie ze swoich źródeł</string>
<string name="opds_feeds_history_entry_content">Ostatnio przeczytane rozdziały</string>
<string name="opds_feeds_all_series_in_library_title">Wszystkie serie</string>
<string name="opds_feeds_all_series_in_library_entry_content">Przeglądaj wszystkie serie zapisane w bibliotece</string>
<string name="opds_feeds_library_sources_title">Źródła</string>
<string name="opds_feeds_library_sources_entry_content">Przeglądaj serie w swojej bibliotece filtrowane według źródła</string>
<string name="opds_facet_sort_popular">Popularność</string>
<string name="opds_facet_sort_latest">Najnowsze</string>
<string name="opds_facet_sort_alpha_asc">Alfabetycznie od A do Z</string>
<string name="opds_facet_sort_alpha_desc">Alfabetycznie Z-A</string>
<string name="opds_facet_sort_last_read_desc">Ostatnio czytane</string>
<string name="opds_facet_sort_latest_chapter_desc">Najnowszy rozdział</string>
<string name="opds_facet_sort_date_added_desc">Data dodania</string>
<string name="opds_facet_sort_unread_desc">Nieprzeczytane rozdziały</string>
<string name="opds_facet_filter_all">Wszystkie</string>
<string name="opds_facet_filter_downloaded">Pobrane</string>
<string name="opds_facet_filter_ongoing">Trwające</string>
</resources>

View File

@@ -71,7 +71,7 @@ fun createAppModule(app: Application): Module {
}
}
single {
single<ProtoBuf> {
ProtoBuf
}
}

View File

@@ -14,9 +14,9 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Cookie
import okhttp3.FormBody
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
@@ -24,6 +24,7 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okio.Buffer
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.injectLazy
import java.io.IOException
@@ -70,7 +71,8 @@ class CloudflareInterceptor(
flareResponse.solution.status in 200..299 &&
flareResponse.solution.response != null
) {
val isImage = flareResponse.solution.response.contains(CHROME_IMAGE_TEMPLATE_REGEX)
val isImage =
flareResponse.solution.response.contains(CHROME_IMAGE_TEMPLATE_REGEX)
if (!isImage) {
logger.debug { "Falling back to FlareSolverr response" }
@@ -87,7 +89,8 @@ class CloudflareInterceptor(
}
}
val request = CFClearance.requestWithFlareSolverr(flareResponse, setUserAgent, originalRequest)
val request =
CFClearance.requestWithFlareSolverr(flareResponse, setUserAgent, originalRequest)
chain.proceed(request)
} catch (e: Exception) {
@@ -187,7 +190,6 @@ object CFClearance {
onlyCookies: Boolean,
): FlareSolverResponse {
val timeout = serverConfig.flareSolverrTimeout.value.seconds
return with(json) {
mutex.withLock {
client.value
@@ -198,7 +200,7 @@ object CFClearance {
Json
.encodeToString(
FlareSolverRequest(
"request.get",
"request.${originalRequest.method.lowercase()}",
originalRequest.url.toString(),
session = serverConfig.flareSolverrSessionName.value,
sessionTtlMinutes = serverConfig.flareSolverrSessionTtl.value,
@@ -208,6 +210,22 @@ object CFClearance {
},
returnOnlyCookies = onlyCookies,
maxTimeout = timeout.inWholeMilliseconds.toInt(),
postData =
if (originalRequest.method == "POST") {
when (val body = originalRequest.body) {
is FormBody -> {
Buffer()
.also { body.writeTo(it) }
.readUtf8()
}
else -> {
""
}
}
} else {
null
},
),
).toRequestBody(jsonMediaType),
),
@@ -238,7 +256,9 @@ object CFClearance {
if (!cookie.path.isNullOrEmpty()) it.path(cookie.path)
// We need to convert the expires time to milliseconds for the persistent cookie store
if (cookie.expires != null && cookie.expires > 0) it.expiresAt((cookie.expires * 1000).toLong())
if (!cookie.domain.startsWith('.')) it.hostOnlyDomain(cookie.domain.removePrefix("."))
if (!cookie.domain.startsWith('.')) {
it.hostOnlyDomain(cookie.domain.removePrefix("."))
}
}.build()
}.groupBy { it.domain }
.flatMap { (domain, cookies) ->

View File

@@ -14,6 +14,8 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import io.github.oshai.kotlinlogging.KotlinLogging
import net.dongliu.apk.parser.ApkFile
import net.dongliu.apk.parser.bean.Icon
import okhttp3.CacheControl
import okio.buffer
import okio.sink
@@ -37,7 +39,9 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
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.ImageResponse.saveImage
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.ApplicationDirs
@@ -115,7 +119,6 @@ object Extension {
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
val jarFilePath = "$dirPathWithoutType.jar"
val dexFilePath = "$dirPathWithoutType.dex"
val packageInfo = getPackageInfo(apkFilePath)
val pkgName = packageInfo.packageName
@@ -155,79 +158,115 @@ object Extension {
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
extractAssetsFromApk(apkFilePath, jarFilePath)
extractAndCacheApkIcon(apkFilePath, apkName)
// clean up
File(apkFilePath).delete()
File(dexFilePath).delete()
// collect sources from the extension
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
val sources: List<CatalogueSource> =
when (extensionMainClassInstance) {
is Source -> listOf(extensionMainClassInstance)
is SourceFactory -> extensionMainClassInstance.createSources()
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
}.map { it as CatalogueSource }
try {
// collect sources from the extension
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
val sources: List<CatalogueSource> =
when (extensionMainClassInstance) {
is Source -> listOf(extensionMainClassInstance)
is SourceFactory -> extensionMainClassInstance.createSources()
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
}.map { it as CatalogueSource }
val langs = sources.map { it.lang }.toSet()
val extensionLang =
when (langs.size) {
0 -> ""
1 -> langs.first()
else -> "all"
}
val langs = sources.map { it.lang }.toSet()
val extensionLang =
when (langs.size) {
0 -> ""
1 -> langs.first()
else -> "all"
}
val extensionName =
packageInfo.applicationInfo.nonLocalizedLabel
.toString()
.substringAfter("Tachiyomi: ")
val extensionName =
packageInfo.applicationInfo.nonLocalizedLabel
.toString()
.substringAfter("Tachiyomi: ")
// update extension info
transaction {
if (ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
ExtensionTable.insert {
// update extension info
transaction {
if (ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
ExtensionTable.insert {
it[this.apkName] = apkName
it[name] = extensionName
it[this.pkgName] = packageInfo.packageName
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode
it[lang] = extensionLang
it[this.isNsfw] = isNsfw
}
}
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[this.apkName] = apkName
it[name] = extensionName
it[this.pkgName] = packageInfo.packageName
it[this.isInstalled] = true
it[this.classFQName] = className
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode
it[lang] = extensionLang
it[this.isNsfw] = isNsfw
}
val extensionId =
ExtensionTable
.selectAll()
.where { ExtensionTable.pkgName eq pkgName }
.first()[ExtensionTable.id]
.value
sources.forEach { httpSource ->
SourceTable.insert {
it[id] = httpSource.id
it[name] = httpSource.name
it[lang] = httpSource.lang
it[extension] = extensionId
it[SourceTable.isNsfw] = isNsfw
}
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
}
}
return 201 // we installed successfully
} catch (e: Throwable) {
// free up the file descriptor if exists
PackageTools.jarLoaderMap.remove(jarFilePath)?.close()
File(jarFilePath).delete()
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[this.apkName] = apkName
it[this.isInstalled] = true
it[this.classFQName] = className
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode
}
val extensionId =
ExtensionTable
.selectAll()
.where { ExtensionTable.pkgName eq pkgName }
.first()[ExtensionTable.id]
.value
sources.forEach { httpSource ->
SourceTable.insert {
it[id] = httpSource.id
it[name] = httpSource.name
it[lang] = httpSource.lang
it[extension] = extensionId
it[SourceTable.isNsfw] = isNsfw
}
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
}
uninstallExtension(pkgName)
throw e
}
return 201 // we installed successfully
} else {
return 302 // extension was already installed
}
}
private fun extractAndCacheApkIcon(
apkFilePath: String,
apkName: String,
) {
val iconCacheDir = "${applicationDirs.extensionsRoot}/icon"
try {
val iconData =
ApkFile(File(apkFilePath)).use { apk ->
apk.allIcons
.filterIsInstance<Icon>()
.mapNotNull { it.data?.let { data -> data to it.density } }
.maxByOrNull { (_, density) -> density }
?.first
}
if (iconData == null) {
logger.warn { "No icon found in APK $apkName" }
return
}
File(iconCacheDir).mkdirs()
clearCachedImage(iconCacheDir, apkName)
saveImage("$iconCacheDir/$apkName", iconData.inputStream(), null)
} catch (e: Exception) {
logger.warn(e) { "Failed to extract icon from APK $apkName" }
}
}
private fun extractAssetsFromApk(
apkPath: String,
jarPath: String,

View File

@@ -71,6 +71,9 @@ object ImageUtil {
if (bytes.compareWith(charByteArrayOf(0xFF, 0x0A))) {
return JXL
}
if (bytes.compareWith(charByteArrayOf(0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A))) {
return JXL
}
} catch (_: Exception) {
}
return null

View File

@@ -206,20 +206,12 @@ object WebInterfaceManager {
if (ServerSubpath.isDefined() && orgIndexHtml.exists()) {
val originalIndexHtml = orgIndexHtml.readText()
val subpathInjectionScript =
"""
<script>
// <<suwayomi-subpath-injection>>
const baseTag = document.createElement('base');
baseTag.href = location.origin + "${ServerSubpath.asRootPath()}";
document.head.appendChild(baseTag);
</script>
""".trimIndent()
val subpathInjectionBaseTag = "<base href=\"${ServerSubpath.asRootPath()}\">"
val indexHtmlWithSubpathInjection =
originalIndexHtml.replace(
"<head>",
"<head>$subpathInjectionScript",
"<head>$subpathInjectionBaseTag",
)
orgIndexHtml.writeText(indexHtmlWithSubpathInjection)