mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-08 05:14:37 -05:00
Compare commits
5 Commits
264b646297
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bc75d5b00 | ||
|
|
fe6dd05411 | ||
|
|
57c0a85a35 | ||
|
|
c2c927ae97 | ||
|
|
2c70700bb7 |
4
.github/workflows/build_pull_request.yml
vendored
4
.github/workflows/build_pull_request.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
export LD_PRELOAD="$(pwd)/scripts/resources/catch_abort.so"
|
||||
JAR=$(ls ./server/build/*.jar| head -1)
|
||||
set +e
|
||||
timeout 30s java \
|
||||
timeout 30s java -DcrashOnFailedMigration=true \
|
||||
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
||||
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
||||
-Dsuwayomi.tachidesk.config.server.databaseType=POSTGRESQL \
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
exit "$ecode"
|
||||
fi
|
||||
|
||||
timeout 30s java \
|
||||
timeout 30s java -DcrashOnFailedMigration=true \
|
||||
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
|
||||
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
|
||||
-jar "$JAR"
|
||||
|
||||
1
.github/workflows/wiki.yml
vendored
1
.github/workflows/wiki.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: GitHub Wiki upload
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
17
CHANGELOG.md
17
CHANGELOG.md
@@ -10,20 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- .
|
||||
|
||||
### Changed
|
||||
- (Database/H2) Use the latest H2 database engine
|
||||
- (Startup) Crash on startup if an unrecoverable error happens
|
||||
- .
|
||||
|
||||
### Fixed
|
||||
- (**CloudFlareInterceptor**) Don't send the `cf_clearance` cookie back to Flaresolverr
|
||||
- (**WebUI**) Handle serving non-default webui with "bundled"
|
||||
- (**WebUI**) Wait until WebUI is ready to open in browser
|
||||
- (**Downloads**) Truncate filenames by byte length to prevent "File name too long" IO errors
|
||||
- (**Downloads**) Fix being unable to find downloads after manga was renamed during an update
|
||||
- (**Downloads**) Fix preserving chapter download states during an update
|
||||
- (**Extension**) Do not indicate an update is available when the extension is not installed
|
||||
- (**Chapter**) Fix losing chapter data on failed chapter list update
|
||||
- (**Chapter**) Fix database error when fetching chapter updates
|
||||
- (**Manga/API**) Fix "mangas" graphql query with active sorting and using a postgresql database (QUERY "mangas")
|
||||
- (CloudFlareInterceptor) Don't send the `cf_clearance` cookie back to Flaresolverr
|
||||
- (WebUI) Handle serving non-default webui with "bundled"
|
||||
- (WebUI) Wait until WebUI is ready to open in browser
|
||||
- (Downloads) Truncate filenames by byte length to prevent "File name too long" IO errors
|
||||
|
||||
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08
|
||||
|
||||
|
||||
@@ -1,69 +1,19 @@
|
||||
# Troubleshooting
|
||||
|
||||
This page is laid out in several sections, where each section describes a specific problem, followed by one or more possible solutions.
|
||||
|
||||
At the end, you will find a General section, which is the nuclear option if nothing else works.
|
||||
|
||||
For further support, visit the [official Suwayomi Discord server](https://discord.gg/DDZdqZWaHA).
|
||||
In such cases, it will be helpful to have logs ready. You can find them in [The Data Directory](./The-Data-Directory) in the logs directory.
|
||||
|
||||
**All steps below assume that you have stopped Suwayomi**.
|
||||
|
||||
|
||||
## Broken database
|
||||
|
||||
- `failed due to
|
||||
org.jetbrains.exposed.exceptions.ExposedSQLException: org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "CATEGORY.SORT_ORDER" not found`
|
||||
- `org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "CHAPTER.KOREADER_HASH" not found`
|
||||
- `java.lang.IllegalStateException: Unable to read the page at position 96170708817765466`
|
||||
- Any other error text that includes "SQL Statement"
|
||||
|
||||
Your database is either corrupted or incompatible.
|
||||
|
||||
One of these is the cause:
|
||||
- You were running a preview version and decided to downgrade to stable.
|
||||
- You did not shut down Suwayomi properly.
|
||||
- Suwayomi crashed in an unexpected way.
|
||||
|
||||
Solutions:
|
||||
- If you downgraded, upgrade to preview again.
|
||||
- Otherwise, you will need to reset and restore from a backup. See [General Troubleshooting](#general-troubleshooting) below.
|
||||
|
||||
|
||||
## `HTTP error 429`
|
||||
|
||||
The source (or, if trackers are enabled, possibly the tracker) has blocked you for sending too many requests.
|
||||
Note that Mass-Migration can result in an unexpectedly high number of requests to both the source and any configured trackers.
|
||||
|
||||
Solution: Use other/more sources, download less, and wait between request-heavy actions.
|
||||
|
||||
|
||||
## Extension times out
|
||||
|
||||
- `Timed out waiting for 20000 ms…`
|
||||
- `Timed out waiting for page list`
|
||||
|
||||
First, check if this is an extension issue or a Suwayomi issue.
|
||||
On the manga page of the problematic entry, click "Open in WebView".
|
||||
|
||||
Solutions:
|
||||
- If the WebView loads: The issue is with the extension. Search [the issues](https://github.com/Suwayomi/Suwayomi-Server/issues) and discord if there are known problems with that extension.
|
||||
- If the WebView errors: Go to [The Data Directory](./The-Data-Directory) and remove the `bin` and `cache` folders.
|
||||
- If the WebView still does not work after a restart, your installation is incomplete. On Linux, refer to [the README](https://github.com/Suwayomi/Suwayomi-Server#webview-support-gnulinux).
|
||||
|
||||
|
||||
## General Troubleshooting
|
||||
This guide will try to fix Suwayomi by reseting it to a clean installation state.
|
||||
|
||||
> [!WARNING]
|
||||
> This will remove all your data, including the library.
|
||||
> Make sure you have copied your backups as described above!
|
||||
|
||||
- Make sure you have a recent backup of your library or create one in the app (if possible) because we **are going to wipe all Suwayomi data**.
|
||||
- Make sure Suwayomi is not running (right click on tray icon and quit or kill it through the way your Operating System provides)
|
||||
- Clear all browsing data on your browser if you use Suwayomi from a browser.
|
||||
- Delete the Suwayomi data directory located below and re-run the app. See the article [The Data Directory](./The-Data-Directory) for information on how to find it.
|
||||
- If you wish to keep your downloads, you may also attempt to surgically remove only parts. You will need to remove `database.mv.db`, `database.trace.db`, `bin`, `cache`, `extensions`, `settings`, `webUI`. Removing only a subset of these files and folders may fail to resolve the problem.
|
||||
- Open Suwayomi and go to Settings > Backup > Restore Backup, and select the latest backup you have.
|
||||
- Restoring from backup does not restore your downloads. If you chose to keep them in the above step, you will now need to re-download all manga. Suwayomi will pick up on the existing files and not actually download anything that isn't new.
|
||||
- Delete the Suwayomi data directory located below and re-run the app.
|
||||
|
||||
Note: Replace `<Account>` with the currently logged in account/username on your pc.
|
||||
|
||||
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
||||
|
||||
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
|
||||
|
||||
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
|
||||
|
||||
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
||||
|
||||
- In the case that you have to periodically perform this fix or the problem persists or the method failed to fix it, open an issue or Join the [Suwayomi discord server](https://discord.gg/DDZdqZWaHA) to hang out with the community and to receive support and help.
|
||||
|
||||
@@ -4,7 +4,7 @@ coroutines = "1.11.0"
|
||||
serialization = "1.11.0"
|
||||
jvmTarget = "21"
|
||||
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
|
||||
javalin = "7.2.2"
|
||||
javalin = "7.2.0"
|
||||
jte = "3.2.4"
|
||||
jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
exposed = "1.2.0"
|
||||
@@ -37,9 +37,9 @@ serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core", version.r
|
||||
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
|
||||
|
||||
# Logging
|
||||
slf4japi = "org.slf4j:slf4j-api:2.0.18"
|
||||
slf4japi = "org.slf4j:slf4j-api:2.0.17"
|
||||
logback = "ch.qos.logback:logback-classic:1.5.32"
|
||||
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.03"
|
||||
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02"
|
||||
|
||||
# OkHttp
|
||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
@@ -116,7 +116,7 @@ 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.6"
|
||||
commonscompress = "org.apache.commons:commons-compress:1.28.0"
|
||||
junrar = "com.github.junrar:junrar:7.6.0"
|
||||
junrar = "com.github.junrar:junrar:7.5.10"
|
||||
|
||||
# AES/CBC/PKCS7Padding Cypher provider
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
|
||||
@@ -88,6 +88,4 @@ object SettingsRegistry {
|
||||
fun get(name: String): SettingMetadata? = settings[name]
|
||||
|
||||
fun getAll(): Map<String, SettingMetadata> = settings.toMap()
|
||||
|
||||
fun clear() = settings.clear()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
* Metadata storage for clients, server/global level.
|
||||
*/
|
||||
object GlobalMetaTable : IntIdTable() {
|
||||
val key = varchar("meta_key", 256)
|
||||
val key = varchar("key", 256)
|
||||
val value = varchar("value", 4096)
|
||||
}
|
||||
|
||||
@@ -15,11 +15,9 @@ import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.greater
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.core.inSubQuery
|
||||
import org.jetbrains.exposed.v1.core.less
|
||||
import org.jetbrains.exposed.v1.core.like
|
||||
import org.jetbrains.exposed.v1.jdbc.select
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
@@ -245,16 +243,13 @@ class MangaQuery {
|
||||
): MangaNodeList {
|
||||
val queryResults =
|
||||
transaction {
|
||||
val mangaIdsQuery =
|
||||
val res =
|
||||
MangaTable
|
||||
.leftJoin(CategoryMangaTable)
|
||||
.select(MangaTable.id)
|
||||
.withDistinct()
|
||||
.select(MangaTable.columns)
|
||||
.withDistinctOn(MangaTable.id)
|
||||
|
||||
mangaIdsQuery.applyOps(condition, filter)
|
||||
|
||||
val res =
|
||||
MangaTable.selectAll().where { MangaTable.id inSubQuery mangaIdsQuery }
|
||||
res.applyOps(condition, filter)
|
||||
|
||||
if (order != null || orderBy != null || (last != null || before != null)) {
|
||||
val baseSort = listOf(MangaOrder(MangaOrderBy.ID, SortOrder.ASC))
|
||||
|
||||
@@ -236,7 +236,7 @@ object Chapter {
|
||||
val deletedChapterNumbers = TreeSet<Float>()
|
||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
||||
val deletedDownloadedChapterNumberToChapter = mutableMapOf<Float, ChapterDataClass>()
|
||||
val deletedDownloadedChapterNumberInfoMap = mutableMapOf<Float, MutableMap<String?, Int>>()
|
||||
val deletedChapterNumberDateFetchMap = mutableMapOf<Float, Long>()
|
||||
|
||||
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
||||
@@ -247,7 +247,13 @@ object Chapter {
|
||||
if (!chapterUrls.contains(dbChapter.url)) {
|
||||
if (dbChapter.read) deletedReadChapterNumbers.add(dbChapter.chapterNumber)
|
||||
if (dbChapter.bookmarked) deletedBookmarkedChapterNumbers.add(dbChapter.chapterNumber)
|
||||
if (dbChapter.downloaded) deletedDownloadedChapterNumberToChapter[dbChapter.chapterNumber] = dbChapter
|
||||
if (dbChapter.downloaded) {
|
||||
val pageCountByScanlator =
|
||||
deletedDownloadedChapterNumberInfoMap.getOrPut(
|
||||
dbChapter.chapterNumber,
|
||||
) { mutableMapOf() }
|
||||
pageCountByScanlator[dbChapter.scanlator] = dbChapter.pageCount
|
||||
}
|
||||
deletedChapterNumbers.add(dbChapter.chapterNumber)
|
||||
deletedChapterNumberDateFetchMap[dbChapter.chapterNumber] = dbChapter.fetchedAt
|
||||
dbChapter.id
|
||||
@@ -256,14 +262,16 @@ object Chapter {
|
||||
}
|
||||
}
|
||||
|
||||
transaction {
|
||||
// we got some clean up due
|
||||
if (chaptersIdsToDelete.isNotEmpty()) {
|
||||
DownloadManager.dequeue(chaptersIdsToDelete)
|
||||
// we got some clean up due
|
||||
if (chaptersIdsToDelete.isNotEmpty()) {
|
||||
DownloadManager.dequeue(chaptersIdsToDelete)
|
||||
transaction {
|
||||
PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
|
||||
ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
|
||||
}
|
||||
}
|
||||
|
||||
transaction {
|
||||
if (chaptersToInsert.isNotEmpty()) {
|
||||
ChapterTable
|
||||
.batchInsert(chaptersToInsert) { chapter ->
|
||||
@@ -279,31 +287,24 @@ object Chapter {
|
||||
this[ChapterTable.isRead] = false
|
||||
this[ChapterTable.isBookmarked] = false
|
||||
this[ChapterTable.isDownloaded] = false
|
||||
this[ChapterTable.pageCount] = -1
|
||||
|
||||
// is recognized chapter number
|
||||
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
|
||||
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
|
||||
this[ChapterTable.isBookmarked] = chapter.chapterNumber in deletedBookmarkedChapterNumbers
|
||||
|
||||
// only preserve download status for chapters of the same scanlator, otherwise,
|
||||
// the downloaded files won't be found anyway
|
||||
val downloadedChapterInfo = deletedDownloadedChapterNumberInfoMap[chapter.chapterNumber]
|
||||
val pageCount = downloadedChapterInfo?.get(chapter.scanlator)
|
||||
if (pageCount != null) {
|
||||
this[ChapterTable.isDownloaded] = true
|
||||
this[ChapterTable.pageCount] = pageCount
|
||||
}
|
||||
// Try to use the fetch date of the original entry to not pollute 'Updates' tab
|
||||
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
||||
this[ChapterTable.fetchedAt] = it
|
||||
}
|
||||
|
||||
deletedDownloadedChapterNumberToChapter[chapter.chapterNumber]?.let {
|
||||
val hasDownloadedPages = it.pageCount > 0
|
||||
val isSameName = it.name == chapter.name
|
||||
val isSameScanlator = it.scanlator == chapter.scanlator
|
||||
|
||||
// Only preserve download status for chapters with the same name and of the same scanlator; otherwise,
|
||||
// the downloaded files won't be found anyway
|
||||
val isDownloadPreservable = hasDownloadedPages && isSameName && isSameScanlator
|
||||
if (isDownloadPreservable) {
|
||||
this[ChapterTable.isDownloaded] = true
|
||||
this[ChapterTable.pageCount] = it.pageCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}.forEach { insertedChapters.add(ChapterTable.toDataClass(it)) }
|
||||
}
|
||||
@@ -313,30 +314,12 @@ object Chapter {
|
||||
.apply {
|
||||
chaptersToUpdate.forEach {
|
||||
addBatch(EntityID(it.id, ChapterTable))
|
||||
|
||||
val currentChapter = chaptersInDb.find { dbChapter -> dbChapter.id == it.id }!!
|
||||
|
||||
this[ChapterTable.name] = it.name
|
||||
this[ChapterTable.date_upload] = it.uploadDate
|
||||
this[ChapterTable.chapter_number] = it.chapterNumber
|
||||
this[ChapterTable.scanlator] = it.scanlator
|
||||
this[ChapterTable.sourceOrder] = it.index
|
||||
this[ChapterTable.realUrl] = it.realUrl
|
||||
this[ChapterTable.isDownloaded] = currentChapter.downloaded
|
||||
this[ChapterTable.pageCount] = currentChapter.pageCount
|
||||
|
||||
if (!currentChapter.downloaded) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val isSameScanlator = currentChapter.scanlator == it.scanlator
|
||||
val isSameName = currentChapter.name == it.name
|
||||
|
||||
val isDownloadPreservable = isSameName && isSameScanlator
|
||||
if (!isDownloadPreservable) {
|
||||
this[ChapterTable.isDownloaded] = false
|
||||
this[ChapterTable.pageCount] = -1
|
||||
}
|
||||
}
|
||||
}.toExecutable()
|
||||
.execute(this@transaction)
|
||||
|
||||
@@ -133,7 +133,7 @@ object Manga {
|
||||
""
|
||||
}
|
||||
if (remoteTitle.isNotEmpty() && remoteTitle != mangaEntry[MangaTable.title]) {
|
||||
val canUpdateTitle = updateMangaDownloadDir(mangaEntry[MangaTable.title], source.toString(), remoteTitle)
|
||||
val canUpdateTitle = updateMangaDownloadDir(mangaId, remoteTitle)
|
||||
|
||||
if (canUpdateTitle) {
|
||||
it[MangaTable.title] = remoteTitle
|
||||
|
||||
@@ -358,7 +358,6 @@ object Extension {
|
||||
} else {
|
||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||
it[isInstalled] = false
|
||||
it[hasUpdate] = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,22 +24,14 @@ private val applicationDirs: ApplicationDirs by injectLazy()
|
||||
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
private fun getMangaDir(
|
||||
title: String,
|
||||
sourceName: String,
|
||||
): String {
|
||||
val sourceDir = SafePath.buildValidFilename(sourceName)
|
||||
val mangaDir = SafePath.buildValidFilename(title)
|
||||
|
||||
return "$sourceDir/$mangaDir"
|
||||
}
|
||||
|
||||
private fun getMangaDir(mangaId: Int): String =
|
||||
transaction {
|
||||
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
getMangaDir(mangaEntry[MangaTable.title], source.toString())
|
||||
val sourceDir = SafePath.buildValidFilename(source.toString())
|
||||
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
|
||||
"$sourceDir/$mangaDir"
|
||||
}
|
||||
|
||||
private fun getChapterDir(
|
||||
@@ -70,18 +62,8 @@ private fun getChapterDir(
|
||||
|
||||
fun getThumbnailDownloadPath(mangaId: Int): String = applicationDirs.thumbnailDownloadsRoot + "/$mangaId"
|
||||
|
||||
fun getMangaDownloadDir(
|
||||
title: String,
|
||||
sourceName: String,
|
||||
): String = applicationDirs.mangaDownloadsRoot + "/" + getMangaDir(title, sourceName)
|
||||
|
||||
fun getMangaDownloadDir(mangaId: Int): String = applicationDirs.mangaDownloadsRoot + "/" + getMangaDir(mangaId)
|
||||
|
||||
fun getMangaCacheDir(
|
||||
title: String,
|
||||
sourceName: String,
|
||||
): String = applicationDirs.tempMangaCacheRoot + "/" + getMangaDir(title, sourceName)
|
||||
|
||||
fun getChapterDownloadPath(
|
||||
mangaId: Int,
|
||||
chapterId: Int,
|
||||
@@ -97,21 +79,38 @@ fun getChapterCachePath(
|
||||
chapterId: Int,
|
||||
): String = applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId)
|
||||
|
||||
private fun updateDownloadDir(
|
||||
currentDir: String,
|
||||
newDir: String,
|
||||
/** return value says if rename/move was successful */
|
||||
fun updateMangaDownloadDir(
|
||||
mangaId: Int,
|
||||
newTitle: String,
|
||||
): Boolean {
|
||||
val currentDirFile = File(currentDir)
|
||||
// Get current manga directory (uses its own transaction)
|
||||
val currentMangaDir = getMangaDir(mangaId)
|
||||
|
||||
// Build new directory path
|
||||
val newMangaDir =
|
||||
transaction {
|
||||
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val sourceDir = SafePath.buildValidFilename(source.toString())
|
||||
val newMangaDirName = SafePath.buildValidFilename(newTitle)
|
||||
"$sourceDir/$newMangaDirName"
|
||||
}
|
||||
|
||||
val oldDir = "${applicationDirs.downloadsRoot}/$currentMangaDir"
|
||||
val newDir = "${applicationDirs.downloadsRoot}/$newMangaDir"
|
||||
|
||||
val oldDirFile = File(oldDir)
|
||||
val newDirFile = File(newDir)
|
||||
|
||||
if (!currentDirFile.exists()) {
|
||||
if (!oldDirFile.exists()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return try {
|
||||
Files.move(currentDirFile.toPath(), newDirFile.toPath())
|
||||
Files.move(oldDirFile.toPath(), newDirFile.toPath())
|
||||
|
||||
if (currentDirFile.exists()) {
|
||||
if (oldDirFile.exists()) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -119,31 +118,9 @@ private fun updateDownloadDir(
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "updateDownloadDir: failed to rename download folder from \"$currentDir\" to \"$newDir\"" }
|
||||
logger.error(e) { "updateMangaDownloadDir: failed to rename manga download folder from \"$oldDir\" to \"$newDir\"" }
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/** return value says if rename/move was successful */
|
||||
fun updateMangaDownloadDir(
|
||||
title: String,
|
||||
sourceName: String,
|
||||
newTitle: String,
|
||||
): Boolean {
|
||||
val currentDownloadDir = getMangaDownloadDir(title, sourceName)
|
||||
val newDownloadDir = getMangaDownloadDir(newTitle, sourceName)
|
||||
|
||||
val renamed = updateDownloadDir(currentDownloadDir, newDownloadDir)
|
||||
|
||||
val tryToKeepCachedFilesUsable = renamed
|
||||
if (tryToKeepCachedFilesUsable) {
|
||||
val currentCacheDir = getMangaCacheDir(title, sourceName)
|
||||
val newCacheDir = getMangaCacheDir(newTitle, sourceName)
|
||||
|
||||
updateDownloadDir(currentCacheDir, newCacheDir)
|
||||
}
|
||||
|
||||
return renamed
|
||||
}
|
||||
|
||||
@@ -7,21 +7,12 @@ 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/. */
|
||||
|
||||
import java.util.Objects
|
||||
import kotlin.math.min
|
||||
|
||||
open class PaginatedList<T>(
|
||||
val page: List<T>,
|
||||
val hasNextPage: Boolean,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is PaginatedList<T>) return false
|
||||
return page == other.page && hasNextPage == other.hasNextPage
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(page, hasNextPage)
|
||||
}
|
||||
)
|
||||
|
||||
const val PAGINATION_FACTOR = 50
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import suwayomi.tachidesk.manga.model.table.CategoryMetaTable.ref
|
||||
* Metadata storage for clients, about Category with id == [ref].
|
||||
*/
|
||||
object CategoryMetaTable : IntIdTable() {
|
||||
val key = varchar("meta_key", 256)
|
||||
val key = varchar("key", 256)
|
||||
val value = varchar("value", 4096)
|
||||
val ref = reference("category_ref", CategoryTable, ReferenceOption.CASCADE)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterMetaTable.ref
|
||||
* }
|
||||
*/
|
||||
object ChapterMetaTable : IntIdTable() {
|
||||
val key = varchar("meta_key", 256)
|
||||
val key = varchar("key", 256)
|
||||
val value = varchar("value", 4096)
|
||||
val ref = reference("chapter_ref", ChapterTable, ReferenceOption.CASCADE)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.model.table.MangaMetaTable.ref
|
||||
* }
|
||||
*/
|
||||
object MangaMetaTable : IntIdTable() {
|
||||
val key = varchar("meta_key", 256)
|
||||
val key = varchar("key", 256)
|
||||
val value = varchar("value", 4096)
|
||||
val ref = reference("manga_ref", MangaTable, ReferenceOption.CASCADE)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import suwayomi.tachidesk.manga.model.table.SourceMetaTable.ref
|
||||
* Metadata storage for clients, about Source with id == [ref].
|
||||
*/
|
||||
object SourceMetaTable : IntIdTable() {
|
||||
val key = varchar("meta_key", 256)
|
||||
val key = varchar("key", 256)
|
||||
val value = varchar("value", 4096)
|
||||
val ref = long("source_ref")
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import android.content.Context
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||
import suwayomi.tachidesk.server.database.H2Migration
|
||||
import suwayomi.tachidesk.server.util.ExitCode
|
||||
import suwayomi.tachidesk.server.util.shutdownApp
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
@@ -101,35 +99,31 @@ private val MIGRATIONS =
|
||||
|
||||
fun runMigrations(applicationDirs: ApplicationDirs) {
|
||||
val logger = KotlinLogging.logger("Migration")
|
||||
try {
|
||||
val migrationPreferences =
|
||||
Injekt
|
||||
.get<Application>()
|
||||
.getSharedPreferences(
|
||||
"migrations",
|
||||
Context.MODE_PRIVATE,
|
||||
)
|
||||
val version = migrationPreferences.getInt("version", 0)
|
||||
|
||||
logger.info { "Running migrations, previous version $version, target version ${MIGRATIONS.size}" }
|
||||
val migrationPreferences =
|
||||
Injekt
|
||||
.get<Application>()
|
||||
.getSharedPreferences(
|
||||
"migrations",
|
||||
Context.MODE_PRIVATE,
|
||||
)
|
||||
val version = migrationPreferences.getInt("version", 0)
|
||||
|
||||
MIGRATIONS.forEachIndexed { index, (migrationName, migrationFunction) ->
|
||||
val migrationVersion = index + 1
|
||||
logger.info { "Running migrations, previous version $version, target version ${MIGRATIONS.size}" }
|
||||
|
||||
val isMigrationRequired = version < migrationVersion
|
||||
if (!isMigrationRequired) {
|
||||
logger.info { "Skipping migration version $migrationVersion: $migrationName" }
|
||||
return@forEachIndexed
|
||||
}
|
||||
MIGRATIONS.forEachIndexed { index, (migrationName, migrationFunction) ->
|
||||
val migrationVersion = index + 1
|
||||
|
||||
logger.info { "Running migration version $migrationVersion: $migrationName" }
|
||||
|
||||
migrationFunction(applicationDirs)
|
||||
|
||||
migrationPreferences.edit().putInt("version", migrationVersion).apply()
|
||||
val isMigrationRequired = version < migrationVersion
|
||||
if (!isMigrationRequired) {
|
||||
logger.info { "Skipping migration version $migrationVersion: $migrationName" }
|
||||
return@forEachIndexed
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Failed to run migrations" }
|
||||
shutdownApp(ExitCode.MigrationsRunFailure)
|
||||
|
||||
logger.info { "Running migration version $migrationVersion: $migrationName" }
|
||||
|
||||
migrationFunction(applicationDirs)
|
||||
|
||||
migrationPreferences.edit().putInt("version", migrationVersion).apply()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import io.github.config4k.toConfig
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.json.JavalinJackson3
|
||||
import io.javalin.json.JavalinJackson
|
||||
import io.javalin.json.JsonMapper
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -222,7 +222,7 @@ fun serverModule(applicationDirs: ApplicationDirs): Module =
|
||||
module {
|
||||
single { applicationDirs }
|
||||
single<IUpdater> { Updater() }
|
||||
single<JsonMapper> { JavalinJackson3() }
|
||||
single<JsonMapper> { JavalinJackson() }
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@@ -366,7 +366,6 @@ fun applicationSetup() {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Exception while creating initial server.conf" }
|
||||
shutdownApp(ExitCode.SetupConfFileFailed)
|
||||
}
|
||||
|
||||
// copy local source icon
|
||||
@@ -379,7 +378,6 @@ fun applicationSetup() {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Exception while copying Local source's icon" }
|
||||
shutdownApp(ExitCode.LocalSourceIconCopyFailure)
|
||||
}
|
||||
|
||||
// fixes #119 , ref:
|
||||
@@ -397,12 +395,7 @@ fun applicationSetup() {
|
||||
|
||||
databaseUp()
|
||||
|
||||
try {
|
||||
LocalSource.register()
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Failed to setup LocalSource" }
|
||||
shutdownApp(ExitCode.LocalSourceSetupFailure)
|
||||
}
|
||||
LocalSource.register()
|
||||
|
||||
serverConfig.subscribeTo(
|
||||
combine<Any, DatabaseSettings>(
|
||||
@@ -532,7 +525,7 @@ fun applicationSetup() {
|
||||
}
|
||||
}
|
||||
}
|
||||
download { github { release("jbr-release-21.0.10b1163.108") } }
|
||||
download { github() }
|
||||
settings {
|
||||
windowlessRenderingEnabled = true
|
||||
cachePath = (Path(applicationDirs.dataRoot) / "cache/kcef").toString()
|
||||
|
||||
@@ -27,6 +27,7 @@ import suwayomi.tachidesk.server.util.ExitCode
|
||||
import suwayomi.tachidesk.server.util.shutdownApp
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.sql.SQLException
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -144,15 +145,14 @@ object DBManager {
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun databaseUp(givenDb: Database? = null) {
|
||||
fun databaseUp() {
|
||||
val db =
|
||||
givenDb
|
||||
?: try {
|
||||
DBManager.setupDatabase()
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Failed to setup Database" }
|
||||
return
|
||||
}
|
||||
try {
|
||||
DBManager.setupDatabase()
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Failed to setup Database" }
|
||||
return
|
||||
}
|
||||
|
||||
logger.info {
|
||||
"Using ${db.vendor} database version ${db.version}"
|
||||
@@ -184,8 +184,10 @@ fun databaseUp(givenDb: Database? = null) {
|
||||
}
|
||||
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java)
|
||||
runMigrations(migrations)
|
||||
} catch (e: Exception) {
|
||||
} catch (e: SQLException) {
|
||||
logger.error(e) { "Error up-to-database migration" }
|
||||
shutdownApp(ExitCode.DbMigrationFailure)
|
||||
if (System.getProperty("crashOnFailedMigration").toBoolean()) {
|
||||
shutdownApp(ExitCode.DbMigrationFailure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,9 @@ import java.net.URLClassLoader
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.bufferedReader
|
||||
import kotlin.io.path.bufferedWriter
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.deleteExisting
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.div
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.name
|
||||
@@ -49,10 +46,6 @@ object H2Migration {
|
||||
}
|
||||
|
||||
val script = Path("$dbBase.${h2Old.substringAfterLast('.')}.sql")
|
||||
script.deleteIfExists()
|
||||
|
||||
val modifiedScript = Path("$dbBase.${h2Old.substringAfterLast('.')}.modified.sql")
|
||||
modifiedScript.deleteIfExists()
|
||||
|
||||
// Backup original database.
|
||||
val backup = Path("$dbBase.mv.db.${h2Old.substringAfterLast('.')}.backup")
|
||||
@@ -79,32 +72,19 @@ object H2Migration {
|
||||
libsDir.resolve("h2-$h2New.bin"),
|
||||
)
|
||||
|
||||
// Delete attempted migration if failed previously
|
||||
val newDatabase = Path(rootDir, "database.${h2New.substringAfterLast('.')}.mv.db")
|
||||
newDatabase.deleteIfExists()
|
||||
|
||||
val modifiedNewDatabase = Path(rootDir, "database.${h2Old.substringAfterLast('.')}.modified.${h2New.substringAfterLast('.')}.mv.db")
|
||||
modifiedNewDatabase.deleteIfExists()
|
||||
|
||||
runMigrationTool(
|
||||
migrationJar = migrationJar,
|
||||
libsDir = libsDir,
|
||||
mvStore = mvStore,
|
||||
script = script,
|
||||
modifiedScript = modifiedScript,
|
||||
h2Old = h2Old,
|
||||
h2New = h2New,
|
||||
)
|
||||
|
||||
// Move database to proper path
|
||||
if (modifiedNewDatabase.exists()) {
|
||||
modifiedNewDatabase.copyTo(mvStore, overwrite = true)
|
||||
modifiedNewDatabase.deleteExisting()
|
||||
newDatabase.deleteIfExists()
|
||||
} else {
|
||||
newDatabase.copyTo(mvStore, overwrite = true)
|
||||
newDatabase.deleteExisting()
|
||||
}
|
||||
val newDatabase = Path(rootDir, "database.${h2New.substringAfterLast('.')}.mv.db")
|
||||
newDatabase.copyTo(mvStore, overwrite = true)
|
||||
newDatabase.deleteExisting()
|
||||
|
||||
logger.info { "H2 migration completed successfully." }
|
||||
}
|
||||
@@ -143,7 +123,6 @@ object H2Migration {
|
||||
libsDir: Path,
|
||||
mvStore: Path,
|
||||
script: Path,
|
||||
modifiedScript: Path,
|
||||
h2Old: String,
|
||||
h2New: String,
|
||||
) {
|
||||
@@ -157,77 +136,32 @@ object H2Migration {
|
||||
val main =
|
||||
clazz.getMethod("main", Array<String>::class.java)
|
||||
|
||||
try {
|
||||
main.invoke(
|
||||
null,
|
||||
arrayOf(
|
||||
// h2 driver dir
|
||||
"-l",
|
||||
libsDir.absolutePathString(),
|
||||
// from version
|
||||
"-f",
|
||||
h2Old,
|
||||
// to version
|
||||
"-t",
|
||||
h2New,
|
||||
// user
|
||||
"-u",
|
||||
"",
|
||||
// password
|
||||
"-p",
|
||||
"",
|
||||
// database.mv.db
|
||||
"-d",
|
||||
mvStore.absolutePathString(),
|
||||
// database backup in SQL
|
||||
"-s",
|
||||
script.absolutePathString(),
|
||||
),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
// Modify raw .sql file as needed for compatibility
|
||||
if (e.stackTraceToString().contains("Unknown data type: \"DATETIME\"; SQL statement:") && script.exists()) {
|
||||
script.bufferedReader().use { reader ->
|
||||
modifiedScript.bufferedWriter().use { writer ->
|
||||
reader.forEachLine { line ->
|
||||
writer.write(
|
||||
line.replace(
|
||||
" \"EXECUTED_AT\" DATETIME(9) NOT NULL",
|
||||
" \"EXECUTED_AT\" TIMESTAMP(9) NOT NULL",
|
||||
),
|
||||
)
|
||||
writer.newLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main.invoke(
|
||||
null,
|
||||
arrayOf(
|
||||
// h2 driver dir
|
||||
"-l",
|
||||
libsDir.absolutePathString(),
|
||||
// from version
|
||||
"-f",
|
||||
h2Old,
|
||||
// to version
|
||||
"-t",
|
||||
h2New,
|
||||
// user
|
||||
"-u",
|
||||
"",
|
||||
// password
|
||||
"-p",
|
||||
"",
|
||||
// database.mv.db
|
||||
"-d",
|
||||
modifiedScript.absolutePathString(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
main.invoke(
|
||||
null,
|
||||
arrayOf(
|
||||
// h2 driver dir
|
||||
"-l",
|
||||
libsDir.absolutePathString(),
|
||||
// from version
|
||||
"-f",
|
||||
h2Old,
|
||||
// to version
|
||||
"-t",
|
||||
h2New,
|
||||
// user
|
||||
"-u",
|
||||
"",
|
||||
// password
|
||||
"-p",
|
||||
"",
|
||||
// database.mv.db
|
||||
"-d",
|
||||
mvStore.absolutePathString(),
|
||||
// database backup in SQL
|
||||
"-s",
|
||||
script.absolutePathString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,17 @@ package suwayomi.tachidesk.server.database.migration
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0023_CategoryMetaRefFix : SQLMigration() {
|
||||
fun String.toSqlName(): String =
|
||||
TransactionManager.defaultDatabase!!.identifierManager.let {
|
||||
it.quoteIfNecessary(
|
||||
it.inProperCase(this),
|
||||
)
|
||||
}
|
||||
|
||||
private val CategoryMetaTable by lazy { "CategoryMeta".toSqlName() }
|
||||
private val CategoryRefColumn by lazy { "category_ref".toSqlName() }
|
||||
private val CategoryTable by lazy { "Category".toSqlName() }
|
||||
|
||||
@@ -8,7 +8,6 @@ package suwayomi.tachidesk.server.database.migration
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0049_FixDuplicatedMetas : SQLMigration() {
|
||||
@@ -16,7 +15,7 @@ class M0049_FixDuplicatedMetas : SQLMigration() {
|
||||
table: String,
|
||||
refColumn: String? = null,
|
||||
): String {
|
||||
val groupBy = listOfNotNull(refColumn, "KEY".toSqlName()).joinToString(", ")
|
||||
val groupBy = listOfNotNull(refColumn, "KEY").joinToString(", ")
|
||||
|
||||
return """
|
||||
DELETE FROM $table
|
||||
@@ -31,11 +30,10 @@ class M0049_FixDuplicatedMetas : SQLMigration() {
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
override val sql: String by lazy {
|
||||
override val sql: String =
|
||||
createMigrationForTable("CATEGORYMETA", "CATEGORY_REF") +
|
||||
createMigrationForTable("CHAPTERMETA", "CHAPTER_REF") +
|
||||
createMigrationForTable("GLOBALMETA") +
|
||||
createMigrationForTable("MANGAMETA", "MANGA_REF") +
|
||||
createMigrationForTable("SOURCEMETA", "SOURCE_REF")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
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.SQLMigration
|
||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
||||
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0055_RenameMetaKeys : SQLMigration() {
|
||||
fun postgresRename(table: String): String =
|
||||
"ALTER TABLE $table " +
|
||||
"RENAME COLUMN " + "KEY".toSqlName() + " TO META_KEY;"
|
||||
|
||||
fun h2Rename(table: String): String =
|
||||
"ALTER TABLE $table " +
|
||||
"ALTER COLUMN " + "KEY".toSqlName() + " RENAME TO META_KEY;"
|
||||
|
||||
fun createRenameMigration(table: String): String =
|
||||
when (serverConfig.databaseType.value) {
|
||||
DatabaseType.H2 -> h2Rename(table.toSqlName())
|
||||
DatabaseType.POSTGRESQL -> postgresRename(table.toSqlName())
|
||||
}
|
||||
|
||||
override val sql: String by lazy {
|
||||
createRenameMigration("CATEGORYMETA") +
|
||||
createRenameMigration("CHAPTERMETA") +
|
||||
createRenameMigration("GLOBALMETA") +
|
||||
createRenameMigration("MANGAMETA") +
|
||||
createRenameMigration("SOURCEMETA")
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
package suwayomi.tachidesk.server.database.migration.helpers
|
||||
|
||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
|
||||
fun String.toSqlName(): String =
|
||||
TransactionManager.current().db.identifierManager.let {
|
||||
it.quoteIfNecessary(
|
||||
it.inProperCase(this),
|
||||
)
|
||||
}
|
||||
|
||||
abstract class RenameFieldMigration(
|
||||
tableName: String,
|
||||
originalName: String,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package suwayomi.tachidesk.server.database.migration.helpers
|
||||
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
|
||||
|
||||
fun String.toSqlName(): String =
|
||||
TransactionManager.current().db.identifierManager.let {
|
||||
it.quoteIfNecessary(
|
||||
it.inProperCase(this),
|
||||
)
|
||||
}
|
||||
@@ -21,10 +21,6 @@ enum class ExitCode(
|
||||
WebUISetupFailure(3),
|
||||
ConfigMigrationMisconfiguredFailure(4),
|
||||
DbMigrationFailure(5),
|
||||
SetupConfFileFailed(6),
|
||||
LocalSourceIconCopyFailure(7),
|
||||
LocalSourceSetupFailure(8),
|
||||
MigrationsRunFailure(9),
|
||||
}
|
||||
|
||||
fun shutdownApp(exitCode: ExitCode) {
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
package masstest
|
||||
|
||||
import android.os.Looper
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.koin.core.context.stopKoin
|
||||
import suwayomi.tachidesk.manga.impl.Source
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.server.applicationSetup
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
import suwayomi.tachidesk.test.BASE_PATH
|
||||
import suwayomi.tachidesk.test.setLoggingEnabled
|
||||
import xyz.nulldev.ts.config.CONFIG_PREFIX
|
||||
@@ -30,11 +25,8 @@ class CloudFlareTest {
|
||||
fun setup() {
|
||||
val dataRoot = File(BASE_PATH).absolutePath
|
||||
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
|
||||
Looper.clearMainLooperForTest()
|
||||
SettingsRegistry.clear()
|
||||
applicationSetup()
|
||||
setLoggingEnabled(false)
|
||||
return
|
||||
|
||||
runBlocking {
|
||||
val extensions = ExtensionsList.getExtensionList()
|
||||
@@ -56,15 +48,9 @@ class CloudFlareTest {
|
||||
setLoggingEnabled(true)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
stopKoin()
|
||||
}
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
fun `test nhentai browse`() =
|
||||
runTest {
|
||||
assert(nhentai.getPopularManga(1).mangas.isNotEmpty()) {
|
||||
|
||||
@@ -7,7 +7,6 @@ package masstest
|
||||
* 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 android.os.Looper
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@@ -18,11 +17,9 @@ import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.koin.core.context.stopKoin
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSourceList
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
||||
@@ -31,7 +28,6 @@ import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||
import suwayomi.tachidesk.server.applicationSetup
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
import suwayomi.tachidesk.test.BASE_PATH
|
||||
import suwayomi.tachidesk.test.setLoggingEnabled
|
||||
import xyz.nulldev.ts.config.CONFIG_PREFIX
|
||||
@@ -55,8 +51,6 @@ class TestExtensionCompatibility {
|
||||
fun setup() {
|
||||
val dataRoot = File(BASE_PATH).absolutePath
|
||||
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
|
||||
Looper.clearMainLooperForTest()
|
||||
SettingsRegistry.clear()
|
||||
applicationSetup()
|
||||
setLoggingEnabled(false)
|
||||
|
||||
@@ -78,22 +72,12 @@ class TestExtensionCompatibility {
|
||||
}
|
||||
}
|
||||
}
|
||||
sources =
|
||||
getSourceList()
|
||||
.filter {
|
||||
// filter local source
|
||||
it.id.toLong() != 0L
|
||||
}.map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
|
||||
sources = getSourceList().map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
|
||||
}
|
||||
setLoggingEnabled(true)
|
||||
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
stopKoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runTest() {
|
||||
runBlocking(Dispatchers.Default) {
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package suwayomi.tachidesk
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.text.StringBuilder
|
||||
|
||||
class LooperThread : Thread() {
|
||||
var mHandler: Handler? = null
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
override fun run() {
|
||||
Looper.prepare()
|
||||
mHandler = Handler(Looper.myLooper())
|
||||
latch.countDown()
|
||||
Looper.loop()
|
||||
}
|
||||
}
|
||||
|
||||
class LooperTest {
|
||||
@Test
|
||||
fun multiplePostWork() {
|
||||
val thread = LooperThread()
|
||||
thread.start()
|
||||
val sb = StringBuilder()
|
||||
val latch = CountDownLatch(1)
|
||||
assertTrue(thread.latch.await(5, TimeUnit.SECONDS))
|
||||
|
||||
thread.mHandler!!.post {
|
||||
Thread.sleep(100)
|
||||
sb.append("a_b_c")
|
||||
}
|
||||
thread.mHandler!!.post {
|
||||
Thread.sleep(100)
|
||||
sb.append("_d_e_f")
|
||||
}
|
||||
thread.mHandler!!.post {
|
||||
Thread.sleep(100)
|
||||
sb.append("_g_h_i")
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
assertNotEquals("a_b_c_d_e_f_g_h_i", sb.toString())
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS))
|
||||
|
||||
assertEquals("a_b_c_d_e_f_g_h_i", sb.toString())
|
||||
thread.mHandler!!.looper.quit()
|
||||
// thread.join()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loopTest() {
|
||||
val thread = LooperThread()
|
||||
thread.start()
|
||||
val sb = StringBuilder()
|
||||
val expected = StringBuilder()
|
||||
val latch = CountDownLatch(1)
|
||||
assertTrue(thread.latch.await(5, TimeUnit.SECONDS))
|
||||
val n = 100
|
||||
|
||||
for (i in 0 until n) {
|
||||
thread.mHandler!!.post {
|
||||
Thread.sleep(10)
|
||||
sb.append("$i")
|
||||
}
|
||||
expected.append("$i")
|
||||
}
|
||||
|
||||
thread.mHandler!!.post {
|
||||
latch.countDown()
|
||||
}
|
||||
|
||||
assertNotEquals(expected.toString(), sb.toString())
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS), "only got to $sb")
|
||||
|
||||
assertEquals(expected.toString(), sb.toString())
|
||||
thread.mHandler!!.looper.quit()
|
||||
// thread.join()
|
||||
}
|
||||
}
|
||||
@@ -18,22 +18,19 @@ import suwayomi.tachidesk.test.clearTables
|
||||
class CategoryControllerTest : ApplicationTest() {
|
||||
@Test
|
||||
fun categoryReorder() {
|
||||
clearTables(
|
||||
CategoryTable,
|
||||
)
|
||||
Category.createCategory("foo")
|
||||
Category.createCategory("bar")
|
||||
val cats = Category.getCategoryList()
|
||||
val foo = cats.asSequence().filter { it.name == "foo" }.first()
|
||||
val bar = cats.asSequence().filter { it.name == "bar" }.first()
|
||||
assertEquals(0, foo.order)
|
||||
assertEquals(1, bar.order)
|
||||
assertEquals(1, foo.order)
|
||||
assertEquals(2, bar.order)
|
||||
Category.reorderCategory(1, 2)
|
||||
val catsReordered = Category.getCategoryList()
|
||||
val fooReordered = catsReordered.asSequence().filter { it.name == "foo" }.first()
|
||||
val barReordered = catsReordered.asSequence().filter { it.name == "bar" }.first()
|
||||
assertEquals(1, fooReordered.order)
|
||||
assertEquals(0, barReordered.order)
|
||||
assertEquals(2, fooReordered.order)
|
||||
assertEquals(1, barReordered.order)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
|
||||
@@ -35,7 +35,7 @@ class CategoryMangaTest : ApplicationTest() {
|
||||
CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,
|
||||
"Manga should not have any unread chapters",
|
||||
)
|
||||
createChapters(mangaId, 10, false, start = 11)
|
||||
createChapters(mangaId, 10, false)
|
||||
assertEquals(
|
||||
10,
|
||||
CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,
|
||||
|
||||
@@ -12,12 +12,9 @@ import eu.kanade.tachiyomi.createAppModule
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.jetbrains.exposed.v1.core.DatabaseConfig
|
||||
import org.jetbrains.exposed.v1.core.ExperimentalKeywordApi
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.context.stopKoin
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import suwayomi.tachidesk.server.JavalinSetup
|
||||
import suwayomi.tachidesk.server.ServerConfig
|
||||
@@ -25,9 +22,7 @@ import suwayomi.tachidesk.server.androidCompat
|
||||
import suwayomi.tachidesk.server.database.databaseUp
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.server.serverModule
|
||||
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
||||
import suwayomi.tachidesk.server.util.ConfigTypeRegistration
|
||||
import suwayomi.tachidesk.server.util.SystemTray
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -60,13 +55,6 @@ open class ApplicationTest {
|
||||
private var initializedTheApp = false
|
||||
|
||||
fun testingSetup() {
|
||||
// register Tachidesk's config which is dubbed "ServerConfig"
|
||||
SettingsRegistry.clear()
|
||||
ConfigTypeRegistration.registerCustomTypes()
|
||||
GlobalConfigManager.registerModule(
|
||||
ServerConfig.register { GlobalConfigManager.config },
|
||||
)
|
||||
|
||||
// Application dirs
|
||||
val applicationDirs = ApplicationDirs()
|
||||
|
||||
@@ -84,9 +72,13 @@ open class ApplicationTest {
|
||||
File(it).mkdirs()
|
||||
}
|
||||
|
||||
// register Tachidesk's config which is dubbed "ServerConfig"
|
||||
GlobalConfigManager.registerModule(
|
||||
ServerConfig.register { GlobalConfigManager.config },
|
||||
)
|
||||
|
||||
// initialize Koin modules
|
||||
val app = App()
|
||||
stopKoin()
|
||||
startKoin {
|
||||
modules(
|
||||
createAppModule(app),
|
||||
@@ -136,14 +128,14 @@ open class ApplicationTest {
|
||||
}
|
||||
|
||||
// create system tray
|
||||
// if (serverConfig.systemTrayEnabled.value) {
|
||||
// try {
|
||||
// SystemTray.create()
|
||||
// } catch (e: Throwable) {
|
||||
// // cover both java.lang.Exception and java.lang.Error
|
||||
// e.printStackTrace()
|
||||
// }
|
||||
// }
|
||||
if (serverConfig.systemTrayEnabled.value) {
|
||||
try {
|
||||
SystemTray.create()
|
||||
} catch (e: Throwable) {
|
||||
// cover both java.lang.Exception and java.lang.Error
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
// Disable jetty's logging
|
||||
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
|
||||
@@ -162,16 +154,8 @@ open class ApplicationTest {
|
||||
// fixes #119 , ref: https://github.com/Suwayomi/Suwayomi-Server/issues/119#issuecomment-894681292 , source Id calculation depends on String.lowercase()
|
||||
Locale.setDefault(Locale.ENGLISH)
|
||||
|
||||
val dbConfig =
|
||||
DatabaseConfig {
|
||||
useNestedTransactions = true
|
||||
@OptIn(ExperimentalKeywordApi::class)
|
||||
preserveKeywordCasing = false
|
||||
defaultSchema = null
|
||||
}
|
||||
|
||||
// in-memory database, don't discard database between connections/transactions
|
||||
val db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", "org.h2.Driver", databaseConfig = dbConfig)
|
||||
val db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", "org.h2.Driver")
|
||||
|
||||
databaseUp(db)
|
||||
|
||||
|
||||
@@ -55,9 +55,8 @@ fun createChapters(
|
||||
mangaId: Int,
|
||||
amount: Int,
|
||||
read: Boolean,
|
||||
start: Int = 1,
|
||||
) {
|
||||
val list = listOf((0 until amount)).flatten().map { it + start }
|
||||
val list = listOf((0 until amount)).flatten().map { 1 }
|
||||
transaction {
|
||||
ChapterTable
|
||||
.batchInsert(list) {
|
||||
|
||||
Reference in New Issue
Block a user