Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da44d3b2b4 | |||
| 99ec2aca6a | |||
| 6c278604ec | |||
| 1e094a467a | |||
| 978ccfeeba | |||
| e93d66d8a1 | |||
| c29a749833 | |||
| b08d5d1261 | |||
| 9b129789e9 | |||
| a76a6d2798 | |||
| 086a760378 | |||
| f78c8d4fd8 | |||
| 7b91489997 | |||
| 36a8980c95 | |||
| 7c65640cb7 | |||
| d70e68495a | |||
| 2586202772 | |||
| b5f771368a | |||
| 0c28320ce3 | |||
| c8b4fbc36b | |||
| e9b07849fe | |||
| 409260af6f | |||
| d3d53d1a4e | |||
| e2db191f70 | |||
| d61816734d | |||
| f4dad8058f | |||
| 70bdb375c3 | |||
| e724ab0a29 | |||
| 7d0ee2ac11 | |||
| 59b7e852e2 | |||
| b2eb1a391d | |||
| 9b3aee98d3 | |||
| 0476f4144c | |||
| ed77f45fae | |||
| 0cd529e746 | |||
| 5969048318 | |||
| d1a7f8baa0 | |||
| 18dc936002 | |||
| b4b7b5d572 | |||
| 291c2e692d | |||
| 8a9a4f21b1 | |||
| cd31332b39 | |||
| cc8d2162a0 | |||
| e6313cdc67 | |||
| 5af64892e7 | |||
| a5578a7ac7 | |||
| fcdda6406e | |||
| 9bdd9f8aa6 | |||
| f3856f051b | |||
| d3a6662c60 | |||
| 5474eddf84 | |||
| b666cd47d4 | |||
| 8a986383fe | |||
| 9fa17f617e |
@@ -1,4 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
// Config API, moved to the global build.gradle
|
|
||||||
// implementation("com.typesafe:config:1.4.0")
|
|
||||||
}
|
|
||||||
@@ -20,16 +20,9 @@ dependencies {
|
|||||||
// Android stub library
|
// Android stub library
|
||||||
implementation(fileTree("lib/"))
|
implementation(fileTree("lib/"))
|
||||||
|
|
||||||
|
|
||||||
// Android JAR libs
|
|
||||||
// compileOnly( fileTree(dir: new File(rootProject.rootDir, "libs/other"), include: "*.jar")
|
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
compileOnly("com.google.code.gson:gson:2.8.6")
|
compileOnly("com.google.code.gson:gson:2.8.6")
|
||||||
|
|
||||||
// Javassist
|
|
||||||
compileOnly("org.javassist:javassist:3.27.0-GA")
|
|
||||||
|
|
||||||
// XML
|
// XML
|
||||||
compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
||||||
|
|
||||||
@@ -43,10 +36,8 @@ dependencies {
|
|||||||
compileOnly("androidx.annotation:annotation:1.2.0-alpha01")
|
compileOnly("androidx.annotation:annotation:1.2.0-alpha01")
|
||||||
|
|
||||||
// substitute for duktape-android
|
// substitute for duktape-android
|
||||||
// 'org.mozilla:rhino' includes some code that we don't need so use 'org.mozilla:rhino-runtime' instead
|
implementation("org.mozilla:rhino-runtime:1.7.13") // slimmer version of 'org.mozilla:rhino'
|
||||||
implementation("org.mozilla:rhino-runtime:1.7.13")
|
implementation("org.mozilla:rhino-engine:1.7.13") // provides the same interface as 'javax.script' a.k.a Nashorn
|
||||||
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
|
|
||||||
implementation("org.mozilla:rhino-engine:1.7.13")
|
|
||||||
|
|
||||||
// Kotlin wrapper around Java Preferences, makes certain things easier
|
// Kotlin wrapper around Java Preferences, makes certain things easier
|
||||||
val multiplatformSettingsVersion = "0.7.7"
|
val multiplatformSettingsVersion = "0.7.7"
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package kotlinx.coroutines.experimental.android
|
|
||||||
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
|
|
||||||
val UI = GlobalScope.coroutineContext
|
|
||||||
@@ -2,7 +2,6 @@ package xyz.nulldev.androidcompat
|
|||||||
|
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import xyz.nulldev.androidcompat.bytecode.ModApplier
|
|
||||||
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
|
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
|
||||||
import xyz.nulldev.androidcompat.config.FilesConfigModule
|
import xyz.nulldev.androidcompat.config.FilesConfigModule
|
||||||
import xyz.nulldev.androidcompat.config.SystemConfigModule
|
import xyz.nulldev.androidcompat.config.SystemConfigModule
|
||||||
@@ -12,12 +11,7 @@ import xyz.nulldev.ts.config.GlobalConfigManager
|
|||||||
* Initializes the Android compatibility module
|
* Initializes the Android compatibility module
|
||||||
*/
|
*/
|
||||||
class AndroidCompatInitializer {
|
class AndroidCompatInitializer {
|
||||||
|
|
||||||
val modApplier by lazy { ModApplier() }
|
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
modApplier.apply()
|
|
||||||
|
|
||||||
DI.global.addImport(AndroidCompatModule().create())
|
DI.global.addImport(AndroidCompatModule().create())
|
||||||
|
|
||||||
//Register config modules
|
//Register config modules
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package xyz.nulldev.androidcompat.bytecode
|
|
||||||
|
|
||||||
import javassist.CtClass
|
|
||||||
import mu.KotlinLogging
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies Javassist modifications
|
|
||||||
*/
|
|
||||||
|
|
||||||
class ModApplier {
|
|
||||||
|
|
||||||
val logger = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
fun apply() {
|
|
||||||
logger.info { "Applying Javassist mods..." }
|
|
||||||
val modifiedClasses = mutableListOf<CtClass>()
|
|
||||||
|
|
||||||
modifiedClasses.forEach {
|
|
||||||
it.toClass()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,21 @@ import java.io.InputStream
|
|||||||
import java.io.Reader
|
import java.io.Reader
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.sql.*
|
|
||||||
import java.sql.Array
|
import java.sql.Array
|
||||||
|
import java.sql.Blob
|
||||||
|
import java.sql.Clob
|
||||||
import java.sql.Date
|
import java.sql.Date
|
||||||
import java.util.*
|
import java.sql.NClob
|
||||||
|
import java.sql.Ref
|
||||||
|
import java.sql.ResultSet
|
||||||
|
import java.sql.ResultSetMetaData
|
||||||
|
import java.sql.RowId
|
||||||
|
import java.sql.SQLXML
|
||||||
|
import java.sql.Time
|
||||||
|
import java.sql.Timestamp
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
||||||
|
|
||||||
private val cachedContent = mutableListOf<ResultSetEntry>()
|
private val cachedContent = mutableListOf<ResultSetEntry>()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ServiceSupport {
|
|||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
fun startService(context: Context, intent: Intent) {
|
fun startService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
|
||||||
val name = intentToClassName(intent)
|
val name = intentToClassName(intent)
|
||||||
|
|
||||||
logger.debug { "Starting service: $name" }
|
logger.debug { "Starting service: $name" }
|
||||||
@@ -35,7 +35,7 @@ class ServiceSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopService(context: Context, intent: Intent) {
|
fun stopService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
|
||||||
val name = intentToClassName(intent)
|
val name = intentToClassName(intent)
|
||||||
stopService(name)
|
stopService(name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ object KodeinGlobalHelper {
|
|||||||
* Get a dependency
|
* Get a dependency
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
@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 -> {
|
||||||
|
|||||||
+21
-36
@@ -2,51 +2,36 @@
|
|||||||
## Where should I start?
|
## Where should I start?
|
||||||
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
|
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
|
||||||
|
|
||||||
**Note to potential contributors:** Notify the developers on Suwayomi discord (#programming channel) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
|
**Note to potential contributors:** Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#programming channel) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
|
||||||
|
|
||||||
## How does Tachidesk work?
|
## How does Tachidesk-Server work?
|
||||||
This project has two components:
|
This project has two components:
|
||||||
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
1. **Server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run jar libraries converted from apk extensions. All this concludes to serving a REST API.
|
||||||
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation.
|
2. **WebUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation located at https://github.com/Suwayomi/Tachidesk-WebUI
|
||||||
|
|
||||||
## Why a web app?
|
## Why a web server app?
|
||||||
This structure is chosen to
|
This structure is chosen to
|
||||||
- Achieve the maximum multi-platform-ness
|
- Achieve the maximum multi-platform-ness
|
||||||
- Gives the ability to acces Tachidesk from a remote web browser e.g. your phone, tablet or smart TV
|
- Gives the ability to acces Tachidesk-Server from a remote client e.g. your phone, tablet or smart TV
|
||||||
- Eaise development of alternative user intefaces for Tachidesk
|
- Eaise development of user intefaces for Tachidesk
|
||||||
|
|
||||||
## User Interfaces for Tachidesk server
|
|
||||||
Currently, there are three known interfaces for Tachidesk:
|
|
||||||
1. [webUI](https://github.com/Suwayomi/Tachidesk/tree/master/webUI/react): The react SPA that Tachidesk is traditionally shipped with.
|
|
||||||
2. [TachideskJUI](https://github.com/Suwayomi/TachideskJUI): A Jetbrains Compose Native app, re-uses components made for the upcoming Tachiyomi 1.x
|
|
||||||
3. [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stages of development.
|
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
You need these software packages installed in order to build the project
|
You need these software packages installed in order to build the project
|
||||||
### Server
|
|
||||||
- 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
|
- Android stubs jar
|
||||||
- Manual download: Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
- **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.
|
- **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.
|
||||||
### webUI
|
|
||||||
- Nodejs LTS or latest
|
|
||||||
- Yarn
|
|
||||||
- Git
|
|
||||||
### building the full-blown jar
|
|
||||||
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
|
||||||
### building without `webUI` bundled(server only)
|
|
||||||
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
|
||||||
### building the Windows package
|
|
||||||
First Build the jar, then cd into the `scripts` directory and run `./windows<bits>-bundler.sh` (or `./windows<bits>-bundler.ps1` if you are on windows), the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win64.zip`.
|
|
||||||
## Running in development mode
|
|
||||||
First satisfy [the prerequisites](#prerequisites)
|
|
||||||
### server
|
|
||||||
run `./gradlew :server:run --stacktrace` to run the server
|
|
||||||
### webUI
|
|
||||||
How to do it is described in `webUI/react/README.md` but for short,
|
|
||||||
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
|
||||||
then `yarn start` to start the development server, if a new browser window doesn't get opened automatically,
|
|
||||||
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
|
||||||
and supports HMR and all the other goodies you'll need.
|
|
||||||
|
|
||||||
|
### 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`.
|
||||||
|
|
||||||
|
### building without `webUI` bundled (server only)
|
||||||
|
Delete `server/src/main/resources/WebUI.zip` if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`.
|
||||||
|
|
||||||
|
### building the Windows package
|
||||||
|
First Build the jar, then cd into the `scripts` directory and run `./windows-bundler.sh win32` or `./windows-bundler.sh win64` depending on the target architecture, the resulting built zip package file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx-winXX.zip`.
|
||||||
|
|
||||||
|
## Running in development mode
|
||||||
|
run `./gradlew :server:run --stacktrace` to run the server
|
||||||
|
|||||||
@@ -3,30 +3,43 @@
|
|||||||
|-------|----------|---------|---------|
|
|-------|----------|---------|---------|
|
||||||
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [](https://discord.gg/DDZdqZWaHA) |
|
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [](https://discord.gg/DDZdqZWaHA) |
|
||||||
|
|
||||||
# Tachidesk
|
# 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 [Tachodesk-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 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 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/).
|
||||||
|
|
||||||
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
|
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
|
||||||
|
|
||||||
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
|
Tachidesk-Server is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
|
||||||
|
|
||||||
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 needs serious front-end dev help for it's reader and other parts, if you like the app and want to see it become better please don't hesitate to contribute some code!**
|
|
||||||
|
|
||||||
## 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:
|
||||||
|
|
||||||
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
|
- From Tachiyomi
|
||||||
- A library to save your mangas and categories to put them into.
|
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources
|
||||||
- Searching and browsing installed sources.
|
- A library to save your mangas and categories to put them into
|
||||||
- A decent chapter reader.
|
- 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 Legacy Backups
|
- Backup and restore support powered by Tachiyomi Legacy Backups
|
||||||
|
- From Aniyomi
|
||||||
|
- Installing and executing Aniyomi's Extensions
|
||||||
|
- Searching and browsing installed sources.
|
||||||
|
- Viewing an anime and it's episodes
|
||||||
|
|
||||||
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting) if it happens.
|
**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
|
||||||
### All Operating Systems
|
### All Operating Systems
|
||||||
|
|||||||
+5
-4
@@ -1,8 +1,8 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.4.32"
|
kotlin("jvm") version kotlinVersion
|
||||||
kotlin("plugin.serialization") version "1.4.32" apply false
|
kotlin("plugin.serialization") version kotlinVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
@@ -46,12 +46,12 @@ configure(projects) {
|
|||||||
testImplementation(kotlin("test-junit5"))
|
testImplementation(kotlin("test-junit5"))
|
||||||
|
|
||||||
// coroutines
|
// coroutines
|
||||||
val coroutinesVersion = "1.4.3"
|
val coroutinesVersion = "1.5.0"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
||||||
|
|
||||||
val kotlinSerializationVersion = "1.1.0"
|
val kotlinSerializationVersion = "1.2.1"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||||
|
|
||||||
@@ -80,6 +80,7 @@ configure(projects) {
|
|||||||
implementation("net.harawata:appdirs:1.2.1")
|
implementation("net.harawata:appdirs:1.2.1")
|
||||||
|
|
||||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||||
|
// note: watch https://github.com/ThexXTURBOXx/dex2jar for future developments
|
||||||
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
||||||
|
|
||||||
// APK parser
|
// APK parser
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("net.lingala.zip4j:zip4j:2.9.0")
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import java.io.BufferedReader
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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/. */
|
||||||
|
|
||||||
|
const val kotlinVersion = "1.5.21"
|
||||||
|
|
||||||
|
const val MainClass = "suwayomi.tachidesk.MainKt"
|
||||||
|
|
||||||
|
// should be bumped with each stable release
|
||||||
|
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.7"
|
||||||
|
|
||||||
|
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r36"
|
||||||
|
|
||||||
|
// counts commits on the the master branch
|
||||||
|
val tachideskRevision = runCatching {
|
||||||
|
System.getenv("ProductRevision") ?: Runtime
|
||||||
|
.getRuntime()
|
||||||
|
.exec("git rev-list HEAD --count")
|
||||||
|
.let { process ->
|
||||||
|
process.waitFor()
|
||||||
|
val output = process.inputStream.use {
|
||||||
|
it.bufferedReader().use(BufferedReader::readText)
|
||||||
|
}
|
||||||
|
process.destroy()
|
||||||
|
"r" + output.trim()
|
||||||
|
}
|
||||||
|
}.getOrDefault("r0")
|
||||||
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
|
:: cleaner output
|
||||||
|
@echo off
|
||||||
|
|
||||||
jre\bin\java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
|
jre\bin\java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||||
|
|
||||||
|
:: Prevent cmd from closing when Tachidesk crashes
|
||||||
|
pause
|
||||||
@@ -1 +1 @@
|
|||||||
jre\bin\javaw "-Dsuwayomi.tachidesk.config.server.webInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
|
jre\bin\javaw "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
|
||||||
+51
-47
@@ -1,15 +1,16 @@
|
|||||||
|
|
||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
||||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
application
|
application
|
||||||
|
kotlin("plugin.serialization")
|
||||||
id("com.github.johnrengelman.shadow") version "7.0.0"
|
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||||
id("org.jmailen.kotlinter") version "3.4.3"
|
id("org.jmailen.kotlinter") version "3.4.3"
|
||||||
id("de.fuerstenau.buildconfig") version "1.1.8"
|
id("com.github.gmazzo.buildconfig") version "3.0.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -53,7 +54,6 @@ dependencies {
|
|||||||
implementation("com.dorkbox:Utilities:1.9")
|
implementation("com.dorkbox:Utilities:1.9")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.9.1")
|
implementation("com.squareup.okhttp3:okhttp:4.9.1")
|
||||||
@@ -81,14 +81,13 @@ dependencies {
|
|||||||
// implementation(fileTree("lib/"))
|
// implementation(fileTree("lib/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val MainClass = "suwayomi.tachidesk.MainKt"
|
|
||||||
application {
|
application {
|
||||||
mainClass.set(MainClass)
|
mainClass.set(MainClass)
|
||||||
|
|
||||||
// for testing electron
|
// uncomment for testing electron
|
||||||
// applicationDefaultJvmArgs = listOf(
|
// applicationDefaultJvmArgs = listOf(
|
||||||
// "-Dsuwayomi.tachidesk.webInterface=electron",
|
// "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
|
||||||
// "-Dsuwayomi.tachidesk.electronPath=/usr/bin/electron"
|
// "-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron"
|
||||||
// )
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,56 +99,40 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// should be bumped with each stable release
|
|
||||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.5"
|
|
||||||
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r23"
|
|
||||||
|
|
||||||
// counts commit count on master
|
|
||||||
val tachideskRevision = runCatching {
|
|
||||||
System.getenv("ProductRevision") ?: Runtime
|
|
||||||
.getRuntime()
|
|
||||||
.exec("git rev-list HEAD --count")
|
|
||||||
.let { process ->
|
|
||||||
process.waitFor()
|
|
||||||
val output = process.inputStream.use {
|
|
||||||
it.bufferedReader().use(BufferedReader::readText)
|
|
||||||
}
|
|
||||||
process.destroy()
|
|
||||||
"r" + output.trim()
|
|
||||||
}
|
|
||||||
}.getOrDefault("r0")
|
|
||||||
|
|
||||||
buildConfig {
|
buildConfig {
|
||||||
clsName = "BuildConfig"
|
className("BuildConfig")
|
||||||
packageName = "suwayomi.tachidesk.server"
|
packageName("suwayomi.tachidesk.server")
|
||||||
|
|
||||||
|
useKotlinOutput()
|
||||||
|
|
||||||
buildConfigField("String", "NAME", rootProject.name)
|
fun quoteWrap(obj: Any): String = """"$obj""""
|
||||||
buildConfigField("String", "VERSION", tachideskVersion)
|
|
||||||
buildConfigField("String", "REVISION", tachideskRevision)
|
buildConfigField("String", "NAME", quoteWrap(rootProject.name))
|
||||||
buildConfigField("String", "BUILD_TYPE", if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview")
|
buildConfigField("String", "VERSION", quoteWrap(tachideskVersion))
|
||||||
|
buildConfigField("String", "REVISION", quoteWrap(tachideskRevision))
|
||||||
|
buildConfigField("String", "BUILD_TYPE", quoteWrap(if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview"))
|
||||||
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
|
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
|
||||||
|
|
||||||
|
|
||||||
buildConfigField("String", "WEBUI_REPO", "https://github.com/Suwayomi/Tachidesk-WebUI-preview")
|
buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview"))
|
||||||
buildConfigField("String", "WEBUI_TAG", webUIRevisionTag)
|
buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag))
|
||||||
|
|
||||||
|
|
||||||
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk")
|
buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk"))
|
||||||
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA")
|
buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
shadowJar {
|
shadowJar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes(
|
attributes(
|
||||||
mapOf(
|
mapOf(
|
||||||
"Main-Class" to MainClass,
|
"Main-Class" to MainClass,
|
||||||
"Implementation-Title" to rootProject.name,
|
"Implementation-Title" to rootProject.name,
|
||||||
"Implementation-Vendor" to "The Suwayomi Project",
|
"Implementation-Vendor" to "The Suwayomi Project",
|
||||||
"Specification-Version" to tachideskVersion,
|
"Specification-Version" to tachideskVersion,
|
||||||
"Implementation-Version" to tachideskRevision
|
"Implementation-Version" to tachideskRevision
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
archiveBaseName.set(rootProject.name)
|
archiveBaseName.set(rootProject.name)
|
||||||
@@ -159,9 +142,10 @@ tasks {
|
|||||||
withType<KotlinCompile> {
|
withType<KotlinCompile> {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
freeCompilerArgs = listOf(
|
freeCompilerArgs = listOf(
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
"-Xopt-in=kotlin.RequiresOptIn",
|
||||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
|
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +159,7 @@ tasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
named("run") {
|
named("run") {
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
dependsOn("formatKotlin", "lintKotlin", "downloadWebUI")
|
||||||
}
|
}
|
||||||
|
|
||||||
named<Copy>("processResources") {
|
named<Copy>("processResources") {
|
||||||
@@ -186,6 +170,26 @@ tasks {
|
|||||||
register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") {
|
register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") {
|
||||||
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
|
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
|
||||||
dest("src/main/resources/WebUI.zip")
|
dest("src/main/resources/WebUI.zip")
|
||||||
|
|
||||||
|
|
||||||
|
fun shouldOverwrite(): Boolean {
|
||||||
|
val zipPath = project.projectDir.absolutePath + "/src/main/resources/WebUI.zip"
|
||||||
|
val zipFile = net.lingala.zip4j.ZipFile(zipPath)
|
||||||
|
|
||||||
|
var shouldOverwrite = true
|
||||||
|
if (zipFile.isValidZipFile) {
|
||||||
|
val zipRevision = zipFile.getInputStream(zipFile.getFileHeader("revision")).bufferedReader().use {
|
||||||
|
it.readText().trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zipRevision == webUIRevisionTag)
|
||||||
|
shouldOverwrite = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldOverwrite
|
||||||
|
}
|
||||||
|
|
||||||
|
overwrite(shouldOverwrite())
|
||||||
}
|
}
|
||||||
|
|
||||||
withType<LintTask> {
|
withType<LintTask> {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||||||
* Note the generated id sets the sign bit to 0.
|
* Note the generated id sets the sign bit to 0.
|
||||||
*/
|
*/
|
||||||
override val id by lazy {
|
override val id by lazy {
|
||||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
val key = "${name.lowercase()}/$lang/$versionId"
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Visible name of the source.
|
* Visible name of the source.
|
||||||
*/
|
*/
|
||||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
override fun toString() = "$name (${lang.uppercase()})"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of anime. Normally it's not needed to
|
* Returns an observable containing a page with a list of anime. Normally it's not needed to
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import okhttp3.OkHttpClient
|
|||||||
// import uy.kohesive.injekt.injectLazy
|
// import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
class NetworkHelper(context: Context) {
|
class NetworkHelper(context: Context) {
|
||||||
|
|
||||||
// private val preferences: PreferencesHelper by injectLazy()
|
// private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
|||||||
// return progressClient.newCall(request)
|
// return progressClient.newCall(request)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
val progressClient = newBuilder()
|
val progressClient = newBuilder()
|
||||||
// .cache(null)
|
// .cache(null)
|
||||||
|
|||||||
@@ -61,3 +61,30 @@ interface SManga : Serializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fun SManga.toMangaInfo(): MangaInfo {
|
||||||
|
// return MangaInfo(
|
||||||
|
// key = this.url,
|
||||||
|
// title = this.title,
|
||||||
|
// artist = this.artist ?: "",
|
||||||
|
// author = this.author ?: "",
|
||||||
|
// description = this.description ?: "",
|
||||||
|
// genres = this.genre?.split(", ") ?: emptyList(),
|
||||||
|
// status = this.status,
|
||||||
|
// cover = this.thumbnail_url ?: ""
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fun MangaInfo.toSManga(): SManga {
|
||||||
|
// val mangaInfo = this
|
||||||
|
// return SManga.create().apply {
|
||||||
|
// url = mangaInfo.key
|
||||||
|
// title = mangaInfo.title
|
||||||
|
// artist = mangaInfo.artist
|
||||||
|
// author = mangaInfo.author
|
||||||
|
// description = mangaInfo.description
|
||||||
|
// genre = mangaInfo.genres.joinToString(", ")
|
||||||
|
// status = mangaInfo.status
|
||||||
|
// thumbnail_url = mangaInfo.cover
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* Note the generated id sets the sign bit to 0.
|
* Note the generated id sets the sign bit to 0.
|
||||||
*/
|
*/
|
||||||
override val id by lazy {
|
override val id by lazy {
|
||||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
val key = "${name.lowercase()}/$lang/$versionId"
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Visible name of the source.
|
* Visible name of the source.
|
||||||
*/
|
*/
|
||||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
override fun toString() = "$name (${lang.uppercase()})"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import suwayomi.tachidesk.anime.impl.AnimeList.getAnimeList
|
|||||||
import suwayomi.tachidesk.anime.impl.Episode.getEpisode
|
import suwayomi.tachidesk.anime.impl.Episode.getEpisode
|
||||||
import suwayomi.tachidesk.anime.impl.Episode.getEpisodeList
|
import suwayomi.tachidesk.anime.impl.Episode.getEpisodeList
|
||||||
import suwayomi.tachidesk.anime.impl.Episode.modifyEpisode
|
import suwayomi.tachidesk.anime.impl.Episode.modifyEpisode
|
||||||
|
import suwayomi.tachidesk.anime.impl.Search.sourceSearch
|
||||||
import suwayomi.tachidesk.anime.impl.Source.getAnimeSource
|
import suwayomi.tachidesk.anime.impl.Source.getAnimeSource
|
||||||
import suwayomi.tachidesk.anime.impl.Source.getSourceList
|
import suwayomi.tachidesk.anime.impl.Source.getSourceList
|
||||||
import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIcon
|
import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIcon
|
||||||
@@ -219,13 +220,13 @@ object AnimeAPI {
|
|||||||
// ctx.json(sourceGlobalSearch(searchTerm))
|
// ctx.json(sourceGlobalSearch(searchTerm))
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// // single source search
|
// single source search
|
||||||
// app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
app.get("/api/v1/anime/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
||||||
// val sourceId = ctx.pathParam("sourceId").toLong()
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
// val searchTerm = ctx.pathParam("searchTerm")
|
val searchTerm = ctx.pathParam("searchTerm")
|
||||||
// val pageNum = ctx.pathParam("pageNum").toInt()
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
// ctx.json(JavalinSetup.future { sourceSearch(sourceId, searchTerm, pageNum) })
|
ctx.json(future { sourceSearch(sourceId, searchTerm, pageNum) })
|
||||||
// }
|
}
|
||||||
//
|
//
|
||||||
// // source filter list
|
// // source filter list
|
||||||
// app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
// app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package suwayomi.tachidesk.anime.impl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 suwayomi.tachidesk.anime.impl.AnimeList.processEntries
|
||||||
|
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||||
|
import suwayomi.tachidesk.anime.model.dataclass.PagedAnimeListDataClass
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||||
|
|
||||||
|
object Search {
|
||||||
|
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedAnimeListDataClass {
|
||||||
|
val source = getAnimeHttpSource(sourceId)
|
||||||
|
val searchManga = source.fetchSearchAnime(pageNum, searchTerm, source.getFilterList()).awaitSingle()
|
||||||
|
return searchManga.processEntries(sourceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,7 +138,7 @@ object Extension {
|
|||||||
else -> "all"
|
else -> "all"
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
|
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Aniyomi: ")
|
||||||
|
|
||||||
// update extension info
|
// update extension info
|
||||||
transaction {
|
transaction {
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ object ExtensionGithubApi {
|
|||||||
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
|
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
|
||||||
}
|
}
|
||||||
.map { element ->
|
.map { element ->
|
||||||
val name = element["name"].string.substringAfter("Tachiyomi: ")
|
val name = element["name"].string.substringAfter("Aniyomi: ")
|
||||||
val pkgName = element["pkg"].string
|
val pkgName = element["pkg"].string
|
||||||
val apkName = element["apk"].string
|
val apkName = element["apk"].string
|
||||||
val versionName = element["version"].string
|
val versionName = element["version"].string
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package suwayomi.tachidesk.manga
|
|||||||
* 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 io.javalin.Javalin
|
|
||||||
import io.javalin.apibuilder.ApiBuilder.delete
|
import io.javalin.apibuilder.ApiBuilder.delete
|
||||||
import io.javalin.apibuilder.ApiBuilder.get
|
import io.javalin.apibuilder.ApiBuilder.get
|
||||||
import io.javalin.apibuilder.ApiBuilder.patch
|
import io.javalin.apibuilder.ApiBuilder.patch
|
||||||
@@ -15,14 +14,14 @@ import io.javalin.apibuilder.ApiBuilder.path
|
|||||||
import io.javalin.apibuilder.ApiBuilder.post
|
import io.javalin.apibuilder.ApiBuilder.post
|
||||||
import io.javalin.apibuilder.ApiBuilder.ws
|
import io.javalin.apibuilder.ApiBuilder.ws
|
||||||
import suwayomi.tachidesk.manga.controller.BackupController
|
import suwayomi.tachidesk.manga.controller.BackupController
|
||||||
|
import suwayomi.tachidesk.manga.controller.CategoryController
|
||||||
import suwayomi.tachidesk.manga.controller.DownloadController
|
import suwayomi.tachidesk.manga.controller.DownloadController
|
||||||
import suwayomi.tachidesk.manga.controller.ExtensionController
|
import suwayomi.tachidesk.manga.controller.ExtensionController
|
||||||
import suwayomi.tachidesk.manga.controller.LibraryController
|
|
||||||
import suwayomi.tachidesk.manga.controller.MangaController
|
import suwayomi.tachidesk.manga.controller.MangaController
|
||||||
import suwayomi.tachidesk.manga.controller.SourceController
|
import suwayomi.tachidesk.manga.controller.SourceController
|
||||||
|
|
||||||
object MangaAPI {
|
object MangaAPI {
|
||||||
fun defineEndpoints(app: Javalin) {
|
fun defineEndpoints() {
|
||||||
path("extension") {
|
path("extension") {
|
||||||
get("list", ExtensionController::list)
|
get("list", ExtensionController::list)
|
||||||
|
|
||||||
@@ -71,27 +70,29 @@ object MangaAPI {
|
|||||||
get(":mangaId/chapter/:chapterIndex/page/:index", MangaController::pageRetrieve)
|
get(":mangaId/chapter/:chapterIndex/page/:index", MangaController::pageRetrieve)
|
||||||
}
|
}
|
||||||
|
|
||||||
path("") {
|
path("category") {
|
||||||
get("library", LibraryController::list)
|
get("", CategoryController::categoryList)
|
||||||
|
post("", CategoryController::categoryCreate)
|
||||||
|
|
||||||
path("category") {
|
get(":categoryId", CategoryController::categoryMangas)
|
||||||
get("", LibraryController::categoryList)
|
patch(":categoryId", CategoryController::categoryModify)
|
||||||
post("", LibraryController::categoryCreate)
|
delete(":categoryId", CategoryController::categoryDelete)
|
||||||
|
|
||||||
get(":categoryId", LibraryController::categoryMangas)
|
patch(
|
||||||
patch(":categoryId", LibraryController::categoryModify)
|
":categoryId/reorder",
|
||||||
delete(":categoryId", LibraryController::categoryDelete)
|
CategoryController::categoryReorder
|
||||||
|
) // TODO: the underlying code doesn't need `:categoryId`, remove it
|
||||||
patch(":categoryId/reorder", LibraryController::categoryReorder)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
path("backup") {
|
path("backup") {
|
||||||
post("legacy/import", BackupController::legacyImport)
|
post("import", BackupController::protobufImport)
|
||||||
post("legacy/import/file", BackupController::legacyImportFile)
|
post("import/file", BackupController::protobufImportFile)
|
||||||
|
|
||||||
get("legacy/export", BackupController::legacyExport)
|
post("validate", BackupController::protobufValidate)
|
||||||
get("legacy/export/file", BackupController::legacyExportFile)
|
post("validate/file", BackupController::protobufValidateFile)
|
||||||
|
|
||||||
|
get("export", BackupController::protobufExport)
|
||||||
|
get("export/file", BackupController::protobufExportFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
path("downloads") {
|
path("downloads") {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package suwayomi.tachidesk.manga.controller
|
|||||||
|
|
||||||
import io.javalin.http.Context
|
import io.javalin.http.Context
|
||||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupExport
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupImport
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
|
||||||
import suwayomi.tachidesk.server.JavalinSetup
|
import suwayomi.tachidesk.server.JavalinSetup
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -16,30 +17,31 @@ import java.util.Date
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
object BackupController {
|
object BackupController {
|
||||||
/** expects a Tachiyomi legacy backup json in the body */
|
|
||||||
fun legacyImport(ctx: Context) {
|
/** expects a Tachiyomi protobuf backup in the body */
|
||||||
ctx.result(
|
fun protobufImport(ctx: Context) {
|
||||||
|
ctx.json(
|
||||||
JavalinSetup.future {
|
JavalinSetup.future {
|
||||||
LegacyBackupImport.restoreLegacyBackup(ctx.bodyAsInputStream())
|
ProtoBackupImport.performRestore(ctx.bodyAsInputStream())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json" */
|
/** expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */
|
||||||
fun legacyImportFile(ctx: Context) {
|
fun protobufImportFile(ctx: Context) {
|
||||||
ctx.result(
|
ctx.json(
|
||||||
JavalinSetup.future {
|
JavalinSetup.future {
|
||||||
LegacyBackupImport.restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
|
ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** returns a Tachiyomi legacy backup json created from the current database as a json body */
|
/** returns a Tachiyomi protobuf backup created from the current database as a body */
|
||||||
fun legacyExport(ctx: Context) {
|
fun protobufExport(ctx: Context) {
|
||||||
ctx.contentType("application/json")
|
ctx.contentType("application/octet-stream")
|
||||||
ctx.result(
|
ctx.result(
|
||||||
JavalinSetup.future {
|
JavalinSetup.future {
|
||||||
LegacyBackupExport.createLegacyBackup(
|
ProtoBackupExport.createBackup(
|
||||||
BackupFlags(
|
BackupFlags(
|
||||||
includeManga = true,
|
includeManga = true,
|
||||||
includeCategories = true,
|
includeCategories = true,
|
||||||
@@ -52,16 +54,15 @@ object BackupController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** returns a Tachiyomi legacy backup json created from the current database as a file */
|
/** returns a Tachiyomi protobuf backup created from the current database as a file */
|
||||||
fun legacyExportFile(ctx: Context) {
|
fun protobufExportFile(ctx: Context) {
|
||||||
ctx.contentType("application/json")
|
ctx.contentType("application/octet-stream")
|
||||||
val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm")
|
val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date())
|
||||||
val currentDate = sdf.format(Date())
|
|
||||||
|
|
||||||
ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
|
ctx.header("Content-Disposition", """attachment; filename="tachidesk_$currentDate.proto.gz"""")
|
||||||
ctx.result(
|
ctx.result(
|
||||||
JavalinSetup.future {
|
JavalinSetup.future {
|
||||||
LegacyBackupExport.createLegacyBackup(
|
ProtoBackupExport.createBackup(
|
||||||
BackupFlags(
|
BackupFlags(
|
||||||
includeManga = true,
|
includeManga = true,
|
||||||
includeCategories = true,
|
includeCategories = true,
|
||||||
@@ -73,4 +74,22 @@ object BackupController {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body */
|
||||||
|
fun protobufValidate(ctx: Context) {
|
||||||
|
ctx.json(
|
||||||
|
JavalinSetup.future {
|
||||||
|
ProtoBackupValidator.validate(ctx.bodyAsInputStream())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */
|
||||||
|
fun protobufValidateFile(ctx: Context) {
|
||||||
|
ctx.json(
|
||||||
|
JavalinSetup.future {
|
||||||
|
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-9
@@ -10,14 +10,8 @@ package suwayomi.tachidesk.manga.controller
|
|||||||
import io.javalin.http.Context
|
import io.javalin.http.Context
|
||||||
import suwayomi.tachidesk.manga.impl.Category
|
import suwayomi.tachidesk.manga.impl.Category
|
||||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||||
import suwayomi.tachidesk.manga.impl.Library
|
|
||||||
|
|
||||||
object LibraryController {
|
|
||||||
/** lists mangas that have no category assigned */
|
|
||||||
fun list(ctx: Context) {
|
|
||||||
ctx.json(Library.getLibraryMangas())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
object CategoryController {
|
||||||
/** category list */
|
/** category list */
|
||||||
fun categoryList(ctx: Context) {
|
fun categoryList(ctx: Context) {
|
||||||
ctx.json(Category.getCategoryList())
|
ctx.json(Category.getCategoryList())
|
||||||
@@ -54,10 +48,9 @@ object LibraryController {
|
|||||||
|
|
||||||
/** category re-ordering */
|
/** category re-ordering */
|
||||||
fun categoryReorder(ctx: Context) {
|
fun categoryReorder(ctx: Context) {
|
||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
|
||||||
val from = ctx.formParam("from")!!.toInt()
|
val from = ctx.formParam("from")!!.toInt()
|
||||||
val to = ctx.formParam("to")!!.toInt()
|
val to = ctx.formParam("to")!!.toInt()
|
||||||
Category.reorderCategory(categoryId, from, to)
|
Category.reorderCategory(from, to)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ object MangaController {
|
|||||||
fun chapterList(ctx: Context) {
|
fun chapterList(ctx: Context) {
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean()
|
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
|
||||||
|
|
||||||
ctx.json(future { Chapter.getChapterList(mangaId, onlineFetch) })
|
ctx.json(future { Chapter.getChapterList(mangaId, onlineFetch) })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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 org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
|
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
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
@@ -15,9 +16,11 @@ import org.jetbrains.exposed.sql.selectAll
|
|||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
import suwayomi.tachidesk.manga.impl.CategoryManga.removeMangaFromCategory
|
import suwayomi.tachidesk.manga.impl.CategoryManga.removeMangaFromCategory
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||||
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.toDataClass
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
|
|
||||||
object Category {
|
object Category {
|
||||||
@@ -25,6 +28,8 @@ object Category {
|
|||||||
* The new category will be placed at the end of the list
|
* The new category will be placed at the end of the list
|
||||||
*/
|
*/
|
||||||
fun createCategory(name: String) {
|
fun createCategory(name: String) {
|
||||||
|
if (name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)) return
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
val count = CategoryTable.selectAll().count()
|
val count = CategoryTable.selectAll().count()
|
||||||
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
||||||
@@ -38,7 +43,7 @@ object Category {
|
|||||||
fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?) {
|
fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?) {
|
||||||
transaction {
|
transaction {
|
||||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||||
if (name != null) it[CategoryTable.name] = name
|
if (name != null && !name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)) it[CategoryTable.name] = name
|
||||||
if (isDefault != null) it[CategoryTable.isDefault] = isDefault
|
if (isDefault != null) it[CategoryTable.isDefault] = isDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,7 +52,7 @@ object Category {
|
|||||||
/**
|
/**
|
||||||
* Move the category from position `from` to `to`
|
* Move the category from position `from` to `to`
|
||||||
*/
|
*/
|
||||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
fun reorderCategory(from: Int, to: Int) {
|
||||||
transaction {
|
transaction {
|
||||||
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||||
categories.add(to - 1, categories.removeAt(from - 1))
|
categories.add(to - 1, categories.removeAt(from - 1))
|
||||||
@@ -68,11 +73,22 @@ object Category {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val DEFAULT_CATEGORY_ID = 0
|
||||||
|
const val DEFAULT_CATEGORY_NAME = "Default"
|
||||||
|
private fun addDefaultIfNecessary(categories: List<CategoryDataClass>): List<CategoryDataClass> =
|
||||||
|
if (MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.isNotEmpty()) {
|
||||||
|
listOf(CategoryDataClass(DEFAULT_CATEGORY_ID, 0, DEFAULT_CATEGORY_NAME, true)) + categories
|
||||||
|
} else {
|
||||||
|
categories
|
||||||
|
}
|
||||||
|
|
||||||
fun getCategoryList(): List<CategoryDataClass> {
|
fun getCategoryList(): List<CategoryDataClass> {
|
||||||
return transaction {
|
return transaction {
|
||||||
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||||
CategoryTable.toDataClass(it)
|
CategoryTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addDefaultIfNecessary(categories)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import org.jetbrains.exposed.sql.insert
|
|||||||
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 org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.manga.impl.Category.DEFAULT_CATEGORY_ID
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.lang.isEmpty
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
|
||||||
@@ -23,8 +25,10 @@ import suwayomi.tachidesk.manga.model.table.toDataClass
|
|||||||
|
|
||||||
object CategoryManga {
|
object CategoryManga {
|
||||||
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||||
|
fun notAlreadyInCategory() = CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.isEmpty()
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
|
if (notAlreadyInCategory()) {
|
||||||
CategoryMangaTable.insert {
|
CategoryMangaTable.insert {
|
||||||
it[CategoryMangaTable.category] = categoryId
|
it[CategoryMangaTable.category] = categoryId
|
||||||
it[CategoryMangaTable.manga] = mangaId
|
it[CategoryMangaTable.manga] = mangaId
|
||||||
@@ -52,6 +56,13 @@ object CategoryManga {
|
|||||||
* list of mangas that belong to a category
|
* list of mangas that belong to a category
|
||||||
*/
|
*/
|
||||||
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
||||||
|
if (categoryId == DEFAULT_CATEGORY_ID)
|
||||||
|
return transaction {
|
||||||
|
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
||||||
|
MangaTable.toDataClass(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return transaction {
|
return transaction {
|
||||||
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
||||||
MangaTable.toDataClass(it)
|
MangaTable.toDataClass(it)
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ import java.time.Instant
|
|||||||
|
|
||||||
object Chapter {
|
object Chapter {
|
||||||
/** get chapter list when showing a manga */
|
/** get chapter list when showing a manga */
|
||||||
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean?): List<ChapterDataClass> {
|
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean = false): List<ChapterDataClass> {
|
||||||
return if (onlineFetch == true) {
|
return if (onlineFetch) {
|
||||||
getSourceChapters(mangaId)
|
getSourceChapters(mangaId)
|
||||||
} else {
|
} else {
|
||||||
transaction {
|
transaction {
|
||||||
@@ -40,21 +40,18 @@ object Chapter {
|
|||||||
ChapterTable.toDataClass(it)
|
ChapterTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
}.ifEmpty {
|
}.ifEmpty {
|
||||||
// If it was explicitly set to offline dont grab chapters
|
getSourceChapters(mangaId)
|
||||||
if (onlineFetch == null) {
|
|
||||||
getSourceChapters(mangaId)
|
|
||||||
} else emptyList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
|
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
|
||||||
val mangaDetails = getManga(mangaId)
|
val manga = getManga(mangaId)
|
||||||
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
val source = getHttpSource(manga.sourceId.toLong())
|
||||||
val chapterList = source.fetchChapterList(
|
val chapterList = source.fetchChapterList(
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
title = mangaDetails.title
|
title = manga.title
|
||||||
url = mangaDetails.url
|
url = manga.url
|
||||||
}
|
}
|
||||||
).awaitSingle()
|
).awaitSingle()
|
||||||
|
|
||||||
@@ -72,7 +69,7 @@ object Chapter {
|
|||||||
it[scanlator] = fetchedChapter.scanlator
|
it[scanlator] = fetchedChapter.scanlator
|
||||||
|
|
||||||
it[chapterIndex] = index + 1
|
it[chapterIndex] = index + 1
|
||||||
it[manga] = mangaId
|
it[ChapterTable.manga] = mangaId
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
||||||
@@ -82,7 +79,7 @@ object Chapter {
|
|||||||
it[scanlator] = fetchedChapter.scanlator
|
it[scanlator] = fetchedChapter.scanlator
|
||||||
|
|
||||||
it[chapterIndex] = index + 1
|
it[chapterIndex] = index + 1
|
||||||
it[manga] = mangaId
|
it[ChapterTable.manga] = mangaId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +160,10 @@ object Chapter {
|
|||||||
// update page list for this chapter
|
// update page list for this chapter
|
||||||
transaction {
|
transaction {
|
||||||
pageList.forEach { page ->
|
pageList.forEach { page ->
|
||||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
val pageEntry = transaction {
|
||||||
|
PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
if (pageEntry == null) {
|
if (pageEntry == null) {
|
||||||
PageTable.insert {
|
PageTable.insert {
|
||||||
it[index] = page.index
|
it[index] = page.index
|
||||||
@@ -210,7 +210,14 @@ object Chapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
|
fun modifyChapter(
|
||||||
|
mangaId: Int,
|
||||||
|
chapterIndex: Int,
|
||||||
|
isRead: Boolean?,
|
||||||
|
isBookmarked: Boolean?,
|
||||||
|
markPrevRead: Boolean?,
|
||||||
|
lastPageRead: Int?
|
||||||
|
) {
|
||||||
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.chapterIndex eq chapterIndex) }) { update ->
|
||||||
@@ -244,9 +251,11 @@ 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 chapter = ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }
|
val chapter =
|
||||||
.first()[ChapterTable.id]
|
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }
|
||||||
val meta = transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapter) and (ChapterMetaTable.key eq key) } }.firstOrNull()
|
.first()[ChapterTable.id]
|
||||||
|
val meta =
|
||||||
|
transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapter) and (ChapterMetaTable.key eq key) } }.firstOrNull()
|
||||||
if (meta == null) {
|
if (meta == null) {
|
||||||
ChapterMetaTable.insert {
|
ChapterMetaTable.insert {
|
||||||
it[ChapterMetaTable.key] = key
|
it[ChapterMetaTable.key] = key
|
||||||
|
|||||||
@@ -7,23 +7,17 @@ package suwayomi.tachidesk.manga.impl
|
|||||||
* 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 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
|
||||||
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 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.model.dataclass.MangaDataClass
|
|
||||||
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 suwayomi.tachidesk.manga.model.table.toDataClass
|
|
||||||
|
|
||||||
object Library {
|
object Library {
|
||||||
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
|
|
||||||
// ..implement that shit at some time...
|
|
||||||
// ..also Consider to rename it to `isDefault`
|
|
||||||
suspend fun addMangaToLibrary(mangaId: Int) {
|
suspend fun addMangaToLibrary(mangaId: Int) {
|
||||||
val manga = getManga(mangaId)
|
val manga = getManga(mangaId)
|
||||||
if (!manga.inLibrary) {
|
if (!manga.inLibrary) {
|
||||||
@@ -57,12 +51,4 @@ object Library {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLibraryMangas(): List<MangaDataClass> {
|
|
||||||
return transaction {
|
|
||||||
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
|
||||||
MangaTable.toDataClass(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,22 +13,25 @@ import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
|||||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||||
|
|
||||||
object Search {
|
object Search {
|
||||||
// TODO
|
|
||||||
fun sourceFilters(sourceId: Long) {
|
|
||||||
val source = getHttpSource(sourceId)
|
|
||||||
// source.getFilterList().toItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = getHttpSource(sourceId)
|
||||||
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).awaitSingle()
|
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).awaitSingle()
|
||||||
return searchManga.processEntries(sourceId)
|
return searchManga.processEntries(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
@Suppress("UNUSED_PARAMETER", "UNUSED_VARIABLE")
|
||||||
|
fun sourceFilters(sourceId: Long) {
|
||||||
|
val source = getHttpSource(sourceId)
|
||||||
|
// source.getFilterList().toItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun sourceGlobalSearch(searchTerm: String) {
|
fun sourceGlobalSearch(searchTerm: String) {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
data class FilterWrapper(
|
data class FilterWrapper(
|
||||||
val type: String,
|
val type: String,
|
||||||
val filter: Any
|
val filter: Any
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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/. */
|
||||||
|
|
||||||
|
abstract class AbstractBackupValidator {
|
||||||
|
data class ValidationResult(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
|
}
|
||||||
-45
@@ -1,45 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 com.github.salomonbrys.kotson.registerTypeAdapter
|
|
||||||
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.models.DHistory
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.serializer.CategoryTypeAdapter
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.serializer.ChapterTypeAdapter
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.serializer.HistoryTypeAdapter
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.serializer.MangaTypeAdapter
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.serializer.TrackTypeAdapter
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
open class LegacyBackupBase {
|
|
||||||
protected val parser: Gson = when (version) {
|
|
||||||
2 -> GsonBuilder()
|
|
||||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
|
||||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
|
||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
|
||||||
.create()
|
|
||||||
else -> throw Exception("Unknown backup version")
|
|
||||||
}
|
|
||||||
|
|
||||||
protected var sourceMapping: Map<Long, String> = emptyMap()
|
|
||||||
|
|
||||||
protected val errors = mutableListOf<Pair<Date, String>>()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
internal const val version = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-154
@@ -1,154 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 com.github.salomonbrys.kotson.set
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import suwayomi.tachidesk.manga.impl.Category.getCategoryList
|
|
||||||
import suwayomi.tachidesk.manga.impl.CategoryManga.getMangaCategories
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.models.Backup
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.models.Backup.CURRENT_VERSION
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.Manga
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl
|
|
||||||
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
|
||||||
|
|
||||||
object LegacyBackupExport : LegacyBackupBase() {
|
|
||||||
|
|
||||||
suspend fun createLegacyBackup(flags: BackupFlags): String? {
|
|
||||||
// Create root object
|
|
||||||
val root = JsonObject()
|
|
||||||
|
|
||||||
// Create manga array
|
|
||||||
val mangaEntries = JsonArray()
|
|
||||||
|
|
||||||
// Create category array
|
|
||||||
val categoryEntries = JsonArray()
|
|
||||||
|
|
||||||
// Create extension ID/name mapping
|
|
||||||
val extensionEntries = JsonArray()
|
|
||||||
|
|
||||||
// Add values to root
|
|
||||||
root[Backup.VERSION] = CURRENT_VERSION
|
|
||||||
root[Backup.MANGAS] = mangaEntries
|
|
||||||
root[Backup.CATEGORIES] = categoryEntries
|
|
||||||
root[Backup.EXTENSIONS] = extensionEntries
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
val mangas = MangaTable.select { (MangaTable.inLibrary eq true) }
|
|
||||||
|
|
||||||
val extensions: MutableSet<String> = mutableSetOf()
|
|
||||||
|
|
||||||
// Backup library manga and its dependencies
|
|
||||||
mangas.map {
|
|
||||||
MangaImpl.fromQuery(it)
|
|
||||||
}.forEach { manga ->
|
|
||||||
|
|
||||||
mangaEntries.add(backupMangaObject(manga, flags))
|
|
||||||
|
|
||||||
// Maintain set of extensions/sources used (excludes local source)
|
|
||||||
if (manga.source != LocalSource.ID) {
|
|
||||||
getHttpSource(manga.source).let {
|
|
||||||
extensions.add("${it.id}:${it.name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup categories
|
|
||||||
if (flags.includeCategories) {
|
|
||||||
backupCategories(categoryEntries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup extension ID/name mapping
|
|
||||||
backupExtensionInfo(extensionEntries, extensions)
|
|
||||||
}
|
|
||||||
|
|
||||||
return parser.toJson(root)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupMangaObject(manga: Manga, options: BackupFlags): JsonElement {
|
|
||||||
// Entry for this manga
|
|
||||||
val entry = JsonObject()
|
|
||||||
|
|
||||||
// Backup manga fields
|
|
||||||
entry[Backup.MANGA] = parser.toJsonTree(manga)
|
|
||||||
val mangaId = manga.id!!.toInt()
|
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
|
||||||
if (options.includeChapters) {
|
|
||||||
// Backup all the chapters
|
|
||||||
val chapters = ChapterTable.select { ChapterTable.manga eq mangaId }.map { ChapterImpl.fromQuery(it) }
|
|
||||||
if (chapters.count() > 0) {
|
|
||||||
val chaptersJson = parser.toJsonTree(chapters)
|
|
||||||
if (chaptersJson.asJsonArray.size() > 0) {
|
|
||||||
entry[Backup.CHAPTERS] = chaptersJson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants category information in backup
|
|
||||||
if (options.includeCategories) {
|
|
||||||
// Backup categories for this manga
|
|
||||||
val categoriesForManga = getMangaCategories(mangaId)
|
|
||||||
if (categoriesForManga.isNotEmpty()) {
|
|
||||||
val categoriesNames = categoriesForManga.map { it.name }
|
|
||||||
entry[Backup.CATEGORIES] = parser.toJsonTree(categoriesNames)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants track information in backup
|
|
||||||
if (options.includeTracking) { // TODO
|
|
||||||
// val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
|
||||||
// if (tracks.isNotEmpty()) {
|
|
||||||
// entry[TRACK] = parser.toJsonTree(tracks)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// // Check if user wants history information in backup
|
|
||||||
if (options.includeHistory) { // TODO
|
|
||||||
// val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
|
||||||
// if (historyForManga.isNotEmpty()) {
|
|
||||||
// val historyData = historyForManga.mapNotNull { history ->
|
|
||||||
// val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
|
||||||
// url?.let { DHistory(url, history.last_read) }
|
|
||||||
// }
|
|
||||||
// val historyJson = parser.toJsonTree(historyData)
|
|
||||||
// if (historyJson.asJsonArray.size() > 0) {
|
|
||||||
// entry[HISTORY] = historyJson
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupCategories(root: JsonArray) {
|
|
||||||
val categories = getCategoryList().map {
|
|
||||||
CategoryImpl().apply {
|
|
||||||
name = it.name
|
|
||||||
order = it.order
|
|
||||||
}
|
|
||||||
}
|
|
||||||
categories.forEach { root.add(parser.toJsonTree(it)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
|
||||||
extensions.sorted().forEach {
|
|
||||||
root.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-210
@@ -1,210 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import org.jetbrains.exposed.sql.and
|
|
||||||
import org.jetbrains.exposed.sql.insert
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.sql.update
|
|
||||||
import suwayomi.tachidesk.manga.impl.Category.createCategory
|
|
||||||
import suwayomi.tachidesk.manga.impl.Category.getCategoryList
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupValidator.ValidationResult
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupValidator.validate
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.models.Backup
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.models.DHistory
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.Chapter
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.Manga
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.Track
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
|
|
||||||
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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/. */
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
object LegacyBackupImport : LegacyBackupBase() {
|
|
||||||
suspend fun restoreLegacyBackup(sourceStream: InputStream): ValidationResult {
|
|
||||||
val reader = sourceStream.bufferedReader()
|
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
|
||||||
|
|
||||||
val validationResult = validate(json)
|
|
||||||
|
|
||||||
val mangasJson = json.get(Backup.MANGAS).asJsonArray
|
|
||||||
|
|
||||||
// Restore categories
|
|
||||||
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
|
||||||
|
|
||||||
// Store source mapping for error messages
|
|
||||||
sourceMapping = LegacyBackupValidator.getSourceMapping(json)
|
|
||||||
|
|
||||||
// Restore individual manga
|
|
||||||
mangasJson.forEach {
|
|
||||||
restoreManga(it.asJsonObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info {
|
|
||||||
"""
|
|
||||||
Restore Errors:
|
|
||||||
${ errors.joinToString("\n") { "${it.first} - ${it.second}" } }
|
|
||||||
Restore Summary:
|
|
||||||
- Missing Sources:
|
|
||||||
${validationResult.missingSources.joinToString("\n")}
|
|
||||||
- Missing Trackers:
|
|
||||||
${validationResult.missingTrackers.joinToString("\n")}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
return validationResult
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreCategories(jsonCategories: JsonElement) {
|
|
||||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
|
||||||
val dbCategories = getCategoryList()
|
|
||||||
|
|
||||||
// Iterate over them and create missing categories
|
|
||||||
backupCategories.forEach { category ->
|
|
||||||
if (dbCategories.none { it.name == category.name }) {
|
|
||||||
createCategory(category.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
|
||||||
val manga = parser.fromJson<MangaImpl>(
|
|
||||||
mangaJson.get(
|
|
||||||
Backup.MANGA
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val chapters = parser.fromJson<List<ChapterImpl>>(
|
|
||||||
mangaJson.get(Backup.CHAPTERS)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val categories = parser.fromJson<List<String>>(
|
|
||||||
mangaJson.get(Backup.CATEGORIES)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val history = parser.fromJson<List<DHistory>>(
|
|
||||||
mangaJson.get(Backup.HISTORY)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val tracks = parser.fromJson<List<TrackImpl>>(
|
|
||||||
mangaJson.get(Backup.TRACK)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
|
|
||||||
val source = try {
|
|
||||||
getHttpSource(manga.source)
|
|
||||||
} catch (e: NullPointerException) {
|
|
||||||
null
|
|
||||||
} catch (e: NoSuchElementException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
|
||||||
|
|
||||||
logger.debug("Restoring Manga: ${manga.title} from $sourceName")
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (source != null) {
|
|
||||||
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
|
||||||
} else {
|
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: Source not found: $sourceName (${manga.source})")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param manga manga data from json
|
|
||||||
* @param source source to get manga data from
|
|
||||||
* @param chapters chapters data from json
|
|
||||||
* @param categories categories data from json
|
|
||||||
* @param history history data from json
|
|
||||||
* @param tracks tracking data from json
|
|
||||||
*/
|
|
||||||
private suspend fun restoreMangaData(
|
|
||||||
manga: Manga,
|
|
||||||
source: Source,
|
|
||||||
chapters: List<Chapter>,
|
|
||||||
categories: List<String>,
|
|
||||||
history: List<DHistory>,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
val fetchedManga = fetchManga(source, manga)
|
|
||||||
|
|
||||||
updateChapters(source, fetchedManga, chapters)
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// backupManager.restoreCategoriesForManga(manga, categories)
|
|
||||||
|
|
||||||
// backupManager.restoreHistoryForManga(history)
|
|
||||||
|
|
||||||
// backupManager.restoreTrackForManga(manga, tracks)
|
|
||||||
|
|
||||||
// updateTracking(fetchedManga, tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches manga information
|
|
||||||
*
|
|
||||||
* @param source source of manga
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return Updated manga.
|
|
||||||
*/
|
|
||||||
private suspend fun fetchManga(source: Source, manga: Manga): SManga {
|
|
||||||
// make sure we have the manga record in library
|
|
||||||
transaction {
|
|
||||||
if (MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }.firstOrNull() == null) {
|
|
||||||
MangaTable.insert {
|
|
||||||
it[url] = manga.url
|
|
||||||
it[title] = manga.title
|
|
||||||
|
|
||||||
it[sourceReference] = manga.source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
|
|
||||||
it[MangaTable.inLibrary] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update manga details
|
|
||||||
val fetchedManga = source.fetchMangaDetails(manga).awaitSingle()
|
|
||||||
transaction {
|
|
||||||
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
|
|
||||||
|
|
||||||
it[artist] = fetchedManga.artist
|
|
||||||
it[author] = fetchedManga.author
|
|
||||||
it[description] = fetchedManga.description
|
|
||||||
it[genre] = fetchedManga.genre
|
|
||||||
it[status] = fetchedManga.status
|
|
||||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
|
|
||||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetchedManga
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateChapters(source: Source, fetchedManga: SManga, chapters: List<Chapter>) {
|
|
||||||
// TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-71
@@ -1,71 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 com.google.gson.JsonObject
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.models.Backup
|
|
||||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
|
||||||
|
|
||||||
object LegacyBackupValidator {
|
|
||||||
data class ValidationResult(val missingSources: List<String>, val missingTrackers: List<String>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks for critical backup file data.
|
|
||||||
*
|
|
||||||
* @throws Exception if version or manga cannot be found.
|
|
||||||
* @return List of missing sources or missing trackers.
|
|
||||||
*/
|
|
||||||
fun validate(json: JsonObject): ValidationResult {
|
|
||||||
val version = json.get(Backup.VERSION)
|
|
||||||
val mangasJson = json.get(Backup.MANGAS)
|
|
||||||
if (version == null || mangasJson == null) {
|
|
||||||
throw Exception("File is missing data.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangas = mangasJson.asJsonArray
|
|
||||||
if (mangas.size() == 0) {
|
|
||||||
throw Exception("Backup does not contain any manga.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val sources = getSourceMapping(json)
|
|
||||||
val missingSources = transaction {
|
|
||||||
sources
|
|
||||||
.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
|
|
||||||
.map { "${it.value} (${it.key})" }
|
|
||||||
.sorted()
|
|
||||||
}
|
|
||||||
|
|
||||||
val trackers = mangas
|
|
||||||
.filter { it.asJsonObject.has("track") }
|
|
||||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
|
||||||
.map { it.asJsonObject["s"].asInt }
|
|
||||||
.distinct()
|
|
||||||
|
|
||||||
val missingTrackers = listOf("")
|
|
||||||
// val missingTrackers = trackers
|
|
||||||
// .mapNotNull { trackManager.getService(it) }
|
|
||||||
// .filter { !it.isLogged }
|
|
||||||
// .map { context.getString(it.nameRes()) }
|
|
||||||
// .sorted()
|
|
||||||
|
|
||||||
return ValidationResult(missingSources, missingTrackers)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
|
||||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
|
||||||
|
|
||||||
return extensionsMapping.asJsonArray
|
|
||||||
.map {
|
|
||||||
val items = it.asString.split(":")
|
|
||||||
items[0].toLong() to items[1]
|
|
||||||
}
|
|
||||||
.toMap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy.models
|
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Json values
|
|
||||||
*/
|
|
||||||
object Backup {
|
|
||||||
const val CURRENT_VERSION = 2
|
|
||||||
const val MANGA = "manga"
|
|
||||||
const val MANGAS = "mangas"
|
|
||||||
const val TRACK = "track"
|
|
||||||
const val CHAPTERS = "chapters"
|
|
||||||
const val CATEGORIES = "categories"
|
|
||||||
const val EXTENSIONS = "extensions"
|
|
||||||
const val HISTORY = "history"
|
|
||||||
const val VERSION = "version"
|
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
|
||||||
return "tachiyomi_$date.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy.models
|
|
||||||
|
|
||||||
data class DHistory(val url: String, val lastRead: Long)
|
|
||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
|
||||||
*/
|
|
||||||
object CategoryTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<CategoryImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.name)
|
|
||||||
value(it.order)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val category = CategoryImpl()
|
|
||||||
category.name = nextString()
|
|
||||||
category.order = nextInt()
|
|
||||||
endArray()
|
|
||||||
category
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-59
@@ -1,59 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
|
||||||
*/
|
|
||||||
object ChapterTypeAdapter {
|
|
||||||
|
|
||||||
private const val URL = "u"
|
|
||||||
private const val READ = "r"
|
|
||||||
private const val BOOKMARK = "b"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<ChapterImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
if (it.read || it.bookmark || it.last_page_read != 0) {
|
|
||||||
beginObject()
|
|
||||||
name(URL)
|
|
||||||
value(it.url)
|
|
||||||
if (it.read) {
|
|
||||||
name(READ)
|
|
||||||
value(1)
|
|
||||||
}
|
|
||||||
if (it.bookmark) {
|
|
||||||
name(BOOKMARK)
|
|
||||||
value(1)
|
|
||||||
}
|
|
||||||
if (it.last_page_read != 0) {
|
|
||||||
name(LAST_READ)
|
|
||||||
value(it.last_page_read)
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
val chapter = ChapterImpl()
|
|
||||||
beginObject()
|
|
||||||
while (hasNext()) {
|
|
||||||
if (peek() == JsonToken.NAME) {
|
|
||||||
when (nextName()) {
|
|
||||||
URL -> chapter.url = nextString()
|
|
||||||
READ -> chapter.read = nextInt() == 1
|
|
||||||
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
|
||||||
LAST_READ -> chapter.last_page_read = nextInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
chapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.legacy.models.DHistory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [DHistory] to / from json
|
|
||||||
*/
|
|
||||||
object HistoryTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<DHistory> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
if (it.lastRead != 0L) {
|
|
||||||
beginArray()
|
|
||||||
value(it.url)
|
|
||||||
value(it.lastRead)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val url = nextString()
|
|
||||||
val lastRead = nextLong()
|
|
||||||
endArray()
|
|
||||||
DHistory(url, lastRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [MangaImpl] to / from json
|
|
||||||
*/
|
|
||||||
object MangaTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<MangaImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.url)
|
|
||||||
value(it.title)
|
|
||||||
value(it.source)
|
|
||||||
value(it.viewer)
|
|
||||||
value(it.chapter_flags)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val manga = MangaImpl()
|
|
||||||
manga.url = nextString()
|
|
||||||
manga.title = nextString()
|
|
||||||
manga.source = nextLong()
|
|
||||||
manga.viewer = nextInt()
|
|
||||||
manga.chapter_flags = nextInt()
|
|
||||||
endArray()
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-59
@@ -1,59 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [TrackImpl] to / from json
|
|
||||||
*/
|
|
||||||
object TrackTypeAdapter {
|
|
||||||
|
|
||||||
private const val SYNC = "s"
|
|
||||||
private const val MEDIA = "r"
|
|
||||||
private const val LIBRARY = "ml"
|
|
||||||
private const val TITLE = "t"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
private const val TRACKING_URL = "u"
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<TrackImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginObject()
|
|
||||||
name(TITLE)
|
|
||||||
value(it.title)
|
|
||||||
name(SYNC)
|
|
||||||
value(it.sync_id)
|
|
||||||
name(MEDIA)
|
|
||||||
value(it.media_id)
|
|
||||||
name(LIBRARY)
|
|
||||||
value(it.library_id)
|
|
||||||
name(LAST_READ)
|
|
||||||
value(it.last_chapter_read)
|
|
||||||
name(TRACKING_URL)
|
|
||||||
value(it.tracking_url)
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
val track = TrackImpl()
|
|
||||||
beginObject()
|
|
||||||
while (hasNext()) {
|
|
||||||
if (peek() == JsonToken.NAME) {
|
|
||||||
when (nextName()) {
|
|
||||||
TITLE -> track.title = nextString()
|
|
||||||
SYNC -> track.sync_id = nextInt()
|
|
||||||
MEDIA -> track.media_id = nextInt()
|
|
||||||
LIBRARY -> track.library_id = nextLong()
|
|
||||||
LAST_READ -> track.last_chapter_read = nextInt()
|
|
||||||
TRACKING_URL -> track.tracking_url = nextString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.models
|
package suwayomi.tachidesk.manga.impl.backup.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
// import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
// import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
|
|
||||||
// import tachiyomi.source.model.MangaInfo
|
// substitute for eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
object OrientationType {
|
||||||
|
const val MASK = 0x00000038
|
||||||
|
}
|
||||||
|
|
||||||
|
// substitute for eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
|
object ReadingModeType {
|
||||||
|
const val MASK = 0x00000007
|
||||||
|
}
|
||||||
|
|
||||||
interface Manga : SManga {
|
interface Manga : SManga {
|
||||||
|
|
||||||
@@ -10,85 +20,100 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var source: Long
|
var source: Long
|
||||||
|
|
||||||
/** is in library */
|
|
||||||
var favorite: Boolean
|
var favorite: Boolean
|
||||||
|
|
||||||
|
// last time the chapter list changed in any way
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
|
// predicted next update time based on latest (by date) 4 chapters' deltas
|
||||||
|
var next_update: Long
|
||||||
|
|
||||||
var date_added: Long
|
var date_added: Long
|
||||||
|
|
||||||
var viewer: Int
|
var viewer_flags: Int
|
||||||
|
|
||||||
var chapter_flags: Int
|
var chapter_flags: Int
|
||||||
|
|
||||||
var cover_last_modified: Long
|
var cover_last_modified: Long
|
||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
fun setChapterOrder(order: Int) {
|
||||||
setFlags(order, SORT_MASK)
|
setChapterFlags(order, CHAPTER_SORT_MASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortDescending(): Boolean {
|
fun sortDescending(): Boolean {
|
||||||
return chapter_flags and SORT_MASK == SORT_DESC
|
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGenres(): List<String>? {
|
fun getGenres(): List<String>? {
|
||||||
return genre?.split(", ")?.map { it.trim() }
|
return genre?.split(", ")?.map { it.trim() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setFlags(flag: Int, mask: Int) {
|
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setViewerFlags(flag: Int, mask: Int) {
|
||||||
|
viewer_flags = viewer_flags and mask.inv() or (flag and mask)
|
||||||
|
}
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
// Used to display the chapter's title one way or another
|
||||||
var displayMode: Int
|
var displayMode: Int
|
||||||
get() = chapter_flags and DISPLAY_MASK
|
get() = chapter_flags and CHAPTER_DISPLAY_MASK
|
||||||
set(mode) = setFlags(mode, DISPLAY_MASK)
|
set(mode) = setChapterFlags(mode, CHAPTER_DISPLAY_MASK)
|
||||||
|
|
||||||
var readFilter: Int
|
var readFilter: Int
|
||||||
get() = chapter_flags and READ_MASK
|
get() = chapter_flags and CHAPTER_READ_MASK
|
||||||
set(filter) = setFlags(filter, READ_MASK)
|
set(filter) = setChapterFlags(filter, CHAPTER_READ_MASK)
|
||||||
|
|
||||||
var downloadedFilter: Int
|
var downloadedFilter: Int
|
||||||
get() = chapter_flags and DOWNLOADED_MASK
|
get() = chapter_flags and CHAPTER_DOWNLOADED_MASK
|
||||||
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
set(filter) = setChapterFlags(filter, CHAPTER_DOWNLOADED_MASK)
|
||||||
|
|
||||||
var bookmarkedFilter: Int
|
var bookmarkedFilter: Int
|
||||||
get() = chapter_flags and BOOKMARKED_MASK
|
get() = chapter_flags and CHAPTER_BOOKMARKED_MASK
|
||||||
set(filter) = setFlags(filter, BOOKMARKED_MASK)
|
set(filter) = setChapterFlags(filter, CHAPTER_BOOKMARKED_MASK)
|
||||||
|
|
||||||
var sorting: Int
|
var sorting: Int
|
||||||
get() = chapter_flags and SORTING_MASK
|
get() = chapter_flags and CHAPTER_SORTING_MASK
|
||||||
set(sort) = setFlags(sort, SORTING_MASK)
|
set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK)
|
||||||
|
|
||||||
|
var readingModeType: Int
|
||||||
|
get() = viewer_flags and ReadingModeType.MASK
|
||||||
|
set(readingMode) = setViewerFlags(readingMode, ReadingModeType.MASK)
|
||||||
|
|
||||||
|
var orientationType: Int
|
||||||
|
get() = viewer_flags and OrientationType.MASK
|
||||||
|
set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val SORT_DESC = 0x00000000
|
|
||||||
const val SORT_ASC = 0x00000001
|
|
||||||
const val SORT_MASK = 0x00000001
|
|
||||||
|
|
||||||
// Generic filter that does not filter anything
|
// Generic filter that does not filter anything
|
||||||
const val SHOW_ALL = 0x00000000
|
const val SHOW_ALL = 0x00000000
|
||||||
|
|
||||||
const val SHOW_UNREAD = 0x00000002
|
const val CHAPTER_SORT_DESC = 0x00000000
|
||||||
const val SHOW_READ = 0x00000004
|
const val CHAPTER_SORT_ASC = 0x00000001
|
||||||
const val READ_MASK = 0x00000006
|
const val CHAPTER_SORT_MASK = 0x00000001
|
||||||
|
|
||||||
const val SHOW_DOWNLOADED = 0x00000008
|
const val CHAPTER_SHOW_UNREAD = 0x00000002
|
||||||
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
const val CHAPTER_SHOW_READ = 0x00000004
|
||||||
const val DOWNLOADED_MASK = 0x00000018
|
const val CHAPTER_READ_MASK = 0x00000006
|
||||||
|
|
||||||
const val SHOW_BOOKMARKED = 0x00000020
|
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008
|
||||||
const val SHOW_NOT_BOOKMARKED = 0x00000040
|
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010
|
||||||
const val BOOKMARKED_MASK = 0x00000060
|
const val CHAPTER_DOWNLOADED_MASK = 0x00000018
|
||||||
|
|
||||||
const val SORTING_SOURCE = 0x00000000
|
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020
|
||||||
const val SORTING_NUMBER = 0x00000100
|
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040
|
||||||
const val SORTING_UPLOAD_DATE = 0x00000200
|
const val CHAPTER_BOOKMARKED_MASK = 0x00000060
|
||||||
const val SORTING_MASK = 0x00000300
|
|
||||||
|
|
||||||
const val DISPLAY_NAME = 0x00000000
|
const val CHAPTER_SORTING_SOURCE = 0x00000000
|
||||||
const val DISPLAY_NUMBER = 0x00100000
|
const val CHAPTER_SORTING_NUMBER = 0x00000100
|
||||||
const val DISPLAY_MASK = 0x00100000
|
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200
|
||||||
|
const val CHAPTER_SORTING_MASK = 0x00000300
|
||||||
|
|
||||||
|
const val CHAPTER_DISPLAY_NAME = 0x00000000
|
||||||
|
const val CHAPTER_DISPLAY_NUMBER = 0x00100000
|
||||||
|
const val CHAPTER_DISPLAY_MASK = 0x00100000
|
||||||
|
|
||||||
fun create(source: Long): Manga = MangaImpl().apply {
|
fun create(source: Long): Manga = MangaImpl().apply {
|
||||||
this.source = source
|
this.source = source
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
|
|||||||
|
|
||||||
open class MangaImpl : Manga {
|
open class MangaImpl : Manga {
|
||||||
|
|
||||||
override var id: Long? = 0
|
override var id: Long? = null
|
||||||
|
|
||||||
override var source: Long = -1
|
override var source: Long = -1
|
||||||
|
|
||||||
@@ -29,6 +29,8 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var last_update: Long = 0
|
override var last_update: Long = 0
|
||||||
|
|
||||||
|
override var next_update: Long = 0
|
||||||
|
|
||||||
override var date_added: Long = 0
|
override var date_added: Long = 0
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
@@ -42,7 +44,7 @@ open class MangaImpl : Manga {
|
|||||||
* 4 -> Webtoon
|
* 4 -> Webtoon
|
||||||
* 5 -> Continues Vertical
|
* 5 -> Continues Vertical
|
||||||
*/
|
*/
|
||||||
override var viewer: Int = 0
|
override var viewer_flags: Int = 0
|
||||||
|
|
||||||
/** Contains some useful info about
|
/** Contains some useful info about
|
||||||
*/
|
*/
|
||||||
@@ -70,7 +72,7 @@ open class MangaImpl : Manga {
|
|||||||
url = mangaRecord[MangaTable.url]
|
url = mangaRecord[MangaTable.url]
|
||||||
title = mangaRecord[MangaTable.title]
|
title = mangaRecord[MangaTable.title]
|
||||||
source = mangaRecord[MangaTable.sourceReference]
|
source = mangaRecord[MangaTable.sourceReference]
|
||||||
viewer = 0 // TODO: implement
|
viewer_flags = 0 // TODO: implement
|
||||||
chapter_flags = 0 // TODO: implement
|
chapter_flags = 0 // TODO: implement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
|
||||||
|
open class ProtoBackupBase {
|
||||||
|
var sourceMapping: Map<Long, String> = emptyMap()
|
||||||
|
|
||||||
|
val parser = ProtoBuf
|
||||||
|
}
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 kotlinx.coroutines.runBlocking
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.sink
|
||||||
|
import org.jetbrains.exposed.sql.Query
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||||
|
import suwayomi.tachidesk.manga.impl.Chapter
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupChapter
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
|
||||||
|
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||||
|
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||||
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
object ProtoBackupExport : ProtoBackupBase() {
|
||||||
|
suspend fun createBackup(flags: BackupFlags): InputStream {
|
||||||
|
// Create root object
|
||||||
|
|
||||||
|
val databaseManga = transaction { MangaTable.select { MangaTable.inLibrary eq true } }
|
||||||
|
|
||||||
|
val backup: Backup = transaction {
|
||||||
|
Backup(
|
||||||
|
backupManga(databaseManga, flags),
|
||||||
|
backupCategories(),
|
||||||
|
backupExtensionInfo(databaseManga)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
||||||
|
|
||||||
|
val byteStream = ByteArrayOutputStream()
|
||||||
|
byteStream.sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
|
|
||||||
|
return byteStream.toByteArray().inputStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupManga(databaseManga: Query, flags: BackupFlags): List<BackupManga> {
|
||||||
|
return databaseManga.map { mangaRow ->
|
||||||
|
val backupManga = BackupManga(
|
||||||
|
mangaRow[MangaTable.sourceReference],
|
||||||
|
mangaRow[MangaTable.url],
|
||||||
|
mangaRow[MangaTable.title],
|
||||||
|
mangaRow[MangaTable.artist],
|
||||||
|
mangaRow[MangaTable.author],
|
||||||
|
mangaRow[MangaTable.description],
|
||||||
|
mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(),
|
||||||
|
MangaStatus.valueOf(mangaRow[MangaTable.status]).value,
|
||||||
|
mangaRow[MangaTable.thumbnail_url],
|
||||||
|
0, // not supported in Tachidesk
|
||||||
|
0, // not supported in Tachidesk
|
||||||
|
)
|
||||||
|
|
||||||
|
val mangaId = mangaRow[MangaTable.id].value
|
||||||
|
|
||||||
|
if (flags.includeChapters) {
|
||||||
|
val chapters = runBlocking { Chapter.getChapterList(mangaId) }
|
||||||
|
backupManga.chapters = chapters.map {
|
||||||
|
BackupChapter(
|
||||||
|
it.url,
|
||||||
|
it.name,
|
||||||
|
it.scanlator,
|
||||||
|
it.read,
|
||||||
|
it.bookmarked,
|
||||||
|
it.lastPageRead,
|
||||||
|
0, // not supported in Tachidesk
|
||||||
|
it.uploadDate,
|
||||||
|
it.chapterNumber,
|
||||||
|
it.index,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.includeCategories) {
|
||||||
|
backupManga.categories = CategoryManga.getMangaCategories(mangaId).map { it.order }
|
||||||
|
}
|
||||||
|
|
||||||
|
// if(flags.includeTracking) {
|
||||||
|
// backupManga.tracking = TODO()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (flags.includeHistory) {
|
||||||
|
// backupManga.history = TODO()
|
||||||
|
// }
|
||||||
|
|
||||||
|
backupManga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupCategories(): List<BackupCategory> {
|
||||||
|
return CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||||
|
CategoryTable.toDataClass(it)
|
||||||
|
}.map {
|
||||||
|
BackupCategory(
|
||||||
|
it.name,
|
||||||
|
it.order,
|
||||||
|
0, // not supported in Tachidesk
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupExtensionInfo(mangas: Query): List<BackupSource> {
|
||||||
|
return mangas
|
||||||
|
.asSequence()
|
||||||
|
.map { it[MangaTable.sourceReference] }
|
||||||
|
.distinct()
|
||||||
|
.map {
|
||||||
|
val sourceRow = SourceTable.select { SourceTable.id eq it }.firstOrNull()
|
||||||
|
BackupSource(
|
||||||
|
sourceRow?.get(SourceTable.name) ?: "",
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 mu.KotlinLogging
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.source
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.tachidesk.manga.impl.Category
|
||||||
|
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator.ValidationResult
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.Chapter
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.Manga
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.Track
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.validate
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
|
||||||
|
import suwayomi.tachidesk.manga.model.table.CategoryTable
|
||||||
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
object ProtoBackupImport : ProtoBackupBase() {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
private var restoreAmount = 0
|
||||||
|
|
||||||
|
private val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
|
suspend fun performRestore(sourceStream: InputStream): ValidationResult {
|
||||||
|
val backupString = sourceStream.source().gzip().buffer().use { it.readByteArray() }
|
||||||
|
val backup = parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
|
|
||||||
|
val validationResult = validate(backup)
|
||||||
|
|
||||||
|
restoreAmount = backup.backupManga.size + 1 // +1 for categories
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
if (backup.backupCategories.isNotEmpty()) {
|
||||||
|
restoreCategories(backup.backupCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
val categoryMapping = transaction {
|
||||||
|
backup.backupCategories.associate {
|
||||||
|
it.order to CategoryTable.select { CategoryTable.name eq it.name }.first()[CategoryTable.id].value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store source mapping for error messages
|
||||||
|
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||||
|
|
||||||
|
// Restore individual manga
|
||||||
|
backup.backupManga.forEach {
|
||||||
|
restoreManga(it, backup.backupCategories, categoryMapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info {
|
||||||
|
"""
|
||||||
|
Restore Errors:
|
||||||
|
${ errors.joinToString("\n") { "${it.first} - ${it.second}" } }
|
||||||
|
Restore Summary:
|
||||||
|
- Missing Sources:
|
||||||
|
${validationResult.missingSources.joinToString("\n ")}
|
||||||
|
- Missing Trackers:
|
||||||
|
${validationResult.missingTrackers.joinToString("\n ")}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationResult
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||||
|
val dbCategories = Category.getCategoryList()
|
||||||
|
|
||||||
|
// Iterate over them and create missing categories
|
||||||
|
backupCategories.forEach { category ->
|
||||||
|
if (dbCategories.none { it.name == category.name }) {
|
||||||
|
Category.createCategory(category.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreManga(
|
||||||
|
backupManga: BackupManga,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
categoryMapping: Map<Int, Int>
|
||||||
|
) { // TODO
|
||||||
|
val manga = backupManga.getMangaImpl()
|
||||||
|
val chapters = backupManga.getChaptersImpl()
|
||||||
|
val categories = backupManga.categories
|
||||||
|
val history = backupManga.history
|
||||||
|
val tracks = backupManga.getTrackingImpl()
|
||||||
|
|
||||||
|
try {
|
||||||
|
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories, categoryMapping)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreMangaData(
|
||||||
|
manga: Manga,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<Int>,
|
||||||
|
history: List<BackupHistory>,
|
||||||
|
tracks: List<Track>,
|
||||||
|
backupCategories: List<BackupCategory>,
|
||||||
|
categoryMapping: Map<Int, Int>
|
||||||
|
) {
|
||||||
|
val dbManga = transaction {
|
||||||
|
MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
|
if (dbManga == null) { // Manga not in database
|
||||||
|
transaction {
|
||||||
|
// insert manga to database
|
||||||
|
val mangaId = MangaTable.insertAndGetId {
|
||||||
|
it[url] = manga.url
|
||||||
|
it[title] = manga.title
|
||||||
|
|
||||||
|
it[artist] = manga.artist
|
||||||
|
it[author] = manga.author
|
||||||
|
it[description] = manga.description
|
||||||
|
it[genre] = manga.genre
|
||||||
|
it[status] = manga.status
|
||||||
|
it[thumbnail_url] = manga.thumbnail_url
|
||||||
|
|
||||||
|
it[sourceReference] = manga.source
|
||||||
|
|
||||||
|
it[initialized] = manga.description != null
|
||||||
|
|
||||||
|
it[inLibrary] = true
|
||||||
|
}.value
|
||||||
|
|
||||||
|
// insert chapter data
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
ChapterTable.insert {
|
||||||
|
it[url] = chapter.url
|
||||||
|
it[name] = chapter.name
|
||||||
|
it[date_upload] = chapter.date_upload
|
||||||
|
it[chapter_number] = chapter.chapter_number
|
||||||
|
it[scanlator] = chapter.scanlator
|
||||||
|
|
||||||
|
it[chapterIndex] = chapter.source_order
|
||||||
|
it[ChapterTable.manga] = mangaId
|
||||||
|
|
||||||
|
it[isRead] = chapter.read
|
||||||
|
it[lastPageRead] = chapter.last_page_read
|
||||||
|
it[isBookmarked] = chapter.bookmark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert categories
|
||||||
|
categories.forEach { backupCategoryOrder ->
|
||||||
|
CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Manga in database
|
||||||
|
// merge chapter data
|
||||||
|
|
||||||
|
// merge categories
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: insert/merge history
|
||||||
|
|
||||||
|
// TODO: insert/merge tracking
|
||||||
|
}
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.source
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer
|
||||||
|
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
object ProtoBackupValidator : AbstractBackupValidator() {
|
||||||
|
fun validate(backup: Backup): ValidationResult {
|
||||||
|
if (backup.backupManga.isEmpty()) {
|
||||||
|
throw Exception("Backup does not contain any manga.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
||||||
|
|
||||||
|
val missingSources = transaction {
|
||||||
|
sources
|
||||||
|
.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
|
||||||
|
.map { "${it.value} (${it.key})" }
|
||||||
|
.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
// val trackers = backup.backupManga
|
||||||
|
// .flatMap { it.tracking }
|
||||||
|
// .map { it.syncId }
|
||||||
|
// .distinct()
|
||||||
|
|
||||||
|
val missingTrackers = listOf("")
|
||||||
|
// val missingTrackers = trackers
|
||||||
|
// .mapNotNull { trackManager.getService(it) }
|
||||||
|
// .filter { !it.isLogged }
|
||||||
|
// .map { context.getString(it.nameRes()) }
|
||||||
|
// .sorted()
|
||||||
|
|
||||||
|
return ValidationResult(missingSources, missingTrackers)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun validate(sourceStream: InputStream): ValidationResult {
|
||||||
|
val backupString = sourceStream.source().gzip().buffer().use { it.readByteArray() }
|
||||||
|
val backup = ProtoBackupImport.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
|
|
||||||
|
return validate(backup)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Backup(
|
||||||
|
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||||
|
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||||
|
// Bump by 100 to specify this is a 0.x value
|
||||||
|
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
|
||||||
|
)
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.Category
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BackupCategory(
|
||||||
|
@ProtoNumber(1) var name: String,
|
||||||
|
@ProtoNumber(2) var order: Int = 0,
|
||||||
|
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
|
||||||
|
// Bump by 100 to specify this is a 0.x value
|
||||||
|
@ProtoNumber(100) var flags: Int = 0,
|
||||||
|
) {
|
||||||
|
fun getCategoryImpl(): CategoryImpl {
|
||||||
|
return CategoryImpl().apply {
|
||||||
|
name = this@BackupCategory.name
|
||||||
|
flags = this@BackupCategory.flags
|
||||||
|
order = this@BackupCategory.order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(category: Category): BackupCategory {
|
||||||
|
return BackupCategory(
|
||||||
|
name = category.name,
|
||||||
|
order = category.order,
|
||||||
|
flags = category.flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.Chapter
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupChapter(
|
||||||
|
// in 1.x some of these values have different names
|
||||||
|
// url is called key in 1.x
|
||||||
|
@ProtoNumber(1) var url: String,
|
||||||
|
@ProtoNumber(2) var name: String,
|
||||||
|
@ProtoNumber(3) var scanlator: String? = null,
|
||||||
|
@ProtoNumber(4) var read: Boolean = false,
|
||||||
|
@ProtoNumber(5) var bookmark: Boolean = false,
|
||||||
|
// lastPageRead is called progress in 1.x
|
||||||
|
@ProtoNumber(6) var lastPageRead: Int = 0,
|
||||||
|
@ProtoNumber(7) var dateFetch: Long = 0,
|
||||||
|
@ProtoNumber(8) var dateUpload: Long = 0,
|
||||||
|
// chapterNumber is called number is 1.x
|
||||||
|
@ProtoNumber(9) var chapterNumber: Float = 0F,
|
||||||
|
@ProtoNumber(10) var sourceOrder: Int = 0,
|
||||||
|
) {
|
||||||
|
fun toChapterImpl(): ChapterImpl {
|
||||||
|
return ChapterImpl().apply {
|
||||||
|
url = this@BackupChapter.url
|
||||||
|
name = this@BackupChapter.name
|
||||||
|
chapter_number = this@BackupChapter.chapterNumber
|
||||||
|
scanlator = this@BackupChapter.scanlator
|
||||||
|
read = this@BackupChapter.read
|
||||||
|
bookmark = this@BackupChapter.bookmark
|
||||||
|
last_page_read = this@BackupChapter.lastPageRead
|
||||||
|
date_fetch = this@BackupChapter.dateFetch
|
||||||
|
date_upload = this@BackupChapter.dateUpload
|
||||||
|
source_order = this@BackupChapter.sourceOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(chapter: Chapter): BackupChapter {
|
||||||
|
return BackupChapter(
|
||||||
|
url = chapter.url,
|
||||||
|
name = chapter.name,
|
||||||
|
chapterNumber = chapter.chapter_number,
|
||||||
|
scanlator = chapter.scanlator,
|
||||||
|
read = chapter.read,
|
||||||
|
bookmark = chapter.bookmark,
|
||||||
|
lastPageRead = chapter.last_page_read,
|
||||||
|
dateFetch = chapter.date_fetch,
|
||||||
|
dateUpload = chapter.date_upload,
|
||||||
|
sourceOrder = chapter.source_order
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object BackupFull {
|
||||||
|
fun getDefaultFilename(): String {
|
||||||
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
|
return "tachiyomi_$date.proto.gz"
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupHistory(
|
||||||
|
@ProtoNumber(0) var url: String,
|
||||||
|
@ProtoNumber(1) var lastRead: Long
|
||||||
|
)
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.Manga
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupManga(
|
||||||
|
// in 1.x some of these values have different names
|
||||||
|
@ProtoNumber(1) var source: Long,
|
||||||
|
// url is called key in 1.x
|
||||||
|
@ProtoNumber(2) var url: String,
|
||||||
|
@ProtoNumber(3) var title: String = "",
|
||||||
|
@ProtoNumber(4) var artist: String? = null,
|
||||||
|
@ProtoNumber(5) var author: String? = null,
|
||||||
|
@ProtoNumber(6) var description: String? = null,
|
||||||
|
@ProtoNumber(7) var genre: List<String> = emptyList(),
|
||||||
|
@ProtoNumber(8) var status: Int = 0,
|
||||||
|
// thumbnailUrl is called cover in 1.x
|
||||||
|
@ProtoNumber(9) var thumbnailUrl: String? = null,
|
||||||
|
// @ProtoNumber(10) val customCover: String = "", 1.x value, not used in 0.x
|
||||||
|
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
||||||
|
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
||||||
|
@ProtoNumber(13) var dateAdded: Long = 0,
|
||||||
|
@ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags
|
||||||
|
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
||||||
|
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
||||||
|
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
||||||
|
@ProtoNumber(18) var tracking: List<BackupTracking> = emptyList(),
|
||||||
|
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
||||||
|
@ProtoNumber(100) var favorite: Boolean = true,
|
||||||
|
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||||
|
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
||||||
|
@ProtoNumber(103) var viewer_flags: Int? = null
|
||||||
|
) {
|
||||||
|
fun getMangaImpl(): MangaImpl {
|
||||||
|
return MangaImpl().apply {
|
||||||
|
url = this@BackupManga.url
|
||||||
|
title = this@BackupManga.title
|
||||||
|
artist = this@BackupManga.artist
|
||||||
|
author = this@BackupManga.author
|
||||||
|
description = this@BackupManga.description
|
||||||
|
genre = this@BackupManga.genre.joinToString()
|
||||||
|
status = this@BackupManga.status
|
||||||
|
thumbnail_url = this@BackupManga.thumbnailUrl
|
||||||
|
favorite = this@BackupManga.favorite
|
||||||
|
source = this@BackupManga.source
|
||||||
|
date_added = this@BackupManga.dateAdded
|
||||||
|
viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer
|
||||||
|
chapter_flags = this@BackupManga.chapterFlags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChaptersImpl(): List<ChapterImpl> {
|
||||||
|
return chapters.map {
|
||||||
|
it.toChapterImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTrackingImpl(): List<TrackImpl> {
|
||||||
|
return tracking.map {
|
||||||
|
it.getTrackingImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(manga: Manga): BackupManga {
|
||||||
|
return BackupManga(
|
||||||
|
url = manga.url,
|
||||||
|
title = manga.title,
|
||||||
|
artist = manga.artist,
|
||||||
|
author = manga.author,
|
||||||
|
description = manga.description,
|
||||||
|
genre = manga.getGenres() ?: emptyList(),
|
||||||
|
status = manga.status,
|
||||||
|
thumbnailUrl = manga.thumbnail_url,
|
||||||
|
favorite = manga.favorite,
|
||||||
|
source = manga.source,
|
||||||
|
dateAdded = manga.date_added,
|
||||||
|
viewer = manga.readingModeType,
|
||||||
|
viewer_flags = manga.viewer_flags,
|
||||||
|
chapterFlags = manga.chapter_flags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializer
|
||||||
|
|
||||||
|
@Serializer(forClass = Backup::class)
|
||||||
|
object BackupSerializer
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupSource(
|
||||||
|
@ProtoNumber(0) var name: String = "",
|
||||||
|
@ProtoNumber(1) var sourceId: Long
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(source: Source): BackupSource {
|
||||||
|
return BackupSource(
|
||||||
|
name = source.name,
|
||||||
|
sourceId = source.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.Track
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupTracking(
|
||||||
|
// in 1.x some of these values have different types or names
|
||||||
|
// syncId is called siteId in 1,x
|
||||||
|
@ProtoNumber(1) var syncId: Int,
|
||||||
|
// LibraryId is not null in 1.x
|
||||||
|
@ProtoNumber(2) var libraryId: Long,
|
||||||
|
@ProtoNumber(3) var mediaId: Int = 0,
|
||||||
|
// trackingUrl is called mediaUrl in 1.x
|
||||||
|
@ProtoNumber(4) var trackingUrl: String = "",
|
||||||
|
@ProtoNumber(5) var title: String = "",
|
||||||
|
// lastChapterRead is called last read, and it has been changed to a float in 1.x
|
||||||
|
@ProtoNumber(6) var lastChapterRead: Float = 0F,
|
||||||
|
@ProtoNumber(7) var totalChapters: Int = 0,
|
||||||
|
@ProtoNumber(8) var score: Float = 0F,
|
||||||
|
@ProtoNumber(9) var status: Int = 0,
|
||||||
|
// startedReadingDate is called startReadTime in 1.x
|
||||||
|
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
||||||
|
// finishedReadingDate is called endReadTime in 1.x
|
||||||
|
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
||||||
|
) {
|
||||||
|
fun getTrackingImpl(): TrackImpl {
|
||||||
|
return TrackImpl().apply {
|
||||||
|
sync_id = this@BackupTracking.syncId
|
||||||
|
media_id = this@BackupTracking.mediaId
|
||||||
|
library_id = this@BackupTracking.libraryId
|
||||||
|
title = this@BackupTracking.title
|
||||||
|
// convert from float to int because of 1.x types
|
||||||
|
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
|
||||||
|
total_chapters = this@BackupTracking.totalChapters
|
||||||
|
score = this@BackupTracking.score
|
||||||
|
status = this@BackupTracking.status
|
||||||
|
started_reading_date = this@BackupTracking.startedReadingDate
|
||||||
|
finished_reading_date = this@BackupTracking.finishedReadingDate
|
||||||
|
tracking_url = this@BackupTracking.trackingUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun copyFrom(track: Track): BackupTracking {
|
||||||
|
return BackupTracking(
|
||||||
|
syncId = track.sync_id,
|
||||||
|
mediaId = track.media_id,
|
||||||
|
// forced not null so its compatible with 1.x backup system
|
||||||
|
libraryId = track.library_id!!,
|
||||||
|
title = track.title,
|
||||||
|
// convert to float for 1.x
|
||||||
|
lastChapterRead = track.last_chapter_read.toFloat(),
|
||||||
|
totalChapters = track.total_chapters,
|
||||||
|
score = track.score,
|
||||||
|
status = track.status,
|
||||||
|
startedReadingDate = track.started_reading_date,
|
||||||
|
finishedReadingDate = track.finished_reading_date,
|
||||||
|
trackingUrl = track.tracking_url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ object BytecodeEditor {
|
|||||||
bytes[2],
|
bytes[2],
|
||||||
bytes[3]
|
bytes[3]
|
||||||
)
|
)
|
||||||
if (cafebabe.toLowerCase() != "cafebabe") {
|
if (cafebabe.lowercase() != "cafebabe") {
|
||||||
// Corrupted class
|
// Corrupted class
|
||||||
return@use null
|
return@use null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ object GetHttpSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val sourceRecord = transaction {
|
val sourceRecord = transaction {
|
||||||
SourceTable.select { SourceTable.id eq sourceId }.first()
|
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionId = sourceRecord[SourceTable.extension]
|
val extensionId = sourceRecord[SourceTable.extension]
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.util.lang
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 org.jetbrains.exposed.sql.Query
|
||||||
|
|
||||||
|
fun Query.isEmpty() = this.count() == 0L
|
||||||
|
|
||||||
|
fun Query.isNotEmpty() = !this.isEmpty()
|
||||||
@@ -81,7 +81,7 @@ object JavalinSetup {
|
|||||||
app.routes {
|
app.routes {
|
||||||
path("api/v1/") {
|
path("api/v1/") {
|
||||||
GlobalAPI.defineEndpoints()
|
GlobalAPI.defineEndpoints()
|
||||||
MangaAPI.defineEndpoints(app)
|
MangaAPI.defineEndpoints()
|
||||||
AnimeAPI.defineEndpoints(app) // TODO: migrate Anime endpoints
|
AnimeAPI.defineEndpoints(app) // TODO: migrate Anime endpoints
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class ServerConfig(config: Config, moduleName: String = "") : ConfigModule(confi
|
|||||||
// webUI
|
// webUI
|
||||||
val webUIEnabled: Boolean by overridableWithSysProperty
|
val webUIEnabled: Boolean by overridableWithSysProperty
|
||||||
val initialOpenInBrowserEnabled: Boolean by overridableWithSysProperty
|
val initialOpenInBrowserEnabled: Boolean by overridableWithSysProperty
|
||||||
val webUIBrowser: String by overridableWithSysProperty
|
val webUIInterface: String by overridableWithSysProperty
|
||||||
val electronPath: String by overridableWithSysProperty
|
val electronPath: String by overridableWithSysProperty
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ fun applicationSetup() {
|
|||||||
logger.error("Exception while creating initial server.conf:\n", e)
|
logger.error("Exception while creating initial server.conf:\n", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixes #119 , ref: https://github.com/Suwayomi/Tachidesk-Server/issues/119#issuecomment-894681292
|
// fixes #119 , ref: https://github.com/Suwayomi/Tachidesk-Server/issues/119#issuecomment-894681292 , source Id calculation depends on String.lowercase()
|
||||||
Locale.setDefault(Locale.ENGLISH)
|
Locale.setDefault(Locale.ENGLISH)
|
||||||
|
|
||||||
databaseUp()
|
databaseUp()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ object Browser {
|
|||||||
|
|
||||||
fun openInBrowser() {
|
fun openInBrowser() {
|
||||||
if (serverConfig.webUIEnabled) {
|
if (serverConfig.webUIEnabled) {
|
||||||
if (serverConfig.webUIBrowser == ("electron")) {
|
if (serverConfig.webUIInterface == ("electron")) {
|
||||||
try {
|
try {
|
||||||
val electronPath = serverConfig.electronPath
|
val electronPath = serverConfig.electronPath
|
||||||
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
|
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ import org.kodein.di.conf.global
|
|||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
import suwayomi.tachidesk.server.BuildConfig
|
import suwayomi.tachidesk.server.BuildConfig
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@@ -58,9 +59,12 @@ fun setupWebUI() {
|
|||||||
val webUIZipFile = File(webUIZipPath)
|
val webUIZipFile = File(webUIZipPath)
|
||||||
|
|
||||||
// try with resources first
|
// try with resources first
|
||||||
val resourceWebUI = try {
|
val resourceWebUI: InputStream? = try {
|
||||||
BuildConfig::class.java.getResourceAsStream("/WebUI.zip")
|
BuildConfig::class.java.getResourceAsStream("/WebUI.zip")
|
||||||
} catch (e: NullPointerException) { null }
|
} catch (e: NullPointerException) {
|
||||||
|
logger.info { "No bundled WebUI.zip found!" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
if (resourceWebUI == null) { // is not bundled
|
if (resourceWebUI == null) { // is not bundled
|
||||||
// download webUI zip
|
// download webUI zip
|
||||||
@@ -71,18 +75,25 @@ fun setupWebUI() {
|
|||||||
val data = ByteArray(1024)
|
val data = ByteArray(1024)
|
||||||
|
|
||||||
webUIZipFile.outputStream().use { webUIZipFileOut ->
|
webUIZipFile.outputStream().use { webUIZipFileOut ->
|
||||||
BufferedInputStream(URL(webUIZipURL).openStream()).use { inp ->
|
|
||||||
|
val connection = URL(webUIZipURL).openConnection() as HttpURLConnection
|
||||||
|
connection.connect()
|
||||||
|
val contentLength = connection.contentLength
|
||||||
|
|
||||||
|
connection.inputStream.buffered().use { inp ->
|
||||||
var totalCount = 0
|
var totalCount = 0
|
||||||
var tresh = 0
|
|
||||||
|
print("Download progress: % 00")
|
||||||
while (true) {
|
while (true) {
|
||||||
val count = inp.read(data, 0, 1024)
|
val count = inp.read(data, 0, 1024)
|
||||||
totalCount += count
|
|
||||||
if (totalCount > tresh + 10 * 1024) {
|
|
||||||
tresh = totalCount
|
|
||||||
print(" *")
|
|
||||||
}
|
|
||||||
if (count == -1)
|
if (count == -1)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
totalCount += count
|
||||||
|
val percentage = (totalCount.toFloat() / contentLength * 100).toInt().toString().padStart(2, '0')
|
||||||
|
print("\b\b$percentage")
|
||||||
|
|
||||||
webUIZipFileOut.write(data, 0, count)
|
webUIZipFileOut.write(data, 0, count)
|
||||||
}
|
}
|
||||||
println()
|
println()
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ server.systemTrayEnabled = true
|
|||||||
# webUI
|
# webUI
|
||||||
server.webUIEnabled = true
|
server.webUIEnabled = true
|
||||||
server.initialOpenInBrowserEnabled = true
|
server.initialOpenInBrowserEnabled = true
|
||||||
server.webUIBrowser = "browser" # "browser" or "electron"
|
server.webUIInterface = "browser" # "browser" or "electron"
|
||||||
server.electronPath = ""
|
server.electronPath = ""
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class TestExtensions {
|
|||||||
sources = getSourceList().map { getHttpSource(it.id.toLong()) }
|
sources = getSourceList().map { getHttpSource(it.id.toLong()) }
|
||||||
}
|
}
|
||||||
setLoggingEnabled(true)
|
setLoggingEnabled(true)
|
||||||
File("tmp/TestDesk/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.toUpperCase()} - ${it.id}" })
|
File("tmp/TestDesk/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -99,7 +99,7 @@ class TestExtensions {
|
|||||||
}.awaitAll()
|
}.awaitAll()
|
||||||
File("tmp/TestDesk/failedToFetch.txt").writeText(
|
File("tmp/TestDesk/failedToFetch.txt").writeText(
|
||||||
failedToFetch.joinToString("\n") { (source, exception) ->
|
failedToFetch.joinToString("\n") { (source, exception) ->
|
||||||
"${source.name} (${source.lang.toUpperCase()}, ${source.id}):" +
|
"${source.name} (${source.lang.uppercase()}, ${source.id}):" +
|
||||||
" ${exception.message}"
|
" ${exception.message}"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user