Compare commits

..

5 Commits

Author SHA1 Message Date
Syer10
3bc75d5b00 Review comments 2026-05-12 09:32:09 -04:00
Syer10
fe6dd05411 Update H2 2026-05-11 19:20:06 -04:00
Syer10
57c0a85a35 Add Kotlinx.DateTime extensions 2026-05-11 00:31:23 -04:00
Syer10
c2c927ae97 Update Exposed 2026-05-11 00:25:44 -04:00
renovate[bot]
2c70700bb7 Update exposed to v1 2026-05-11 04:16:03 +00:00
35 changed files with 190 additions and 527 deletions

View File

@@ -67,7 +67,7 @@ jobs:
export LD_PRELOAD="$(pwd)/scripts/resources/catch_abort.so" export LD_PRELOAD="$(pwd)/scripts/resources/catch_abort.so"
JAR=$(ls ./server/build/*.jar| head -1) JAR=$(ls ./server/build/*.jar| head -1)
set +e set +e
timeout 30s java \ timeout 30s java -DcrashOnFailedMigration=true \
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \ -Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \ -Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
-Dsuwayomi.tachidesk.config.server.databaseType=POSTGRESQL \ -Dsuwayomi.tachidesk.config.server.databaseType=POSTGRESQL \
@@ -83,7 +83,7 @@ jobs:
exit "$ecode" exit "$ecode"
fi fi
timeout 30s java \ timeout 30s java -DcrashOnFailedMigration=true \
-Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \ -Dsuwayomi.tachidesk.config.server.systemTrayEnabled=false \
-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \ -Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false \
-jar "$JAR" -jar "$JAR"

View File

@@ -1,7 +1,6 @@
name: GitHub Wiki upload name: GitHub Wiki upload
on: on:
workflow_dispatch:
push: push:
branches: branches:
- master - master

View File

@@ -10,20 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- . - .
### Changed ### Changed
- (Database/H2) Use the latest H2 database engine - .
- (Startup) Crash on startup if an unrecoverable error happens
### Fixed ### Fixed
- (**CloudFlareInterceptor**) Don't send the `cf_clearance` cookie back to Flaresolverr - (CloudFlareInterceptor) Don't send the `cf_clearance` cookie back to Flaresolverr
- (**WebUI**) Handle serving non-default webui with "bundled" - (WebUI) Handle serving non-default webui with "bundled"
- (**WebUI**) Wait until WebUI is ready to open in browser - (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) 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")
## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08 ## [v2.2.2100] + [WebUI: v20260508.01] - 2026-05-08

View File

@@ -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 ## General Troubleshooting
This guide will try to fix Suwayomi by reseting it to a clean installation state. 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 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) - 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. - 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. - Delete the Suwayomi data directory located below and re-run the app.
- 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. Note: Replace `<Account>` with the currently logged in account/username on your pc.
- 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.
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. - 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.

View File

@@ -4,7 +4,7 @@ coroutines = "1.11.0"
serialization = "1.11.0" serialization = "1.11.0"
jvmTarget = "21" jvmTarget = "21"
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
javalin = "7.2.2" javalin = "7.2.0"
jte = "3.2.4" jte = "3.2.4"
jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency` jackson = "3.1.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "1.2.0" exposed = "1.2.0"
@@ -37,7 +37,7 @@ serialization-xml-core = { module = "io.github.pdvrieze.xmlutil:core", version.r
serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" } serialization-xml = { module = "io.github.pdvrieze.xmlutil:serialization-jvm", version.ref = "xmlserialization" }
# Logging # 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" logback = "ch.qos.logback:logback-classic:1.5.32"
kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02" kotlinlogging = "io.github.oshai:kotlin-logging-jvm:8.0.02"
@@ -116,7 +116,7 @@ appdirs = "ca.gosyer:kotlin-multiplatform-appdirs:2.0.0"
cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0" cache4k = "io.github.reactivecircus.cache4k:cache4k:0.14.0"
zip4j = "net.lingala.zip4j:zip4j:2.11.6" zip4j = "net.lingala.zip4j:zip4j:2.11.6"
commonscompress = "org.apache.commons:commons-compress:1.28.0" 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 # AES/CBC/PKCS7Padding Cypher provider
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84" bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.84"

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
retries=0 retries=0
retryBackOffMs=500 retryBackOffMs=500

View File

@@ -88,6 +88,4 @@ object SettingsRegistry {
fun get(name: String): SettingMetadata? = settings[name] fun get(name: String): SettingMetadata? = settings[name]
fun getAll(): Map<String, SettingMetadata> = settings.toMap() fun getAll(): Map<String, SettingMetadata> = settings.toMap()
fun clear() = settings.clear()
} }

View File

@@ -13,6 +13,6 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
* Metadata storage for clients, server/global level. * Metadata storage for clients, server/global level.
*/ */
object GlobalMetaTable : IntIdTable() { object GlobalMetaTable : IntIdTable() {
val key = varchar("meta_key", 256) val key = varchar("key", 256)
val value = varchar("value", 4096) val value = varchar("value", 4096)
} }

View File

@@ -15,11 +15,9 @@ import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.SortOrder import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.core.greater import org.jetbrains.exposed.v1.core.greater
import org.jetbrains.exposed.v1.core.inList 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.less
import org.jetbrains.exposed.v1.core.like import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.jdbc.select import org.jetbrains.exposed.v1.jdbc.select
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import suwayomi.tachidesk.graphql.directives.RequireAuth import suwayomi.tachidesk.graphql.directives.RequireAuth
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
@@ -245,16 +243,13 @@ class MangaQuery {
): MangaNodeList { ): MangaNodeList {
val queryResults = val queryResults =
transaction { transaction {
val mangaIdsQuery = val res =
MangaTable MangaTable
.leftJoin(CategoryMangaTable) .leftJoin(CategoryMangaTable)
.select(MangaTable.id) .select(MangaTable.columns)
.withDistinct() .withDistinctOn(MangaTable.id)
mangaIdsQuery.applyOps(condition, filter) res.applyOps(condition, filter)
val res =
MangaTable.selectAll().where { MangaTable.id inSubQuery mangaIdsQuery }
if (order != null || orderBy != null || (last != null || before != null)) { if (order != null || orderBy != null || (last != null || before != null)) {
val baseSort = listOf(MangaOrder(MangaOrderBy.ID, SortOrder.ASC)) val baseSort = listOf(MangaOrder(MangaOrderBy.ID, SortOrder.ASC))

View File

@@ -236,7 +236,7 @@ object Chapter {
val deletedChapterNumbers = TreeSet<Float>() val deletedChapterNumbers = TreeSet<Float>()
val deletedReadChapterNumbers = TreeSet<Float>() val deletedReadChapterNumbers = TreeSet<Float>()
val deletedBookmarkedChapterNumbers = TreeSet<Float>() val deletedBookmarkedChapterNumbers = TreeSet<Float>()
val deletedDownloadedChapterNumberToChapter = mutableMapOf<Float, ChapterDataClass>() val deletedDownloadedChapterNumberInfoMap = mutableMapOf<Float, MutableMap<String?, Int>>()
val deletedChapterNumberDateFetchMap = mutableMapOf<Float, Long>() val deletedChapterNumberDateFetchMap = mutableMapOf<Float, Long>()
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList` // 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 (!chapterUrls.contains(dbChapter.url)) {
if (dbChapter.read) deletedReadChapterNumbers.add(dbChapter.chapterNumber) if (dbChapter.read) deletedReadChapterNumbers.add(dbChapter.chapterNumber)
if (dbChapter.bookmarked) deletedBookmarkedChapterNumbers.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) deletedChapterNumbers.add(dbChapter.chapterNumber)
deletedChapterNumberDateFetchMap[dbChapter.chapterNumber] = dbChapter.fetchedAt deletedChapterNumberDateFetchMap[dbChapter.chapterNumber] = dbChapter.fetchedAt
dbChapter.id dbChapter.id
@@ -256,14 +262,16 @@ object Chapter {
} }
} }
transaction {
// we got some clean up due // we got some clean up due
if (chaptersIdsToDelete.isNotEmpty()) { if (chaptersIdsToDelete.isNotEmpty()) {
DownloadManager.dequeue(chaptersIdsToDelete) DownloadManager.dequeue(chaptersIdsToDelete)
transaction {
PageTable.deleteWhere { chapter inList chaptersIdsToDelete } PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
ChapterTable.deleteWhere { id inList chaptersIdsToDelete } ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
} }
}
transaction {
if (chaptersToInsert.isNotEmpty()) { if (chaptersToInsert.isNotEmpty()) {
ChapterTable ChapterTable
.batchInsert(chaptersToInsert) { chapter -> .batchInsert(chaptersToInsert) { chapter ->
@@ -279,31 +287,24 @@ object Chapter {
this[ChapterTable.isRead] = false this[ChapterTable.isRead] = false
this[ChapterTable.isBookmarked] = false this[ChapterTable.isBookmarked] = false
this[ChapterTable.isDownloaded] = false this[ChapterTable.isDownloaded] = false
this[ChapterTable.pageCount] = -1
// is recognized chapter number // is recognized chapter number
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) { if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
this[ChapterTable.isBookmarked] = chapter.chapterNumber in deletedBookmarkedChapterNumbers 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 // Try to use the fetch date of the original entry to not pollute 'Updates' tab
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let { deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
this[ChapterTable.fetchedAt] = it this[ChapterTable.fetchedAt] = it
} }
val deletedChapter = deletedDownloadedChapterNumberToChapter[chapter.chapterNumber]!!
val hasDownloadedPages = deletedChapter.pageCount > 0
val isSameName = deletedChapter.name == chapter.name
val isSameScanlator = deletedChapter.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] = deletedChapter.pageCount
}
} }
}.forEach { insertedChapters.add(ChapterTable.toDataClass(it)) } }.forEach { insertedChapters.add(ChapterTable.toDataClass(it)) }
} }
@@ -313,30 +314,12 @@ object Chapter {
.apply { .apply {
chaptersToUpdate.forEach { chaptersToUpdate.forEach {
addBatch(EntityID(it.id, ChapterTable)) addBatch(EntityID(it.id, ChapterTable))
val currentChapter = chaptersInDb.find { dbChapter -> dbChapter.id == it.id }!!
this[ChapterTable.name] = it.name this[ChapterTable.name] = it.name
this[ChapterTable.date_upload] = it.uploadDate this[ChapterTable.date_upload] = it.uploadDate
this[ChapterTable.chapter_number] = it.chapterNumber this[ChapterTable.chapter_number] = it.chapterNumber
this[ChapterTable.scanlator] = it.scanlator this[ChapterTable.scanlator] = it.scanlator
this[ChapterTable.sourceOrder] = it.index this[ChapterTable.sourceOrder] = it.index
this[ChapterTable.realUrl] = it.realUrl 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() }.toExecutable()
.execute(this@transaction) .execute(this@transaction)

View File

@@ -133,7 +133,7 @@ object Manga {
"" ""
} }
if (remoteTitle.isNotEmpty() && remoteTitle != mangaEntry[MangaTable.title]) { if (remoteTitle.isNotEmpty() && remoteTitle != mangaEntry[MangaTable.title]) {
val canUpdateTitle = updateMangaDownloadDir(mangaEntry[MangaTable.title], source.toString(), remoteTitle) val canUpdateTitle = updateMangaDownloadDir(mangaId, remoteTitle)
if (canUpdateTitle) { if (canUpdateTitle) {
it[MangaTable.title] = remoteTitle it[MangaTable.title] = remoteTitle

View File

@@ -358,7 +358,6 @@ object Extension {
} else { } else {
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) { ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[isInstalled] = false it[isInstalled] = false
it[hasUpdate] = false
} }
} }

View File

@@ -24,22 +24,14 @@ private val applicationDirs: ApplicationDirs by injectLazy()
private val logger = KotlinLogging.logger { } 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 = private fun getMangaDir(mangaId: Int): String =
transaction { transaction {
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference]) 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( private fun getChapterDir(
@@ -70,18 +62,8 @@ private fun getChapterDir(
fun getThumbnailDownloadPath(mangaId: Int): String = applicationDirs.thumbnailDownloadsRoot + "/$mangaId" 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 getMangaDownloadDir(mangaId: Int): String = applicationDirs.mangaDownloadsRoot + "/" + getMangaDir(mangaId)
fun getMangaCacheDir(
title: String,
sourceName: String,
): String = applicationDirs.tempMangaCacheRoot + "/" + getMangaDir(title, sourceName)
fun getChapterDownloadPath( fun getChapterDownloadPath(
mangaId: Int, mangaId: Int,
chapterId: Int, chapterId: Int,
@@ -97,21 +79,38 @@ fun getChapterCachePath(
chapterId: Int, chapterId: Int,
): String = applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId) ): String = applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId)
private fun updateDownloadDir( /** return value says if rename/move was successful */
currentDir: String, fun updateMangaDownloadDir(
newDir: String, mangaId: Int,
newTitle: String,
): Boolean { ): 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) val newDirFile = File(newDir)
if (!currentDirFile.exists()) { if (!oldDirFile.exists()) {
return true return true
} }
return try { return try {
Files.move(currentDirFile.toPath(), newDirFile.toPath()) Files.move(oldDirFile.toPath(), newDirFile.toPath())
if (currentDirFile.exists()) { if (oldDirFile.exists()) {
return false return false
} }
@@ -119,31 +118,9 @@ private fun updateDownloadDir(
return false return false
} }
return true true
} catch (e: Exception) { } 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 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
}

View File

@@ -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 * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import java.util.Objects
import kotlin.math.min import kotlin.math.min
open class PaginatedList<T>( open class PaginatedList<T>(
val page: List<T>, val page: List<T>,
val hasNextPage: Boolean, 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 const val PAGINATION_FACTOR = 50

View File

@@ -15,7 +15,7 @@ import suwayomi.tachidesk.manga.model.table.CategoryMetaTable.ref
* Metadata storage for clients, about Category with id == [ref]. * Metadata storage for clients, about Category with id == [ref].
*/ */
object CategoryMetaTable : IntIdTable() { object CategoryMetaTable : IntIdTable() {
val key = varchar("meta_key", 256) val key = varchar("key", 256)
val value = varchar("value", 4096) val value = varchar("value", 4096)
val ref = reference("category_ref", CategoryTable, ReferenceOption.CASCADE) val ref = reference("category_ref", CategoryTable, ReferenceOption.CASCADE)
} }

View File

@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterMetaTable.ref
* } * }
*/ */
object ChapterMetaTable : IntIdTable() { object ChapterMetaTable : IntIdTable() {
val key = varchar("meta_key", 256) val key = varchar("key", 256)
val value = varchar("value", 4096) val value = varchar("value", 4096)
val ref = reference("chapter_ref", ChapterTable, ReferenceOption.CASCADE) val ref = reference("chapter_ref", ChapterTable, ReferenceOption.CASCADE)
} }

View File

@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.model.table.MangaMetaTable.ref
* } * }
*/ */
object MangaMetaTable : IntIdTable() { object MangaMetaTable : IntIdTable() {
val key = varchar("meta_key", 256) val key = varchar("key", 256)
val value = varchar("value", 4096) val value = varchar("value", 4096)
val ref = reference("manga_ref", MangaTable, ReferenceOption.CASCADE) val ref = reference("manga_ref", MangaTable, ReferenceOption.CASCADE)
} }

View File

@@ -14,7 +14,7 @@ import suwayomi.tachidesk.manga.model.table.SourceMetaTable.ref
* Metadata storage for clients, about Source with id == [ref]. * Metadata storage for clients, about Source with id == [ref].
*/ */
object SourceMetaTable : IntIdTable() { object SourceMetaTable : IntIdTable() {
val key = varchar("meta_key", 256) val key = varchar("key", 256)
val value = varchar("value", 4096) val value = varchar("value", 4096)
val ref = long("source_ref") val ref = long("source_ref")
} }

View File

@@ -5,8 +5,6 @@ import android.content.Context
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.server.database.H2Migration 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@@ -101,7 +99,7 @@ private val MIGRATIONS =
fun runMigrations(applicationDirs: ApplicationDirs) { fun runMigrations(applicationDirs: ApplicationDirs) {
val logger = KotlinLogging.logger("Migration") val logger = KotlinLogging.logger("Migration")
try {
val migrationPreferences = val migrationPreferences =
Injekt Injekt
.get<Application>() .get<Application>()
@@ -128,8 +126,4 @@ fun runMigrations(applicationDirs: ApplicationDirs) {
migrationPreferences.edit().putInt("version", migrationVersion).apply() migrationPreferences.edit().putInt("version", migrationVersion).apply()
} }
} catch (e: Exception) {
logger.error(e) { "Failed to run migrations" }
shutdownApp(ExitCode.MigrationsRunFailure)
}
} }

View File

@@ -22,7 +22,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import io.github.config4k.toConfig import io.github.config4k.toConfig
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.json.JavalinJackson3 import io.javalin.json.JavalinJackson
import io.javalin.json.JsonMapper import io.javalin.json.JsonMapper
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -222,7 +222,7 @@ fun serverModule(applicationDirs: ApplicationDirs): Module =
module { module {
single { applicationDirs } single { applicationDirs }
single<IUpdater> { Updater() } single<IUpdater> { Updater() }
single<JsonMapper> { JavalinJackson3() } single<JsonMapper> { JavalinJackson() }
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@@ -366,7 +366,6 @@ fun applicationSetup() {
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.error(e) { "Exception while creating initial server.conf" } logger.error(e) { "Exception while creating initial server.conf" }
shutdownApp(ExitCode.SetupConfFileFailed)
} }
// copy local source icon // copy local source icon
@@ -379,7 +378,6 @@ fun applicationSetup() {
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.error(e) { "Exception while copying Local source's icon" } logger.error(e) { "Exception while copying Local source's icon" }
shutdownApp(ExitCode.LocalSourceIconCopyFailure)
} }
// fixes #119 , ref: // fixes #119 , ref:
@@ -397,12 +395,7 @@ fun applicationSetup() {
databaseUp() databaseUp()
try {
LocalSource.register() LocalSource.register()
} catch (e: Exception) {
logger.error(e) { "Failed to setup LocalSource" }
shutdownApp(ExitCode.LocalSourceSetupFailure)
}
serverConfig.subscribeTo( serverConfig.subscribeTo(
combine<Any, DatabaseSettings>( combine<Any, DatabaseSettings>(
@@ -532,7 +525,7 @@ fun applicationSetup() {
} }
} }
} }
download { github { release("jbr-release-21.0.10b1163.108") } } download { github() }
settings { settings {
windowlessRenderingEnabled = true windowlessRenderingEnabled = true
cachePath = (Path(applicationDirs.dataRoot) / "cache/kcef").toString() cachePath = (Path(applicationDirs.dataRoot) / "cache/kcef").toString()

View File

@@ -27,6 +27,7 @@ import suwayomi.tachidesk.server.util.ExitCode
import suwayomi.tachidesk.server.util.shutdownApp import suwayomi.tachidesk.server.util.shutdownApp
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.sql.SQLException
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -144,10 +145,9 @@ object DBManager {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
fun databaseUp(givenDb: Database? = null) { fun databaseUp() {
val db = val db =
givenDb try {
?: try {
DBManager.setupDatabase() DBManager.setupDatabase()
} catch (e: Exception) { } catch (e: Exception) {
logger.error(e) { "Failed to setup Database" } logger.error(e) { "Failed to setup Database" }
@@ -184,8 +184,10 @@ fun databaseUp(givenDb: Database? = null) {
} }
val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java) val migrations = loadMigrationsFrom("suwayomi.tachidesk.server.database.migration", ServerConfig::class.java)
runMigrations(migrations) runMigrations(migrations)
} catch (e: Exception) { } catch (e: SQLException) {
logger.error(e) { "Error up-to-database migration" } logger.error(e) { "Error up-to-database migration" }
if (System.getProperty("crashOnFailedMigration").toBoolean()) {
shutdownApp(ExitCode.DbMigrationFailure) shutdownApp(ExitCode.DbMigrationFailure)
} }
}
} }

View File

@@ -9,12 +9,9 @@ import java.net.URLClassLoader
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.Path import kotlin.io.path.Path
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
import kotlin.io.path.bufferedReader
import kotlin.io.path.bufferedWriter
import kotlin.io.path.copyTo import kotlin.io.path.copyTo
import kotlin.io.path.createDirectories import kotlin.io.path.createDirectories
import kotlin.io.path.deleteExisting import kotlin.io.path.deleteExisting
import kotlin.io.path.deleteIfExists
import kotlin.io.path.div import kotlin.io.path.div
import kotlin.io.path.exists import kotlin.io.path.exists
import kotlin.io.path.name import kotlin.io.path.name
@@ -49,10 +46,6 @@ object H2Migration {
} }
val script = Path("$dbBase.${h2Old.substringAfterLast('.')}.sql") val script = Path("$dbBase.${h2Old.substringAfterLast('.')}.sql")
script.deleteIfExists()
val modifiedScript = Path("$dbBase.${h2Old.substringAfterLast('.')}.modified.sql")
modifiedScript.deleteIfExists()
// Backup original database. // Backup original database.
val backup = Path("$dbBase.mv.db.${h2Old.substringAfterLast('.')}.backup") val backup = Path("$dbBase.mv.db.${h2Old.substringAfterLast('.')}.backup")
@@ -79,32 +72,19 @@ object H2Migration {
libsDir.resolve("h2-$h2New.bin"), 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( runMigrationTool(
migrationJar = migrationJar, migrationJar = migrationJar,
libsDir = libsDir, libsDir = libsDir,
mvStore = mvStore, mvStore = mvStore,
script = script, script = script,
modifiedScript = modifiedScript,
h2Old = h2Old, h2Old = h2Old,
h2New = h2New, h2New = h2New,
) )
// Move database to proper path // Move database to proper path
if (modifiedNewDatabase.exists()) { val newDatabase = Path(rootDir, "database.${h2New.substringAfterLast('.')}.mv.db")
modifiedNewDatabase.copyTo(mvStore, overwrite = true)
modifiedNewDatabase.deleteExisting()
newDatabase.deleteIfExists()
} else {
newDatabase.copyTo(mvStore, overwrite = true) newDatabase.copyTo(mvStore, overwrite = true)
newDatabase.deleteExisting() newDatabase.deleteExisting()
}
logger.info { "H2 migration completed successfully." } logger.info { "H2 migration completed successfully." }
} }
@@ -143,7 +123,6 @@ object H2Migration {
libsDir: Path, libsDir: Path,
mvStore: Path, mvStore: Path,
script: Path, script: Path,
modifiedScript: Path,
h2Old: String, h2Old: String,
h2New: String, h2New: String,
) { ) {
@@ -157,7 +136,6 @@ object H2Migration {
val main = val main =
clazz.getMethod("main", Array<String>::class.java) clazz.getMethod("main", Array<String>::class.java)
try {
main.invoke( main.invoke(
null, null,
arrayOf( arrayOf(
@@ -184,50 +162,6 @@ object H2Migration {
script.absolutePathString(), 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
}
}
} }
} }
} }

View File

@@ -10,10 +10,17 @@ package suwayomi.tachidesk.server.database.migration
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import de.neonew.exposed.migrations.helpers.SQLMigration 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") @Suppress("ClassName", "unused")
class M0023_CategoryMetaRefFix : SQLMigration() { 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 CategoryMetaTable by lazy { "CategoryMeta".toSqlName() }
private val CategoryRefColumn by lazy { "category_ref".toSqlName() } private val CategoryRefColumn by lazy { "category_ref".toSqlName() }
private val CategoryTable by lazy { "Category".toSqlName() } private val CategoryTable by lazy { "Category".toSqlName() }

View File

@@ -8,7 +8,6 @@ package suwayomi.tachidesk.server.database.migration
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import de.neonew.exposed.migrations.helpers.SQLMigration import de.neonew.exposed.migrations.helpers.SQLMigration
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
@Suppress("ClassName", "unused") @Suppress("ClassName", "unused")
class M0049_FixDuplicatedMetas : SQLMigration() { class M0049_FixDuplicatedMetas : SQLMigration() {
@@ -16,7 +15,7 @@ class M0049_FixDuplicatedMetas : SQLMigration() {
table: String, table: String,
refColumn: String? = null, refColumn: String? = null,
): String { ): String {
val groupBy = listOfNotNull(refColumn, "KEY".toSqlName()).joinToString(", ") val groupBy = listOfNotNull(refColumn, "KEY").joinToString(", ")
return """ return """
DELETE FROM $table DELETE FROM $table
@@ -31,11 +30,10 @@ class M0049_FixDuplicatedMetas : SQLMigration() {
""".trimIndent() """.trimIndent()
} }
override val sql: String by lazy { override val sql: String =
createMigrationForTable("CATEGORYMETA", "CATEGORY_REF") + createMigrationForTable("CATEGORYMETA", "CATEGORY_REF") +
createMigrationForTable("CHAPTERMETA", "CHAPTER_REF") + createMigrationForTable("CHAPTERMETA", "CHAPTER_REF") +
createMigrationForTable("GLOBALMETA") + createMigrationForTable("GLOBALMETA") +
createMigrationForTable("MANGAMETA", "MANGA_REF") + createMigrationForTable("MANGAMETA", "MANGA_REF") +
createMigrationForTable("SOURCEMETA", "SOURCE_REF") createMigrationForTable("SOURCEMETA", "SOURCE_REF")
}
} }

View File

@@ -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")
}
}

View File

@@ -1,9 +1,17 @@
package suwayomi.tachidesk.server.database.migration.helpers package suwayomi.tachidesk.server.database.migration.helpers
import de.neonew.exposed.migrations.helpers.SQLMigration import de.neonew.exposed.migrations.helpers.SQLMigration
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
import suwayomi.tachidesk.graphql.types.DatabaseType import suwayomi.tachidesk.graphql.types.DatabaseType
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
fun String.toSqlName(): String =
TransactionManager.current().db.identifierManager.let {
it.quoteIfNecessary(
it.inProperCase(this),
)
}
abstract class RenameFieldMigration( abstract class RenameFieldMigration(
tableName: String, tableName: String,
originalName: String, originalName: String,

View File

@@ -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),
)
}

View File

@@ -21,10 +21,6 @@ enum class ExitCode(
WebUISetupFailure(3), WebUISetupFailure(3),
ConfigMigrationMisconfiguredFailure(4), ConfigMigrationMisconfiguredFailure(4),
DbMigrationFailure(5), DbMigrationFailure(5),
SetupConfFileFailed(6),
LocalSourceIconCopyFailure(7),
LocalSourceSetupFailure(8),
MigrationsRunFailure(9),
} }
fun shutdownApp(exitCode: ExitCode) { fun shutdownApp(exitCode: ExitCode) {

View File

@@ -1,22 +1,17 @@
package masstest package masstest
import android.os.Looper
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.koin.core.context.stopKoin
import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.server.applicationSetup import suwayomi.tachidesk.server.applicationSetup
import suwayomi.tachidesk.server.settings.SettingsRegistry
import suwayomi.tachidesk.test.BASE_PATH import suwayomi.tachidesk.test.BASE_PATH
import suwayomi.tachidesk.test.setLoggingEnabled import suwayomi.tachidesk.test.setLoggingEnabled
import xyz.nulldev.ts.config.CONFIG_PREFIX import xyz.nulldev.ts.config.CONFIG_PREFIX
@@ -30,11 +25,8 @@ class CloudFlareTest {
fun setup() { fun setup() {
val dataRoot = File(BASE_PATH).absolutePath val dataRoot = File(BASE_PATH).absolutePath
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot) System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
Looper.clearMainLooperForTest()
SettingsRegistry.clear()
applicationSetup() applicationSetup()
setLoggingEnabled(false) setLoggingEnabled(false)
return
runBlocking { runBlocking {
val extensions = ExtensionsList.getExtensionList() val extensions = ExtensionsList.getExtensionList()
@@ -56,15 +48,9 @@ class CloudFlareTest {
setLoggingEnabled(true) setLoggingEnabled(true)
} }
@AfterAll
fun teardown() {
stopKoin()
}
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@Test @Test
@Disabled
fun `test nhentai browse`() = fun `test nhentai browse`() =
runTest { runTest {
assert(nhentai.getPopularManga(1).mangas.isNotEmpty()) { assert(nhentai.getPopularManga(1).mangas.isNotEmpty()) {

View File

@@ -7,7 +7,6 @@ package masstest
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.os.Looper
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@@ -18,11 +17,9 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.koin.core.context.stopKoin
import suwayomi.tachidesk.manga.impl.Source.getSourceList import suwayomi.tachidesk.manga.impl.Source.getSourceList
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension 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.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.applicationSetup import suwayomi.tachidesk.server.applicationSetup
import suwayomi.tachidesk.server.settings.SettingsRegistry
import suwayomi.tachidesk.test.BASE_PATH import suwayomi.tachidesk.test.BASE_PATH
import suwayomi.tachidesk.test.setLoggingEnabled import suwayomi.tachidesk.test.setLoggingEnabled
import xyz.nulldev.ts.config.CONFIG_PREFIX import xyz.nulldev.ts.config.CONFIG_PREFIX
@@ -55,8 +51,6 @@ class TestExtensionCompatibility {
fun setup() { fun setup() {
val dataRoot = File(BASE_PATH).absolutePath val dataRoot = File(BASE_PATH).absolutePath
System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot) System.setProperty("$CONFIG_PREFIX.server.rootDir", dataRoot)
Looper.clearMainLooperForTest()
SettingsRegistry.clear()
applicationSetup() applicationSetup()
setLoggingEnabled(false) setLoggingEnabled(false)
@@ -78,22 +72,12 @@ class TestExtensionCompatibility {
} }
} }
} }
sources = sources = getSourceList().map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
getSourceList()
.filter {
// filter local source
it.id.toLong() != 0L
}.map { getCatalogueSourceOrNull(it.id.toLong())!! as HttpSource }
} }
setLoggingEnabled(true) setLoggingEnabled(true)
File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" }) File("$BASE_PATH/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
} }
@AfterAll
fun teardown() {
stopKoin()
}
@Test @Test
fun runTest() { fun runTest() {
runBlocking(Dispatchers.Default) { runBlocking(Dispatchers.Default) {

View File

@@ -1,56 +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()
}
}

View File

@@ -18,22 +18,19 @@ import suwayomi.tachidesk.test.clearTables
class CategoryControllerTest : ApplicationTest() { class CategoryControllerTest : ApplicationTest() {
@Test @Test
fun categoryReorder() { fun categoryReorder() {
clearTables(
CategoryTable,
)
Category.createCategory("foo") Category.createCategory("foo")
Category.createCategory("bar") Category.createCategory("bar")
val cats = Category.getCategoryList() val cats = Category.getCategoryList()
val foo = cats.asSequence().filter { it.name == "foo" }.first() val foo = cats.asSequence().filter { it.name == "foo" }.first()
val bar = cats.asSequence().filter { it.name == "bar" }.first() val bar = cats.asSequence().filter { it.name == "bar" }.first()
assertEquals(0, foo.order) assertEquals(1, foo.order)
assertEquals(1, bar.order) assertEquals(2, bar.order)
Category.reorderCategory(1, 2) Category.reorderCategory(1, 2)
val catsReordered = Category.getCategoryList() val catsReordered = Category.getCategoryList()
val fooReordered = catsReordered.asSequence().filter { it.name == "foo" }.first() val fooReordered = catsReordered.asSequence().filter { it.name == "foo" }.first()
val barReordered = catsReordered.asSequence().filter { it.name == "bar" }.first() val barReordered = catsReordered.asSequence().filter { it.name == "bar" }.first()
assertEquals(1, fooReordered.order) assertEquals(2, fooReordered.order)
assertEquals(0, barReordered.order) assertEquals(1, barReordered.order)
} }
@AfterEach @AfterEach

View File

@@ -35,7 +35,7 @@ class CategoryMangaTest : ApplicationTest() {
CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount, CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,
"Manga should not have any unread chapters", "Manga should not have any unread chapters",
) )
createChapters(mangaId, 10, false, start = 11) createChapters(mangaId, 10, false)
assertEquals( assertEquals(
10, 10,
CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount, CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount,

View File

@@ -12,12 +12,9 @@ import eu.kanade.tachiyomi.createAppModule
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import io.github.oshai.kotlinlogging.KotlinLogging 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.jetbrains.exposed.v1.jdbc.Database
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup
import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.ServerConfig
@@ -25,9 +22,7 @@ import suwayomi.tachidesk.server.androidCompat
import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.database.databaseUp
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.serverModule import suwayomi.tachidesk.server.serverModule
import suwayomi.tachidesk.server.settings.SettingsRegistry
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
import suwayomi.tachidesk.server.util.ConfigTypeRegistration
import suwayomi.tachidesk.server.util.SystemTray import suwayomi.tachidesk.server.util.SystemTray
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -60,13 +55,6 @@ open class ApplicationTest {
private var initializedTheApp = false private var initializedTheApp = false
fun testingSetup() { fun testingSetup() {
// register Tachidesk's config which is dubbed "ServerConfig"
SettingsRegistry.clear()
ConfigTypeRegistration.registerCustomTypes()
GlobalConfigManager.registerModule(
ServerConfig.register { GlobalConfigManager.config },
)
// Application dirs // Application dirs
val applicationDirs = ApplicationDirs() val applicationDirs = ApplicationDirs()
@@ -84,9 +72,13 @@ open class ApplicationTest {
File(it).mkdirs() File(it).mkdirs()
} }
// register Tachidesk's config which is dubbed "ServerConfig"
GlobalConfigManager.registerModule(
ServerConfig.register { GlobalConfigManager.config },
)
// initialize Koin modules // initialize Koin modules
val app = App() val app = App()
stopKoin()
startKoin { startKoin {
modules( modules(
createAppModule(app), createAppModule(app),
@@ -136,14 +128,14 @@ open class ApplicationTest {
} }
// create system tray // create system tray
// if (serverConfig.systemTrayEnabled.value) { if (serverConfig.systemTrayEnabled.value) {
// try { try {
// SystemTray.create() SystemTray.create()
// } catch (e: Throwable) { } catch (e: Throwable) {
// // cover both java.lang.Exception and java.lang.Error // cover both java.lang.Exception and java.lang.Error
// e.printStackTrace() e.printStackTrace()
// } }
// } }
// Disable jetty's logging // Disable jetty's logging
System.setProperty("org.eclipse.jetty.util.log.announce", "false") 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() // fixes #119 , ref: https://github.com/Suwayomi/Suwayomi-Server/issues/119#issuecomment-894681292 , source Id calculation depends on String.lowercase()
Locale.setDefault(Locale.ENGLISH) 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 // 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) databaseUp(db)

View File

@@ -55,9 +55,8 @@ fun createChapters(
mangaId: Int, mangaId: Int,
amount: Int, amount: Int,
read: Boolean, 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 { transaction {
ChapterTable ChapterTable
.batchInsert(list) { .batchInsert(list) {