Compare commits

...

44 Commits

Author SHA1 Message Date
Aria Moradi d35e31e02d bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 16s
CI Publish / Build artifacts and release (push) Failing after 15s
2021-10-18 14:41:01 +03:30
Aria Moradi 6c4ca36c09 update WebUI 2021-10-18 14:37:13 +03:30
Aria Moradi 70355dc505 update 2021-10-11 00:21:10 +03:30
Aria Moradi 20aeaf2a05 update 2021-10-11 00:18:49 +03:30
Mitchell Syer 1f13e1d08b Use a custom task to run electron (#220) 2021-10-11 00:16:32 +03:30
Aria Moradi eb9d35c123 code cleanup 2021-10-11 00:15:15 +03:30
Mitchell Syer 45808cd530 Support using a CatalogueSource instead of only HttpSources (#219) 2021-10-11 00:01:04 +03:30
Aria Moradi fd715a3f92 fix workflow 2021-10-10 23:52:35 +03:30
Aria Moradi e3b32367a7 update 2021-10-10 23:51:21 +03:30
Aria Moradi bf9554a746 update 2021-10-10 23:32:19 +03:30
Aria Moradi b9f8ca1488 update 2021-10-10 23:01:58 +03:30
Aria Moradi e8c4159678 use correct conversion units 2021-10-10 21:31:38 +03:30
Aria Moradi e57e71629e update 2021-10-10 21:05:24 +03:30
Aria Moradi 2bfd9d24a4 remove isNsfw annotation detection 2021-10-10 21:04:28 +03:30
Aria Moradi b18b8fe22f update 2021-10-10 20:58:10 +03:30
Aria Moradi b154ff2f9d fix export chapter ordering, include new props in backup 2021-10-10 20:53:50 +03:30
Aria Moradi e9b764b63c update 2021-10-10 12:27:50 +03:30
Aria Moradi 7216b97d92 mimic Tachyomi's behaviour more closely, fixes ReadComicOnline (EN) 2021-10-10 12:18:21 +03:30
Aria Moradi 0e9d93b194 prepare CHANGELOG 2021-10-06 21:18:21 +03:30
Aria Moradi 2cbee62f0a update docs 2021-09-28 18:06:20 +03:30
Aria Moradi 379e9da5fe clenup 2021-09-28 18:00:24 +03:30
Aria Moradi ae7caa4901 merge 2021-09-28 17:57:55 +03:30
Aria Moradi cd8b4c9dd7 convert android.jar lib to a maven repo 2021-09-28 17:55:50 +03:30
Aria Moradi 60cd61dfd2 Update README.md 2021-09-28 03:42:20 +03:30
Aria Moradi 5a6637d9fc Update README.md 2021-09-28 03:39:45 +03:30
Aria Moradi dca7ed23f5 Update README.md 2021-09-28 03:36:06 +03:30
Aria Moradi 8cb5791f3b Update README.md 2021-09-28 03:34:14 +03:30
Aria Moradi 9b67f2c58f Update CHANGELOG.md 2021-09-28 03:24:57 +03:30
Aria Moradi 3815810d4f Update for release 2021-09-28 01:11:24 +03:30
Aria Moradi 819ceba17d bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 16s
2021-09-28 00:52:49 +03:30
Aria Moradi 0aa0d62e03 update changelog file and it's template 2021-09-28 00:51:05 +03:30
Aria Moradi b3e2a35880 update WebUI 2021-09-28 00:50:33 +03:30
Aria Moradi 15ec20c65d fix sorting 2021-09-27 20:27:40 +03:30
Aria Moradi d4d6d7e12f add recentChapters endpoint 2021-09-27 18:27:05 +03:30
Aria Moradi 2e7a4f1421 remove no longer relevant comment 2021-09-27 14:44:48 +03:30
Aria Moradi ab8a52faf3 rename ChapterTable.chapterIndex to ChapterTable.sourceOrder 2021-09-27 14:36:06 +03:30
Aria Moradi bd465559fb Update README.md 2021-09-26 23:48:29 +03:30
Aria Moradi 13ec45a95c aftermath of adding kotlinter to all modules 2021-09-25 04:34:02 +03:30
Mitchell Syer 13b034875b Workaround StdLib issue and add KtLint to all modules (#206)
* Workaround buildconfig kotlin stdlib issue

* Add KtLint to all modules
2021-09-25 04:31:03 +03:30
Aria Moradi bb701fb088 fix macOS-arm64 java path 2021-09-24 14:06:19 +03:30
Aria Moradi b367414865 changes 2021-09-24 13:56:26 +03:30
Aria Moradi 4b00eec608 update CHANGELOG 2021-09-19 18:01:13 +04:30
Aria Moradi 5e11b51152 update CHANGELOG 2021-09-19 17:59:37 +04:30
Aria Moradi 9fb43b996e CHANGELOG update 2021-09-19 17:39:28 +04:30
63 changed files with 565 additions and 424 deletions
-4
View File
@@ -45,10 +45,6 @@ jobs:
mkdir -p ~/.gradle mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Build Jar - name: Build Jar
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
-16
View File
@@ -47,11 +47,6 @@ jobs:
mkdir -p ~/.gradle mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Build Jar - name: Build Jar
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
env: env:
@@ -64,12 +59,6 @@ jobs:
dependencies-cache-enabled: true dependencies-cache-enabled: true
configuration-cache-enabled: true configuration-cache-enabled: true
# - name: Mock Build and copy webUI, Build Jar
# run: |
# mkdir -p master/server/build
# cd master/server/build
# echo "test" > Tachidesk-v0.3.8-r583.jar
- name: Generate Tag Name - name: Generate Tag Name
id: GenTagName id: GenTagName
run: | run: |
@@ -87,11 +76,6 @@ jobs:
./unix-bundler.sh macOS-x64 ./unix-bundler.sh macOS-x64
./unix-bundler.sh macOS-arm64 ./unix-bundler.sh macOS-arm64
# - name: Mock make windows packages
# run: |
# cd master/server/build
# echo test > Tachidesk-v0.3.8-r580-win32.zip
- name: Checkout preview branch - name: Checkout preview branch
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
-5
View File
@@ -46,11 +46,6 @@ jobs:
mkdir -p ~/.gradle mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download android.jar
run: |
cd master
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Build and copy webUI, Build Jar - name: Build and copy webUI, Build Jar
uses: eskatos/gradle-command-action@v1 uses: eskatos/gradle-command-action@v1
env: env:
@@ -14,7 +14,7 @@ const val CONFIG_PREFIX = "suwayomi.tachidesk.config"
val ApplicationRootDir: String val ApplicationRootDir: String
get(): String { get(): String {
return System.getProperty( return System.getProperty(
"$CONFIG_PREFIX.server.rootDir", "$CONFIG_PREFIX.server.rootDir",
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null) AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
) )
} }
@@ -6,7 +6,7 @@ import org.kodein.di.singleton
class ConfigKodeinModule { class ConfigKodeinModule {
fun create() = DI.Module("ConfigManager") { fun create() = DI.Module("ConfigManager") {
//Config module // Config module
bind<ConfigManager>() with singleton { GlobalConfigManager } bind<ConfigManager>() with singleton { GlobalConfigManager }
} }
} }
@@ -21,7 +21,7 @@ open class ConfigManager {
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>() private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
val config by lazy { loadConfigs() } val config by lazy { loadConfigs() }
//Public read-only view of modules // Public read-only view of modules
val loadedModules: Map<Class<out ConfigModule>, ConfigModule> val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
get() = generatedModules get() = generatedModules
@@ -42,29 +42,28 @@ open class ConfigManager {
* Load configs * Load configs
*/ */
fun loadConfigs(): Config { fun loadConfigs(): Config {
//Load reference configs // Load reference configs
val compatConfig = ConfigFactory.parseResources("compat-reference.conf") val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val serverConfig = ConfigFactory.parseResources("server-reference.conf") val serverConfig = ConfigFactory.parseResources("server-reference.conf")
val baseConfig = val baseConfig =
ConfigFactory.parseMap( ConfigFactory.parseMap(
mapOf( mapOf(
"androidcompat.rootDir" to "$ApplicationRootDir/android-compat" // override AndroidCompat's rootDir "androidcompat.rootDir" to "$ApplicationRootDir/android-compat" // override AndroidCompat's rootDir
)
) )
)
//Load user config // Load user config
val userConfig = val userConfig =
File(ApplicationRootDir, "server.conf").let { File(ApplicationRootDir, "server.conf").let {
ConfigFactory.parseFile(it) ConfigFactory.parseFile(it)
} }
val config = ConfigFactory.empty() val config = ConfigFactory.empty()
.withFallback(baseConfig) .withFallback(baseConfig)
.withFallback(userConfig) .withFallback(userConfig)
.withFallback(compatConfig) .withFallback(compatConfig)
.withFallback(serverConfig) .withFallback(serverConfig)
.resolve() .resolve()
// set log level early // set log level early
if (debugLogsEnabled(config)) { if (debugLogsEnabled(config)) {
@@ -20,7 +20,7 @@ abstract class ConfigModule(config: Config)
/** /**
* Abstract jvm-commandline-argument-overridable config module. * Abstract jvm-commandline-argument-overridable config module.
*/ */
abstract class SystemPropertyOverridableConfigModule(config: Config, moduleName: String): ConfigModule(config) { abstract class SystemPropertyOverridableConfigModule(config: Config, moduleName: String) : ConfigModule(config) {
val overridableConfig = SystemPropertyOverrideDelegate(config, moduleName) val overridableConfig = SystemPropertyOverrideDelegate(config, moduleName)
} }
@@ -34,7 +34,7 @@ class SystemPropertyOverrideDelegate(val config: Config, val moduleName: String)
configValue.toString() configValue.toString()
) )
return when(T::class.simpleName) { return when (T::class.simpleName) {
"Int" -> combined.toInt() "Int" -> combined.toInt()
"Boolean" -> combined.toBoolean() "Boolean" -> combined.toBoolean()
// add more types as needed // add more types as needed
@@ -16,5 +16,5 @@ fun setLogLevel(level: Level) {
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level (KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level
} }
fun debugLogsEnabled(config: Config) fun debugLogsEnabled(config: Config) =
= System.getProperty("suwayomi.tachidesk.config.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean() System.getProperty("suwayomi.tachidesk.config.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
@@ -3,4 +3,4 @@ package xyz.nulldev.ts.config.util
import com.typesafe.config.Config import com.typesafe.config.Config
operator fun Config.get(key: String) = getString(key) operator fun Config.get(key: String) = getString(key)
?: throw IllegalStateException("Could not find value for config entry: $key!") ?: throw IllegalStateException("Could not find value for config entry: $key!")
+1 -1
View File
@@ -1,6 +1,6 @@
dependencies { dependencies {
// Android stub library // Android stub library
implementation(fileTree("lib/")) implementation("com.github.Suwayomi:android-jar:1.0.0")
// XML // XML
compileOnly("xmlpull:xmlpull:1.1.3.4a") compileOnly("xmlpull:xmlpull:1.1.3.4a")
-1
View File
@@ -1 +0,0 @@
android.jar
@@ -9,8 +9,10 @@ import android.content.Context
class PreferenceManager { class PreferenceManager {
companion object { companion object {
@JvmStatic @JvmStatic
fun getDefaultSharedPreferences(context: Context) fun getDefaultSharedPreferences(context: Context) =
= context.getSharedPreferences(context.applicationInfo.packageName, context.getSharedPreferences(
Context.MODE_PRIVATE)!! context.applicationInfo.packageName,
Context.MODE_PRIVATE
)!!
} }
} }
@@ -14,7 +14,7 @@ class AndroidCompatInitializer {
fun init() { fun init() {
DI.global.addImport(AndroidCompatModule().create()) DI.global.addImport(AndroidCompatModule().create())
//Register config modules // Register config modules
GlobalConfigManager.registerModules( GlobalConfigManager.registerModules(
FilesConfigModule.register(GlobalConfigManager.config), FilesConfigModule.register(GlobalConfigManager.config),
ApplicationInfoConfigModule.register(GlobalConfigManager.config), ApplicationInfoConfigModule.register(GlobalConfigManager.config),
@@ -29,7 +29,7 @@ class AndroidCompatModule {
bind<PackageController>() with singleton { PackageController() } bind<PackageController>() with singleton { PackageController() }
//Context // Context
bind<CustomContext>() with singleton { CustomContext() } bind<CustomContext>() with singleton { CustomContext() }
bind<Context>() with singleton { bind<Context>() with singleton {
val context: Context by DI.global.instance<CustomContext>() val context: Context by DI.global.instance<CustomContext>()
@@ -13,7 +13,7 @@ class ApplicationInfoConfigModule(config: Config) : ConfigModule(config) {
val debug: Boolean by config val debug: Boolean by config
companion object { companion object {
fun register(config: Config) fun register(config: Config) =
= ApplicationInfoConfigModule(config.getConfig("android.app")) ApplicationInfoConfigModule(config.getConfig("android.app"))
} }
} }
@@ -9,26 +9,26 @@ import xyz.nulldev.ts.config.ConfigModule
*/ */
class FilesConfigModule(config: Config) : ConfigModule(config) { class FilesConfigModule(config: Config) : ConfigModule(config) {
val dataDir:String by config val dataDir: String by config
val filesDir:String by config val filesDir: String by config
val noBackupFilesDir:String by config val noBackupFilesDir: String by config
val externalFilesDirs: MutableList<String> by config val externalFilesDirs: MutableList<String> by config
val obbDirs: MutableList<String> by config val obbDirs: MutableList<String> by config
val cacheDir:String by config val cacheDir: String by config
val codeCacheDir:String by config val codeCacheDir: String by config
val externalCacheDirs: MutableList<String> by config val externalCacheDirs: MutableList<String> by config
val externalMediaDirs: MutableList<String> by config val externalMediaDirs: MutableList<String> by config
val rootDir:String by config val rootDir: String by config
val externalStorageDir:String by config val externalStorageDir: String by config
val downloadCacheDir:String by config val downloadCacheDir: String by config
val databasesDir:String by config val databasesDir: String by config
val prefsDir:String by config val prefsDir: String by config
val packageDir:String by config val packageDir: String by config
companion object { companion object {
fun register(config: Config) fun register(config: Config) =
= FilesConfigModule(config.getConfig("android.files")) FilesConfigModule(config.getConfig("android.files"))
} }
} }
@@ -1,8 +1,8 @@
package xyz.nulldev.androidcompat.config package xyz.nulldev.androidcompat.config
import com.typesafe.config.Config import com.typesafe.config.Config
import xyz.nulldev.ts.config.ConfigModule
import io.github.config4k.getValue import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule
class SystemConfigModule(val config: Config) : ConfigModule(config) { class SystemConfigModule(val config: Config) : ConfigModule(config) {
val isDebuggable: Boolean by config val isDebuggable: Boolean by config
@@ -16,7 +16,7 @@ class SystemConfigModule(val config: Config) : ConfigModule(config) {
fun hasProperty(property: String) = config.hasPath("$propertyPrefix$property") fun hasProperty(property: String) = config.hasPath("$propertyPrefix$property")
companion object { companion object {
fun register(config: Config) fun register(config: Config) =
= SystemConfigModule(config.getConfig("android.system")) SystemConfigModule(config.getConfig("android.system"))
} }
} }
@@ -29,7 +29,7 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
val parentMetadata = parent.metaData val parentMetadata = parent.metaData
val columnCount = parentMetadata.columnCount val columnCount = parentMetadata.columnCount
val columnLabels = (1 .. columnCount).map { val columnLabels = (1..columnCount).map {
parentMetadata.getColumnLabel(it) parentMetadata.getColumnLabel(it)
}.toTypedArray() }.toTypedArray()
@@ -41,10 +41,10 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
// How can we optimize this? // How can we optimize this?
// We need to fill the cache as the set is loaded // We need to fill the cache as the set is loaded
//Fill cache // Fill cache
while(parent.next()) { while (parent.next()) {
cachedContent += ResultSetEntry().apply { cachedContent += ResultSetEntry().apply {
for(i in 1 .. columnCount) for (i in 1..columnCount)
data += parent.getObject(i) data += parent.getObject(i)
} }
resultSetLength++ resultSetLength++
@@ -60,8 +60,8 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
private fun internalMove(row: Int) { private fun internalMove(row: Int) {
if(cursor < 0) cursor = 0 if (cursor < 0) cursor = 0
else if(cursor > resultSetLength + 1) cursor = resultSetLength + 1 else if (cursor > resultSetLength + 1) cursor = resultSetLength + 1
else cursor = row else cursor = row
} }
@@ -75,10 +75,10 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return obj(cachedFindColumn(column)) return obj(cachedFindColumn(column))
} }
private fun cachedFindColumn(column: String?) private fun cachedFindColumn(column: String?) =
= columnCache.getOrPut(column!!, { columnCache.getOrPut(column!!, {
findColumn(column) findColumn(column)
}) })
override fun getNClob(columnIndex: Int): NClob { override fun getNClob(columnIndex: Int): NClob {
return obj(columnIndex) as NClob return obj(columnIndex) as NClob
@@ -157,27 +157,27 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getDate(columnIndex: Int): Date { override fun getDate(columnIndex: Int): Date {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getDate(columnLabel: String?): Date { override fun getDate(columnLabel: String?): Date {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getDate(columnIndex: Int, cal: Calendar?): Date { override fun getDate(columnIndex: Int, cal: Calendar?): Date {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getDate(columnLabel: String?, cal: Calendar?): Date { override fun getDate(columnLabel: String?, cal: Calendar?): Date {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun beforeFirst() { override fun beforeFirst() {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -202,12 +202,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getBigDecimal(columnIndex: Int, scale: Int): BigDecimal { override fun getBigDecimal(columnIndex: Int, scale: Int): BigDecimal {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getBigDecimal(columnLabel: String?, scale: Int): BigDecimal { override fun getBigDecimal(columnLabel: String?, scale: Int): BigDecimal {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -236,22 +236,22 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getTime(columnIndex: Int): Time { override fun getTime(columnIndex: Int): Time {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTime(columnLabel: String?): Time { override fun getTime(columnLabel: String?): Time {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTime(columnIndex: Int, cal: Calendar?): Time { override fun getTime(columnIndex: Int, cal: Calendar?): Time {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTime(columnLabel: String?, cal: Calendar?): Time { override fun getTime(columnLabel: String?, cal: Calendar?): Time {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -272,28 +272,28 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun absolute(row: Int): Boolean { override fun absolute(row: Int): Boolean {
if(row > 0) { if (row > 0) {
internalMove(row) internalMove(row)
} else { } else {
last() last()
for(i in 1 .. row) for (i in 1..row)
previous() previous()
} }
return cursorValid() return cursorValid()
} }
override fun getSQLXML(columnIndex: Int): SQLXML? { override fun getSQLXML(columnIndex: Int): SQLXML? {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getSQLXML(columnLabel: String?): SQLXML? { override fun getSQLXML(columnLabel: String?): SQLXML? {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun <T : Any?> unwrap(iface: Class<T>?): T { override fun <T : Any?> unwrap(iface: Class<T>?): T {
if(thisIsWrapperFor(iface)) if (thisIsWrapperFor(iface))
return this as T return this as T
else else
return parent.unwrap(iface) return parent.unwrap(iface)
@@ -426,12 +426,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getBlob(columnIndex: Int): Blob { override fun getBlob(columnIndex: Int): Blob {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getBlob(columnLabel: String?): Blob { override fun getBlob(columnLabel: String?): Blob {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -500,12 +500,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getObject(columnIndex: Int, map: MutableMap<String, Class<*>>?): Any { override fun getObject(columnIndex: Int, map: MutableMap<String, Class<*>>?): Any {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getObject(columnLabel: String?, map: MutableMap<String, Class<*>>?): Any { override fun getObject(columnLabel: String?, map: MutableMap<String, Class<*>>?): Any {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -531,9 +531,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
private fun castToLong(obj: Any?): Long { private fun castToLong(obj: Any?): Long {
if(obj == null) return 0 if (obj == null) return 0
else if(obj is Long) return obj else if (obj is Long) return obj
else if(obj is Number) return obj.toLong() else if (obj is Number) return obj.toLong()
else throw IllegalStateException("Object is not a long!") else throw IllegalStateException("Object is not a long!")
} }
@@ -546,12 +546,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getClob(columnIndex: Int): Clob { override fun getClob(columnIndex: Int): Clob {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getClob(columnLabel: String?): Clob { override fun getClob(columnLabel: String?): Clob {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -604,12 +604,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getArray(columnIndex: Int): Array { override fun getArray(columnIndex: Int): Array {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getArray(columnLabel: String?): Array { override fun getArray(columnLabel: String?): Array {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -688,32 +688,32 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getTimestamp(columnIndex: Int): Timestamp { override fun getTimestamp(columnIndex: Int): Timestamp {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTimestamp(columnLabel: String?): Timestamp { override fun getTimestamp(columnLabel: String?): Timestamp {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTimestamp(columnIndex: Int, cal: Calendar?): Timestamp { override fun getTimestamp(columnIndex: Int, cal: Calendar?): Timestamp {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getTimestamp(columnLabel: String?, cal: Calendar?): Timestamp { override fun getTimestamp(columnLabel: String?, cal: Calendar?): Timestamp {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getRef(columnIndex: Int): Ref { override fun getRef(columnIndex: Int): Ref {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getRef(columnLabel: String?): Ref { override fun getRef(columnLabel: String?): Ref {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -792,12 +792,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
} }
override fun getRowId(columnIndex: Int): RowId { override fun getRowId(columnIndex: Int): RowId {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
override fun getRowId(columnLabel: String?): RowId { override fun getRowId(columnLabel: String?): RowId {
//TODO Maybe? // TODO Maybe?
notImplemented() notImplemented()
} }
@@ -14,8 +14,6 @@ import java.io.File
import javax.imageio.ImageIO import javax.imageio.ImageIO
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
data class InstalledPackage(val root: File) { data class InstalledPackage(val root: File) {
val apk = File(root, "package.apk") val apk = File(root, "package.apk")
val jar = File(root, "translated.jar") val jar = File(root, "translated.jar")
@@ -40,20 +38,24 @@ data class InstalledPackage(val root: File) {
}?.filter { }?.filter {
it.tagName == "meta-data" it.tagName == "meta-data"
}?.map { }?.map {
putString(it.attributes.getNamedItem("android:name").nodeValue, putString(
it.attributes.getNamedItem("android:value").nodeValue) it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue
)
} }
} }
it.signatures = (parsed.apkSingers.flatMap { it.certificateMetas } it.signatures = (
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72 parsed.apkSingers.flatMap { it.certificateMetas }
.map { Signature(it.data) }.toTypedArray() /*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
} }
fun verify(): Boolean { fun verify(): Boolean {
val res = ApkVerifier.Builder(apk) val res = ApkVerifier.Builder(apk)
.build() .build()
.verify() .verify()
return res.isVerified return res.isVerified
} }
@@ -69,7 +71,7 @@ data class InstalledPackage(val root: File) {
}.sortedByDescending { it.width * it.height }.firstOrNull() ?: return }.sortedByDescending { it.width * it.height }.firstOrNull() ?: return
ImageIO.write(read, "png", icon) ImageIO.write(read, "png", icon)
} catch(e: Exception) { } catch (e: Exception) {
icon.delete() icon.delete()
} }
} }
@@ -77,7 +79,7 @@ data class InstalledPackage(val root: File) {
fun writeJar() { fun writeJar() {
try { try {
Dex2jar.from(apk).to(jar.toPath()) Dex2jar.from(apk).to(jar.toPath())
} catch(e: Exception) { } catch (e: Exception) {
jar.delete() jar.delete()
} }
} }
@@ -48,7 +48,7 @@ class PackageController {
if (!installed.jar.exists()) { if (!installed.jar.exists()) {
throw IllegalStateException("Failed to translate APK dex!") throw IllegalStateException("Failed to translate APK dex!")
} }
} catch(t: Throwable) { } catch (t: Throwable) {
root.deleteRecursively() root.deleteRecursively()
throw t throw t
} }
@@ -63,7 +63,7 @@ class PackageController {
} }
fun deletePackage(pack: InstalledPackage) { fun deletePackage(pack: InstalledPackage) {
if(!pack.root.exists()) error("Package was never installed!") if (!pack.root.exists()) error("Package was never installed!")
val packageName = pack.info.packageName val packageName = pack.info.packageName
pack.root.deleteRecursively() pack.root.deleteRecursively()
@@ -74,7 +74,7 @@ class PackageController {
fun findPackage(packageName: String): InstalledPackage? { fun findPackage(packageName: String): InstalledPackage? {
val file = File(androidFiles.packagesDir, packageName) val file = File(androidFiles.packagesDir, packageName)
return if(file.exists()) return if (file.exists())
InstalledPackage(file) InstalledPackage(file)
else else
null null
@@ -27,10 +27,10 @@ class ServiceSupport {
runningServices[name] = service runningServices[name] = service
//Setup service // Setup service
thread { thread {
callOnCreate(service) callOnCreate(service)
//TODO Handle more complex cases // TODO Handle more complex cases
service.onStartCommand(intent, 0, 0) service.onStartCommand(intent, 0, 0)
} }
} }
@@ -43,7 +43,7 @@ class ServiceSupport {
fun stopService(name: String) { fun stopService(name: String) {
logger.debug { "Stopping service: $name" } logger.debug { "Stopping service: $name" }
val service = runningServices.remove(name) val service = runningServices.remove(name)
if(service == null) { if (service == null) {
logger.warn { "An attempt was made to stop a service that is not running: $name" } logger.warn { "An attempt was made to stop a service that is not running: $name" }
} else { } else {
thread { thread {
@@ -63,6 +63,6 @@ class ServiceSupport {
fun serviceInstanceFromClass(className: String): Service { fun serviceInstanceFromClass(className: String): Service {
val clazzObj = Class.forName(className) val clazzObj = Class.forName(className)
return clazzObj.getDeclaredConstructor().newInstance() as? Service return clazzObj.getDeclaredConstructor().newInstance() as? Service
?: throw IllegalArgumentException("$className is not a Service!") ?: throw IllegalArgumentException("$className is not a Service!")
} }
} }
@@ -27,7 +27,7 @@ object KodeinGlobalHelper {
@JvmStatic @JvmStatic
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T { fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T {
return when(type) { return when (type) {
AndroidFiles::class.java -> { AndroidFiles::class.java -> {
val instance: AndroidFiles by (kodein ?: kodein()).instance() val instance: AndroidFiles by (kodein ?: kodein()).instance()
instance as T instance as T
@@ -64,5 +64,4 @@ object KodeinGlobalHelper {
fun <T : Any> instance(type: Class<T>): T { fun <T : Any> instance(type: Class<T>): T {
return instance(type, null) return instance(type, null)
} }
} }
+3 -20
View File
@@ -1,28 +1,11 @@
# Server: v0.X.Y-rXXX + WebUI: rXXX # Server: v0.X.Y-next + WebUI: rXXX
## TL;DR ## TL;DR
<!-- TODO: fill before release -->
## Tachidesk-Server
### Public API
#### Non-breaking changes
- N/A - N/A
#### Breaking changes ## Tachidesk-Server Changelog
- N/A - N/A
#### Bug fixes ## Tachidesk-WebUI Changelog
- N/A
### Private API
- N/A - N/A
## Tachidesk-WebUI
#### Visible changes
- N/A
#### Bug fixes
- N/A
#### Internal changes
- N/A
+51 -3
View File
@@ -1,3 +1,50 @@
# Server: v0.5.4 + WebUI: r820
## TL;DR
- Fixed ReadComicOnline, Toonily and possibly other sources not working
- Backup and Restore now includes Updates tab data
- Removed Anime support from WebUI, Anime support will also be removed from Tachidesk-Server in a future update
## Tachidesk-Server Changelog
- (r973) convert android.jar lib to a maven repo
- (r978) mimic Tachiyomi's behaviour more closely, fixes ReadComicOnline (EN)
- (r980) fix export chapter ordering, include new props in backup
- (r982) remove isNsfw annotation detection
- (r984) use correct time conversion units when doing backups
- (r989) Support using a CatalogueSource instead of only HttpSources ([#219](https://github.com/Suwayomi/Tachidesk-Server/pull/219) by @Syer10)
- (r991) Use a custom task to run electron ([#220](https://github.com/Suwayomi/Tachidesk-Server/pull/220) by @Syer10)
## Tachidesk-WebUI Changelog
- (r810) fix wrong strings in set Server Address dialog, fixes [#39](https://github.com/Suwayomi/Tachidesk-WebUI/issues/39)
- (r811) fix chapterFetch loop
- (r812) fix overlapping requests
- (r813) fix typo
- (r814) Better portrait support ([#41](https://github.com/Suwayomi/Tachidesk-WebUI/issues/41) by @minhe7735)
- (r815) fixes Reader navbar colors when in light mode ([#43](https://github.com/Suwayomi/Tachidesk-WebUI/issues/43) by @abhijeetChawla)
- (r816) default languages cleanup, force Local source enabled
- (r817) force Local source at LangSelect
- (r818) rename ExtensionLangSelect: generic name for generic use
- (r819) don't show anime anymore
- (r820) Remove Anime support
# Server: v0.5.3 + WebUI: r809
## TL;DR
- added support for a equivalent page to Tachiyomi's Updates tab
- fix launchers not working on macOS M1/arm64
## Tachidesk-Server Changelog
- (r956) fix macOS-arm64 bundle launchers not working
- (r957) Workaround StdLib issue and add KtLint to all modules ([#206](https://github.com/Suwayomi/Tachidesk-Server/pull/206) by @Syer10)
- (r960-r963) Add recently updated chapters(Updates) endpoint
## Tachidesk-WebUI Changelog
- (r808) fix chapter list not calling onlineFetch=true
- (r809) add support for Updates
# Server: v0.5.2 + WebUI: r807 # Server: v0.5.2 + WebUI: r807
## TL;DR ## TL;DR
- Fixed Local source not working on Windows - Fixed Local source not working on Windows
@@ -12,11 +59,12 @@
- N/A - N/A
#### Bug fixes #### Bug fixes
- N/A - (r948) Fix ManaToki (KO) and NewToki (KO) (issue [#202](https://github.com/Suwayomi/Tachidesk-Server/issue/202))
- (r949) Local source: fix windows paths
### Private API ### Private API
- (r942) Gradle Updates ([#199](https://github.com/Suwayomi/Tachidesk-WebUI/pull/199) by @Syer10) - (r941) Update BytecodeEditor to use Java NIO Paths ([#200](https://github.com/Suwayomi/Tachidesk-Server/pull/200) by @Syer10)
- (r941) Update BytecodeEditor to use Java NIO Paths ([#200](https://github.com/Suwayomi/Tachidesk-WebUI/pull/200) by @Syer10) - (r942) Gradle Updates ([#199](https://github.com/Suwayomi/Tachidesk-Server/pull/199) by @Syer10)
## Tachidesk-WebUI ## Tachidesk-WebUI
+6 -3
View File
@@ -22,9 +22,6 @@ This structure is chosen to
You need these software packages installed in order to build the project You need these software packages installed in order to build the project
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works) - Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
- Android stubs jar
- **Manual download:** Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
- **Automated download:** Run `AndroidCompat/getAndroid.sh`(macOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
### building the full-blown jar (Tachidesk-Server + Tachidesk-WebUI bundle) ### building the full-blown jar (Tachidesk-Server + Tachidesk-WebUI bundle)
Run `./gradlew server:downloadWebUI server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`. Run `./gradlew server:downloadWebUI server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`.
@@ -37,3 +34,9 @@ First Build the jar, then cd into the `scripts` directory and run `./windows-bun
## Running in development mode ## Running in development mode
run `./gradlew :server:run --stacktrace` to run the server run `./gradlew :server:run --stacktrace` to run the server
## Building the android-jar maven repository
Run `AndroidCompat/getAndroid.sh`(macOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows)
from project's root directory to download and rebuild the jar file from Google's repository,
then use `AndroidCompat/lib/android.jar` to manually create a maven repository inside the `android-jar` git branch.
Update the dependency declaration afterwards.
+11 -12
View File
@@ -3,16 +3,7 @@
|-------|----------|---------|---------| |-------|----------|---------|---------|
| ![CI](https://github.com/Suwayomi/Tachidesk/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Tachidesk.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Tachidesk/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Tachidesk-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) | | ![CI](https://github.com/Suwayomi/Tachidesk/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Tachidesk.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Tachidesk/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Tachidesk-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) |
# Tachidesk-Server is a server app! You may not want to Download Tachidesk-Server directly. # What is Tachidesk?
Yes, you need a client/user interface app as a front-end for Tachidesk-Server, if you Directly Download Tachidesk-Server you'll get a bundled version of [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI) with it.
Here's a list of known clients/user interfaces for Tachidesk-Server:
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The "official" front-end for Tachidesk-Server, A native desktop Application.
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/electrion front-end that Tachidesk-Server is traditionally shipped with.
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development.
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stage of development.
# What is Tachidesk then?
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/> <img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
A free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org/). A free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
@@ -23,6 +14,15 @@ Tachidesk-Server is as multi-platform as you can get. Any platform that runs jav
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature. Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
# Tachidesk-Server is a server app! You may not want to Download Tachidesk-Server directly.
Yes, you need a client/user interface app as a front-end for Tachidesk-Server, if you Directly Download Tachidesk-Server you'll get a bundled version of [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI) with it.
Here's a list of known clients/user interfaces for Tachidesk-Server:
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The "official" native desktop front-end for Tachidesk-Server. Currently the most advanced.
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/ElectronJS front-end that Tachidesk-Server is traditionally shipped with. Usually gets new features faster.
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development.
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stage of development.
## Is this application usable? Should I test it? ## Is this application usable? Should I test it?
Here is a list of current features: Here is a list of current features:
@@ -32,6 +32,7 @@ Here is a list of current features:
- Searching and browsing installed sources - Searching and browsing installed sources
- Ability to download Manga for offline read - Ability to download Manga for offline read
- Backup and restore support powered by Tachiyomi Backups - Backup and restore support powered by Tachiyomi Backups
- Viewing latest updated chapters.
- From Aniyomi - From Aniyomi
- Installing and executing Aniyomi's Extensions - Installing and executing Aniyomi's Extensions
- Searching and browsing installed sources. - Searching and browsing installed sources.
@@ -39,8 +40,6 @@ Here is a list of current features:
**Note:** These are capabilities of Tachidesk-Server, the actual working support is provided by each front-end app, checkout their respective readme for more info. **Note:** These are capabilities of Tachidesk-Server, the actual working support is provided by each front-end app, checkout their respective readme for more info.
**Note:** Tachidesk-Server is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk-Server/wiki/Troubleshooting) if it happens.
# Downloading and Running the app # Downloading and Running the app
## General Requirements ## General Requirements
In order to use the app effectively you need the following: In order to use the app effectively you need the following:
+4
View File
@@ -6,6 +6,7 @@ plugins {
kotlin("jvm") version kotlinVersion kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion kotlin("plugin.serialization") version kotlinVersion
id("org.jmailen.kotlinter") version "3.6.0" id("org.jmailen.kotlinter") version "3.6.0"
id("com.github.gmazzo.buildconfig") version "3.0.3" apply false
} }
allprojects { allprojects {
@@ -17,6 +18,7 @@ allprojects {
mavenCentral() mavenCentral()
google() google()
maven("https://jitpack.io") maven("https://jitpack.io")
maven("https://github.com/Suwayomi/Tachidesk-Server/raw/android-jar/")
} }
} }
@@ -29,6 +31,7 @@ val projects = listOf(
configure(projects) { configure(projects) {
apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization") apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
apply(plugin = "org.jmailen.kotlinter")
java { java {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
@@ -37,6 +40,7 @@ configure(projects) {
tasks { tasks {
withType<KotlinCompile> { withType<KotlinCompile> {
dependsOn(formatKotlin)
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs = listOf( freeCompilerArgs = listOf(
+2 -2
View File
@@ -12,9 +12,9 @@ const val kotlinVersion = "1.5.30"
const val MainClass = "suwayomi.tachidesk.MainKt" const val MainClass = "suwayomi.tachidesk.MainKt"
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.5.2" val tachideskVersion = System.getenv("ProductVersion") ?: "v0.5.4"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r807" val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r820"
// counts commits on the master branch // counts commits on the master branch
val tachideskRevision = runCatching { val tachideskRevision = runCatching {
+2 -2
View File
@@ -1,4 +1,4 @@
#!/bin/bash #/bin/bash
# Copyright (C) Contributors to the Suwayomi project # Copyright (C) Contributors to the Suwayomi project
# #
@@ -24,7 +24,7 @@ elif [ $1 = "macOS-arm64" ]; then
jre="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64.tar.gz" jre="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64.tar.gz"
jre_release="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64" jre_release="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64"
jre_url="https://cdn.azul.com/zulu/bin/$jre" jre_url="https://cdn.azul.com/zulu/bin/$jre"
jre_dir="$jre_release" jre_dir="$jre_release/zulu-8.jre"
electron="electron-$electron_version-darwin-arm64.zip" electron="electron-$electron_version-darwin-arm64.zip"
else else
echo "Unsupported arch value: $1" echo "Unsupported arch value: $1"
+15 -16
View File
@@ -1,11 +1,10 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import de.undercouch.gradle.tasks.download.Download import de.undercouch.gradle.tasks.download.Download
import java.time.Instant import java.time.Instant
plugins { plugins {
application application
id("com.github.johnrengelman.shadow") version "7.0.0" id("com.github.johnrengelman.shadow") version "7.0.0"
id("com.github.gmazzo.buildconfig") version "3.0.3" id("com.github.gmazzo.buildconfig")
} }
dependencies { dependencies {
@@ -70,16 +69,11 @@ dependencies {
// uncomment to test extensions directly // uncomment to test extensions directly
// implementation(fileTree("lib/")) // implementation(fileTree("lib/"))
implementation(kotlin("script-runtime"))
} }
application { application {
mainClass.set(MainClass) mainClass.set(MainClass)
// uncomment for testing electron
// applicationDefaultJvmArgs = listOf(
// "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
// "-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron"
// )
} }
sourceSets { sourceSets {
@@ -127,20 +121,13 @@ tasks {
archiveBaseName.set(rootProject.name) archiveBaseName.set(rootProject.name)
archiveVersion.set(tachideskVersion) archiveVersion.set(tachideskVersion)
archiveClassifier.set(tachideskRevision) archiveClassifier.set(tachideskRevision)
destinationDirectory.set(File("$rootDir/server/build"))
} }
test { test {
useJUnit() useJUnit()
} }
withType<ShadowJar> {
destinationDirectory.set(File("$rootDir/server/build"))
}
named("run") {
dependsOn(":formatKotlin", ":lintKotlin")
}
named<Copy>("processResources") { named<Copy>("processResources") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE duplicatesStrategy = DuplicatesStrategy.INCLUDE
mustRunAfter("downloadWebUI") mustRunAfter("downloadWebUI")
@@ -171,4 +158,16 @@ tasks {
overwrite(shouldOverwrite()) overwrite(shouldOverwrite())
} }
register("runElectron") {
group = "application"
finalizedBy(run)
doFirst {
application.applicationDefaultJvmArgs = listOf(
"-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
// Change this to the installed electron application
"-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron"
)
}
}
} }
@@ -1,5 +0,0 @@
package eu.kanade.tachiyomi.annoations
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Nsfw
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.source.local package eu.kanade.tachiyomi.source.local
import com.github.junrar.Archive import com.github.junrar.Archive
import eu.kanade.tachiyomi.source.local.FileSystemInterceptor.fakeUrlFrom import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Directory import eu.kanade.tachiyomi.source.local.LocalSource.Format.Directory
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Epub import eu.kanade.tachiyomi.source.local.LocalSource.Format.Epub
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Rar import eu.kanade.tachiyomi.source.local.LocalSource.Format.Rar
@@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
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.util.chapter.ChapterRecognition import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
@@ -27,15 +26,6 @@ import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.asResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okio.buffer
import okio.source
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -51,14 +41,12 @@ import suwayomi.tachidesk.server.ApplicationDirs
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream import java.io.InputStream
import java.net.URLDecoder
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile import java.util.zip.ZipFile
class LocalSource : HttpSource() { class LocalSource : CatalogueSource {
companion object { companion object {
const val ID = 0L const val ID = 0L
const val LANG = "localsourcelang" const val LANG = "localsourcelang"
@@ -133,13 +121,8 @@ class LocalSource : HttpSource() {
override val id = ID override val id = ID
override val name = NAME override val name = NAME
override val lang = LANG override val lang = LANG
override val baseUrl: String = ""
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(FileSystemInterceptor)
.build()
private val json: Json by injectLazy() private val json: Json by injectLazy()
override fun toString() = name override fun toString() = name
@@ -181,7 +164,7 @@ class LocalSource : HttpSource() {
// Try to find the cover // Try to find the cover
val cover = getCoverFile(File("${applicationDirs.localMangaRoot}/$url")) val cover = getCoverFile(File("${applicationDirs.localMangaRoot}/$url"))
if (cover != null && cover.exists()) { if (cover != null && cover.exists()) {
thumbnail_url = fakeUrlFrom(cover.absolutePath) thumbnail_url = cover.absolutePath
} }
val chapters = fetchChapterList(this).toBlocking().first() val chapters = fetchChapterList(this).toBlocking().first()
@@ -197,8 +180,7 @@ class LocalSource : HttpSource() {
// Copy the cover from the first chapter found. // Copy the cover from the first chapter found.
if (thumbnail_url == null) { if (thumbnail_url == null) {
try { try {
val dest = updateCover(chapter, this) thumbnail_url = updateCover(chapter, this)?.absolutePath
thumbnail_url = dest?.absolutePath?.let { fakeUrlFrom(it) }
} catch (e: Exception) { } catch (e: Exception) {
logger.error { e } logger.error { e }
} }
@@ -311,7 +293,7 @@ class LocalSource : HttpSource() {
chapterFile.listFiles().orEmpty().sortedBy { it.name }.mapIndexed { index, page -> chapterFile.listFiles().orEmpty().sortedBy { it.name }.mapIndexed { index, page ->
Page( Page(
index, index,
imageUrl = fakeUrlFrom(applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name) imageUrl = applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name
) )
} }
) )
@@ -412,67 +394,4 @@ class LocalSource : HttpSource() {
data class Rar(val file: File) : Format() data class Rar(val file: File) : Format()
data class Epub(val file: File) : Format() data class Epub(val file: File) : Format()
} }
// ///////////////////// Not used ///////////////////// //
override fun mangaDetailsParse(response: Response): SManga = throw Exception("Not used")
override fun chapterListParse(response: Response): List<SChapter> = throw Exception("Not used")
override fun pageListParse(response: Response): List<Page> = throw Exception("Not used")
override fun imageUrlParse(response: Response): String = throw Exception("Not used")
override fun popularMangaRequest(page: Int): Request = throw Exception("Not used")
override fun popularMangaParse(response: Response): MangasPage = throw Exception("Not used")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
throw Exception("Not used")
override fun searchMangaParse(response: Response): MangasPage = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesParse(response: Response): MangasPage = throw Exception("Not used")
}
private object FileSystemInterceptor : Interceptor {
fun fakeUrlFrom(path: String): String = "http://$path"
private fun restoreFilePath(url: String): String {
val path = URLDecoder.decode(url.replaceFirst("http://", ""), "UTF-8")
// Windows
if (System.getProperty("os.name").lowercase().startsWith("win")) {
// convert paths like "c/Users/..." to "c:/Users/..."
return StringBuilder(path).insert(1, ":").toString()
}
return "/$path"
}
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url
val filePath = restoreFilePath(url.toString())
return try {
Response.Builder()
.body(File(filePath).source().buffer().asResponseBody())
.code(200)
.message("Some file")
.protocol(Protocol.HTTP_1_0)
.request(request)
.build()
} catch (e: FileNotFoundException) {
Response.Builder()
.body("".toResponseBody())
.code(404)
.message(e.message ?: "File not found ($filePath)")
.protocol(Protocol.HTTP_1_0)
.request(request)
.build()
}
}
} }
@@ -19,6 +19,7 @@ import suwayomi.tachidesk.manga.controller.DownloadController
import suwayomi.tachidesk.manga.controller.ExtensionController import suwayomi.tachidesk.manga.controller.ExtensionController
import suwayomi.tachidesk.manga.controller.MangaController import suwayomi.tachidesk.manga.controller.MangaController
import suwayomi.tachidesk.manga.controller.SourceController import suwayomi.tachidesk.manga.controller.SourceController
import suwayomi.tachidesk.manga.controller.UpdateController
object MangaAPI { object MangaAPI {
fun defineEndpoints() { fun defineEndpoints() {
@@ -106,5 +107,9 @@ object MangaAPI {
get("{mangaId}/chapter/{chapterIndex}", DownloadController::queueChapter) get("{mangaId}/chapter/{chapterIndex}", DownloadController::queueChapter)
delete("{mangaId}/chapter/{chapterIndex}", DownloadController::unqueueChapter) delete("{mangaId}/chapter/{chapterIndex}", DownloadController::unqueueChapter)
} }
path("update") {
get("recentChapters", UpdateController::recentChapters)
}
} }
} }
@@ -0,0 +1,23 @@
package suwayomi.tachidesk.manga.controller
import io.javalin.http.Context
import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.server.JavalinSetup.future
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
object UpdateController {
/** get recently updated manga chapters */
fun recentChapters(ctx: Context) {
ctx.future(
future {
Chapter.getRecentChapters()
}
)
}
}
@@ -9,9 +9,10 @@ package suwayomi.tachidesk.manga.impl
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.util.chapter.ChapterRecognition import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.SortOrder.DESC import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
@@ -20,11 +21,12 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Manga.getManga import suwayomi.tachidesk.manga.impl.Manga.getManga
import suwayomi.tachidesk.manga.impl.Page.getPageName import suwayomi.tachidesk.manga.impl.Page.getPageName
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.getChapterDir import suwayomi.tachidesk.manga.impl.util.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
@@ -40,7 +42,7 @@ object Chapter {
getSourceChapters(mangaId) getSourceChapters(mangaId)
} else { } else {
transaction { transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC) ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.map { .map {
ChapterTable.toDataClass(it) ChapterTable.toDataClass(it)
} }
@@ -52,7 +54,7 @@ object Chapter {
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> { private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
val manga = getManga(mangaId) val manga = getManga(mangaId)
val source = getHttpSource(manga.sourceId.toLong()) val source = getCatalogueSourceOrStub(manga.sourceId.toLong())
val sManga = SManga.create().apply { val sManga = SManga.create().apply {
title = manga.title title = manga.title
@@ -63,11 +65,12 @@ object Chapter {
// Recognize number for new chapters. // Recognize number for new chapters.
chapterList.forEach { chapterList.forEach {
source.prepareNewChapter(it, sManga) (source as? HttpSource)?.prepareNewChapter(it, sManga)
ChapterRecognition.parseChapterNumber(it, sManga) ChapterRecognition.parseChapterNumber(it, sManga)
} }
val chapterCount = chapterList.count() val chapterCount = chapterList.count()
var now = Instant.now().epochSecond
transaction { transaction {
chapterList.reversed().forEachIndexed { index, fetchedChapter -> chapterList.reversed().forEachIndexed { index, fetchedChapter ->
@@ -80,7 +83,8 @@ object Chapter {
it[chapter_number] = fetchedChapter.chapter_number it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1 it[sourceOrder] = index + 1
it[fetchedAt] = now++
it[ChapterTable.manga] = mangaId it[ChapterTable.manga] = mangaId
} }
} else { } else {
@@ -90,7 +94,7 @@ object Chapter {
it[chapter_number] = fetchedChapter.chapter_number it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1 it[sourceOrder] = index + 1
it[ChapterTable.manga] = mangaId it[ChapterTable.manga] = mangaId
} }
} }
@@ -103,8 +107,8 @@ object Chapter {
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.toList() } val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.toList() }
dbChapterList.forEach { dbChapterList.forEach {
if (it[ChapterTable.chapterIndex] >= chapterList.size || if (it[ChapterTable.sourceOrder] >= chapterList.size ||
chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url] chapterList[it[ChapterTable.sourceOrder] - 1].url != it[ChapterTable.url]
) { ) {
transaction { transaction {
PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] } PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] }
@@ -137,6 +141,7 @@ object Chapter {
dbChapter[ChapterTable.lastReadAt], dbChapter[ChapterTable.lastReadAt],
chapterCount - index, chapterCount - index,
dbChapter[ChapterTable.fetchedAt],
dbChapter[ChapterTable.isDownloaded], dbChapter[ChapterTable.isDownloaded],
dbChapter[ChapterTable.pageCount], dbChapter[ChapterTable.pageCount],
@@ -151,7 +156,7 @@ object Chapter {
suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass { suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
val chapterEntry = transaction { val chapterEntry = transaction {
ChapterTable.select { ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId) (ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first() }.first()
} }
@@ -159,13 +164,13 @@ object Chapter {
chapterEntry[ChapterTable.isDownloaded] && firstPageExists(mangaId, chapterEntry[ChapterTable.id].value) chapterEntry[ChapterTable.isDownloaded] && firstPageExists(mangaId, chapterEntry[ChapterTable.id].value)
return if (!isReallyDownloaded) { return if (!isReallyDownloaded) {
transaction { transaction {
ChapterTable.update({ (ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId) }) { ChapterTable.update({ (ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId) }) {
it[isDownloaded] = false it[isDownloaded] = false
} }
} }
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList( val pageList = source.fetchPageList(
SChapter.create().apply { SChapter.create().apply {
@@ -203,7 +208,7 @@ object Chapter {
val pageCount = pageList.count() val pageCount = pageList.count()
transaction { transaction {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) { ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }) {
it[ChapterTable.pageCount] = pageCount it[ChapterTable.pageCount] = pageCount
} }
} }
@@ -219,7 +224,8 @@ object Chapter {
chapterEntry[ChapterTable.lastPageRead], chapterEntry[ChapterTable.lastPageRead],
chapterEntry[ChapterTable.lastReadAt], chapterEntry[ChapterTable.lastReadAt],
chapterEntry[ChapterTable.chapterIndex], chapterEntry[ChapterTable.sourceOrder],
chapterEntry[ChapterTable.fetchedAt],
chapterEntry[ChapterTable.isDownloaded], chapterEntry[ChapterTable.isDownloaded],
pageCount, pageCount,
chapterCount.toInt(), chapterCount.toInt(),
@@ -249,7 +255,7 @@ object Chapter {
) { ) {
transaction { transaction {
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) { if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) { update -> ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }) { update ->
isRead?.also { isRead?.also {
update[ChapterTable.isRead] = it update[ChapterTable.isRead] = it
} }
@@ -264,7 +270,7 @@ object Chapter {
} }
markPrevRead?.let { markPrevRead?.let {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex less chapterIndex) }) { ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder less chapterIndex) }) {
it[ChapterTable.isRead] = markPrevRead it[ChapterTable.isRead] = markPrevRead
} }
} }
@@ -281,7 +287,7 @@ object Chapter {
fun modifyChapterMeta(mangaId: Int, chapterIndex: Int, key: String, value: String) { fun modifyChapterMeta(mangaId: Int, chapterIndex: Int, key: String, value: String) {
transaction { transaction {
val chapterId = val chapterId =
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) } ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
.first()[ChapterTable.id].value .first()[ChapterTable.id].value
val meta = val meta =
transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } }.firstOrNull() transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } }.firstOrNull()
@@ -302,16 +308,30 @@ object Chapter {
fun deleteChapter(mangaId: Int, chapterIndex: Int) { fun deleteChapter(mangaId: Int, chapterIndex: Int) {
transaction { transaction {
val chapterId = val chapterId =
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) } ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
.first()[ChapterTable.id].value .first()[ChapterTable.id].value
val chapterDir = getChapterDir(mangaId, chapterId) val chapterDir = getChapterDir(mangaId, chapterId)
File(chapterDir).deleteRecursively() File(chapterDir).deleteRecursively()
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) { ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }) {
it[isDownloaded] = false it[isDownloaded] = false
} }
} }
} }
fun getRecentChapters(): List<MangaChapterDataClass> {
return transaction {
(ChapterTable innerJoin MangaTable)
.select { (MangaTable.inLibrary eq true) and (ChapterTable.fetchedAt greater MangaTable.inLibraryAt) }
.orderBy(ChapterTable.fetchedAt to SortOrder.DESC)
.map {
MangaChapterDataClass(
MangaTable.toDataClass(it),
ChapterTable.toDataClass(it)
)
}
}
}
} }
@@ -16,6 +16,7 @@ import suwayomi.tachidesk.manga.impl.Manga.getManga
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import java.time.Instant
object Library { object Library {
suspend fun addMangaToLibrary(mangaId: Int) { suspend fun addMangaToLibrary(mangaId: Int) {
@@ -25,8 +26,9 @@ object Library {
val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList() val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList()
MangaTable.update({ MangaTable.id eq manga.id }) { MangaTable.update({ MangaTable.id eq manga.id }) {
it[MangaTable.inLibrary] = true it[inLibrary] = true
it[MangaTable.defaultCategory] = defaultCategories.isEmpty() it[inLibraryAt] = Instant.now().epochSecond
it[defaultCategory] = defaultCategories.isEmpty()
} }
defaultCategories.forEach { category -> defaultCategories.forEach { category ->
@@ -8,7 +8,9 @@ package suwayomi.tachidesk.manga.impl
* 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 eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.local.LocalSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@@ -19,11 +21,12 @@ import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
import suwayomi.tachidesk.manga.impl.Source.getSource import suwayomi.tachidesk.manga.impl.Source.getSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.network.await import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.toGenreList import suwayomi.tachidesk.manga.model.dataclass.toGenreList
@@ -31,6 +34,8 @@ import suwayomi.tachidesk.manga.model.table.MangaMetaTable
import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import java.io.File
import java.io.IOException
import java.io.InputStream import java.io.InputStream
object Manga { object Manga {
@@ -61,43 +66,43 @@ object Manga {
mangaEntry[MangaTable.genre].toGenreList(), mangaEntry[MangaTable.genre].toGenreList(),
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary], mangaEntry[MangaTable.inLibrary],
mangaEntry[MangaTable.inLibraryAt],
getSource(mangaEntry[MangaTable.sourceReference]), getSource(mangaEntry[MangaTable.sourceReference]),
getMangaMetaMap(mangaId), getMangaMetaMap(mangaId),
mangaEntry[MangaTable.realUrl], mangaEntry[MangaTable.realUrl],
false false
) )
} else { // initialize manga } else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val sManga = SManga.create().apply { val sManga = SManga.create().apply {
url = mangaEntry[MangaTable.url] url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title] title = mangaEntry[MangaTable.title]
} }
val fetchedManga = source.fetchMangaDetails(sManga).awaitSingle() val networkManga = source.fetchMangaDetails(sManga).awaitSingle()
sManga.copyFrom(networkManga)
transaction { transaction {
MangaTable.update({ MangaTable.id eq mangaId }) { MangaTable.update({ MangaTable.id eq mangaId }) {
if (fetchedManga.title != mangaEntry[MangaTable.title]) { if (sManga.title != mangaEntry[MangaTable.title]) {
val canUpdateTitle = updateMangaDownloadDir(mangaId, fetchedManga.title) val canUpdateTitle = updateMangaDownloadDir(mangaId, sManga.title)
if (canUpdateTitle) if (canUpdateTitle)
it[MangaTable.title] = fetchedManga.title it[MangaTable.title] = sManga.title
} }
it[MangaTable.initialized] = true it[MangaTable.initialized] = true
it[MangaTable.artist] = fetchedManga.artist it[MangaTable.artist] = sManga.artist
it[MangaTable.author] = fetchedManga.author it[MangaTable.author] = sManga.author
it[MangaTable.description] = truncate(fetchedManga.description, 4096) it[MangaTable.description] = truncate(sManga.description, 4096)
it[MangaTable.genre] = fetchedManga.genre it[MangaTable.genre] = sManga.genre
it[MangaTable.status] = fetchedManga.status it[MangaTable.status] = sManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty()) if (sManga.thumbnail_url != null && sManga.thumbnail_url.orEmpty().isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url it[MangaTable.thumbnail_url] = sManga.thumbnail_url
it[MangaTable.realUrl] = try { it[MangaTable.realUrl] = runCatching {
source.mangaDetailsRequest(sManga).url.toString() (source as? HttpSource)?.mangaDetailsRequest(sManga)?.url?.toString()
} catch (e: Exception) { }.getOrNull()
null
}
} }
} }
@@ -115,12 +120,13 @@ object Manga {
true, true,
fetchedManga.artist, sManga.artist,
fetchedManga.author, sManga.author,
fetchedManga.description, sManga.description,
fetchedManga.genre.toGenreList(), sManga.genre.toGenreList(),
MangaStatus.valueOf(fetchedManga.status).name, MangaStatus.valueOf(sManga.status).name,
mangaEntry[MangaTable.inLibrary], mangaEntry[MangaTable.inLibrary],
mangaEntry[MangaTable.inLibraryAt],
getSource(mangaEntry[MangaTable.sourceReference]), getSource(mangaEntry[MangaTable.sourceReference]),
getMangaMetaMap(mangaId), getMangaMetaMap(mangaId),
mangaEntry[MangaTable.realUrl], mangaEntry[MangaTable.realUrl],
@@ -161,25 +167,39 @@ object Manga {
val saveDir = applicationDirs.mangaThumbnailsRoot val saveDir = applicationDirs.mangaThumbnailsRoot
val fileName = mangaId.toString() val fileName = mangaId.toString()
return getImageResponse(saveDir, fileName, useCache) { val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val sourceId = mangaEntry[MangaTable.sourceReference]
val sourceId = mangaEntry[MangaTable.sourceReference] return when (val source = getCatalogueSourceOrStub(sourceId)) {
val source = getHttpSource(sourceId) is HttpSource -> getImageResponse(saveDir, fileName, useCache) {
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
?: if (!mangaEntry[MangaTable.initialized]) {
// initialize then try again
getManga(mangaId)
transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }[MangaTable.thumbnail_url]!!
} else {
// source provides no thumbnail url for this manga
throw NullPointerException()
}
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url] source.client.newCall(
?: if (!mangaEntry[MangaTable.initialized]) { GET(thumbnailUrl, source.headers)
// initialize then try again ).await()
getManga(mangaId) }
transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }[MangaTable.thumbnail_url]!! is LocalSource -> {
} else { val imageFile = mangaEntry[MangaTable.thumbnail_url]?.let {
// source provides no thumbnail url for this manga val file = File(it)
throw NullPointerException() if (file.exists()) {
} file
} else {
source.client.newCall( null
GET(thumbnailUrl, source.headers) }
).await() } ?: throw IOException("Thumbnail does not exist")
val contentType = ImageUtil.findImageType { imageFile.inputStream() }?.mime
?: "image/jpeg"
imageFile.inputStream() to contentType
}
else -> throw IllegalArgumentException("Unknown source")
} }
} }
@@ -13,8 +13,8 @@ import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.manga.impl.Manga.getMangaMetaMap import suwayomi.tachidesk.manga.impl.Manga.getMangaMetaMap
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
import suwayomi.tachidesk.manga.model.dataclass.toGenreList import suwayomi.tachidesk.manga.model.dataclass.toGenreList
@@ -27,14 +27,15 @@ object MangaList {
} }
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass { suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId) val source = getCatalogueSourceOrStub(sourceId)
val mangasPage = if (popular) { val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).awaitSingle() source.fetchPopularManga(pageNum).awaitSingle()
} else { } else {
if (source.supportsLatest) if (source.supportsLatest) {
source.fetchLatestUpdates(pageNum).awaitSingle() source.fetchLatestUpdates(pageNum).awaitSingle()
else } else {
throw Exception("Source $source doesn't support latest") throw Exception("Source $source doesn't support latest")
}
} }
return mangasPage.processEntries(sourceId) return mangasPage.processEntries(sourceId)
} }
@@ -81,6 +82,7 @@ object MangaList {
manga.genre.toGenreList(), manga.genre.toGenreList(),
MangaStatus.valueOf(manga.status).name, MangaStatus.valueOf(manga.status).name,
false, // It's a new manga entry false, // It's a new manga entry
0,
meta = getMangaMetaMap(mangaId), meta = getMangaMetaMap(mangaId),
realUrl = mangaEntry[MangaTable.realUrl], realUrl = mangaEntry[MangaTable.realUrl],
freshData = true freshData = true
@@ -103,6 +105,7 @@ object MangaList {
mangaEntry[MangaTable.genre].toGenreList(), mangaEntry[MangaTable.genre].toGenreList(),
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary], mangaEntry[MangaTable.inLibrary],
mangaEntry[MangaTable.inLibraryAt],
meta = getMangaMetaMap(mangaId), meta = getMangaMetaMap(mangaId),
realUrl = mangaEntry[MangaTable.realUrl], realUrl = mangaEntry[MangaTable.realUrl],
freshData = false freshData = false
@@ -17,11 +17,12 @@ import org.jetbrains.exposed.sql.update
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.getChapterDir import suwayomi.tachidesk.manga.impl.util.getChapterDir
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.PageTable import suwayomi.tachidesk.manga.model.table.PageTable
@@ -43,10 +44,10 @@ object Page {
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int, useCache: Boolean = true): Pair<InputStream, String> { suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int, useCache: Boolean = true): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction { val chapterEntry = transaction {
ChapterTable.select { ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId) (ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first() }.first()
} }
val chapterId = chapterEntry[ChapterTable.id].value val chapterId = chapterEntry[ChapterTable.id].value
@@ -61,19 +62,20 @@ object Page {
) )
// we treat Local source differently // we treat Local source differently
if (mangaEntry[MangaTable.sourceReference] == LocalSource.ID) { if (source.id == LocalSource.ID) {
// is of archive format // is of archive format
if (LocalSource.pageCache.containsKey(chapterEntry[ChapterTable.url])) { if (LocalSource.pageCache.containsKey(chapterEntry[ChapterTable.url])) {
val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]() val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]
return pageStream to "image/jpeg" return pageStream() to (ImageUtil.findImageType { pageStream() }?.mime ?: "image/jpeg")
} }
// is of directory format // is of directory format
return ImageResponse.getNoCacheImageResponse { val imageFile = File(tachiyomiPage.imageUrl!!)
source.fetchImage(tachiyomiPage).awaitSingle() return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
}
} }
source as HttpSource
if (pageEntry[PageTable.imageUrl] == null) { if (pageEntry[PageTable.imageUrl] == null) {
val trueImageUrl = getTrueImageUrl(tachiyomiPage, source) val trueImageUrl = getTrueImageUrl(tachiyomiPage, source)
transaction { transaction {
@@ -10,13 +10,13 @@ package suwayomi.tachidesk.manga.impl
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import suwayomi.tachidesk.manga.impl.MangaList.processEntries import suwayomi.tachidesk.manga.impl.MangaList.processEntries
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
object Search { object Search {
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getHttpSource(sourceId) val source = getCatalogueSourceOrStub(sourceId)
val searchManga = source.fetchSearchManga(pageNum, searchTerm, getFilterListOf(sourceId)).awaitSingle() val searchManga = source.fetchSearchManga(pageNum, searchTerm, getFilterListOf(sourceId)).awaitSingle()
return searchManga.processEntries(sourceId) return searchManga.processEntries(sourceId)
} }
@@ -25,7 +25,7 @@ object Search {
private fun getFilterListOf(sourceId: Long, reset: Boolean = false): FilterList { private fun getFilterListOf(sourceId: Long, reset: Boolean = false): FilterList {
if (reset || !filterListCache.containsKey(sourceId)) { if (reset || !filterListCache.containsKey(sourceId)) {
filterListCache[sourceId] = getHttpSource(sourceId).getFilterList() filterListCache[sourceId] = getCatalogueSourceOrStub(sourceId).getFilterList()
} }
return filterListCache[sourceId]!! return filterListCache[sourceId]!!
} }
@@ -12,7 +12,6 @@ import android.content.Context
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.getPreferenceKey import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.source.local.LocalSource
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
@@ -21,8 +20,9 @@ import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.invalidateSourceCache import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.invalidateSourceCache
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
@@ -36,7 +36,7 @@ object Source {
fun getSourceList(): List<SourceDataClass> { fun getSourceList(): List<SourceDataClass> {
return transaction { return transaction {
SourceTable.selectAll().map { SourceTable.selectAll().map {
val httpSource = getHttpSource(it[SourceTable.id].value) val catalogueSource = getCatalogueSourceOrStub(it[SourceTable.id].value)
val sourceExtension = ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first() val sourceExtension = ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()
SourceDataClass( SourceDataClass(
@@ -44,10 +44,10 @@ object Source {
it[SourceTable.name], it[SourceTable.name],
it[SourceTable.lang], it[SourceTable.lang],
getExtensionIconUrl(sourceExtension[ExtensionTable.apkName]), getExtensionIconUrl(sourceExtension[ExtensionTable.apkName]),
httpSource.supportsLatest, catalogueSource.supportsLatest,
httpSource is ConfigurableSource, catalogueSource is ConfigurableSource,
it[SourceTable.isNsfw], it[SourceTable.isNsfw],
httpSource.toString(), catalogueSource.toString(),
) )
} }
} }
@@ -55,13 +55,8 @@ object Source {
fun getSource(sourceId: Long): SourceDataClass { // all the data extracted fresh form the source instance fun getSource(sourceId: Long): SourceDataClass { // all the data extracted fresh form the source instance
return transaction { return transaction {
if (sourceId == LocalSource.ID) {
// initialize local source
getHttpSource(sourceId)
}
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull() val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
val httpSource = source?.let { getHttpSource(sourceId) } val catalogueSource = source?.let { getCatalogueSource(sourceId) }
val extension = source?.let { val extension = source?.let {
ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first() ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()
} }
@@ -75,10 +70,10 @@ object Source {
extension!![ExtensionTable.apkName] extension!![ExtensionTable.apkName]
) )
}, },
httpSource?.supportsLatest, catalogueSource?.supportsLatest,
httpSource?.let { it is ConfigurableSource }, catalogueSource?.let { it is ConfigurableSource },
source?.get(SourceTable.isNsfw), source?.get(SourceTable.isNsfw),
httpSource?.toString() catalogueSource?.toString()
) )
} }
} }
@@ -103,7 +98,7 @@ object Source {
* Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap] * Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap]
*/ */
fun getSourcePreferences(sourceId: Long): List<PreferenceObject> { fun getSourcePreferences(sourceId: Long): List<PreferenceObject> {
val source = getHttpSource(sourceId) val source = getCatalogueSourceOrStub(sourceId)
if (source is ConfigurableSource) { if (source is ConfigurableSource) {
val sourceShardPreferences = val sourceShardPreferences =
@@ -32,6 +32,7 @@ import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.manga.model.table.toDataClass
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit
object ProtoBackupExport : ProtoBackupBase() { object ProtoBackupExport : ProtoBackupBase() {
suspend fun createBackup(flags: BackupFlags): InputStream { suspend fun createBackup(flags: BackupFlags): InputStream {
@@ -68,7 +69,7 @@ object ProtoBackupExport : ProtoBackupBase() {
mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(), mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(),
MangaStatus.valueOf(mangaRow[MangaTable.status]).value, MangaStatus.valueOf(mangaRow[MangaTable.status]).value,
mangaRow[MangaTable.thumbnail_url], mangaRow[MangaTable.thumbnail_url],
0, // not supported in Tachidesk TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]),
0, // not supported in Tachidesk 0, // not supported in Tachidesk
) )
@@ -84,10 +85,10 @@ object ProtoBackupExport : ProtoBackupBase() {
it.read, it.read,
it.bookmarked, it.bookmarked,
it.lastPageRead, it.lastPageRead,
0, // not supported in Tachidesk TimeUnit.SECONDS.toMillis(it.fetchedAt),
it.uploadDate, it.uploadDate,
it.chapterNumber, it.chapterNumber,
it.index, chapters.size - it.index,
) )
} }
} }
@@ -34,6 +34,7 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
import java.io.InputStream import java.io.InputStream
import java.lang.Integer.max import java.lang.Integer.max
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit
object ProtoBackupImport : ProtoBackupBase() { object ProtoBackupImport : ProtoBackupBase() {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -148,6 +149,8 @@ object ProtoBackupImport : ProtoBackupBase() {
it[initialized] = manga.description != null it[initialized] = manga.description != null
it[inLibrary] = manga.favorite it[inLibrary] = manga.favorite
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
}.value }.value
// insert chapter data // insert chapter data
@@ -160,12 +163,14 @@ object ProtoBackupImport : ProtoBackupBase() {
it[chapter_number] = chapter.chapter_number it[chapter_number] = chapter.chapter_number
it[scanlator] = chapter.scanlator it[scanlator] = chapter.scanlator
it[chapterIndex] = chaptersLength - chapter.source_order it[sourceOrder] = chaptersLength - chapter.source_order
it[ChapterTable.manga] = mangaId it[ChapterTable.manga] = mangaId
it[isRead] = chapter.read it[isRead] = chapter.read
it[lastPageRead] = chapter.last_page_read it[lastPageRead] = chapter.last_page_read
it[isBookmarked] = chapter.bookmark it[isBookmarked] = chapter.bookmark
it[fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch)
} }
} }
@@ -190,6 +195,8 @@ object ProtoBackupImport : ProtoBackupBase() {
it[initialized] = dbManga[initialized] || manga.description != null it[initialized] = dbManga[initialized] || manga.description != null
it[inLibrary] = manga.favorite || dbManga[inLibrary] it[inLibrary] = manga.favorite || dbManga[inLibrary]
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
} }
// merge chapter data // merge chapter data
@@ -207,7 +214,7 @@ object ProtoBackupImport : ProtoBackupBase() {
it[chapter_number] = chapter.chapter_number it[chapter_number] = chapter.chapter_number
it[scanlator] = chapter.scanlator it[scanlator] = chapter.scanlator
it[chapterIndex] = chaptersLength - chapter.source_order it[sourceOrder] = chaptersLength - chapter.source_order
it[ChapterTable.manga] = mangaId it[ChapterTable.manga] = mangaId
it[isRead] = chapter.read it[isRead] = chapter.read
@@ -77,7 +77,7 @@ object DownloadManager {
mangaId, mangaId,
chapter = ChapterTable.toDataClass( chapter = ChapterTable.toDataClass(
transaction { transaction {
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) } ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
.first() .first()
} }
) )
@@ -61,7 +61,7 @@ class Downloader(private val downloadQueue: CopyOnWriteArrayList<DownloadChapter
} }
download.state = Finished download.state = Finished
transaction { transaction {
ChapterTable.update({ (ChapterTable.manga eq download.mangaId) and (ChapterTable.chapterIndex eq download.chapterIndex) }) { ChapterTable.update({ (ChapterTable.manga eq download.mangaId) and (ChapterTable.sourceOrder eq download.chapterIndex) }) {
it[isDownloaded] = true it[isDownloaded] = true
} }
} }
@@ -28,7 +28,6 @@ import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.extensionTableAsDataClass import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.extensionTableAsDataClass
import suwayomi.tachidesk.manga.impl.extension.github.ExtensionGithubApi import suwayomi.tachidesk.manga.impl.extension.github.ExtensionGithubApi
import suwayomi.tachidesk.manga.impl.util.GetHttpSource
import suwayomi.tachidesk.manga.impl.util.PackageTools import suwayomi.tachidesk.manga.impl.util.PackageTools
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
@@ -39,6 +38,7 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools.dex2jar
import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
import suwayomi.tachidesk.manga.impl.util.network.await import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
@@ -51,9 +51,6 @@ object Extension {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
private fun Any.isNsfw(): Boolean =
this::class.annotations.any { it.toString() == "@eu.kanade.tachiyomi.annotations.Nsfw()" }
suspend fun installExtension(pkgName: String): Int { suspend fun installExtension(pkgName: String): Int {
logger.debug("Installing $pkgName") logger.debug("Installing $pkgName")
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName } val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
@@ -189,7 +186,7 @@ object Extension {
it[name] = httpSource.name it[name] = httpSource.name
it[lang] = httpSource.lang it[lang] = httpSource.lang
it[extension] = extensionId it[extension] = extensionId
it[SourceTable.isNsfw] = isNsfw || extensionMainClassInstance.isNsfw() it[SourceTable.isNsfw] = isNsfw
} }
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" } logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
} }
@@ -243,7 +240,7 @@ object Extension {
PackageTools.jarLoaderMap.remove(jarPath)?.close() PackageTools.jarLoaderMap.remove(jarPath)?.close()
// clear all loaded sources // clear all loaded sources
sources.forEach { GetHttpSource.invalidateSourceCache(it) } sources.forEach { GetCatalogueSource.invalidateSourceCache(it) }
File(jarPath).delete() File(jarPath).delete()
} }
@@ -12,6 +12,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.impl.util.storage.SafePath import suwayomi.tachidesk.manga.impl.util.storage.SafePath
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
@@ -22,7 +23,7 @@ private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getMangaDir(mangaId: Int): String { fun getMangaDir(mangaId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = GetHttpSource.getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val sourceDir = source.toString() val sourceDir = source.toString()
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title]) val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
@@ -46,7 +47,7 @@ fun getChapterDir(mangaId: Int, chapterId: Int): String {
/** return value says if rename/move was successful */ /** return value says if rename/move was successful */
fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean { fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = GetHttpSource.getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val sourceDir = source.toString() val sourceDir = source.toString()
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title]) val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
@@ -1,4 +1,4 @@
package suwayomi.tachidesk.manga.impl.util package suwayomi.tachidesk.manga.impl.util.source
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.impl.util
* 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 eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
@@ -22,23 +23,21 @@ import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
object GetHttpSource { object GetCatalogueSource {
private val sourceCache = ConcurrentHashMap<Long, HttpSource>() private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>(
mapOf(LocalSource.ID to LocalSource())
)
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getHttpSource(sourceId: Long): HttpSource { fun getCatalogueSource(sourceId: Long): CatalogueSource? {
val cachedResult: HttpSource? = sourceCache[sourceId] val cachedResult: CatalogueSource? = sourceCache[sourceId]
if (cachedResult != null) { if (cachedResult != null) {
return cachedResult return cachedResult
} }
val sourceRecord = transaction { val sourceRecord = transaction {
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
} } ?: return null
if (sourceId == LocalSource.ID) {
return LocalSource()
}
val extensionId = sourceRecord[SourceTable.extension] val extensionId = sourceRecord[SourceTable.extension]
val extensionRecord = transaction { val extensionRecord = transaction {
@@ -60,6 +59,10 @@ object GetHttpSource {
return sourceCache[sourceId]!! return sourceCache[sourceId]!!
} }
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource {
return getCatalogueSource(sourceId) ?: StubSource(sourceId)
}
fun invalidateSourceCache(sourceId: Long) { fun invalidateSourceCache(sourceId: Long) {
sourceCache.remove(sourceId) sourceCache.remove(sourceId)
} }
@@ -0,0 +1,62 @@
package suwayomi.tachidesk.manga.impl.util.source
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable
class StubSource(override val id: Long) : CatalogueSource {
override val lang: String = "other"
override val supportsLatest: Boolean = false
override val name: String
get() = id.toString()
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return Observable.error(getSourceNotInstalledException())
}
override fun getFilterList(): FilterList {
return FilterList()
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.error(getSourceNotInstalledException())
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return Observable.error(getSourceNotInstalledException())
}
override fun toString(): String {
return name
}
private fun getSourceNotInstalledException(): SourceNotInstalledException {
return SourceNotInstalledException(id)
}
inner class SourceNotInstalledException(val id: Long) :
Exception("Source not installed: $id")
}
@@ -27,9 +27,13 @@ data class ChapterDataClass(
/** last read page, zero means not read/no data */ /** last read page, zero means not read/no data */
val lastReadAt: Long, val lastReadAt: Long,
// TODO(v0.6.0): rename to sourceOrder
/** this chapter's index, starts with 1 */ /** this chapter's index, starts with 1 */
val index: Int, val index: Int,
/** the date we fist saw this chapter*/
val fetchedAt: Long,
/** is chapter downloaded */ /** is chapter downloaded */
val downloaded: Boolean, val downloaded: Boolean,
@@ -0,0 +1,13 @@
package suwayomi.tachidesk.manga.model.dataclass
/*
* 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/. */
data class MangaChapterDataClass(
val manga: MangaDataClass,
val chapter: ChapterDataClass,
)
@@ -26,6 +26,7 @@ data class MangaDataClass(
val genre: List<String> = emptyList(), val genre: List<String> = emptyList(),
val status: String = MangaStatus.UNKNOWN.name, val status: String = MangaStatus.UNKNOWN.name,
val inLibrary: Boolean = false, val inLibrary: Boolean = false,
val inLibraryAt: Long = 0,
val source: SourceDataClass? = null, val source: SourceDataClass? = null,
/** meta data for clients */ /** meta data for clients */
@@ -25,9 +25,9 @@ object ChapterTable : IntIdTable() {
val isBookmarked = bool("bookmark").default(false) val isBookmarked = bool("bookmark").default(false)
val lastPageRead = integer("last_page_read").default(0) val lastPageRead = integer("last_page_read").default(0)
val lastReadAt = long("last_read_at").default(0) val lastReadAt = long("last_read_at").default(0)
val fetchedAt = long("fetched_at").default(0)
// index is reserved by a function val sourceOrder = integer("source_order")
val chapterIndex = integer("index")
val isDownloaded = bool("is_downloaded").default(false) val isDownloaded = bool("is_downloaded").default(false)
@@ -48,7 +48,8 @@ fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
chapterEntry[isBookmarked], chapterEntry[isBookmarked],
chapterEntry[lastPageRead], chapterEntry[lastPageRead],
chapterEntry[lastReadAt], chapterEntry[lastReadAt],
chapterEntry[chapterIndex], chapterEntry[sourceOrder],
chapterEntry[fetchedAt],
chapterEntry[isDownloaded], chapterEntry[isDownloaded],
chapterEntry[pageCount], chapterEntry[pageCount],
transaction { ChapterTable.select { manga eq chapterEntry[manga].value }.count().toInt() }, transaction { ChapterTable.select { manga eq chapterEntry[manga].value }.count().toInt() },
@@ -31,6 +31,7 @@ object MangaTable : IntIdTable() {
val inLibrary = bool("in_library").default(false) val inLibrary = bool("in_library").default(false)
val defaultCategory = bool("default_category").default(true) val defaultCategory = bool("default_category").default(true)
val inLibraryAt = long("in_library_at").default(0)
// the [source] field name is used by some ancestor of IntIdTable // the [source] field name is used by some ancestor of IntIdTable
val sourceReference = long("source") val sourceReference = long("source")
@@ -56,6 +57,7 @@ fun MangaTable.toDataClass(mangaEntry: ResultRow) =
mangaEntry[genre].toGenreList(), mangaEntry[genre].toGenreList(),
Companion.valueOf(mangaEntry[status]).name, Companion.valueOf(mangaEntry[status]).name,
mangaEntry[inLibrary], mangaEntry[inLibrary],
mangaEntry[inLibraryAt],
meta = getMangaMetaMap(mangaEntry[id].value), meta = getMangaMetaMap(mangaEntry[id].value),
realUrl = mangaEntry[realUrl], realUrl = mangaEntry[realUrl],
) )
@@ -0,0 +1,17 @@
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
@Suppress("ClassName", "unused")
class M0016_ChapterIndexRenameToSourceOrder : SQLMigration() {
override val sql = """
ALTER TABLE CHAPTER ALTER COLUMN INDEX RENAME TO SOURCE_ORDER;
""".trimIndent()
}
@@ -0,0 +1,18 @@
package suwayomi.tachidesk.server.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import de.neonew.exposed.migrations.helpers.AddColumnMigration
@Suppress("ClassName", "unused")
class M0017_ChapterFetchedAt : AddColumnMigration(
"Chapter",
"fetched_at",
"BIGINT",
"0"
)
@@ -0,0 +1,18 @@
package suwayomi.tachidesk.server.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import de.neonew.exposed.migrations.helpers.AddColumnMigration
@Suppress("ClassName", "unused")
class M0018_MangaInLibraryAt : AddColumnMigration(
"Manga",
"in_library_at",
"BIGINT",
"0"
)
@@ -26,8 +26,8 @@ import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSource
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 java.io.File import java.io.File
@@ -69,7 +69,7 @@ class TestExtensions {
} }
} }
} }
sources = getSourceList().map { getHttpSource(it.id.toLong()) } sources = getSourceList().map { getCatalogueSource(it.id.toLong())!! as HttpSource }
} }
setLoggingEnabled(true) setLoggingEnabled(true)
File("tmp/TestDesk/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" }) File("tmp/TestDesk/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })