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
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
uses: eskatos/gradle-command-action@v1
-16
View File
@@ -47,11 +47,6 @@ jobs:
mkdir -p ~/.gradle
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
uses: eskatos/gradle-command-action@v1
env:
@@ -64,12 +59,6 @@ jobs:
dependencies-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
id: GenTagName
run: |
@@ -87,11 +76,6 @@ jobs:
./unix-bundler.sh macOS-x64
./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
uses: actions/checkout@v2
with:
-5
View File
@@ -46,11 +46,6 @@ jobs:
mkdir -p ~/.gradle
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
uses: eskatos/gradle-command-action@v1
env:
@@ -14,7 +14,7 @@ const val CONFIG_PREFIX = "suwayomi.tachidesk.config"
val ApplicationRootDir: String
get(): String {
return System.getProperty(
"$CONFIG_PREFIX.server.rootDir",
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
"$CONFIG_PREFIX.server.rootDir",
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
)
}
}
@@ -6,7 +6,7 @@ import org.kodein.di.singleton
class ConfigKodeinModule {
fun create() = DI.Module("ConfigManager") {
//Config module
// Config module
bind<ConfigManager>() with singleton { GlobalConfigManager }
}
}
}
@@ -21,7 +21,7 @@ open class ConfigManager {
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
val config by lazy { loadConfigs() }
//Public read-only view of modules
// Public read-only view of modules
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
get() = generatedModules
@@ -42,29 +42,28 @@ open class ConfigManager {
* Load configs
*/
fun loadConfigs(): Config {
//Load reference configs
// Load reference configs
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
val baseConfig =
ConfigFactory.parseMap(
mapOf(
"androidcompat.rootDir" to "$ApplicationRootDir/android-compat" // override AndroidCompat's rootDir
)
ConfigFactory.parseMap(
mapOf(
"androidcompat.rootDir" to "$ApplicationRootDir/android-compat" // override AndroidCompat's rootDir
)
)
//Load user config
// Load user config
val userConfig =
File(ApplicationRootDir, "server.conf").let {
ConfigFactory.parseFile(it)
}
File(ApplicationRootDir, "server.conf").let {
ConfigFactory.parseFile(it)
}
val config = ConfigFactory.empty()
.withFallback(baseConfig)
.withFallback(userConfig)
.withFallback(compatConfig)
.withFallback(serverConfig)
.resolve()
.withFallback(baseConfig)
.withFallback(userConfig)
.withFallback(compatConfig)
.withFallback(serverConfig)
.resolve()
// set log level early
if (debugLogsEnabled(config)) {
@@ -20,7 +20,7 @@ abstract class ConfigModule(config: Config)
/**
* 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)
}
@@ -34,7 +34,7 @@ class SystemPropertyOverrideDelegate(val config: Config, val moduleName: String)
configValue.toString()
)
return when(T::class.simpleName) {
return when (T::class.simpleName) {
"Int" -> combined.toInt()
"Boolean" -> combined.toBoolean()
// 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
}
fun debugLogsEnabled(config: Config)
= System.getProperty("suwayomi.tachidesk.config.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
fun debugLogsEnabled(config: Config) =
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
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 {
// Android stub library
implementation(fileTree("lib/"))
implementation("com.github.Suwayomi:android-jar:1.0.0")
// XML
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 {
companion object {
@JvmStatic
fun getDefaultSharedPreferences(context: Context)
= context.getSharedPreferences(context.applicationInfo.packageName,
Context.MODE_PRIVATE)!!
fun getDefaultSharedPreferences(context: Context) =
context.getSharedPreferences(
context.applicationInfo.packageName,
Context.MODE_PRIVATE
)!!
}
}
@@ -5,4 +5,4 @@ import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory
class RequerySQLiteOpenHelperFactory {
fun create(configuration: SupportSQLiteOpenHelper.Configuration) = FrameworkSQLiteOpenHelperFactory().create(configuration)
}
}
@@ -14,4 +14,4 @@ class AndroidCompat {
application.attach(context)
application.onCreate()
}
}
}
@@ -14,7 +14,7 @@ class AndroidCompatInitializer {
fun init() {
DI.global.addImport(AndroidCompatModule().create())
//Register config modules
// Register config modules
GlobalConfigManager.registerModules(
FilesConfigModule.register(GlobalConfigManager.config),
ApplicationInfoConfigModule.register(GlobalConfigManager.config),
@@ -29,7 +29,7 @@ class AndroidCompatModule {
bind<PackageController>() with singleton { PackageController() }
//Context
// Context
bind<CustomContext>() with singleton { CustomContext() }
bind<Context>() with singleton {
val context: Context by DI.global.instance<CustomContext>()
@@ -13,7 +13,7 @@ class ApplicationInfoConfigModule(config: Config) : ConfigModule(config) {
val debug: Boolean by config
companion object {
fun register(config: Config)
= ApplicationInfoConfigModule(config.getConfig("android.app"))
fun register(config: Config) =
ApplicationInfoConfigModule(config.getConfig("android.app"))
}
}
@@ -9,26 +9,26 @@ import xyz.nulldev.ts.config.ConfigModule
*/
class FilesConfigModule(config: Config) : ConfigModule(config) {
val dataDir:String by config
val filesDir:String by config
val noBackupFilesDir:String by config
val dataDir: String by config
val filesDir: String by config
val noBackupFilesDir: String by config
val externalFilesDirs: MutableList<String> by config
val obbDirs: MutableList<String> by config
val cacheDir:String by config
val codeCacheDir:String by config
val cacheDir: String by config
val codeCacheDir: String by config
val externalCacheDirs: MutableList<String> by config
val externalMediaDirs: MutableList<String> by config
val rootDir:String by config
val externalStorageDir:String by config
val downloadCacheDir:String by config
val databasesDir:String by config
val rootDir: String by config
val externalStorageDir: String by config
val downloadCacheDir: 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 {
fun register(config: Config)
= FilesConfigModule(config.getConfig("android.files"))
fun register(config: Config) =
FilesConfigModule(config.getConfig("android.files"))
}
}
}
@@ -1,8 +1,8 @@
package xyz.nulldev.androidcompat.config
import com.typesafe.config.Config
import xyz.nulldev.ts.config.ConfigModule
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule
class SystemConfigModule(val config: Config) : ConfigModule(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")
companion object {
fun register(config: Config)
= SystemConfigModule(config.getConfig("android.system"))
fun register(config: Config) =
SystemConfigModule(config.getConfig("android.system"))
}
}
@@ -29,7 +29,7 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
val parentMetadata = parent.metaData
val columnCount = parentMetadata.columnCount
val columnLabels = (1 .. columnCount).map {
val columnLabels = (1..columnCount).map {
parentMetadata.getColumnLabel(it)
}.toTypedArray()
@@ -41,10 +41,10 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
// How can we optimize this?
// We need to fill the cache as the set is loaded
//Fill cache
while(parent.next()) {
// Fill cache
while (parent.next()) {
cachedContent += ResultSetEntry().apply {
for(i in 1 .. columnCount)
for (i in 1..columnCount)
data += parent.getObject(i)
}
resultSetLength++
@@ -60,8 +60,8 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
private fun internalMove(row: Int) {
if(cursor < 0) cursor = 0
else if(cursor > resultSetLength + 1) cursor = resultSetLength + 1
if (cursor < 0) cursor = 0
else if (cursor > resultSetLength + 1) cursor = resultSetLength + 1
else cursor = row
}
@@ -75,10 +75,10 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
return obj(cachedFindColumn(column))
}
private fun cachedFindColumn(column: String?)
= columnCache.getOrPut(column!!, {
findColumn(column)
})
private fun cachedFindColumn(column: String?) =
columnCache.getOrPut(column!!, {
findColumn(column)
})
override fun getNClob(columnIndex: Int): NClob {
return obj(columnIndex) as NClob
@@ -157,27 +157,27 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
override fun getDate(columnIndex: Int): Date {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getDate(columnLabel: String?): Date {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getDate(columnIndex: Int, cal: Calendar?): Date {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getDate(columnLabel: String?, cal: Calendar?): Date {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun beforeFirst() {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
@@ -202,12 +202,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
override fun getBigDecimal(columnIndex: Int, scale: Int): BigDecimal {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getBigDecimal(columnLabel: String?, scale: Int): BigDecimal {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
@@ -236,22 +236,22 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
override fun getTime(columnIndex: Int): Time {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getTime(columnLabel: String?): Time {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getTime(columnIndex: Int, cal: Calendar?): Time {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getTime(columnLabel: String?, cal: Calendar?): Time {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
@@ -272,28 +272,28 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
override fun absolute(row: Int): Boolean {
if(row > 0) {
if (row > 0) {
internalMove(row)
} else {
last()
for(i in 1 .. row)
for (i in 1..row)
previous()
}
return cursorValid()
}
override fun getSQLXML(columnIndex: Int): SQLXML? {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getSQLXML(columnLabel: String?): SQLXML? {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun <T : Any?> unwrap(iface: Class<T>?): T {
if(thisIsWrapperFor(iface))
if (thisIsWrapperFor(iface))
return this as T
else
return parent.unwrap(iface)
@@ -426,12 +426,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
override fun getBlob(columnIndex: Int): Blob {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getBlob(columnLabel: String?): Blob {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
@@ -500,12 +500,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
override fun getObject(columnIndex: Int, map: MutableMap<String, Class<*>>?): Any {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getObject(columnLabel: String?, map: MutableMap<String, Class<*>>?): Any {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
@@ -531,9 +531,9 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
private fun castToLong(obj: Any?): Long {
if(obj == null) return 0
else if(obj is Long) return obj
else if(obj is Number) return obj.toLong()
if (obj == null) return 0
else if (obj is Long) return obj
else if (obj is Number) return obj.toLong()
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 {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getClob(columnLabel: String?): Clob {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
@@ -604,12 +604,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
override fun getArray(columnIndex: Int): Array {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getArray(columnLabel: String?): Array {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
@@ -688,32 +688,32 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
override fun getTimestamp(columnIndex: Int): Timestamp {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getTimestamp(columnLabel: String?): Timestamp {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getTimestamp(columnIndex: Int, cal: Calendar?): Timestamp {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getTimestamp(columnLabel: String?, cal: Calendar?): Timestamp {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getRef(columnIndex: Int): Ref {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getRef(columnLabel: String?): Ref {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
@@ -792,12 +792,12 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
}
override fun getRowId(columnIndex: Int): RowId {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
override fun getRowId(columnLabel: String?): RowId {
//TODO Maybe?
// TODO Maybe?
notImplemented()
}
@@ -848,4 +848,4 @@ class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
class ResultSetEntry {
val data = mutableListOf<Any?>()
}
}
}
@@ -174,4 +174,4 @@ class JavaSharedPreferences(key: String) : SharedPreferences {
javaPreferences.removeNode()
return true
}
}
}
@@ -14,8 +14,6 @@ import java.io.File
import javax.imageio.ImageIO
import javax.xml.parsers.DocumentBuilderFactory
data class InstalledPackage(val root: File) {
val apk = File(root, "package.apk")
val jar = File(root, "translated.jar")
@@ -40,20 +38,24 @@ data class InstalledPackage(val root: File) {
}?.filter {
it.tagName == "meta-data"
}?.map {
putString(it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue)
putString(
it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue
)
}
}
it.signatures = (parsed.apkSingers.flatMap { it.certificateMetas }
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
it.signatures = (
parsed.apkSingers.flatMap { it.certificateMetas }
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
}
fun verify(): Boolean {
val res = ApkVerifier.Builder(apk)
.build()
.verify()
.build()
.verify()
return res.isVerified
}
@@ -69,7 +71,7 @@ data class InstalledPackage(val root: File) {
}.sortedByDescending { it.width * it.height }.firstOrNull() ?: return
ImageIO.write(read, "png", icon)
} catch(e: Exception) {
} catch (e: Exception) {
icon.delete()
}
}
@@ -77,7 +79,7 @@ data class InstalledPackage(val root: File) {
fun writeJar() {
try {
Dex2jar.from(apk).to(jar.toPath())
} catch(e: Exception) {
} catch (e: Exception) {
jar.delete()
}
}
@@ -92,4 +94,4 @@ data class InstalledPackage(val root: File) {
return out
}
}
}
}
@@ -48,7 +48,7 @@ class PackageController {
if (!installed.jar.exists()) {
throw IllegalStateException("Failed to translate APK dex!")
}
} catch(t: Throwable) {
} catch (t: Throwable) {
root.deleteRecursively()
throw t
}
@@ -63,7 +63,7 @@ class PackageController {
}
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
pack.root.deleteRecursively()
@@ -74,7 +74,7 @@ class PackageController {
fun findPackage(packageName: String): InstalledPackage? {
val file = File(androidFiles.packagesDir, packageName)
return if(file.exists())
return if (file.exists())
InstalledPackage(file)
else
null
@@ -84,4 +84,4 @@ class PackageController {
val pkgName = ApkParsers.getMetaInfo(apkFile).packageName
return findPackage(pkgName)?.jar
}
}
}
@@ -24,4 +24,4 @@ fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
sourceDir = apk.absolutePath
}
}
}
}
@@ -24,4 +24,4 @@ interface Resource {
fun getType(): Class<out Resource>
fun getValue(): Any?
}
}
@@ -27,10 +27,10 @@ class ServiceSupport {
runningServices[name] = service
//Setup service
// Setup service
thread {
callOnCreate(service)
//TODO Handle more complex cases
// TODO Handle more complex cases
service.onStartCommand(intent, 0, 0)
}
}
@@ -43,7 +43,7 @@ class ServiceSupport {
fun stopService(name: String) {
logger.debug { "Stopping service: $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" }
} else {
thread {
@@ -63,6 +63,6 @@ class ServiceSupport {
fun serviceInstanceFromClass(className: String): Service {
val clazzObj = Class.forName(className)
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
@Suppress("UNCHECKED_CAST")
fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T {
return when(type) {
return when (type) {
AndroidFiles::class.java -> {
val instance: AndroidFiles by (kodein ?: kodein()).instance()
instance as T
@@ -64,5 +64,4 @@ object KodeinGlobalHelper {
fun <T : Any> instance(type: Class<T>): T {
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
<!-- TODO: fill before release -->
## Tachidesk-Server
### Public API
#### Non-breaking changes
- N/A
#### Breaking changes
## Tachidesk-Server Changelog
- N/A
#### Bug fixes
- N/A
### Private API
## Tachidesk-WebUI Changelog
- 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
## TL;DR
- Fixed Local source not working on Windows
@@ -12,11 +59,12 @@
- N/A
#### 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
- (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-WebUI/pull/200) by @Syer10)
- (r941) Update BytecodeEditor to use Java NIO Paths ([#200](https://github.com/Suwayomi/Tachidesk-Server/pull/200) by @Syer10)
- (r942) Gradle Updates ([#199](https://github.com/Suwayomi/Tachidesk-Server/pull/199) by @Syer10)
## 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
- 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)
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
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) |
# 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" 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?
# What is Tachidesk?
<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/).
@@ -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.
# 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?
Here is a list of current features:
@@ -32,6 +32,7 @@ Here is a list of current features:
- Searching and browsing installed sources
- Ability to download Manga for offline read
- Backup and restore support powered by Tachiyomi Backups
- Viewing latest updated chapters.
- From Aniyomi
- Installing and executing Aniyomi's Extensions
- 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:** 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
## General Requirements
In order to use the app effectively you need the following:
+4
View File
@@ -6,6 +6,7 @@ plugins {
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
id("org.jmailen.kotlinter") version "3.6.0"
id("com.github.gmazzo.buildconfig") version "3.0.3" apply false
}
allprojects {
@@ -17,6 +18,7 @@ allprojects {
mavenCentral()
google()
maven("https://jitpack.io")
maven("https://github.com/Suwayomi/Tachidesk-Server/raw/android-jar/")
}
}
@@ -29,6 +31,7 @@ val projects = listOf(
configure(projects) {
apply(plugin = "org.jetbrains.kotlin.jvm")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
apply(plugin = "org.jmailen.kotlinter")
java {
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -37,6 +40,7 @@ configure(projects) {
tasks {
withType<KotlinCompile> {
dependsOn(formatKotlin)
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs = listOf(
+2 -2
View File
@@ -12,9 +12,9 @@ const val kotlinVersion = "1.5.30"
const val MainClass = "suwayomi.tachidesk.MainKt"
// 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
val tachideskRevision = runCatching {
+2 -2
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#/bin/bash
# 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_release="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64"
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"
else
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 java.time.Instant
plugins {
application
id("com.github.johnrengelman.shadow") version "7.0.0"
id("com.github.gmazzo.buildconfig") version "3.0.3"
id("com.github.gmazzo.buildconfig")
}
dependencies {
@@ -70,16 +69,11 @@ dependencies {
// uncomment to test extensions directly
// implementation(fileTree("lib/"))
implementation(kotlin("script-runtime"))
}
application {
mainClass.set(MainClass)
// uncomment for testing electron
// applicationDefaultJvmArgs = listOf(
// "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
// "-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron"
// )
}
sourceSets {
@@ -127,20 +121,13 @@ tasks {
archiveBaseName.set(rootProject.name)
archiveVersion.set(tachideskVersion)
archiveClassifier.set(tachideskRevision)
destinationDirectory.set(File("$rootDir/server/build"))
}
test {
useJUnit()
}
withType<ShadowJar> {
destinationDirectory.set(File("$rootDir/server/build"))
}
named("run") {
dependsOn(":formatKotlin", ":lintKotlin")
}
named<Copy>("processResources") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
mustRunAfter("downloadWebUI")
@@ -171,4 +158,16 @@ tasks {
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
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.Epub
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.SChapter
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.lang.compareToCaseInsensitiveNaturalOrder
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.jsonPrimitive
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.insertAndGetId
import org.jetbrains.exposed.sql.select
@@ -51,14 +41,12 @@ import suwayomi.tachidesk.server.ApplicationDirs
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.net.URLDecoder
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
class LocalSource : HttpSource() {
class LocalSource : CatalogueSource {
companion object {
const val ID = 0L
const val LANG = "localsourcelang"
@@ -133,13 +121,8 @@ class LocalSource : HttpSource() {
override val id = ID
override val name = NAME
override val lang = LANG
override val baseUrl: String = ""
override val supportsLatest = true
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(FileSystemInterceptor)
.build()
private val json: Json by injectLazy()
override fun toString() = name
@@ -181,7 +164,7 @@ class LocalSource : HttpSource() {
// Try to find the cover
val cover = getCoverFile(File("${applicationDirs.localMangaRoot}/$url"))
if (cover != null && cover.exists()) {
thumbnail_url = fakeUrlFrom(cover.absolutePath)
thumbnail_url = cover.absolutePath
}
val chapters = fetchChapterList(this).toBlocking().first()
@@ -197,8 +180,7 @@ class LocalSource : HttpSource() {
// Copy the cover from the first chapter found.
if (thumbnail_url == null) {
try {
val dest = updateCover(chapter, this)
thumbnail_url = dest?.absolutePath?.let { fakeUrlFrom(it) }
thumbnail_url = updateCover(chapter, this)?.absolutePath
} catch (e: Exception) {
logger.error { e }
}
@@ -311,7 +293,7 @@ class LocalSource : HttpSource() {
chapterFile.listFiles().orEmpty().sortedBy { it.name }.mapIndexed { index, page ->
Page(
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 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.MangaController
import suwayomi.tachidesk.manga.controller.SourceController
import suwayomi.tachidesk.manga.controller.UpdateController
object MangaAPI {
fun defineEndpoints() {
@@ -106,5 +107,9 @@ object MangaAPI {
get("{mangaId}/chapter/{chapterIndex}", DownloadController::queueChapter)
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.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
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.deleteWhere
import org.jetbrains.exposed.sql.insert
@@ -20,11 +21,12 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Manga.getManga
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.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
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.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
@@ -40,7 +42,7 @@ object Chapter {
getSourceChapters(mangaId)
} else {
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 {
ChapterTable.toDataClass(it)
}
@@ -52,7 +54,7 @@ object Chapter {
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
val manga = getManga(mangaId)
val source = getHttpSource(manga.sourceId.toLong())
val source = getCatalogueSourceOrStub(manga.sourceId.toLong())
val sManga = SManga.create().apply {
title = manga.title
@@ -63,11 +65,12 @@ object Chapter {
// Recognize number for new chapters.
chapterList.forEach {
source.prepareNewChapter(it, sManga)
(source as? HttpSource)?.prepareNewChapter(it, sManga)
ChapterRecognition.parseChapterNumber(it, sManga)
}
val chapterCount = chapterList.count()
var now = Instant.now().epochSecond
transaction {
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
@@ -80,7 +83,8 @@ object Chapter {
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[sourceOrder] = index + 1
it[fetchedAt] = now++
it[ChapterTable.manga] = mangaId
}
} else {
@@ -90,7 +94,7 @@ object Chapter {
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[sourceOrder] = index + 1
it[ChapterTable.manga] = mangaId
}
}
@@ -103,8 +107,8 @@ object Chapter {
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.toList() }
dbChapterList.forEach {
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url]
if (it[ChapterTable.sourceOrder] >= chapterList.size ||
chapterList[it[ChapterTable.sourceOrder] - 1].url != it[ChapterTable.url]
) {
transaction {
PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] }
@@ -137,6 +141,7 @@ object Chapter {
dbChapter[ChapterTable.lastReadAt],
chapterCount - index,
dbChapter[ChapterTable.fetchedAt],
dbChapter[ChapterTable.isDownloaded],
dbChapter[ChapterTable.pageCount],
@@ -151,7 +156,7 @@ object Chapter {
suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
val chapterEntry = transaction {
ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first()
}
@@ -159,13 +164,13 @@ object Chapter {
chapterEntry[ChapterTable.isDownloaded] && firstPageExists(mangaId, chapterEntry[ChapterTable.id].value)
return if (!isReallyDownloaded) {
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
}
}
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(
SChapter.create().apply {
@@ -203,7 +208,7 @@ object Chapter {
val pageCount = pageList.count()
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
}
}
@@ -219,7 +224,8 @@ object Chapter {
chapterEntry[ChapterTable.lastPageRead],
chapterEntry[ChapterTable.lastReadAt],
chapterEntry[ChapterTable.chapterIndex],
chapterEntry[ChapterTable.sourceOrder],
chapterEntry[ChapterTable.fetchedAt],
chapterEntry[ChapterTable.isDownloaded],
pageCount,
chapterCount.toInt(),
@@ -249,7 +255,7 @@ object Chapter {
) {
transaction {
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 {
update[ChapterTable.isRead] = it
}
@@ -264,7 +270,7 @@ object Chapter {
}
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
}
}
@@ -281,7 +287,7 @@ object Chapter {
fun modifyChapterMeta(mangaId: Int, chapterIndex: Int, key: String, value: String) {
transaction {
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
val meta =
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) {
transaction {
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
val chapterDir = getChapterDir(mangaId, chapterId)
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
}
}
}
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.CategoryTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import java.time.Instant
object Library {
suspend fun addMangaToLibrary(mangaId: Int) {
@@ -25,8 +26,9 @@ object Library {
val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList()
MangaTable.update({ MangaTable.id eq manga.id }) {
it[MangaTable.inLibrary] = true
it[MangaTable.defaultCategory] = defaultCategories.isEmpty()
it[inLibrary] = true
it[inLibraryAt] = Instant.now().epochSecond
it[defaultCategory] = defaultCategories.isEmpty()
}
defaultCategories.forEach { category ->
@@ -8,7 +8,9 @@ package suwayomi.tachidesk.manga.impl
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
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.online.HttpSource
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
@@ -19,11 +21,12 @@ import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
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.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.getImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
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.MangaTable
import suwayomi.tachidesk.server.ApplicationDirs
import java.io.File
import java.io.IOException
import java.io.InputStream
object Manga {
@@ -61,43 +66,43 @@ object Manga {
mangaEntry[MangaTable.genre].toGenreList(),
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary],
mangaEntry[MangaTable.inLibraryAt],
getSource(mangaEntry[MangaTable.sourceReference]),
getMangaMetaMap(mangaId),
mangaEntry[MangaTable.realUrl],
false
)
} else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val sManga = SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
val fetchedManga = source.fetchMangaDetails(sManga).awaitSingle()
val networkManga = source.fetchMangaDetails(sManga).awaitSingle()
sManga.copyFrom(networkManga)
transaction {
MangaTable.update({ MangaTable.id eq mangaId }) {
if (fetchedManga.title != mangaEntry[MangaTable.title]) {
val canUpdateTitle = updateMangaDownloadDir(mangaId, fetchedManga.title)
if (sManga.title != mangaEntry[MangaTable.title]) {
val canUpdateTitle = updateMangaDownloadDir(mangaId, sManga.title)
if (canUpdateTitle)
it[MangaTable.title] = fetchedManga.title
it[MangaTable.title] = sManga.title
}
it[MangaTable.initialized] = true
it[MangaTable.artist] = fetchedManga.artist
it[MangaTable.author] = fetchedManga.author
it[MangaTable.description] = truncate(fetchedManga.description, 4096)
it[MangaTable.genre] = fetchedManga.genre
it[MangaTable.status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
it[MangaTable.artist] = sManga.artist
it[MangaTable.author] = sManga.author
it[MangaTable.description] = truncate(sManga.description, 4096)
it[MangaTable.genre] = sManga.genre
it[MangaTable.status] = sManga.status
if (sManga.thumbnail_url != null && sManga.thumbnail_url.orEmpty().isNotEmpty())
it[MangaTable.thumbnail_url] = sManga.thumbnail_url
it[MangaTable.realUrl] = try {
source.mangaDetailsRequest(sManga).url.toString()
} catch (e: Exception) {
null
}
it[MangaTable.realUrl] = runCatching {
(source as? HttpSource)?.mangaDetailsRequest(sManga)?.url?.toString()
}.getOrNull()
}
}
@@ -115,12 +120,13 @@ object Manga {
true,
fetchedManga.artist,
fetchedManga.author,
fetchedManga.description,
fetchedManga.genre.toGenreList(),
MangaStatus.valueOf(fetchedManga.status).name,
sManga.artist,
sManga.author,
sManga.description,
sManga.genre.toGenreList(),
MangaStatus.valueOf(sManga.status).name,
mangaEntry[MangaTable.inLibrary],
mangaEntry[MangaTable.inLibraryAt],
getSource(mangaEntry[MangaTable.sourceReference]),
getMangaMetaMap(mangaId),
mangaEntry[MangaTable.realUrl],
@@ -161,25 +167,39 @@ object Manga {
val saveDir = applicationDirs.mangaThumbnailsRoot
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]
val source = getHttpSource(sourceId)
return when (val source = getCatalogueSourceOrStub(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]
?: 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()
}
source.client.newCall(
GET(thumbnailUrl, source.headers)
).await()
source.client.newCall(
GET(thumbnailUrl, source.headers)
).await()
}
is LocalSource -> {
val imageFile = mangaEntry[MangaTable.thumbnail_url]?.let {
val file = File(it)
if (file.exists()) {
file
} else {
null
}
} ?: 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.transactions.transaction
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.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
@@ -27,14 +27,15 @@ object MangaList {
}
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId)
val source = getCatalogueSourceOrStub(sourceId)
val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).awaitSingle()
} else {
if (source.supportsLatest)
if (source.supportsLatest) {
source.fetchLatestUpdates(pageNum).awaitSingle()
else
} else {
throw Exception("Source $source doesn't support latest")
}
}
return mangasPage.processEntries(sourceId)
}
@@ -81,6 +82,7 @@ object MangaList {
manga.genre.toGenreList(),
MangaStatus.valueOf(manga.status).name,
false, // It's a new manga entry
0,
meta = getMangaMetaMap(mangaId),
realUrl = mangaEntry[MangaTable.realUrl],
freshData = true
@@ -103,6 +105,7 @@ object MangaList {
mangaEntry[MangaTable.genre].toGenreList(),
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary],
mangaEntry[MangaTable.inLibraryAt],
meta = getMangaMetaMap(mangaId),
realUrl = mangaEntry[MangaTable.realUrl],
freshData = false
@@ -17,11 +17,12 @@ import org.jetbrains.exposed.sql.update
import org.kodein.di.DI
import org.kodein.di.conf.global
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.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.getImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
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> {
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 {
ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first()
}
val chapterId = chapterEntry[ChapterTable.id].value
@@ -61,19 +62,20 @@ object Page {
)
// we treat Local source differently
if (mangaEntry[MangaTable.sourceReference] == LocalSource.ID) {
if (source.id == LocalSource.ID) {
// is of archive format
if (LocalSource.pageCache.containsKey(chapterEntry[ChapterTable.url])) {
val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]()
return pageStream to "image/jpeg"
val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]
return pageStream() to (ImageUtil.findImageType { pageStream() }?.mime ?: "image/jpeg")
}
// is of directory format
return ImageResponse.getNoCacheImageResponse {
source.fetchImage(tachiyomiPage).awaitSingle()
}
val imageFile = File(tachiyomiPage.imageUrl!!)
return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
}
source as HttpSource
if (pageEntry[PageTable.imageUrl] == null) {
val trueImageUrl = getTrueImageUrl(tachiyomiPage, source)
transaction {
@@ -10,13 +10,13 @@ package suwayomi.tachidesk.manga.impl
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
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.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
object Search {
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()
return searchManga.processEntries(sourceId)
}
@@ -25,7 +25,7 @@ object Search {
private fun getFilterListOf(sourceId: Long, reset: Boolean = false): FilterList {
if (reset || !filterListCache.containsKey(sourceId)) {
filterListCache[sourceId] = getHttpSource(sourceId).getFilterList()
filterListCache[sourceId] = getCatalogueSourceOrStub(sourceId).getFilterList()
}
return filterListCache[sourceId]!!
}
@@ -12,7 +12,6 @@ import android.content.Context
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.source.local.LocalSource
import mu.KotlinLogging
import org.jetbrains.exposed.sql.select
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.instance
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.invalidateSourceCache
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSource
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.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
@@ -36,7 +36,7 @@ object Source {
fun getSourceList(): List<SourceDataClass> {
return transaction {
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()
SourceDataClass(
@@ -44,10 +44,10 @@ object Source {
it[SourceTable.name],
it[SourceTable.lang],
getExtensionIconUrl(sourceExtension[ExtensionTable.apkName]),
httpSource.supportsLatest,
httpSource is ConfigurableSource,
catalogueSource.supportsLatest,
catalogueSource is ConfigurableSource,
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
return transaction {
if (sourceId == LocalSource.ID) {
// initialize local source
getHttpSource(sourceId)
}
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 {
ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()
}
@@ -75,10 +70,10 @@ object Source {
extension!![ExtensionTable.apkName]
)
},
httpSource?.supportsLatest,
httpSource?.let { it is ConfigurableSource },
catalogueSource?.supportsLatest,
catalogueSource?.let { it is ConfigurableSource },
source?.get(SourceTable.isNsfw),
httpSource?.toString()
catalogueSource?.toString()
)
}
}
@@ -103,7 +98,7 @@ object Source {
* Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap]
*/
fun getSourcePreferences(sourceId: Long): List<PreferenceObject> {
val source = getHttpSource(sourceId)
val source = getCatalogueSourceOrStub(sourceId)
if (source is ConfigurableSource) {
val sourceShardPreferences =
@@ -32,6 +32,7 @@ import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.manga.model.table.toDataClass
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.util.concurrent.TimeUnit
object ProtoBackupExport : ProtoBackupBase() {
suspend fun createBackup(flags: BackupFlags): InputStream {
@@ -68,7 +69,7 @@ object ProtoBackupExport : ProtoBackupBase() {
mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(),
MangaStatus.valueOf(mangaRow[MangaTable.status]).value,
mangaRow[MangaTable.thumbnail_url],
0, // not supported in Tachidesk
TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]),
0, // not supported in Tachidesk
)
@@ -84,10 +85,10 @@ object ProtoBackupExport : ProtoBackupBase() {
it.read,
it.bookmarked,
it.lastPageRead,
0, // not supported in Tachidesk
TimeUnit.SECONDS.toMillis(it.fetchedAt),
it.uploadDate,
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.lang.Integer.max
import java.util.Date
import java.util.concurrent.TimeUnit
object ProtoBackupImport : ProtoBackupBase() {
private val logger = KotlinLogging.logger {}
@@ -148,6 +149,8 @@ object ProtoBackupImport : ProtoBackupBase() {
it[initialized] = manga.description != null
it[inLibrary] = manga.favorite
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
}.value
// insert chapter data
@@ -160,12 +163,14 @@ object ProtoBackupImport : ProtoBackupBase() {
it[chapter_number] = chapter.chapter_number
it[scanlator] = chapter.scanlator
it[chapterIndex] = chaptersLength - chapter.source_order
it[sourceOrder] = chaptersLength - chapter.source_order
it[ChapterTable.manga] = mangaId
it[isRead] = chapter.read
it[lastPageRead] = chapter.last_page_read
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[inLibrary] = manga.favorite || dbManga[inLibrary]
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
}
// merge chapter data
@@ -207,7 +214,7 @@ object ProtoBackupImport : ProtoBackupBase() {
it[chapter_number] = chapter.chapter_number
it[scanlator] = chapter.scanlator
it[chapterIndex] = chaptersLength - chapter.source_order
it[sourceOrder] = chaptersLength - chapter.source_order
it[ChapterTable.manga] = mangaId
it[isRead] = chapter.read
@@ -77,7 +77,7 @@ object DownloadManager {
mangaId,
chapter = ChapterTable.toDataClass(
transaction {
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
.first()
}
)
@@ -61,7 +61,7 @@ class Downloader(private val downloadQueue: CopyOnWriteArrayList<DownloadChapter
}
download.state = Finished
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
}
}
@@ -28,7 +28,6 @@ import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.extensionTableAsDataClass
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.EXTENSION_FEATURE
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.loadExtensionSources
import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
@@ -51,9 +51,6 @@ object Extension {
private val logger = KotlinLogging.logger {}
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 {
logger.debug("Installing $pkgName")
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
@@ -189,7 +186,7 @@ object Extension {
it[name] = httpSource.name
it[lang] = httpSource.lang
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}" }
}
@@ -243,7 +240,7 @@ object Extension {
PackageTools.jarLoaderMap.remove(jarPath)?.close()
// clear all loaded sources
sources.forEach { GetHttpSource.invalidateSourceCache(it) }
sources.forEach { GetCatalogueSource.invalidateSourceCache(it) }
File(jarPath).delete()
}
@@ -12,6 +12,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI
import org.kodein.di.conf.global
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.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
@@ -22,7 +23,7 @@ private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getMangaDir(mangaId: Int): String {
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 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 */
fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean {
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 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
@@ -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
* 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.SourceFactory
import eu.kanade.tachiyomi.source.local.LocalSource
@@ -22,23 +23,21 @@ import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.ApplicationDirs
import java.util.concurrent.ConcurrentHashMap
object GetHttpSource {
private val sourceCache = ConcurrentHashMap<Long, HttpSource>()
object GetCatalogueSource {
private val sourceCache = ConcurrentHashMap<Long, CatalogueSource>(
mapOf(LocalSource.ID to LocalSource())
)
private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getHttpSource(sourceId: Long): HttpSource {
val cachedResult: HttpSource? = sourceCache[sourceId]
fun getCatalogueSource(sourceId: Long): CatalogueSource? {
val cachedResult: CatalogueSource? = sourceCache[sourceId]
if (cachedResult != null) {
return cachedResult
}
val sourceRecord = transaction {
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
}
if (sourceId == LocalSource.ID) {
return LocalSource()
}
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
} ?: return null
val extensionId = sourceRecord[SourceTable.extension]
val extensionRecord = transaction {
@@ -60,6 +59,10 @@ object GetHttpSource {
return sourceCache[sourceId]!!
}
fun getCatalogueSourceOrStub(sourceId: Long): CatalogueSource {
return getCatalogueSource(sourceId) ?: StubSource(sourceId)
}
fun invalidateSourceCache(sourceId: Long) {
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 */
val lastReadAt: Long,
// TODO(v0.6.0): rename to sourceOrder
/** this chapter's index, starts with 1 */
val index: Int,
/** the date we fist saw this chapter*/
val fetchedAt: Long,
/** is chapter downloaded */
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 status: String = MangaStatus.UNKNOWN.name,
val inLibrary: Boolean = false,
val inLibraryAt: Long = 0,
val source: SourceDataClass? = null,
/** meta data for clients */
@@ -25,9 +25,9 @@ object ChapterTable : IntIdTable() {
val isBookmarked = bool("bookmark").default(false)
val lastPageRead = integer("last_page_read").default(0)
val lastReadAt = long("last_read_at").default(0)
val fetchedAt = long("fetched_at").default(0)
// index is reserved by a function
val chapterIndex = integer("index")
val sourceOrder = integer("source_order")
val isDownloaded = bool("is_downloaded").default(false)
@@ -48,7 +48,8 @@ fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
chapterEntry[isBookmarked],
chapterEntry[lastPageRead],
chapterEntry[lastReadAt],
chapterEntry[chapterIndex],
chapterEntry[sourceOrder],
chapterEntry[fetchedAt],
chapterEntry[isDownloaded],
chapterEntry[pageCount],
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 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
val sourceReference = long("source")
@@ -56,6 +57,7 @@ fun MangaTable.toDataClass(mangaEntry: ResultRow) =
mangaEntry[genre].toGenreList(),
Companion.valueOf(mangaEntry[status]).name,
mangaEntry[inLibrary],
mangaEntry[inLibraryAt],
meta = getMangaMetaMap(mangaEntry[id].value),
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.updateExtension
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.source.GetCatalogueSource.getCatalogueSource
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.applicationSetup
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)
File("tmp/TestDesk/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })