mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-05 11:54:38 -05:00
Compare commits
2 Commits
v0.3.0-rc1
...
v0.2.6-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6e57e2700 | ||
|
|
c5f467ce3d |
31
.gitattributes
vendored
31
.gitattributes
vendored
@@ -1,27 +1,6 @@
|
|||||||
* text=auto
|
#
|
||||||
* text eol=lf
|
# https://help.github.com/articles/dealing-with-line-endings/
|
||||||
|
#
|
||||||
|
# These are explicitly windows files and should use crlf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
|
||||||
# Windows forced line-endings
|
|
||||||
/.idea/* text eol=crlf
|
|
||||||
*.bat text eol=crlf
|
|
||||||
*.ps1 text eol=crlf
|
|
||||||
|
|
||||||
# Gradle wrapper
|
|
||||||
*.jar binary
|
|
||||||
|
|
||||||
# Images
|
|
||||||
*.webp binary
|
|
||||||
*.png binary
|
|
||||||
*.jpg binary
|
|
||||||
*.jpeg binary
|
|
||||||
*.gif binary
|
|
||||||
*.ico binary
|
|
||||||
*.gz binary
|
|
||||||
*.zip binary
|
|
||||||
*.7z binary
|
|
||||||
*.ttf binary
|
|
||||||
*.eot binary
|
|
||||||
*.woff binary
|
|
||||||
*.pyc binary
|
|
||||||
*.swp binary
|
|
||||||
*.pdf binary
|
|
||||||
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -25,7 +25,6 @@ Note that the issue will be automatically closed if you do not fill out the titl
|
|||||||
## Device information
|
## Device information
|
||||||
- Tachidesk version: (Example: v0.2.3-r255-win32)
|
- Tachidesk version: (Example: v0.2.3-r255-win32)
|
||||||
- Server Operating System: (Example: Ubuntu 20.04)
|
- Server Operating System: (Example: Ubuntu 20.04)
|
||||||
- Server Desktop Environment: N/A or (Example: Gnome 40)
|
|
||||||
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
|
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
|
||||||
- Client Operating System: <usually the same as above Server Operating System>
|
- Client Operating System: <usually the same as above Server Operating System>
|
||||||
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
|
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
|
||||||
|
|||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -60,12 +60,12 @@ jobs:
|
|||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||||
|
|
||||||
- name: Build and copy webUI, Build Jar and launch4j
|
- name: Build Jar and launch4j
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
arguments: :webUI:copyBuild :server:windowsPackage --stacktrace
|
arguments: :server:windowsPackage --stacktrace
|
||||||
wrapper-cache-enabled: true
|
wrapper-cache-enabled: true
|
||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
2
.github/workflows/issue_closer.yml
vendored
2
.github/workflows/issue_closer.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "body",
|
"type": "body",
|
||||||
"regex": "(Tachidesk version|Server Operating System|Server Desktop Environment|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
|
"regex": "(Tachidesk version|Server Operating System|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
|
||||||
"message": "The requested information was not filled out"
|
"message": "The requested information was not filled out"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
19
.github/workflows/publish.yml
vendored
19
.github/workflows/publish.yml
vendored
@@ -58,12 +58,27 @@ jobs:
|
|||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
|
||||||
- name: Build and copy webUI, Build Jar and launch4j
|
- name: Build no-webUI Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
arguments: :webUI:copyBuild :server:windowsPackage --stacktrace
|
arguments: :server:shadowJar -x :webUI:copyBuild --stacktrace
|
||||||
|
wrapper-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
- name: Rename the no-webUI Jar
|
||||||
|
run: |
|
||||||
|
cd master/server/build
|
||||||
|
mv Tachidesk-*.jar $(ls *.jar | sed 's/\.jar/-no-webUI\.jar/g')
|
||||||
|
|
||||||
|
- name: Build Jar and launch4j
|
||||||
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
with:
|
||||||
|
build-root-directory: master
|
||||||
|
wrapper-directory: master
|
||||||
|
arguments: :server:windowsPackage --stacktrace
|
||||||
wrapper-cache-enabled: true
|
wrapper-cache-enabled: true
|
||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,11 +1,8 @@
|
|||||||
# Ignore Gradle project-specific cache directory
|
# Ignore Gradle project-specific cache directory
|
||||||
.gradle
|
.gradle
|
||||||
.idea
|
.idea
|
||||||
gradle.properties
|
|
||||||
|
|
||||||
# Ignore Gradle build output directory
|
# Ignore Gradle build output directory
|
||||||
build
|
build
|
||||||
|
|
||||||
server/src/main/resources/react
|
server/src/main/resources/react
|
||||||
server/tmp/
|
|
||||||
server/tachiserver-data/
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package xyz.nulldev.ts.config
|
|
||||||
|
|
||||||
import net.harawata.appdirs.AppDirsFactory
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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/. */
|
|
||||||
|
|
||||||
val ApplicationRootDir: String
|
|
||||||
get(): String {
|
|
||||||
return System.getProperty(
|
|
||||||
"ir.armor.tachidesk.rootDir",
|
|
||||||
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,18 @@
|
|||||||
package xyz.nulldev.ts.config
|
package xyz.nulldev.ts.config
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import com.typesafe.config.ConfigRenderOptions
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import net.harawata.appdirs.AppDirsFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages app config.
|
* Manages app config.
|
||||||
*/
|
*/
|
||||||
open class ConfigManager {
|
open class ConfigManager {
|
||||||
|
private val dataRoot by lazy { AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!! }
|
||||||
|
|
||||||
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
||||||
val config by lazy { loadConfigs() }
|
val config by lazy { loadConfigs() }
|
||||||
|
|
||||||
@@ -24,6 +20,8 @@ open class ConfigManager {
|
|||||||
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
|
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
|
||||||
get() = generatedModules
|
get() = generatedModules
|
||||||
|
|
||||||
|
open val appConfigFile: String = "$dataRoot/server.conf"
|
||||||
|
|
||||||
val logger = KotlinLogging.logger {}
|
val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,8 +44,8 @@ open class ConfigManager {
|
|||||||
|
|
||||||
//Load user config
|
//Load user config
|
||||||
val userConfig =
|
val userConfig =
|
||||||
File(ApplicationRootDir, "server.conf").let {
|
File(appConfigFile).let{
|
||||||
ConfigFactory.parseFile(it)
|
ConfigFactory.parseFile(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val config = ConfigFactory.empty()
|
val config = ConfigFactory.empty()
|
||||||
@@ -64,7 +62,7 @@ open class ConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun registerModule(module: ConfigModule) {
|
fun registerModule(module: ConfigModule) {
|
||||||
generatedModules[module.javaClass] = module
|
generatedModules.put(module.javaClass, module)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerModules(vararg modules: ConfigModule) {
|
fun registerModules(vararg modules: ConfigModule) {
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Android stub library
|
// Android stub library
|
||||||
|
// compileOnly( fileTree(File(rootProject.rootDir, "libs/android"), include: "*.jar")
|
||||||
implementation(fileTree("lib/"))
|
implementation(fileTree("lib/"))
|
||||||
|
implementation(fileTree("${rootProject.rootDir}/server/lib/dex2jar/"))
|
||||||
|
|
||||||
|
|
||||||
// Android JAR libs
|
// Android JAR libs
|
||||||
@@ -30,11 +32,22 @@ dependencies {
|
|||||||
// Javassist
|
// Javassist
|
||||||
compileOnly( "org.javassist:javassist:3.27.0-GA")
|
compileOnly( "org.javassist:javassist:3.27.0-GA")
|
||||||
|
|
||||||
|
// Coroutines
|
||||||
|
val kotlinx_coroutines_version = "1.4.2"
|
||||||
|
compileOnly( "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version")
|
||||||
|
compileOnly( "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinx_coroutines_version")
|
||||||
|
|
||||||
// XML
|
// XML
|
||||||
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
||||||
|
|
||||||
// Config API
|
// Config API
|
||||||
implementation(project(":AndroidCompat:Config"))
|
implementation( project(":AndroidCompat:Config"))
|
||||||
|
|
||||||
|
// dex2jar
|
||||||
|
// compileOnly( "dex2jar:dex-translator")
|
||||||
|
|
||||||
|
// APK parser
|
||||||
|
compileOnly("net.dongliu:apk-parser:2.6.10")
|
||||||
|
|
||||||
// APK sig verifier
|
// APK sig verifier
|
||||||
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13")
|
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13")
|
||||||
@@ -42,11 +55,7 @@ dependencies {
|
|||||||
// AndroidX annotations
|
// AndroidX annotations
|
||||||
compileOnly( "androidx.annotation:annotation:1.2.0-alpha01")
|
compileOnly( "androidx.annotation:annotation:1.2.0-alpha01")
|
||||||
|
|
||||||
// substitute for duktape-android
|
// compileOnly("io.reactivex:rxjava:1.3.8")
|
||||||
// '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")
|
|
||||||
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
|
|
||||||
implementation("org.mozilla:rhino-engine:1.7.13")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
|
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
# 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/.
|
|
||||||
|
|
||||||
# This is a windows only PowerShell script to create android.jar stubs
|
|
||||||
|
|
||||||
# foolproof against running from AndroidCompat dir instead of running from project root
|
|
||||||
if ($(Split-Path -Path (Get-Location) -Leaf) -eq "AndroidCompat" ) {
|
|
||||||
Set-Location ..
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Output "Getting required Android.jar..."
|
|
||||||
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
|
|
||||||
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
|
|
||||||
|
|
||||||
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT").content
|
|
||||||
|
|
||||||
$android_jar = (Get-Location).Path + "\tmp\android.jar"
|
|
||||||
|
|
||||||
[IO.File]::WriteAllBytes($android_jar, [Convert]::FromBase64String($androidEncoded))
|
|
||||||
|
|
||||||
# We need to remove any stub classes that we have implementations for
|
|
||||||
Write-Output "Patching JAR..."
|
|
||||||
|
|
||||||
function Remove-Files-Zip($zipfile, $path)
|
|
||||||
{
|
|
||||||
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') | Out-Null
|
|
||||||
|
|
||||||
$stream = New-Object IO.FileStream($zipfile, [IO.FileMode]::Open)
|
|
||||||
$mode = [IO.Compression.ZipArchiveMode]::Update
|
|
||||||
$zip = New-Object IO.Compression.ZipArchive($stream, $mode)
|
|
||||||
|
|
||||||
($zip.Entries | Where-Object { $_.FullName -like $path }) | ForEach-Object { Write-Output "Deleting: $($_.FullName)"; $_.Delete() }
|
|
||||||
|
|
||||||
$zip.Dispose()
|
|
||||||
$stream.Close()
|
|
||||||
$stream.Dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Output "Removing org.json..."
|
|
||||||
Remove-Files-Zip $android_jar 'org/json/*'
|
|
||||||
|
|
||||||
Write-Output "Removing org.apache..."
|
|
||||||
Remove-Files-Zip $android_jar 'org/apache/*'
|
|
||||||
|
|
||||||
Write-Output "Removing org.w3c..."
|
|
||||||
Remove-Files-Zip $android_jar 'org/w3c/*'
|
|
||||||
|
|
||||||
Write-Output "Removing org.xml..."
|
|
||||||
Remove-Files-Zip $android_jar 'org/xml/*'
|
|
||||||
|
|
||||||
Write-Output "Removing org.xmlpull..."
|
|
||||||
Remove-Files-Zip $android_jar 'org/xmlpull/*'
|
|
||||||
|
|
||||||
Write-Output "Removing junit..."
|
|
||||||
Remove-Files-Zip $android_jar 'junit/*'
|
|
||||||
|
|
||||||
Write-Output "Removing javax..."
|
|
||||||
Remove-Files-Zip $android_jar 'javax/*'
|
|
||||||
|
|
||||||
Write-Output "Removing java..."
|
|
||||||
Remove-Files-Zip $android_jar 'java/*'
|
|
||||||
|
|
||||||
Write-Output "Removing overriden classes..."
|
|
||||||
Remove-Files-Zip $android_jar 'android/app/Application.class'
|
|
||||||
Remove-Files-Zip $android_jar 'android/app/Service.class'
|
|
||||||
Remove-Files-Zip $android_jar 'android/net/Uri.class'
|
|
||||||
Remove-Files-Zip $android_jar 'android/net/Uri$Builder.class'
|
|
||||||
Remove-Files-Zip $android_jar 'android/os/Environment.class'
|
|
||||||
Remove-Files-Zip $android_jar 'android/text/format/Formatter.class'
|
|
||||||
Remove-Files-Zip $android_jar 'android/text/Html.class'
|
|
||||||
|
|
||||||
function Dedupe($path)
|
|
||||||
{
|
|
||||||
Push-Location $path
|
|
||||||
$classes = Get-ChildItem . *.* -Recurse | Where-Object { !$_.PSIsContainer }
|
|
||||||
$classes | ForEach-Object {
|
|
||||||
"Processing class: $($_.FullName)"
|
|
||||||
Remove-Files-Zip $android_jar "$($_.Name).class" | Out-Null
|
|
||||||
Remove-Files-Zip $android_jar "$($_.Name)$*.class" | Out-Null
|
|
||||||
Remove-Files-Zip $android_jar "$($_.Name)Kt.class" | Out-Null
|
|
||||||
Remove-Files-Zip $android_jar "$($_.Name)Kt$*.class" | Out-Null
|
|
||||||
}
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
|
|
||||||
Dedupe "AndroidCompat/src/main/java"
|
|
||||||
Dedupe "server/src/main/java"
|
|
||||||
Dedupe "server/src/main/kotlin"
|
|
||||||
|
|
||||||
Write-Output "Copying Android.jar to library folder..."
|
|
||||||
Move-Item -Force $android_jar "AndroidCompat/lib/android.jar"
|
|
||||||
|
|
||||||
Write-Output "Cleaning up..."
|
|
||||||
Remove-Item -Recurse -Force "tmp"
|
|
||||||
|
|
||||||
Write-Output "Done!"
|
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# 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/.
|
|
||||||
|
|
||||||
# This is a bash script to create android.jar stubs
|
|
||||||
|
|
||||||
# foolproof against running from AndroidCompat dir instead of running from project root
|
# foolproof against running from AndroidCompat dir instead of running from project root
|
||||||
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
|
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
|
||||||
cd ..
|
cd ..
|
||||||
@@ -21,7 +13,7 @@ pushd "tmp"
|
|||||||
|
|
||||||
curl "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" | base64 --decode > android.jar
|
curl "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" | base64 --decode > android.jar
|
||||||
|
|
||||||
# We need to remove any stub classes that we have implementations for
|
# We need to remove any stub classes that we might use
|
||||||
echo "Patching JAR..."
|
echo "Patching JAR..."
|
||||||
|
|
||||||
echo "Removing org.json..."
|
echo "Removing org.json..."
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
package com.squareup.duktape;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) 2015 Square, Inc.
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* you may not use this file except in compliance with the License.
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.squareup.duktape;
|
||||||
|
|
||||||
import kotlin.NotImplementedError;
|
import kotlin.NotImplementedError;
|
||||||
|
|
||||||
@@ -14,18 +22,11 @@ import javax.script.ScriptEngineManager;
|
|||||||
import javax.script.ScriptException;
|
import javax.script.ScriptException;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
|
|
||||||
/* Note (March 2021):
|
/** A simple EMCAScript (Javascript) interpreter. */
|
||||||
* The old implementation for duktape-android used the nashorn engine which is deprecated.
|
|
||||||
* This new implementation uses Mozilla's Rhino: https://github.com/mozilla/rhino
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple EMCAScript (Javascript) interpreter.
|
|
||||||
*/
|
|
||||||
public final class Duktape implements Closeable, AutoCloseable {
|
public final class Duktape implements Closeable, AutoCloseable {
|
||||||
|
|
||||||
private ScriptEngineManager factory = new ScriptEngineManager();
|
private ScriptEngineManager factory = new ScriptEngineManager();
|
||||||
private ScriptEngine engine = factory.getEngineByName("rhino");
|
private ScriptEngine engine = factory.getEngineByName("JavaScript");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new interpreter instance. Calls to this method <strong>must</strong> matched with
|
* Create a new interpreter instance. Calls to this method <strong>must</strong> matched with
|
||||||
@@ -37,6 +38,17 @@ public final class Duktape implements Closeable, AutoCloseable {
|
|||||||
|
|
||||||
private Duktape() {}
|
private Duktape() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate {@code script} and return a result. {@code fileName} will be used in error
|
||||||
|
* reporting. Note that the result must be one of the supported Java types or the call will
|
||||||
|
* return null.
|
||||||
|
*
|
||||||
|
* @throws DuktapeException if there is an error evaluating the script.
|
||||||
|
*/
|
||||||
|
public synchronized Object evaluate(String script, String fileName) {
|
||||||
|
throw new NotImplementedError("Not implemented!");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluate {@code script} and return a result. Note that the result must be one of the
|
* Evaluate {@code script} and return a result. Note that the result must be one of the
|
||||||
* supported Java types or the call will return null.
|
* supported Java types or the call will return null.
|
||||||
@@ -64,18 +76,18 @@ public final class Duktape implements Closeable, AutoCloseable {
|
|||||||
throw new NotImplementedError("Not implemented!");
|
throw new NotImplementedError("Not implemented!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * Attaches to a global JavaScript object called {@code name} that implements {@code type}.
|
* Attaches to a global JavaScript object called {@code name} that implements {@code type}.
|
||||||
// * {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
|
* {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
|
||||||
// * {@code type} must be an interface that does not extend any other interfaces, and cannot define
|
* {@code type} must be an interface that does not extend any other interfaces, and cannot define
|
||||||
// * any overloaded methods.
|
* any overloaded methods.
|
||||||
// * <p>Methods of the interface may return {@code void} or any of the following supported argument
|
* <p>Methods of the interface may return {@code void} or any of the following supported argument
|
||||||
// * types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
|
* types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
|
||||||
// * {@link Double}, {@link String}.
|
* {@link Double}, {@link String}.
|
||||||
// */
|
*/
|
||||||
// public synchronized <T> T get(final String name, final Class<T> type) {
|
public synchronized <T> T get(final String name, final Class<T> type) {
|
||||||
// throw new NotImplementedError("Not implemented!");
|
throw new NotImplementedError("Not implemented!");
|
||||||
// }
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release the native resources associated with this object. You <strong>must</strong> call this
|
* Release the native resources associated with this object. You <strong>must</strong> call this
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.squareup.duktape;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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/. */
|
|
||||||
// part of tachiyomi-extensions which was originally licensed under Apache License Version 2.0
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/** This is the reference Duktape stub that tachiyomi's extensions depend on.
|
|
||||||
* Intended to be used as a reference.
|
|
||||||
*/
|
|
||||||
public class DuktapeStub implements Closeable {
|
|
||||||
|
|
||||||
public static Duktape create() {
|
|
||||||
throw new RuntimeException("Stub!");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void close() throws IOException {
|
|
||||||
throw new RuntimeException("Stub!");
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized Object evaluate(String script) {
|
|
||||||
throw new RuntimeException("Stub!");
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized <T> void set(String name, Class<T> type, T object) {
|
|
||||||
throw new RuntimeException("Stub!");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,7 @@ data class InstalledPackage(val root: File) {
|
|||||||
val icon = File(root, "icon.png")
|
val icon = File(root, "icon.png")
|
||||||
|
|
||||||
val info: PackageInfo
|
val info: PackageInfo
|
||||||
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(apk).also {
|
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(root, apk).also {
|
||||||
val parsed = ApkFile(apk)
|
val parsed = ApkFile(apk)
|
||||||
val dbFactory = DocumentBuilderFactory.newInstance()
|
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||||
val dBuilder = dbFactory.newDocumentBuilder()
|
val dBuilder = dbFactory.newDocumentBuilder()
|
||||||
@@ -82,14 +82,12 @@ data class InstalledPackage(val root: File) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private fun NodeList.toList(): List<Node> {
|
||||||
fun NodeList.toList(): List<Node> {
|
val out = mutableListOf<Node>()
|
||||||
val out = mutableListOf<Node>()
|
|
||||||
|
|
||||||
for (i in 0 until length)
|
for(i in 0 until length)
|
||||||
out += item(i)
|
out += item(i)
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import android.content.pm.PackageInfo
|
|||||||
import net.dongliu.apk.parser.bean.ApkMeta
|
import net.dongliu.apk.parser.bean.ApkMeta
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
|
fun ApkMeta.toPackageInfo(root: File, apk: File): PackageInfo {
|
||||||
return PackageInfo().also {
|
return PackageInfo().also {
|
||||||
it.packageName = packageName
|
it.packageName = packageName
|
||||||
it.versionCode = versionCode.toInt()
|
it.versionCode = versionCode.toInt()
|
||||||
|
|||||||
37
README.md
37
README.md
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||

|

|
||||||
# Tachidesk
|
# Tachidesk
|
||||||
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||||
|
|
||||||
@@ -13,23 +13,23 @@ Here is a list of current features:
|
|||||||
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
|
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
|
||||||
- A library to save your mangas and categories to put them into.
|
- A library to save your mangas and categories to put them into.
|
||||||
- Searching and browsing installed sources.
|
- Searching and browsing installed sources.
|
||||||
- A decent chapter reader.
|
- A minimal chapter reader.
|
||||||
- Ability to download Mangas for offline read(This partially works)
|
- Ability to download Mangas for offline read(This partially works)
|
||||||
|
|
||||||
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
|
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
|
||||||
|
|
||||||
Anyways, for more info checkout [finished milestone #1](https://github.com/Suwayomi/Tachidesk/issues/2) and [milestone #2](https://github.com/Suwayomi/Tachidesk/projects/1) to see what's implemented in more detail.
|
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented in more detail.
|
||||||
|
|
||||||
## Downloading and Running the app
|
## Downloading and Running the app
|
||||||
### All Operating Systems
|
### All Operating Systems
|
||||||
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
|
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
|
||||||
|
|
||||||
Download the latest jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
|
Download the latest jar release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
|
||||||
|
|
||||||
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
Download the latest win32 release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
|
Download the latest win32 release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
|
||||||
|
|
||||||
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`. The rest works like the previous section.
|
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`. The rest works like the previous section.
|
||||||
|
|
||||||
@@ -62,35 +62,26 @@ This project has two components:
|
|||||||
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
### Prerequisite: Get Android stubs jar
|
### Get Android stubs jar
|
||||||
#### Manual download
|
#### Manual download
|
||||||
Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||||
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
|
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
|
||||||
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.
|
Run `AndroidCompat/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
|
||||||
### Prerequisite: Software dependencies
|
### building the jar
|
||||||
You need this software packages installed in order to build this project:
|
Run `./gradlew shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||||
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
|
||||||
- Nodejs LTS or latest
|
|
||||||
- Yarn
|
|
||||||
### 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
|
### building the Windows package
|
||||||
Run `./gradlew :server:windowsPackage` to build a server only bundle and `./gradlew :webUI:copyBuild :server:windowsPackage` to get a full bundle , the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
||||||
## Running for development purposes
|
## Running for development purposes
|
||||||
### `server` module
|
### `server` module
|
||||||
Follow [Get Android stubs jar](#prerequisite-get-android-stubs-jar) then run `./gradlew :server:run --stacktrace` to run the server
|
Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
|
||||||
### `webUI` module
|
### `webUI` module
|
||||||
How to do it is described in `webUI/react/README.md` but for short,
|
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)
|
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 opned automatically,
|
then `yarn start` to start the client if a new browser window doesn't start automatically,
|
||||||
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
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.
|
and supports HMR and all the other goodies you'll need.
|
||||||
|
|
||||||
## Credit
|
## Credit
|
||||||
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
|
|
||||||
|
|
||||||
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
||||||
|
|
||||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
||||||
@@ -101,7 +92,7 @@ Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this pro
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (C) Contributors to the Suwayomi project
|
Copyright (C) 2020-2021 Aria Moradi and contributors
|
||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
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
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "ir.armor.tachidesk"
|
group = "xyz.nulldev.ts"
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://maven.google.com/")
|
|
||||||
maven("https://jitpack.io")
|
maven("https://jitpack.io")
|
||||||
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
||||||
maven("https://dl.bintray.com/inorichi/maven")
|
maven("https://dl.bintray.com/inorichi/maven")
|
||||||
@@ -21,13 +20,13 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val projects = listOf(
|
val javaProjects = listOf(
|
||||||
project(":AndroidCompat"),
|
project(":AndroidCompat"),
|
||||||
project(":AndroidCompat:Config"),
|
project(":AndroidCompat:Config"),
|
||||||
project(":server")
|
project(":server")
|
||||||
)
|
)
|
||||||
|
|
||||||
configure(projects) {
|
configure(javaProjects) {
|
||||||
apply(plugin = "java")
|
apply(plugin = "java")
|
||||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||||
|
|
||||||
@@ -46,22 +45,23 @@ configure(projects) {
|
|||||||
// Kotlin
|
// Kotlin
|
||||||
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
||||||
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
||||||
implementation(kotlin("reflect", version = "1.4.21"))
|
|
||||||
testImplementation(kotlin("test", version = "1.4.21"))
|
testImplementation(kotlin("test", version = "1.4.21"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// coroutines
|
configure(listOf(
|
||||||
val coroutinesVersion = "1.4.2"
|
project(":AndroidCompat"),
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
project(":server"),
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
project(":AndroidCompat:Config")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
|
||||||
|
|
||||||
|
|
||||||
|
)) {
|
||||||
|
dependencies {
|
||||||
// Dependency Injection
|
// Dependency Injection
|
||||||
implementation("org.kodein.di:kodein-di-conf-jvm:7.1.0")
|
implementation("org.kodein.di:kodein-di-conf-jvm:7.1.0")
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("org.slf4j:slf4j-api:1.7.30")
|
implementation("org.slf4j:slf4j-api:1.7.30")
|
||||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
implementation("org.slf4j:slf4j-simple:1.7.30")
|
||||||
implementation("io.github.microutils:kotlin-logging:2.0.3")
|
implementation("io.github.microutils:kotlin-logging:2.0.3")
|
||||||
|
|
||||||
// RxJava
|
// RxJava
|
||||||
@@ -71,18 +71,13 @@ configure(projects) {
|
|||||||
// JSoup
|
// JSoup
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
implementation("org.jsoup:jsoup:1.13.1")
|
||||||
|
|
||||||
|
// Kotlin
|
||||||
|
implementation(kotlin("reflect", version = "1.4.21"))
|
||||||
|
|
||||||
// dependency of :AndroidCompat:Config
|
// dependency of :AndroidCompat:Config
|
||||||
implementation("com.typesafe:config:1.4.0")
|
implementation("com.typesafe:config:1.4.0")
|
||||||
implementation("io.github.config4k:config4k:0.4.2")
|
|
||||||
|
|
||||||
// to get application content root
|
// to get application content root
|
||||||
implementation("net.harawata:appdirs:1.2.0")
|
implementation("net.harawata:appdirs:1.2.0")
|
||||||
|
|
||||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
|
||||||
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
|
||||||
|
|
||||||
// APK parser
|
|
||||||
implementation("net.dongliu:apk-parser:2.6.10")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ plugins {
|
|||||||
id("edu.sc.seis.launch4j") version "2.4.9"
|
id("edu.sc.seis.launch4j") version "2.4.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
val TachideskVersion = "v0.2.7"
|
val TachideskVersion = "v0.2.6"
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -30,19 +30,19 @@ dependencies {
|
|||||||
|
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
val okhttpVersion = "4.10.0-RC1"
|
val okhttp_version = "4.10.0-RC1"
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp:$okhttp_version")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttp_version")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version")
|
||||||
implementation("com.squareup.okio:okio:2.9.0")
|
implementation("com.squareup.okio:okio:2.9.0")
|
||||||
|
|
||||||
|
|
||||||
// retrofit
|
// retrofit
|
||||||
val retrofitVersion = "2.9.0"
|
val retrofit_version = "2.9.0"
|
||||||
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
|
implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
|
||||||
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
|
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
|
||||||
implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion")
|
implementation("com.squareup.retrofit2:converter-gson:$retrofit_version")
|
||||||
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofitVersion")
|
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofit_version")
|
||||||
|
|
||||||
|
|
||||||
// reactivex
|
// reactivex
|
||||||
@@ -57,17 +57,26 @@ dependencies {
|
|||||||
|
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
implementation("org.jsoup:jsoup:1.13.1")
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
||||||
|
|
||||||
|
|
||||||
|
val coroutinesVersion = "1.3.9"
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
|
|
||||||
|
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||||
|
implementation(fileTree("lib/dex2jar/"))
|
||||||
|
|
||||||
// api
|
// api
|
||||||
implementation("io.javalin:javalin:3.12.0")
|
implementation("io.javalin:javalin:3.12.0")
|
||||||
|
implementation("org.slf4j:slf4j-simple:1.8.0-beta4")
|
||||||
|
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
||||||
|
|
||||||
// Exposed ORM
|
// Exposed ORM
|
||||||
val exposedVersion = "0.28.1"
|
val exposed_version = "0.28.1"
|
||||||
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
|
||||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
||||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
||||||
implementation("com.h2database:h2:1.4.199")
|
implementation("com.h2database:h2:1.4.199")
|
||||||
|
|
||||||
// tray icon
|
// tray icon
|
||||||
@@ -78,11 +87,9 @@ dependencies {
|
|||||||
implementation(project(":AndroidCompat"))
|
implementation(project(":AndroidCompat"))
|
||||||
implementation(project(":AndroidCompat:Config"))
|
implementation(project(":AndroidCompat:Config"))
|
||||||
|
|
||||||
// uncomment to test extensions directly
|
|
||||||
// implementation(fileTree("lib/"))
|
|
||||||
|
|
||||||
// Testing
|
// testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||||
testImplementation(kotlin("test-junit5"))
|
// testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||||
}
|
}
|
||||||
|
|
||||||
val name = "ir.armor.tachidesk.Main"
|
val name = "ir.armor.tachidesk.Main"
|
||||||
@@ -131,18 +138,6 @@ tasks {
|
|||||||
archiveVersion.set(TachideskVersion)
|
archiveVersion.set(TachideskVersion)
|
||||||
archiveClassifier.set(TachideskRevision)
|
archiveClassifier.set(TachideskRevision)
|
||||||
}
|
}
|
||||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
|
||||||
kotlinOptions {
|
|
||||||
freeCompilerArgs = listOf(
|
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
|
||||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
|
||||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
test {
|
|
||||||
useJUnit()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
launch4j { //used for windows
|
launch4j { //used for windows
|
||||||
@@ -203,8 +198,12 @@ tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<ShadowJar> {
|
tasks.withType<ShadowJar> {
|
||||||
destinationDirectory.set(File("$rootDir/server/build"))
|
destinationDir = File("$rootDir/server/build")
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
dependsOn("lintKotlin")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("processResources") {
|
||||||
|
dependsOn(":webUI:copyBuild")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named("run") {
|
tasks.named("run") {
|
||||||
|
|||||||
BIN
server/lib/dex2jar/ST4-4.0.8.jar
Normal file
BIN
server/lib/dex2jar/ST4-4.0.8.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/antlr-3.5.2.jar
Normal file
BIN
server/lib/dex2jar/antlr-3.5.2.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/antlr-runtime-3.5.2.jar
Normal file
BIN
server/lib/dex2jar/antlr-runtime-3.5.2.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/antlr4-4.5.jar
Normal file
BIN
server/lib/dex2jar/antlr4-4.5.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/antlr4-runtime-4.5.jar
Normal file
BIN
server/lib/dex2jar/antlr4-runtime-4.5.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/asm-debug-all-5.0.3.jar
Normal file
BIN
server/lib/dex2jar/asm-debug-all-5.0.3.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/d2j-base-cmd-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/d2j-base-cmd-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/d2j-jasmin-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/d2j-jasmin-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/d2j-smali-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/d2j-smali-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/dex-ir-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-ir-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/dex-reader-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-reader-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/dex-reader-api-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-reader-api-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/dex-tools-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-tools-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/dex-translator-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-translator-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/dex-writer-2.1-20190905-lanchon.jar
Normal file
BIN
server/lib/dex2jar/dex-writer-2.1-20190905-lanchon.jar
Normal file
Binary file not shown.
BIN
server/lib/dex2jar/dx-27.0.3.jar
Normal file
BIN
server/lib/dex2jar/dx-27.0.3.jar
Normal file
Binary file not shown.
67
server/lib/dex2jar/open-source-license.txt
Normal file
67
server/lib/dex2jar/open-source-license.txt
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
==== dx-*.jar
|
||||||
|
Apache 2.0 http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
|
||||||
|
==== antlr-*.jar
|
||||||
|
[The BSD License]
|
||||||
|
Copyright (c) 2003-2007, Terence Parr
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in
|
||||||
|
the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of the author nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||||
|
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||||
|
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
==== asm-*.jar
|
||||||
|
|
||||||
|
ASM: a very small and fast Java bytecode manipulation framework
|
||||||
|
Copyright (c) 2000-2005 INRIA, France Telecom
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
3. Neither the name of the copyright holders nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||||
|
THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
BIN
server/lib/dex2jar/org.abego.treelayout.core-1.0.1.jar
Normal file
BIN
server/lib/dex2jar/org.abego.treelayout.core-1.0.1.jar
Normal file
Binary file not shown.
252
server/src/main/java/ir/armor/tachidesk/APKExtractor.java
Normal file
252
server/src/main/java/ir/armor/tachidesk/APKExtractor.java
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
package ir.armor.tachidesk;
|
||||||
|
|
||||||
|
/* 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.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.NamedNodeMap;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
import org.xml.sax.InputSource;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
public class APKExtractor {
|
||||||
|
// decompressXML -- Parse the 'compressed' binary form of Android XML docs
|
||||||
|
// such as for AndroidManifest.xml in .apk files
|
||||||
|
public static int endDocTag = 0x00100101;
|
||||||
|
public static int startTag = 0x00100102;
|
||||||
|
public static int endTag = 0x00100103;
|
||||||
|
|
||||||
|
static void prt(String str) {
|
||||||
|
//System.err.print(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String decompressXML(byte[] xml) {
|
||||||
|
|
||||||
|
StringBuilder finalXML = new StringBuilder();
|
||||||
|
|
||||||
|
// Compressed XML file/bytes starts with 24x bytes of data,
|
||||||
|
// 9 32 bit words in little endian order (LSB first):
|
||||||
|
// 0th word is 03 00 08 00
|
||||||
|
// 3rd word SEEMS TO BE: Offset at then of StringTable
|
||||||
|
// 4th word is: Number of strings in string table
|
||||||
|
// WARNING: Sometime I indiscriminently display or refer to word in
|
||||||
|
// little endian storage format, or in integer format (ie MSB first).
|
||||||
|
int numbStrings = LEW(xml, 4 * 4);
|
||||||
|
|
||||||
|
// StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
|
||||||
|
// of the length/string data in the StringTable.
|
||||||
|
int sitOff = 0x24; // Offset of start of StringIndexTable
|
||||||
|
|
||||||
|
// StringTable, each string is represented with a 16 bit little endian
|
||||||
|
// character count, followed by that number of 16 bit (LE) (Unicode)
|
||||||
|
// chars.
|
||||||
|
int stOff = sitOff + numbStrings * 4; // StringTable follows
|
||||||
|
// StrIndexTable
|
||||||
|
|
||||||
|
// XMLTags, The XML tag tree starts after some unknown content after the
|
||||||
|
// StringTable. There is some unknown data after the StringTable, scan
|
||||||
|
// forward from this point to the flag for the start of an XML start
|
||||||
|
// tag.
|
||||||
|
int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd
|
||||||
|
// word.
|
||||||
|
// Scan forward until we find the bytes: 0x02011000(x00100102 in normal
|
||||||
|
// int)
|
||||||
|
for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) {
|
||||||
|
if (LEW(xml, ii) == startTag) {
|
||||||
|
xmlTagOff = ii;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} // end of hack, scanning for start of first start tag
|
||||||
|
|
||||||
|
// XML tags and attributes:
|
||||||
|
// Every XML start and end tag consists of 6 32 bit words:
|
||||||
|
// 0th word: 02011000 for startTag and 03011000 for endTag
|
||||||
|
// 1st word: a flag?, like 38000000
|
||||||
|
// 2nd word: Line of where this tag appeared in the original source file
|
||||||
|
// 3rd word: FFFFFFFF ??
|
||||||
|
// 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
|
||||||
|
// 5th word: StringIndex of Element Name
|
||||||
|
// (Note: 01011000 in 0th word means end of XML document, endDocTag)
|
||||||
|
|
||||||
|
// Start tags (not end tags) contain 3 more words:
|
||||||
|
// 6th word: 14001400 meaning??
|
||||||
|
// 7th word: Number of Attributes that follow this tag(follow word 8th)
|
||||||
|
// 8th word: 00000000 meaning??
|
||||||
|
|
||||||
|
// Attributes consist of 5 words:
|
||||||
|
// 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
|
||||||
|
// 1st word: StringIndex of Attribute Name
|
||||||
|
// 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId
|
||||||
|
// used
|
||||||
|
// 3rd word: Flags?
|
||||||
|
// 4th word: str ind of attr value again, or ResourceId of value
|
||||||
|
|
||||||
|
// TMP, dump string table to tr for debugging
|
||||||
|
// tr.addSelect("strings", null);
|
||||||
|
// for (int ii=0; ii<numbStrings; ii++) {
|
||||||
|
// // Length of string starts at StringTable plus offset in StrIndTable
|
||||||
|
// String str = compXmlString(xml, sitOff, stOff, ii);
|
||||||
|
// tr.add(String.valueOf(ii), str);
|
||||||
|
// }
|
||||||
|
// tr.parent();
|
||||||
|
|
||||||
|
// Step through the XML tree element tags and attributes
|
||||||
|
int off = xmlTagOff;
|
||||||
|
int indent = 0;
|
||||||
|
int startTagLineNo = -2;
|
||||||
|
while (off < xml.length) {
|
||||||
|
int tag0 = LEW(xml, off);
|
||||||
|
// int tag1 = LEW(xml, off+1*4);
|
||||||
|
int lineNo = LEW(xml, off + 2 * 4);
|
||||||
|
// int tag3 = LEW(xml, off+3*4);
|
||||||
|
int nameNsSi = LEW(xml, off + 4 * 4);
|
||||||
|
int nameSi = LEW(xml, off + 5 * 4);
|
||||||
|
|
||||||
|
if (tag0 == startTag) { // XML START TAG
|
||||||
|
int tag6 = LEW(xml, off + 6 * 4); // Expected to be 14001400
|
||||||
|
int numbAttrs = LEW(xml, off + 7 * 4); // Number of Attributes
|
||||||
|
// to follow
|
||||||
|
// int tag8 = LEW(xml, off+8*4); // Expected to be 00000000
|
||||||
|
off += 9 * 4; // Skip over 6+3 words of startTag data
|
||||||
|
String name = compXmlString(xml, sitOff, stOff, nameSi);
|
||||||
|
// tr.addSelect(name, null);
|
||||||
|
startTagLineNo = lineNo;
|
||||||
|
|
||||||
|
// Look for the Attributes
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
for (int ii = 0; ii < numbAttrs; ii++) {
|
||||||
|
int attrNameNsSi = LEW(xml, off); // AttrName Namespace Str
|
||||||
|
// Ind, or FFFFFFFF
|
||||||
|
int attrNameSi = LEW(xml, off + 1 * 4); // AttrName String
|
||||||
|
// Index
|
||||||
|
int attrValueSi = LEW(xml, off + 2 * 4); // AttrValue Str
|
||||||
|
// Ind, or
|
||||||
|
// FFFFFFFF
|
||||||
|
int attrFlags = LEW(xml, off + 3 * 4);
|
||||||
|
int attrResId = LEW(xml, off + 4 * 4); // AttrValue
|
||||||
|
// ResourceId or dup
|
||||||
|
// AttrValue StrInd
|
||||||
|
off += 5 * 4; // Skip over the 5 words of an attribute
|
||||||
|
|
||||||
|
String attrName = compXmlString(xml, sitOff, stOff,
|
||||||
|
attrNameSi);
|
||||||
|
String attrValue = attrValueSi != -1 ? compXmlString(xml,
|
||||||
|
sitOff, stOff, attrValueSi) : "resourceID 0x"
|
||||||
|
+ Integer.toHexString(attrResId);
|
||||||
|
sb.append(" " + attrName + "=\"" + attrValue + "\"");
|
||||||
|
// tr.add(attrName, attrValue);
|
||||||
|
}
|
||||||
|
finalXML.append("<" + name + sb + ">");
|
||||||
|
prtIndent(indent, "<" + name + sb + ">");
|
||||||
|
indent++;
|
||||||
|
|
||||||
|
} else if (tag0 == endTag) { // XML END TAG
|
||||||
|
indent--;
|
||||||
|
off += 6 * 4; // Skip over 6 words of endTag data
|
||||||
|
String name = compXmlString(xml, sitOff, stOff, nameSi);
|
||||||
|
finalXML.append("</" + name + ">");
|
||||||
|
prtIndent(indent, "</" + name + "> (line " + startTagLineNo
|
||||||
|
+ "-" + lineNo + ")");
|
||||||
|
// tr.parent(); // Step back up the NobTree
|
||||||
|
|
||||||
|
} else if (tag0 == endDocTag) { // END OF XML DOC TAG
|
||||||
|
break;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
prt(" Unrecognized tag code '" + Integer.toHexString(tag0)
|
||||||
|
+ "' at offset " + off);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} // end of while loop scanning tags and attributes of XML tree
|
||||||
|
//prt(" end at offset " + off);
|
||||||
|
return finalXML.toString();
|
||||||
|
} // end of decompressXML
|
||||||
|
|
||||||
|
public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) {
|
||||||
|
if (strInd < 0)
|
||||||
|
return null;
|
||||||
|
int strOff = stOff + LEW(xml, sitOff + strInd * 4);
|
||||||
|
return compXmlStringAt(xml, strOff);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String spaces = " ";
|
||||||
|
|
||||||
|
public static void prtIndent(int indent, String str) {
|
||||||
|
prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// compXmlStringAt -- Return the string stored in StringTable format at
|
||||||
|
// offset strOff. This offset points to the 16 bit string length, which
|
||||||
|
// is followed by that number of 16 bit (Unicode) chars.
|
||||||
|
public static String compXmlStringAt(byte[] arr, int strOff) {
|
||||||
|
int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff;
|
||||||
|
byte[] chars = new byte[strLen];
|
||||||
|
for (int ii = 0; ii < strLen; ii++) {
|
||||||
|
chars[ii] = arr[strOff + 2 + ii * 2];
|
||||||
|
}
|
||||||
|
return new String(chars); // Hack, just use 8 byte chars
|
||||||
|
} // end of compXmlStringAt
|
||||||
|
|
||||||
|
// LEW -- Return value of a Little Endian 32 bit word from the byte array
|
||||||
|
// at offset off.
|
||||||
|
public static int LEW(byte[] arr, int off) {
|
||||||
|
return arr[off + 3] << 24 & 0xff000000 | arr[off + 2] << 16 & 0xff0000
|
||||||
|
| arr[off + 1] << 8 & 0xff00 | arr[off] & 0xFF;
|
||||||
|
} // end of LEW
|
||||||
|
|
||||||
|
public static Document loadXMLFromString(String xml) throws Exception {
|
||||||
|
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||||
|
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
|
||||||
|
return docBuilder.parse(new InputSource(new StringReader(xml)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException {
|
||||||
|
ZipFile zip = null;
|
||||||
|
|
||||||
|
zip = new ZipFile(filePath);
|
||||||
|
ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml");
|
||||||
|
ZipEntry classesDex = zip.getEntry("classes.dex");
|
||||||
|
|
||||||
|
// write dex file
|
||||||
|
InputStream dexStream = zip.getInputStream(classesDex);
|
||||||
|
try (OutputStream os = Files.newOutputStream(Paths.get(dexPath))) {
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
while ((len = dexStream.read(buffer)) > 0) {
|
||||||
|
os.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read xml file
|
||||||
|
InputStream is = zip.getInputStream(androidManifest);
|
||||||
|
byte[] buf = new byte[1024000]; // 100 kb
|
||||||
|
is.read(buf);
|
||||||
|
is.close();
|
||||||
|
zip.close();
|
||||||
|
|
||||||
|
String xml = APKExtractor.decompressXML(buf);
|
||||||
|
try {
|
||||||
|
Document xmlDoc = loadXMLFromString(xml);
|
||||||
|
String pkg = xmlDoc.getDocumentElement().getAttribute("package");
|
||||||
|
NodeList nodes = xmlDoc.getElementsByTagName("meta-data");
|
||||||
|
for (int i = 0; i < nodes.getLength(); i++) {
|
||||||
|
NamedNodeMap attributes = nodes.item(i).getAttributes();
|
||||||
|
System.out.println(attributes.getNamedItem("name").getNodeValue());
|
||||||
|
if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class"))
|
||||||
|
return pkg + attributes.getNamedItem("value").getNodeValue();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
// import android.content.res.Configuration
|
// import android.content.res.Configuration
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 android.app.Application
|
import android.app.Application
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
// import eu.kanade.tachiyomi.data.cache.ChapterCache
|
// import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
|||||||
@@ -1,23 +1,51 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
package eu.kanade.tachiyomi.extension.api
|
||||||
|
|
||||||
/*
|
// import android.content.Context
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||||
|
// import kotlinx.coroutines.Dispatchers
|
||||||
|
// import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.int
|
import kotlinx.serialization.json.int
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
// import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
object ExtensionGithubApi {
|
internal class ExtensionGithubApi {
|
||||||
const val BASE_URL = "https://raw.githubusercontent.com"
|
|
||||||
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
|
// private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
|
val service: ExtensionGithubService = ExtensionGithubService.create()
|
||||||
|
|
||||||
|
val response = service.getRepo()
|
||||||
|
return parseResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// suspend fun checkForUpdates(): List<Extension.Installed> {
|
||||||
|
// val extensions = fin dExtensions()
|
||||||
|
//
|
||||||
|
// // preferences.lastExtCheck().set(Date().time)
|
||||||
|
//
|
||||||
|
// val installedExtensions = ExtensionLoader.loadExtensions(context)
|
||||||
|
// .filterIsInstance<LoadResult.Success>()
|
||||||
|
// .map { it.extension }
|
||||||
|
//
|
||||||
|
// val extensionsWithUpdate = mutableListOf<Extension.Installed>()
|
||||||
|
// for (installedExt in installedExtensions) {
|
||||||
|
// val pkgName = installedExt.pkgName
|
||||||
|
// val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
|
||||||
|
//
|
||||||
|
// val hasUpdate = availableExt.versionCode > installedExt.versionCode
|
||||||
|
// if (hasUpdate) {
|
||||||
|
// extensionsWithUpdate.add(installedExt)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return extensionsWithUpdate
|
||||||
|
// }
|
||||||
|
|
||||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||||
return json
|
return json
|
||||||
@@ -40,14 +68,16 @@ object ExtensionGithubApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
val service: ExtensionGithubService = ExtensionGithubService.create()
|
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||||
|
|
||||||
val response = service.getRepo()
|
|
||||||
return parseResponse(response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApkUrl(extension: ExtensionDataClass): String {
|
fun getApkUrl(extension: ExtensionDataClass): String {
|
||||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||||
|
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.extension.api
|
|||||||
|
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
@@ -10,6 +9,8 @@ import retrofit2.Retrofit
|
|||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
// import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to get the extension repo listing from GitHub.
|
* Used to get the extension repo listing from GitHub.
|
||||||
*/
|
*/
|
||||||
@@ -29,7 +30,6 @@ interface ExtensionGithubService {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalSerializationApi
|
|
||||||
fun create(): ExtensionGithubService {
|
fun create(): ExtensionGithubService {
|
||||||
val adapter = Retrofit.Builder()
|
val adapter = Retrofit.Builder()
|
||||||
.baseUrl(ExtensionGithubApi.BASE_URL)
|
.baseUrl(ExtensionGithubApi.BASE_URL)
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.extension.util
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 android.annotation.SuppressLint
|
// import android.annotation.SuppressLint
|
||||||
// import android.content.Context
|
// import android.content.Context
|
||||||
// import android.content.pm.PackageInfo
|
// import android.content.pm.PackageInfo
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 android.annotation.SuppressLint
|
// import android.annotation.SuppressLint
|
||||||
// import android.content.Context
|
// import android.content.Context
|
||||||
// import android.os.Build
|
// import android.os.Build
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 android.content.Context
|
// import android.content.Context
|
||||||
// import eu.kanade.tachiyomi.BuildConfig
|
// import eu.kanade.tachiyomi.BuildConfig
|
||||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
|||||||
@@ -1,358 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
// import com.github.junrar.Archive
|
|
||||||
// import com.google.gson.JsonParser
|
|
||||||
// import eu.kanade.tachiyomi.R
|
|
||||||
// import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
// import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
// import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
// import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
// import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
// import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
// import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
|
||||||
// import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
|
||||||
// import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|
||||||
// import eu.kanade.tachiyomi.util.storage.EpubFile
|
|
||||||
// import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
// import rx.Observable
|
|
||||||
// import timber.log.Timber
|
|
||||||
// import java.io.File
|
|
||||||
// import java.io.FileInputStream
|
|
||||||
// import java.io.InputStream
|
|
||||||
// import java.util.Locale
|
|
||||||
// import java.util.concurrent.TimeUnit
|
|
||||||
// import java.util.zip.ZipFile
|
|
||||||
|
|
||||||
class LocalSource(private val context: Context) : CatalogueSource {
|
|
||||||
companion object {
|
|
||||||
const val ID = 0L
|
|
||||||
// const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
|
||||||
//
|
|
||||||
// private const val COVER_NAME = "cover.jpg"
|
|
||||||
// private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
|
||||||
//
|
|
||||||
// private val POPULAR_FILTERS = FilterList(OrderBy())
|
|
||||||
// private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
|
||||||
// private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
|
||||||
//
|
|
||||||
// fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
|
||||||
// val dir = getBaseDirectories(context).firstOrNull()
|
|
||||||
// if (dir == null) {
|
|
||||||
// input.close()
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
// val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
|
||||||
//
|
|
||||||
// // It might not exist if using the external SD card
|
|
||||||
// cover.parentFile?.mkdirs()
|
|
||||||
// input.use {
|
|
||||||
// cover.outputStream().use {
|
|
||||||
// input.copyTo(it)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return cover
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun getBaseDirectories(context: Context): List<File> {
|
|
||||||
// val c = context.getString(R.string.app_name) + File.separator + "local"
|
|
||||||
// return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
override val id = ID
|
|
||||||
override val name = "Local source"
|
|
||||||
override val lang = ""
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// override fun toString() = context.getString(R.string.local_source)
|
|
||||||
//
|
|
||||||
// override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
|
||||||
//
|
|
||||||
// override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
// val baseDirs = getBaseDirectories(context)
|
|
||||||
//
|
|
||||||
// val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
|
||||||
// var mangaDirs = baseDirs
|
|
||||||
// .asSequence()
|
|
||||||
// .mapNotNull { it.listFiles()?.toList() }
|
|
||||||
// .flatten()
|
|
||||||
// .filter { it.isDirectory }
|
|
||||||
// .filterNot { it.name.startsWith('.') }
|
|
||||||
// .filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
|
||||||
// .distinctBy { it.name }
|
|
||||||
//
|
|
||||||
// val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
|
||||||
// when (state?.index) {
|
|
||||||
// 0 -> {
|
|
||||||
// mangaDirs = if (state.ascending) {
|
|
||||||
// mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
|
|
||||||
// } else {
|
|
||||||
// mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// 1 -> {
|
|
||||||
// mangaDirs = if (state.ascending) {
|
|
||||||
// mangaDirs.sortedBy(File::lastModified)
|
|
||||||
// } else {
|
|
||||||
// mangaDirs.sortedByDescending(File::lastModified)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val mangas = mangaDirs.map { mangaDir ->
|
|
||||||
// SManga.create().apply {
|
|
||||||
// title = mangaDir.name
|
|
||||||
// url = mangaDir.name
|
|
||||||
//
|
|
||||||
// // Try to find the cover
|
|
||||||
// for (dir in baseDirs) {
|
|
||||||
// val cover = File("${dir.absolutePath}/$url", COVER_NAME)
|
|
||||||
// if (cover.exists()) {
|
|
||||||
// thumbnail_url = cover.absolutePath
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val chapters = fetchChapterList(this).toBlocking().first()
|
|
||||||
// if (chapters.isNotEmpty()) {
|
|
||||||
// val chapter = chapters.last()
|
|
||||||
// val format = getFormat(chapter)
|
|
||||||
// if (format is Format.Epub) {
|
|
||||||
// EpubFile(format.file).use { epub ->
|
|
||||||
// epub.fillMangaMetadata(this)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Copy the cover from the first chapter found.
|
|
||||||
// if (thumbnail_url == null) {
|
|
||||||
// try {
|
|
||||||
// val dest = updateCover(chapter, this)
|
|
||||||
// thumbnail_url = dest?.absolutePath
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// Timber.e(e)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return Observable.just(MangasPage(mangas.toList(), false))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
|
||||||
//
|
|
||||||
// override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
// getBaseDirectories(context)
|
|
||||||
// .asSequence()
|
|
||||||
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
|
||||||
// .flatten()
|
|
||||||
// .firstOrNull { it.extension == "json" }
|
|
||||||
// ?.apply {
|
|
||||||
// val reader = this.inputStream().bufferedReader()
|
|
||||||
// val json = JsonParser.parseReader(reader).asJsonObject
|
|
||||||
//
|
|
||||||
// manga.title = json["title"]?.asString ?: manga.title
|
|
||||||
// manga.author = json["author"]?.asString ?: manga.author
|
|
||||||
// manga.artist = json["artist"]?.asString ?: manga.artist
|
|
||||||
// manga.description = json["description"]?.asString ?: manga.description
|
|
||||||
// manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString }
|
|
||||||
// ?: manga.genre
|
|
||||||
// manga.status = json["status"]?.asInt ?: manga.status
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return Observable.just(manga)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
// val chapters = getBaseDirectories(context)
|
|
||||||
// .asSequence()
|
|
||||||
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
|
||||||
// .flatten()
|
|
||||||
// .filter { it.isDirectory || isSupportedFile(it.extension) }
|
|
||||||
// .map { chapterFile ->
|
|
||||||
// SChapter.create().apply {
|
|
||||||
// url = "${manga.url}/${chapterFile.name}"
|
|
||||||
// name = if (chapterFile.isDirectory) {
|
|
||||||
// chapterFile.name
|
|
||||||
// } else {
|
|
||||||
// chapterFile.nameWithoutExtension
|
|
||||||
// }
|
|
||||||
// date_upload = chapterFile.lastModified()
|
|
||||||
//
|
|
||||||
// val format = getFormat(this)
|
|
||||||
// if (format is Format.Epub) {
|
|
||||||
// EpubFile(format.file).use { epub ->
|
|
||||||
// epub.fillChapterMetadata(this)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val chapNameCut = stripMangaTitle(name, manga.title)
|
|
||||||
// if (chapNameCut.isNotEmpty()) name = chapNameCut
|
|
||||||
// ChapterRecognition.parseChapterNumber(this, manga)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .sortedWith(
|
|
||||||
// Comparator { c1, c2 ->
|
|
||||||
// val c = c2.chapter_number.compareTo(c1.chapter_number)
|
|
||||||
// if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// .toList()
|
|
||||||
//
|
|
||||||
// return Observable.just(chapters)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace
|
|
||||||
// * characters.
|
|
||||||
// */
|
|
||||||
// private fun stripMangaTitle(chapterName: String, mangaTitle: String): String {
|
|
||||||
// var chapterNameIndex = 0
|
|
||||||
// var mangaTitleIndex = 0
|
|
||||||
// while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
|
||||||
// val chapterChar = chapterName[chapterNameIndex]
|
|
||||||
// val mangaChar = mangaTitle[mangaTitleIndex]
|
|
||||||
// if (!chapterChar.equals(mangaChar, true)) {
|
|
||||||
// val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
|
||||||
// val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
|
||||||
//
|
|
||||||
// if (!invalidChapterChar && !invalidMangaChar) {
|
|
||||||
// return chapterName
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (invalidChapterChar) {
|
|
||||||
// chapterNameIndex++
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (invalidMangaChar) {
|
|
||||||
// mangaTitleIndex++
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// chapterNameIndex++
|
|
||||||
// mangaTitleIndex++
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
|
||||||
// return Observable.error(Exception("Unused"))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun isSupportedFile(extension: String): Boolean {
|
|
||||||
// return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fun getFormat(chapter: SChapter): Format {
|
|
||||||
// val baseDirs = getBaseDirectories(context)
|
|
||||||
//
|
|
||||||
// for (dir in baseDirs) {
|
|
||||||
// val chapFile = File(dir, chapter.url)
|
|
||||||
// if (!chapFile.exists()) continue
|
|
||||||
//
|
|
||||||
// return getFormat(chapFile)
|
|
||||||
// }
|
|
||||||
// throw Exception("Chapter not found")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun getFormat(file: File): Format {
|
|
||||||
// val extension = file.extension
|
|
||||||
// return if (file.isDirectory) {
|
|
||||||
// Format.Directory(file)
|
|
||||||
// } else if (extension.equals("zip", true) || extension.equals("cbz", true)) {
|
|
||||||
// Format.Zip(file)
|
|
||||||
// } else if (extension.equals("rar", true) || extension.equals("cbr", true)) {
|
|
||||||
// Format.Rar(file)
|
|
||||||
// } else if (extension.equals("epub", true)) {
|
|
||||||
// Format.Epub(file)
|
|
||||||
// } else {
|
|
||||||
// throw Exception("Invalid chapter format")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
|
||||||
// return when (val format = getFormat(chapter)) {
|
|
||||||
// is Format.Directory -> {
|
|
||||||
// val entry = format.file.listFiles()
|
|
||||||
// ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
|
||||||
// ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
|
||||||
//
|
|
||||||
// entry?.let { updateCover(context, manga, it.inputStream()) }
|
|
||||||
// }
|
|
||||||
// is Format.Zip -> {
|
|
||||||
// ZipFile(format.file).use { zip ->
|
|
||||||
// val entry = zip.entries().toList()
|
|
||||||
// .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
|
||||||
// .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
|
||||||
//
|
|
||||||
// entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// is Format.Rar -> {
|
|
||||||
// Archive(format.file).use { archive ->
|
|
||||||
// val entry = archive.fileHeaders
|
|
||||||
// .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
|
||||||
// .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
|
||||||
//
|
|
||||||
// entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// is Format.Epub -> {
|
|
||||||
// EpubFile(format.file).use { epub ->
|
|
||||||
// val entry = epub.getImagesFromPages()
|
|
||||||
// .firstOrNull()
|
|
||||||
// ?.let { epub.getEntry(it) }
|
|
||||||
//
|
|
||||||
// entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Selection(0, true))
|
|
||||||
//
|
|
||||||
// override fun getFilterList() = FilterList(OrderBy())
|
|
||||||
//
|
|
||||||
// sealed class Format {
|
|
||||||
// data class Directory(val file: File) : Format()
|
|
||||||
// data class Zip(val file: File) : Format()
|
|
||||||
// data class Rar(val file: File) : Format()
|
|
||||||
// data class Epub(val file: File) : Format()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.util.lang
|
|
||||||
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
object Hash {
|
|
||||||
|
|
||||||
private val chars = charArrayOf(
|
|
||||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
|
||||||
'a', 'b', 'c', 'd', 'e', 'f'
|
|
||||||
)
|
|
||||||
|
|
||||||
private val MD5 get() = MessageDigest.getInstance("MD5")
|
|
||||||
|
|
||||||
private val SHA256 get() = MessageDigest.getInstance("SHA-256")
|
|
||||||
|
|
||||||
fun sha256(bytes: ByteArray): String {
|
|
||||||
return encodeHex(SHA256.digest(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sha256(string: String): String {
|
|
||||||
return sha256(string.toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun md5(bytes: ByteArray): String {
|
|
||||||
return encodeHex(MD5.digest(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun md5(string: String): String {
|
|
||||||
return md5(string.toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun encodeHex(data: ByteArray): String {
|
|
||||||
val l = data.size
|
|
||||||
val out = CharArray(l shl 1)
|
|
||||||
var i = 0
|
|
||||||
var j = 0
|
|
||||||
while (i < l) {
|
|
||||||
out[j++] = chars[(240 and data[i].toInt()).ushr(4)]
|
|
||||||
out[j++] = chars[15 and data[i].toInt()]
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return String(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,260 @@
|
|||||||
package ir.armor.tachidesk
|
package ir.armor.tachidesk
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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 ir.armor.tachidesk.server.JavalinSetup.javalinSetup
|
import io.javalin.Javalin
|
||||||
import ir.armor.tachidesk.server.applicationSetup
|
import ir.armor.tachidesk.util.addMangaToCategory
|
||||||
|
import ir.armor.tachidesk.util.addMangaToLibrary
|
||||||
|
import ir.armor.tachidesk.util.createCategory
|
||||||
|
import ir.armor.tachidesk.util.getCategoryList
|
||||||
|
import ir.armor.tachidesk.util.getCategoryMangaList
|
||||||
|
import ir.armor.tachidesk.util.getChapter
|
||||||
|
import ir.armor.tachidesk.util.getChapterList
|
||||||
|
import ir.armor.tachidesk.util.getExtensionIcon
|
||||||
|
import ir.armor.tachidesk.util.getExtensionList
|
||||||
|
import ir.armor.tachidesk.util.getLibraryMangas
|
||||||
|
import ir.armor.tachidesk.util.getManga
|
||||||
|
import ir.armor.tachidesk.util.getMangaCategories
|
||||||
|
import ir.armor.tachidesk.util.getMangaList
|
||||||
|
import ir.armor.tachidesk.util.getPageImage
|
||||||
|
import ir.armor.tachidesk.util.getSource
|
||||||
|
import ir.armor.tachidesk.util.getSourceList
|
||||||
|
import ir.armor.tachidesk.util.getThumbnail
|
||||||
|
import ir.armor.tachidesk.util.installAPK
|
||||||
|
import ir.armor.tachidesk.util.openInBrowser
|
||||||
|
import ir.armor.tachidesk.util.removeCategory
|
||||||
|
import ir.armor.tachidesk.util.removeExtension
|
||||||
|
import ir.armor.tachidesk.util.removeMangaFromCategory
|
||||||
|
import ir.armor.tachidesk.util.removeMangaFromLibrary
|
||||||
|
import ir.armor.tachidesk.util.reorderCategory
|
||||||
|
import ir.armor.tachidesk.util.sourceFilters
|
||||||
|
import ir.armor.tachidesk.util.sourceGlobalSearch
|
||||||
|
import ir.armor.tachidesk.util.sourceSearch
|
||||||
|
import ir.armor.tachidesk.util.updateCategory
|
||||||
|
|
||||||
class Main {
|
class Main {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
applicationSetup()
|
serverSetup()
|
||||||
javalinSetup()
|
|
||||||
|
var hasWebUiBundled: Boolean = false
|
||||||
|
|
||||||
|
val app = Javalin.create { config ->
|
||||||
|
try {
|
||||||
|
this::class.java.classLoader.getResource("/react/index.html")
|
||||||
|
hasWebUiBundled = true
|
||||||
|
config.addStaticFiles("/react")
|
||||||
|
config.addSinglePageRoot("/", "/react/index.html")
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
println("Warning: react build files are missing.")
|
||||||
|
hasWebUiBundled = false
|
||||||
|
}
|
||||||
|
config.enableCorsForAllOrigins()
|
||||||
|
}.start(serverConfig.ip, serverConfig.port)
|
||||||
|
if (hasWebUiBundled) {
|
||||||
|
openInBrowser()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.exception(NullPointerException::class.java) { _, ctx ->
|
||||||
|
ctx.status(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/extension/list") { ctx ->
|
||||||
|
ctx.json(getExtensionList())
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
||||||
|
val apkName = ctx.pathParam("apkName")
|
||||||
|
println("installing $apkName")
|
||||||
|
|
||||||
|
ctx.status(
|
||||||
|
installAPK(apkName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
|
||||||
|
val apkName = ctx.pathParam("apkName")
|
||||||
|
println("uninstalling $apkName")
|
||||||
|
removeExtension(apkName)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// icon for extension named `apkName`
|
||||||
|
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
||||||
|
val apkName = ctx.pathParam("apkName")
|
||||||
|
val result = getExtensionIcon(apkName)
|
||||||
|
|
||||||
|
ctx.result(result.first)
|
||||||
|
ctx.header("content-type", result.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// list of sources
|
||||||
|
app.get("/api/v1/source/list") { ctx ->
|
||||||
|
ctx.json(getSourceList())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch source with id `sourceId`
|
||||||
|
app.get("/api/v1/source/:sourceId") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
ctx.json(getSource(sourceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// popular mangas from source with id `sourceId`
|
||||||
|
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
|
ctx.json(getMangaList(sourceId, pageNum, popular = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// latest mangas from source with id `sourceId`
|
||||||
|
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
|
ctx.json(getMangaList(sourceId, pageNum, popular = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// get manga info
|
||||||
|
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
ctx.json(getManga(mangaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// manga thumbnail
|
||||||
|
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val result = getThumbnail(mangaId)
|
||||||
|
|
||||||
|
ctx.result(result.first)
|
||||||
|
ctx.header("content-type", result.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// adds the manga to library
|
||||||
|
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
addMangaToLibrary(mangaId)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes the manga from the library
|
||||||
|
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
removeMangaFromLibrary(mangaId)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// list manga's categories
|
||||||
|
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
ctx.json(getMangaCategories(mangaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// adds the manga to category
|
||||||
|
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
addMangaToCategory(mangaId, categoryId)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes the manga from the category
|
||||||
|
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
removeMangaFromCategory(mangaId, categoryId)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
ctx.json(getChapterList(mangaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||||
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
ctx.json(getChapter(chapterIndex, mangaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
|
||||||
|
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val index = ctx.pathParam("index").toInt()
|
||||||
|
val result = getPageImage(mangaId, chapterId, index)
|
||||||
|
|
||||||
|
ctx.result(result.first)
|
||||||
|
ctx.header("content-type", result.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// global search
|
||||||
|
app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||||
|
val searchTerm = ctx.pathParam("searchTerm")
|
||||||
|
ctx.json(sourceGlobalSearch(searchTerm))
|
||||||
|
}
|
||||||
|
|
||||||
|
// single source search
|
||||||
|
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
val searchTerm = ctx.pathParam("searchTerm")
|
||||||
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
|
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
|
||||||
|
}
|
||||||
|
|
||||||
|
// source filter list
|
||||||
|
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
ctx.json(sourceFilters(sourceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// lists mangas that have no category assigned
|
||||||
|
app.get("/api/v1/library/") { ctx ->
|
||||||
|
ctx.json(getLibraryMangas())
|
||||||
|
}
|
||||||
|
|
||||||
|
// category list
|
||||||
|
app.get("/api/v1/category/") { ctx ->
|
||||||
|
ctx.json(getCategoryList())
|
||||||
|
}
|
||||||
|
|
||||||
|
// category create
|
||||||
|
app.post("/api/v1/category/") { ctx ->
|
||||||
|
val name = ctx.formParam("name")!!
|
||||||
|
createCategory(name)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// category modification
|
||||||
|
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
val name = ctx.formParam("name")
|
||||||
|
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
||||||
|
updateCategory(categoryId, name, isLanding)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// category re-ordering
|
||||||
|
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
val from = ctx.formParam("from")!!.toInt()
|
||||||
|
val to = ctx.formParam("to")!!.toInt()
|
||||||
|
reorderCategory(categoryId, from, to)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// category delete
|
||||||
|
app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
removeCategory(categoryId)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the manga list associated with a category
|
||||||
|
app.get("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
ctx.json(getCategoryMangaList(categoryId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
server/src/main/kotlin/ir/armor/tachidesk/ServerConfig.kt
Normal file
25
server/src/main/kotlin/ir/armor/tachidesk/ServerConfig.kt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package ir.armor.tachidesk
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import xyz.nulldev.ts.config.ConfigModule
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ServerConfig(config: Config) : ConfigModule(config) {
|
||||||
|
val ip = config.getString("ip")
|
||||||
|
val port = config.getInt("port")
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
val socksProxy = config.getBoolean("socksProxy")
|
||||||
|
val socksProxyHost = config.getString("socksProxyHost")
|
||||||
|
val socksProxyPort = config.getString("socksProxyPort")
|
||||||
|
|
||||||
|
fun registerFile(file: String): File {
|
||||||
|
return File(file).apply {
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun register(config: Config) = ServerConfig(config.getConfig("server"))
|
||||||
|
}
|
||||||
|
}
|
||||||
60
server/src/main/kotlin/ir/armor/tachidesk/ServerSetup.kt
Normal file
60
server/src/main/kotlin/ir/armor/tachidesk/ServerSetup.kt
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package ir.armor.tachidesk
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.App
|
||||||
|
import ir.armor.tachidesk.database.makeDataBaseTables
|
||||||
|
import ir.armor.tachidesk.util.systemTray
|
||||||
|
import net.harawata.appdirs.AppDirsFactory
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import xyz.nulldev.androidcompat.AndroidCompat
|
||||||
|
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||||
|
import xyz.nulldev.ts.config.ConfigKodeinModule
|
||||||
|
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object applicationDirs {
|
||||||
|
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
|
||||||
|
val extensionsRoot = "$dataRoot/extensions"
|
||||||
|
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||||
|
val mangaRoot = "$dataRoot/manga"
|
||||||
|
}
|
||||||
|
|
||||||
|
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
||||||
|
|
||||||
|
val systemTray by lazy { systemTray() }
|
||||||
|
|
||||||
|
val androidCompat by lazy { AndroidCompat() }
|
||||||
|
|
||||||
|
fun serverSetup() {
|
||||||
|
// register server config
|
||||||
|
GlobalConfigManager.registerModule(
|
||||||
|
ServerConfig.register(GlobalConfigManager.config)
|
||||||
|
)
|
||||||
|
|
||||||
|
// make dirs we need
|
||||||
|
listOf(
|
||||||
|
applicationDirs.dataRoot,
|
||||||
|
applicationDirs.extensionsRoot,
|
||||||
|
"${applicationDirs.extensionsRoot}/icon",
|
||||||
|
applicationDirs.thumbnailsRoot
|
||||||
|
).forEach {
|
||||||
|
File(it).mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
makeDataBaseTables()
|
||||||
|
|
||||||
|
// create system tray
|
||||||
|
systemTray
|
||||||
|
|
||||||
|
// Load config API
|
||||||
|
DI.global.addImport(ConfigKodeinModule().create())
|
||||||
|
// Load Android compatibility dependencies
|
||||||
|
AndroidCompatInitializer().init()
|
||||||
|
// start app
|
||||||
|
androidCompat.startApp(App())
|
||||||
|
|
||||||
|
// socks proxy settings
|
||||||
|
System.getProperties()["proxySet"] = serverConfig.socksProxy.toString()
|
||||||
|
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
|
||||||
|
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
|
||||||
|
}
|
||||||
@@ -1,30 +1,23 @@
|
|||||||
package ir.armor.tachidesk.model.dataclass
|
package ir.armor.tachidesk.database
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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 ir.armor.tachidesk.model.database.CategoryMangaTable
|
import ir.armor.tachidesk.applicationDirs
|
||||||
import ir.armor.tachidesk.model.database.CategoryTable
|
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||||
import ir.armor.tachidesk.model.database.ChapterTable
|
import ir.armor.tachidesk.database.table.CategoryTable
|
||||||
import ir.armor.tachidesk.model.database.ExtensionTable
|
import ir.armor.tachidesk.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.model.database.PageTable
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
import ir.armor.tachidesk.database.table.PageTable
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
import ir.armor.tachidesk.database.table.SourceTable
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.conf.global
|
|
||||||
import org.kodein.di.instance
|
|
||||||
|
|
||||||
object DBMangaer {
|
object DBMangaer {
|
||||||
val db by lazy {
|
val db by lazy {
|
||||||
val applicationDirs by DI.global.instance<ApplicationDirs>()
|
|
||||||
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package ir.armor.tachidesk.model.dataclass
|
package ir.armor.tachidesk.database.dataclass
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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/. */
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ir.armor.tachidesk.database.dataclass
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
data class ChapterDataClass(
|
||||||
|
val id: Int,
|
||||||
|
val url: String,
|
||||||
|
val name: String,
|
||||||
|
val date_upload: Long,
|
||||||
|
val chapter_number: Float,
|
||||||
|
val scanlator: String?,
|
||||||
|
val mangaId: Int,
|
||||||
|
val chapterIndex: Int,
|
||||||
|
val chapterCount: Int,
|
||||||
|
val pageCount: Int? = null,
|
||||||
|
)
|
||||||
@@ -1,24 +1,18 @@
|
|||||||
package ir.armor.tachidesk.model.dataclass
|
package ir.armor.tachidesk.database.dataclass
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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/. */
|
||||||
|
|
||||||
data class ExtensionDataClass(
|
data class ExtensionDataClass(
|
||||||
val apkName: String,
|
|
||||||
val iconUrl: String,
|
|
||||||
|
|
||||||
val name: String,
|
val name: String,
|
||||||
val pkgName: String,
|
val pkgName: String,
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
val versionCode: Int,
|
val versionCode: Int,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val isNsfw: Boolean,
|
val isNsfw: Boolean,
|
||||||
|
val apkName: String,
|
||||||
|
val iconUrl: String,
|
||||||
val installed: Boolean,
|
val installed: Boolean,
|
||||||
val hasUpdate: Boolean,
|
val classFQName: String,
|
||||||
val obsolete: Boolean,
|
|
||||||
)
|
)
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
package ir.armor.tachidesk.model.dataclass
|
package ir.armor.tachidesk.database.dataclass
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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 ir.armor.tachidesk.model.database.MangaStatus
|
import ir.armor.tachidesk.database.table.MangaStatus
|
||||||
|
|
||||||
data class MangaDataClass(
|
data class MangaDataClass(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package ir.armor.tachidesk.model.dataclass
|
package ir.armor.tachidesk.database.dataclass
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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/. */
|
||||||
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package ir.armor.tachidesk.model.dataclass
|
package ir.armor.tachidesk.database.dataclass
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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/. */
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package ir.armor.tachidesk.database.entity
|
||||||
|
|
||||||
|
/* 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 ir.armor.tachidesk.database.table.ExtensionTable
|
||||||
|
import org.jetbrains.exposed.dao.IntEntity
|
||||||
|
import org.jetbrains.exposed.dao.IntEntityClass
|
||||||
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
|
||||||
|
class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||||
|
companion object : IntEntityClass<ExtensionEntity>(ExtensionTable)
|
||||||
|
|
||||||
|
var name by ExtensionTable.name
|
||||||
|
var pkgName by ExtensionTable.pkgName
|
||||||
|
var versionName by ExtensionTable.versionName
|
||||||
|
var versionCode by ExtensionTable.versionCode
|
||||||
|
var lang by ExtensionTable.lang
|
||||||
|
var isNsfw by ExtensionTable.isNsfw
|
||||||
|
var apkName by ExtensionTable.apkName
|
||||||
|
var iconUrl by ExtensionTable.iconUrl
|
||||||
|
var installed by ExtensionTable.installed
|
||||||
|
var classFQName by ExtensionTable.classFQName
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package ir.armor.tachidesk.database.entity
|
||||||
|
|
||||||
|
/* 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 ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import org.jetbrains.exposed.dao.IntEntity
|
||||||
|
import org.jetbrains.exposed.dao.IntEntityClass
|
||||||
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
|
||||||
|
class MangaEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||||
|
companion object : IntEntityClass<MangaEntity>(MangaTable)
|
||||||
|
|
||||||
|
var url by MangaTable.url
|
||||||
|
var title by MangaTable.title
|
||||||
|
var initialized by MangaTable.initialized
|
||||||
|
|
||||||
|
var artist by MangaTable.artist
|
||||||
|
var author by MangaTable.author
|
||||||
|
var description by MangaTable.description
|
||||||
|
var genre by MangaTable.genre
|
||||||
|
var status by MangaTable.status
|
||||||
|
var thumbnail_url by MangaTable.thumbnail_url
|
||||||
|
|
||||||
|
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ir.armor.tachidesk.database.entity
|
||||||
|
|
||||||
|
/* 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 ir.armor.tachidesk.database.table.SourceTable
|
||||||
|
import org.jetbrains.exposed.dao.EntityClass
|
||||||
|
import org.jetbrains.exposed.dao.LongEntity
|
||||||
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
|
||||||
|
class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
|
||||||
|
companion object : EntityClass<Long, SourceEntity>(SourceTable, null)
|
||||||
|
|
||||||
|
var sourceId by SourceTable.id
|
||||||
|
var name by SourceTable.name
|
||||||
|
var lang by SourceTable.lang
|
||||||
|
var extension by ExtensionEntity referencedOn SourceTable.extension
|
||||||
|
var partOfFactorySource by SourceTable.partOfFactorySource
|
||||||
|
var positionInFactorySource by SourceTable.positionInFactorySource
|
||||||
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
object CategoryMangaTable : IntIdTable() {
|
object CategoryMangaTable : IntIdTable() {
|
||||||
val category = reference("category", CategoryTable)
|
val category = reference("category", CategoryTable)
|
||||||
val manga = reference("manga", MangaTable)
|
val manga = reference("manga", MangaTable)
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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 ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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/. */
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* 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.dao.id.IntIdTable
|
||||||
|
|
||||||
|
object ExtensionTable : IntIdTable() {
|
||||||
|
val name = varchar("name", 128)
|
||||||
|
val pkgName = varchar("pkg_name", 128)
|
||||||
|
val versionName = varchar("version_name", 16)
|
||||||
|
val versionCode = integer("version_code")
|
||||||
|
val lang = varchar("lang", 10)
|
||||||
|
val isNsfw = bool("is_nsfw")
|
||||||
|
val apkName = varchar("apk_name", 1024)
|
||||||
|
val iconUrl = varchar("icon_url", 2048)
|
||||||
|
|
||||||
|
val installed = bool("installed").default(false)
|
||||||
|
val classFQName = varchar("class_name", 256).default("") // fully qualified name
|
||||||
|
}
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
import ir.armor.tachidesk.util.proxyThumbnailUrl
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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/. */
|
||||||
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
/*
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* 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
|
* 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/. */
|
||||||
|
|
||||||
@@ -15,4 +12,5 @@ object SourceTable : IdTable<Long>() {
|
|||||||
val lang = varchar("lang", 10)
|
val lang = varchar("lang", 10)
|
||||||
val extension = reference("extension", ExtensionTable)
|
val extension = reference("extension", ExtensionTable)
|
||||||
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
||||||
|
val positionInFactorySource = integer("position_in_factory_source").nullable()
|
||||||
}
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
|
||||||
import ir.armor.tachidesk.model.database.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.CategoryTable
|
|
||||||
import ir.armor.tachidesk.model.database.toDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
|
||||||
import org.jetbrains.exposed.sql.insert
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.sql.update
|
|
||||||
|
|
||||||
object Category {
|
|
||||||
/**
|
|
||||||
* The new category will be placed at the end of the list
|
|
||||||
*/
|
|
||||||
fun createCategory(name: String) {
|
|
||||||
transaction {
|
|
||||||
val count = CategoryTable.selectAll().count()
|
|
||||||
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
|
||||||
CategoryTable.insert {
|
|
||||||
it[CategoryTable.name] = name
|
|
||||||
it[CategoryTable.order] = count.toInt() + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
|
||||||
transaction {
|
|
||||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
|
||||||
if (name != null) it[CategoryTable.name] = name
|
|
||||||
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the category from position `from` to `to`
|
|
||||||
*/
|
|
||||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
|
||||||
transaction {
|
|
||||||
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
|
||||||
categories.add(to - 1, categories.removeAt(from - 1))
|
|
||||||
categories.forEachIndexed { index, cat ->
|
|
||||||
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
|
|
||||||
it[CategoryTable.order] = index + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeCategory(categoryId: Int) {
|
|
||||||
transaction {
|
|
||||||
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
|
|
||||||
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
|
|
||||||
}
|
|
||||||
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCategoryList(): List<CategoryDataClass> {
|
|
||||||
return transaction {
|
|
||||||
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
|
||||||
CategoryTable.toDataClass(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 ir.armor.tachidesk.model.database.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.CategoryTable
|
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.toDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
|
||||||
import org.jetbrains.exposed.sql.and
|
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
|
||||||
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
|
|
||||||
|
|
||||||
object CategoryManga {
|
|
||||||
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
|
||||||
transaction {
|
|
||||||
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
|
|
||||||
CategoryMangaTable.insert {
|
|
||||||
it[CategoryMangaTable.category] = categoryId
|
|
||||||
it[CategoryMangaTable.manga] = mangaId
|
|
||||||
}
|
|
||||||
|
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
|
||||||
it[MangaTable.defaultCategory] = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
|
||||||
transaction {
|
|
||||||
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
|
||||||
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
|
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
|
||||||
it[MangaTable.defaultCategory] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* list of mangas that belong to a category
|
|
||||||
*/
|
|
||||||
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
|
||||||
return transaction {
|
|
||||||
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
|
||||||
MangaTable.toDataClass(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* list of categories that a manga belongs to
|
|
||||||
*/
|
|
||||||
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
|
|
||||||
return transaction {
|
|
||||||
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
|
||||||
CategoryTable.toDataClass(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
|
||||||
import ir.armor.tachidesk.model.database.ChapterTable
|
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.PageTable
|
|
||||||
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
|
|
||||||
import org.jetbrains.exposed.sql.and
|
|
||||||
import org.jetbrains.exposed.sql.insert
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.sql.update
|
|
||||||
|
|
||||||
object Chapter {
|
|
||||||
/** get chapter list when showing a manga */
|
|
||||||
suspend fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
|
||||||
val mangaDetails = getManga(mangaId)
|
|
||||||
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
|
||||||
|
|
||||||
val chapterList = source.fetchChapterList(
|
|
||||||
SManga.create().apply {
|
|
||||||
title = mangaDetails.title
|
|
||||||
url = mangaDetails.url
|
|
||||||
}
|
|
||||||
).awaitSingle()
|
|
||||||
|
|
||||||
val chapterCount = chapterList.count()
|
|
||||||
|
|
||||||
return transaction {
|
|
||||||
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
|
||||||
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
|
||||||
if (chapterEntry == null) {
|
|
||||||
ChapterTable.insert {
|
|
||||||
it[url] = fetchedChapter.url
|
|
||||||
it[name] = fetchedChapter.name
|
|
||||||
it[date_upload] = fetchedChapter.date_upload
|
|
||||||
it[chapter_number] = fetchedChapter.chapter_number
|
|
||||||
it[scanlator] = fetchedChapter.scanlator
|
|
||||||
|
|
||||||
it[chapterIndex] = index + 1
|
|
||||||
it[manga] = mangaId
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
|
||||||
it[name] = fetchedChapter.name
|
|
||||||
it[date_upload] = fetchedChapter.date_upload
|
|
||||||
it[chapter_number] = fetchedChapter.chapter_number
|
|
||||||
it[scanlator] = fetchedChapter.scanlator
|
|
||||||
|
|
||||||
it[chapterIndex] = index + 1
|
|
||||||
it[manga] = mangaId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear any orphaned chapters that are in the db but not in `chapterList`
|
|
||||||
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
|
|
||||||
if (dbChapterCount > chapterCount) { // we got some clean up due
|
|
||||||
// TODO: delete orphan chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
chapterList.map { it ->
|
|
||||||
ChapterDataClass(
|
|
||||||
it.url,
|
|
||||||
it.name,
|
|
||||||
it.date_upload,
|
|
||||||
it.chapter_number,
|
|
||||||
it.scanlator,
|
|
||||||
mangaId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** used to display a chapter, get a chapter in order to show it's pages */
|
|
||||||
suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
|
||||||
val chapterEntry = transaction {
|
|
||||||
ChapterTable.select {
|
|
||||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
|
||||||
}.firstOrNull()!!
|
|
||||||
}
|
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
|
||||||
|
|
||||||
val pageList = source.fetchPageList(
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = chapterEntry[ChapterTable.url]
|
|
||||||
name = chapterEntry[ChapterTable.name]
|
|
||||||
}
|
|
||||||
).awaitSingle()
|
|
||||||
|
|
||||||
val chapterId = chapterEntry[ChapterTable.id].value
|
|
||||||
val chapterCount = transaction { ChapterTable.selectAll().count() }
|
|
||||||
|
|
||||||
// update page list for this chapter
|
|
||||||
transaction {
|
|
||||||
pageList.forEach { page ->
|
|
||||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
|
||||||
if (pageEntry == null) {
|
|
||||||
PageTable.insert {
|
|
||||||
it[index] = page.index
|
|
||||||
it[url] = page.url
|
|
||||||
it[imageUrl] = page.imageUrl
|
|
||||||
it[chapter] = chapterId
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
|
|
||||||
it[url] = page.url
|
|
||||||
it[imageUrl] = page.imageUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ChapterDataClass(
|
|
||||||
chapterEntry[ChapterTable.url],
|
|
||||||
chapterEntry[ChapterTable.name],
|
|
||||||
chapterEntry[ChapterTable.date_upload],
|
|
||||||
chapterEntry[ChapterTable.chapter_number],
|
|
||||||
chapterEntry[ChapterTable.scanlator],
|
|
||||||
mangaId,
|
|
||||||
chapterEntry[ChapterTable.chapterIndex],
|
|
||||||
chapterCount.toInt(),
|
|
||||||
pageList.count()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 android.net.Uri
|
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass
|
|
||||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.EXTENSION_FEATURE
|
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MAX
|
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MIN
|
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.METADATA_NSFW
|
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.METADATA_SOURCE_CLASS
|
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.dex2jar
|
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.getPackageInfo
|
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.getSignatureHash
|
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
|
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.trustedSignatures
|
|
||||||
import ir.armor.tachidesk.impl.util.await
|
|
||||||
import ir.armor.tachidesk.model.database.ExtensionTable
|
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import okhttp3.Request
|
|
||||||
import okio.buffer
|
|
||||||
import okio.sink
|
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
|
||||||
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 org.kodein.di.DI
|
|
||||||
import org.kodein.di.conf.global
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
object Extension {
|
|
||||||
private val logger = KotlinLogging.logger {}
|
|
||||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
|
||||||
|
|
||||||
data class InstallableAPK(
|
|
||||||
val apkFilePath: String,
|
|
||||||
val pkgName: String
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun installExtension(pkgName: String): Int {
|
|
||||||
logger.debug("Installing $pkgName")
|
|
||||||
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
|
|
||||||
|
|
||||||
return installAPK {
|
|
||||||
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
|
|
||||||
val apkName = Uri.parse(apkURL).lastPathSegment!!
|
|
||||||
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
|
||||||
// download apk file
|
|
||||||
downloadAPKFile(apkURL, apkSavePath)
|
|
||||||
|
|
||||||
apkSavePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun installAPK(fetcher: suspend () -> String): Int {
|
|
||||||
val apkFilePath = fetcher()
|
|
||||||
val apkName = File(apkFilePath).name
|
|
||||||
|
|
||||||
// check if we don't have the extension already installed
|
|
||||||
// if it's installed and we want to update, it first has to be uninstalled
|
|
||||||
val isInstalled = transaction {
|
|
||||||
ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()
|
|
||||||
}?.get(ExtensionTable.isInstalled) ?: false
|
|
||||||
|
|
||||||
if (!isInstalled) {
|
|
||||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
|
||||||
|
|
||||||
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
|
||||||
val jarFilePath = "$dirPathWithoutType.jar"
|
|
||||||
val dexFilePath = "$dirPathWithoutType.dex"
|
|
||||||
|
|
||||||
val packageInfo = getPackageInfo(apkFilePath)
|
|
||||||
val pkgName = packageInfo.packageName
|
|
||||||
|
|
||||||
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
|
|
||||||
throw Exception("This apk is not a Tachiyomi extension")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate lib version
|
|
||||||
val libVersion = packageInfo.versionName.substringBeforeLast('.').toDouble()
|
|
||||||
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
|
||||||
throw Exception(
|
|
||||||
"Lib version is $libVersion, while only versions " +
|
|
||||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val signatureHash = getSignatureHash(packageInfo)
|
|
||||||
|
|
||||||
if (signatureHash == null) {
|
|
||||||
throw Exception("Package $pkgName isn't signed")
|
|
||||||
} else if (signatureHash !in trustedSignatures) {
|
|
||||||
// TODO: allow trusting keys
|
|
||||||
throw Exception("This apk is not a signed with the official tachiyomi signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
|
|
||||||
|
|
||||||
val className = packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
|
||||||
|
|
||||||
logger.debug("Main class for extension is $className")
|
|
||||||
|
|
||||||
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
|
||||||
|
|
||||||
// clean up
|
|
||||||
// File(apkFilePath).delete()
|
|
||||||
File(dexFilePath).delete()
|
|
||||||
|
|
||||||
// collect sources from the extension
|
|
||||||
val sources: List<CatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) {
|
|
||||||
is Source -> listOf(instance)
|
|
||||||
is SourceFactory -> instance.createSources()
|
|
||||||
else -> throw RuntimeException("Unknown source class type! ${instance.javaClass}")
|
|
||||||
}.map { it as CatalogueSource }
|
|
||||||
|
|
||||||
val langs = sources.map { it.lang }.toSet()
|
|
||||||
val extensionLang = when (langs.size) {
|
|
||||||
0 -> ""
|
|
||||||
1 -> langs.first()
|
|
||||||
else -> "all"
|
|
||||||
}
|
|
||||||
|
|
||||||
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
|
|
||||||
|
|
||||||
// update extension info
|
|
||||||
transaction {
|
|
||||||
if (ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
|
|
||||||
ExtensionTable.insert {
|
|
||||||
it[this.apkName] = apkName
|
|
||||||
it[name] = extensionName
|
|
||||||
it[this.pkgName] = packageInfo.packageName
|
|
||||||
it[versionName] = packageInfo.versionName
|
|
||||||
it[versionCode] = packageInfo.versionCode
|
|
||||||
it[lang] = extensionLang
|
|
||||||
it[this.isNsfw] = isNsfw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
|
||||||
it[this.isInstalled] = true
|
|
||||||
it[this.classFQName] = className
|
|
||||||
}
|
|
||||||
|
|
||||||
val extensionId = ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!![ExtensionTable.id].value
|
|
||||||
|
|
||||||
sources.forEach { httpSource ->
|
|
||||||
SourceTable.insert {
|
|
||||||
it[id] = httpSource.id
|
|
||||||
it[name] = httpSource.name
|
|
||||||
it[lang] = httpSource.lang
|
|
||||||
it[extension] = extensionId
|
|
||||||
}
|
|
||||||
logger.debug("Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 201 // we installed successfully
|
|
||||||
} else {
|
|
||||||
return 302 // extension was already installed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
|
||||||
|
|
||||||
private suspend fun downloadAPKFile(url: String, savePath: String) {
|
|
||||||
val request = Request.Builder().url(url).build()
|
|
||||||
val response = network.client.newCall(request).await()
|
|
||||||
|
|
||||||
val downloadedFile = File(savePath)
|
|
||||||
downloadedFile.sink().buffer().use { sink ->
|
|
||||||
response.body!!.source().use { source ->
|
|
||||||
sink.writeAll(source)
|
|
||||||
sink.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uninstallExtension(pkgName: String) {
|
|
||||||
logger.debug("Uninstalling $pkgName")
|
|
||||||
|
|
||||||
val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!! }
|
|
||||||
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
|
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
|
||||||
transaction {
|
|
||||||
val extensionId = extensionRecord[ExtensionTable.id].value
|
|
||||||
|
|
||||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
|
||||||
if (extensionRecord[ExtensionTable.isObsolete])
|
|
||||||
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
|
|
||||||
else
|
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
|
||||||
it[isInstalled] = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File(jarPath).exists()) {
|
|
||||||
File(jarPath).delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateExtension(pkgName: String): Int {
|
|
||||||
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
|
|
||||||
uninstallExtension(pkgName)
|
|
||||||
transaction {
|
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
|
||||||
it[name] = targetExtension.name
|
|
||||||
it[versionName] = targetExtension.versionName
|
|
||||||
it[versionCode] = targetExtension.versionCode
|
|
||||||
it[lang] = targetExtension.lang
|
|
||||||
it[isNsfw] = targetExtension.isNsfw
|
|
||||||
it[apkName] = targetExtension.apkName
|
|
||||||
it[iconUrl] = targetExtension.iconUrl
|
|
||||||
it[hasUpdate] = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return installExtension(pkgName)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
|
||||||
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
|
||||||
|
|
||||||
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, apkName) {
|
|
||||||
network.client.newCall(
|
|
||||||
GET(iconUrl)
|
|
||||||
).await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getExtensionIconUrl(apkName: String): String {
|
|
||||||
return "/api/v1/extension/icon/$apkName"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
|
||||||
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
|
|
||||||
import ir.armor.tachidesk.model.database.ExtensionTable
|
|
||||||
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
|
||||||
import org.jetbrains.exposed.sql.insert
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.sql.update
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
object ExtensionsList {
|
|
||||||
private val logger = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
var lastUpdateCheck: Long = 0
|
|
||||||
var updateMap = ConcurrentHashMap<String, Extension.Available>()
|
|
||||||
|
|
||||||
/** 60,000 milliseconds = 60 seconds */
|
|
||||||
private const val ExtensionUpdateDelayTime = 60 * 1000
|
|
||||||
|
|
||||||
suspend fun getExtensionList(): List<ExtensionDataClass> {
|
|
||||||
// update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty
|
|
||||||
if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) {
|
|
||||||
logger.debug("Getting extensions list from the internet")
|
|
||||||
lastUpdateCheck = System.currentTimeMillis()
|
|
||||||
|
|
||||||
val foundExtensions = ExtensionGithubApi.findExtensions()
|
|
||||||
updateExtensionDatabase(foundExtensions)
|
|
||||||
} else {
|
|
||||||
logger.debug("used cached extension list")
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensionTableAsDataClass()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun extensionTableAsDataClass() = transaction {
|
|
||||||
ExtensionTable.selectAll().map {
|
|
||||||
ExtensionDataClass(
|
|
||||||
it[ExtensionTable.apkName],
|
|
||||||
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
|
||||||
it[ExtensionTable.name],
|
|
||||||
it[ExtensionTable.pkgName],
|
|
||||||
it[ExtensionTable.versionName],
|
|
||||||
it[ExtensionTable.versionCode],
|
|
||||||
it[ExtensionTable.lang],
|
|
||||||
it[ExtensionTable.isNsfw],
|
|
||||||
it[ExtensionTable.isInstalled],
|
|
||||||
it[ExtensionTable.hasUpdate],
|
|
||||||
it[ExtensionTable.isObsolete],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateExtensionDatabase(foundExtensions: List<Extension.Available>) {
|
|
||||||
transaction {
|
|
||||||
foundExtensions.forEach { foundExtension ->
|
|
||||||
val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
|
|
||||||
if (extensionRecord != null) {
|
|
||||||
if (extensionRecord[ExtensionTable.isInstalled]) {
|
|
||||||
when {
|
|
||||||
foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode] -> {
|
|
||||||
// there is an update
|
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
|
|
||||||
it[hasUpdate] = true
|
|
||||||
}
|
|
||||||
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
|
|
||||||
}
|
|
||||||
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> {
|
|
||||||
// some how the user installed an invalid version
|
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
|
|
||||||
it[isObsolete] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// extension is not installed so we can overwrite the data without a care
|
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
|
|
||||||
it[name] = foundExtension.name
|
|
||||||
it[versionName] = foundExtension.versionName
|
|
||||||
it[versionCode] = foundExtension.versionCode
|
|
||||||
it[lang] = foundExtension.lang
|
|
||||||
it[isNsfw] = foundExtension.isNsfw
|
|
||||||
it[apkName] = foundExtension.apkName
|
|
||||||
it[iconUrl] = foundExtension.iconUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// insert new record
|
|
||||||
ExtensionTable.insert {
|
|
||||||
it[name] = foundExtension.name
|
|
||||||
it[pkgName] = foundExtension.pkgName
|
|
||||||
it[versionName] = foundExtension.versionName
|
|
||||||
it[versionCode] = foundExtension.versionCode
|
|
||||||
it[lang] = foundExtension.lang
|
|
||||||
it[isNsfw] = foundExtension.isNsfw
|
|
||||||
it[apkName] = foundExtension.apkName
|
|
||||||
it[iconUrl] = foundExtension.iconUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// deal with obsolete extensions
|
|
||||||
ExtensionTable.selectAll().forEach { extensionRecord ->
|
|
||||||
val foundExtension = foundExtensions.find { it.pkgName == extensionRecord[ExtensionTable.pkgName] }
|
|
||||||
if (foundExtension == null) {
|
|
||||||
// not in the repo, so this extensions is obsolete
|
|
||||||
if (extensionRecord[ExtensionTable.isInstalled]) {
|
|
||||||
// is installed so we should mark it as obsolete
|
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }) {
|
|
||||||
it[isObsolete] = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// is not installed so we can remove the record without a care
|
|
||||||
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 ir.armor.tachidesk.impl.Manga.getManga
|
|
||||||
import ir.armor.tachidesk.model.database.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.toDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
|
||||||
import org.jetbrains.exposed.sql.and
|
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.sql.update
|
|
||||||
|
|
||||||
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) {
|
|
||||||
val manga = getManga(mangaId)
|
|
||||||
if (!manga.inLibrary) {
|
|
||||||
transaction {
|
|
||||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
|
||||||
it[inLibrary] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun removeMangaFromLibrary(mangaId: Int) {
|
|
||||||
val manga = getManga(mangaId)
|
|
||||||
if (manga.inLibrary) {
|
|
||||||
transaction {
|
|
||||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
|
||||||
it[inLibrary] = false
|
|
||||||
it[defaultCategory] = true
|
|
||||||
}
|
|
||||||
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLibraryMangas(): List<MangaDataClass> {
|
|
||||||
return transaction {
|
|
||||||
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
|
||||||
MangaTable.toDataClass(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
|
||||||
import ir.armor.tachidesk.impl.Source.getSource
|
|
||||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.await
|
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
|
||||||
import ir.armor.tachidesk.model.database.MangaStatus
|
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.sql.update
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.conf.global
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
object Manga {
|
|
||||||
suspend fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
|
||||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
|
||||||
|
|
||||||
return if (mangaEntry[MangaTable.initialized]) {
|
|
||||||
MangaDataClass(
|
|
||||||
mangaId,
|
|
||||||
mangaEntry[MangaTable.sourceReference].toString(),
|
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
|
||||||
mangaEntry[MangaTable.title],
|
|
||||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
|
||||||
|
|
||||||
true,
|
|
||||||
|
|
||||||
mangaEntry[MangaTable.artist],
|
|
||||||
mangaEntry[MangaTable.author],
|
|
||||||
mangaEntry[MangaTable.description],
|
|
||||||
mangaEntry[MangaTable.genre],
|
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
|
||||||
mangaEntry[MangaTable.inLibrary],
|
|
||||||
getSource(mangaEntry[MangaTable.sourceReference])
|
|
||||||
)
|
|
||||||
} else { // initialize manga
|
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
|
||||||
val fetchedManga = source.fetchMangaDetails(
|
|
||||||
SManga.create().apply {
|
|
||||||
url = mangaEntry[MangaTable.url]
|
|
||||||
title = mangaEntry[MangaTable.title]
|
|
||||||
}
|
|
||||||
).awaitSingle()
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
|
||||||
|
|
||||||
it[MangaTable.initialized] = true
|
|
||||||
|
|
||||||
it[MangaTable.artist] = fetchedManga.artist
|
|
||||||
it[MangaTable.author] = fetchedManga.author
|
|
||||||
it[MangaTable.description] = fetchedManga.description
|
|
||||||
it[MangaTable.genre] = fetchedManga.genre
|
|
||||||
it[MangaTable.status] = fetchedManga.status
|
|
||||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
|
||||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
|
||||||
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
|
|
||||||
MangaDataClass(
|
|
||||||
mangaId,
|
|
||||||
mangaEntry[MangaTable.sourceReference].toString(),
|
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
|
||||||
mangaEntry[MangaTable.title],
|
|
||||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
|
|
||||||
|
|
||||||
true,
|
|
||||||
|
|
||||||
fetchedManga.artist,
|
|
||||||
fetchedManga.author,
|
|
||||||
fetchedManga.description,
|
|
||||||
fetchedManga.genre,
|
|
||||||
MangaStatus.valueOf(fetchedManga.status).name,
|
|
||||||
false,
|
|
||||||
getSource(mangaEntry[MangaTable.sourceReference])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
|
||||||
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
|
||||||
val saveDir = applicationDirs.thumbnailsRoot
|
|
||||||
val fileName = mangaId.toString()
|
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, fileName) {
|
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
|
||||||
val source = getHttpSource(sourceId)
|
|
||||||
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
|
||||||
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
|
||||||
}
|
|
||||||
|
|
||||||
source.client.newCall(
|
|
||||||
GET(thumbnailUrl, source.headers)
|
|
||||||
).await()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
|
||||||
import ir.armor.tachidesk.model.database.MangaStatus
|
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
|
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
|
|
||||||
object MangaList {
|
|
||||||
fun proxyThumbnailUrl(mangaId: Int): String {
|
|
||||||
return "/api/v1/manga/$mangaId/thumbnail"
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
|
||||||
val source = getHttpSource(sourceId)
|
|
||||||
val mangasPage = if (popular) {
|
|
||||||
source.fetchPopularManga(pageNum).awaitSingle()
|
|
||||||
} else {
|
|
||||||
if (source.supportsLatest)
|
|
||||||
source.fetchLatestUpdates(pageNum).awaitSingle()
|
|
||||||
else
|
|
||||||
throw Exception("Source $source doesn't support latest")
|
|
||||||
}
|
|
||||||
return mangasPage.processEntries(sourceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
|
||||||
val mangasPage = this
|
|
||||||
val mangaList = transaction {
|
|
||||||
return@transaction mangasPage.mangas.map { manga ->
|
|
||||||
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
|
||||||
if (mangaEntry == null) { // create manga entry
|
|
||||||
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] = sourceId
|
|
||||||
}.value
|
|
||||||
|
|
||||||
MangaDataClass(
|
|
||||||
mangaId,
|
|
||||||
sourceId.toString(),
|
|
||||||
|
|
||||||
manga.url,
|
|
||||||
manga.title,
|
|
||||||
proxyThumbnailUrl(mangaId),
|
|
||||||
|
|
||||||
manga.initialized,
|
|
||||||
|
|
||||||
manga.artist,
|
|
||||||
manga.author,
|
|
||||||
manga.description,
|
|
||||||
manga.genre,
|
|
||||||
MangaStatus.valueOf(manga.status).name
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val mangaId = mangaEntry[MangaTable.id].value
|
|
||||||
MangaDataClass(
|
|
||||||
mangaId,
|
|
||||||
sourceId.toString(),
|
|
||||||
|
|
||||||
manga.url,
|
|
||||||
manga.title,
|
|
||||||
proxyThumbnailUrl(mangaId),
|
|
||||||
|
|
||||||
true,
|
|
||||||
|
|
||||||
mangaEntry[MangaTable.artist],
|
|
||||||
mangaEntry[MangaTable.author],
|
|
||||||
mangaEntry[MangaTable.description],
|
|
||||||
mangaEntry[MangaTable.genre],
|
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
|
||||||
mangaEntry[MangaTable.inLibrary]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return PagedMangaListDataClass(
|
|
||||||
mangaList,
|
|
||||||
mangasPage.hasNextPage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
|
||||||
import ir.armor.tachidesk.model.database.ChapterTable
|
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.PageTable
|
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
|
||||||
import org.jetbrains.exposed.sql.and
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.sql.update
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.conf.global
|
|
||||||
import org.kodein.di.instance
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
object Page {
|
|
||||||
/**
|
|
||||||
* A page might have a imageUrl ready from the get go, or we might need to
|
|
||||||
* go an extra step and call fetchImageUrl to get it.
|
|
||||||
*/
|
|
||||||
suspend fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
|
||||||
if (page.imageUrl == null) {
|
|
||||||
page.imageUrl = source.fetchImageUrl(page).awaitSingle()
|
|
||||||
}
|
|
||||||
return page.imageUrl!!
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
|
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
|
||||||
val chapterEntry = transaction {
|
|
||||||
ChapterTable.select {
|
|
||||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
|
||||||
}.firstOrNull()!!
|
|
||||||
}
|
|
||||||
val chapterId = chapterEntry[ChapterTable.id].value
|
|
||||||
|
|
||||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
|
|
||||||
|
|
||||||
val tachiPage = Page(
|
|
||||||
pageEntry[PageTable.index],
|
|
||||||
pageEntry[PageTable.url],
|
|
||||||
pageEntry[PageTable.imageUrl]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (pageEntry[PageTable.imageUrl] == null) {
|
|
||||||
val trueImageUrl = getTrueImageUrl(tachiPage, source)
|
|
||||||
transaction {
|
|
||||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
|
|
||||||
it[imageUrl] = trueImageUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val saveDir = getChapterDir(mangaId, chapterId)
|
|
||||||
File(saveDir).mkdirs()
|
|
||||||
val fileName = index.toString()
|
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, fileName) {
|
|
||||||
source.fetchImage(tachiPage).awaitSingle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: rewrite this to match tachiyomi
|
|
||||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
|
||||||
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
|
||||||
val source = getHttpSource(sourceId)
|
|
||||||
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
|
||||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
|
||||||
|
|
||||||
val chapterDir = when {
|
|
||||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
|
||||||
else -> chapterEntry[ChapterTable.name]
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangaTitle = mangaEntry[MangaTable.title]
|
|
||||||
val sourceName = source.toString()
|
|
||||||
|
|
||||||
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
|
||||||
// make sure dirs exist
|
|
||||||
File(mangaDir).mkdirs()
|
|
||||||
return mangaDir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.model.database.ExtensionTable
|
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
|
||||||
import ir.armor.tachidesk.model.dataclass.SourceDataClass
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
|
|
||||||
object Source {
|
|
||||||
private val logger = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
fun getSourceList(): List<SourceDataClass> {
|
|
||||||
return transaction {
|
|
||||||
SourceTable.selectAll().map {
|
|
||||||
SourceDataClass(
|
|
||||||
it[SourceTable.id].value.toString(),
|
|
||||||
it[SourceTable.name],
|
|
||||||
it[SourceTable.lang],
|
|
||||||
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
|
|
||||||
getHttpSource(it[SourceTable.id].value).supportsLatest
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSource(sourceId: Long): SourceDataClass {
|
|
||||||
return transaction {
|
|
||||||
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
|
|
||||||
|
|
||||||
SourceDataClass(
|
|
||||||
sourceId.toString(),
|
|
||||||
source?.get(SourceTable.name),
|
|
||||||
source?.get(SourceTable.lang),
|
|
||||||
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
|
|
||||||
source?.let { getHttpSource(sourceId).supportsLatest }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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/. */
|
|
||||||
|
|
||||||
data class BackupFlags(
|
|
||||||
val includeManga: Boolean,
|
|
||||||
val includeCategories: Boolean,
|
|
||||||
val includeChapters: Boolean,
|
|
||||||
val includeTracking: Boolean,
|
|
||||||
val includeHistory: Boolean,
|
|
||||||
)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 ir.armor.tachidesk.impl.backup.legacy.models.DHistory
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.CategoryTypeAdapter
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.ChapterTypeAdapter
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.HistoryTypeAdapter
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.MangaTypeAdapter
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.TrackTypeAdapter
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
|
||||||
import ir.armor.tachidesk.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 ir.armor.tachidesk.impl.backup.BackupFlags
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup.CURRENT_VERSION
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.Manga
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.model.database.ChapterTable
|
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
|
||||||
if (options.includeChapters && false) { // TODO
|
|
||||||
// Backup all the chapters
|
|
||||||
val mangaId = manga.id!!.toInt()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO the rest
|
|
||||||
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupCategories(root: JsonArray) { // TODO
|
|
||||||
// val categories = databaseHelper.getCategories().executeAsBlocking()
|
|
||||||
// categories.forEach { root.add(parser.toJsonTree(it)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
|
||||||
extensions.sorted().forEach {
|
|
||||||
root.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 ir.armor.tachidesk.impl.backup.legacy.LegacyBackupRestoreValidator.ValidationResult
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupRestoreValidator.validate
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.Chapter
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.Manga
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.Track
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
|
||||||
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 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 = LegacyBackupRestoreValidator.getSourceMapping(json)
|
|
||||||
|
|
||||||
// Restore individual manga
|
|
||||||
mangasJson.forEach {
|
|
||||||
restoreManga(it.asJsonObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info {
|
|
||||||
"""
|
|
||||||
Restore Errors:
|
|
||||||
${
|
|
||||||
errors.map {
|
|
||||||
"${it.first} - ${it.second}"
|
|
||||||
}.joinToString("\n")
|
|
||||||
}
|
|
||||||
Restore Summary:
|
|
||||||
- Missing Sources:
|
|
||||||
${validationResult.missingSources.joinToString("\n")}
|
|
||||||
- Missing Trackers:
|
|
||||||
${validationResult.missingTrackers.joinToString("\n")}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
return validationResult
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: JsonElement) { // TODO
|
|
||||||
// db.inTransaction {
|
|
||||||
// backupManager.restoreCategories(categoriesJson.asJsonArray)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// restoreProgress += 1
|
|
||||||
// showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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>
|
|
||||||
) {
|
|
||||||
fetchManga(source, manga)
|
|
||||||
|
|
||||||
// updateChapters(source, fetchedManga, chapters)
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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!!.isNotEmpty())
|
|
||||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetchedManga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package ir.armor.tachidesk.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 ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
|
|
||||||
object LegacyBackupRestoreValidator {
|
|
||||||
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 ir.armor.tachidesk.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 ir.armor.tachidesk.impl.backup.legacy.models
|
|
||||||
|
|
||||||
data class DHistory(val url: String, val lastRead: Long)
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import ir.armor.tachidesk.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import ir.armor.tachidesk.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import ir.armor.tachidesk.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import ir.armor.tachidesk.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import ir.armor.tachidesk.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,23 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
interface Category : Serializable {
|
|
||||||
|
|
||||||
var id: Int?
|
|
||||||
|
|
||||||
var name: String
|
|
||||||
|
|
||||||
var order: Int
|
|
||||||
|
|
||||||
var flags: Int
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun create(name: String): Category = CategoryImpl().apply {
|
|
||||||
this.name = name
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
class CategoryImpl : Category {
|
|
||||||
|
|
||||||
override var id: Int? = null
|
|
||||||
|
|
||||||
override lateinit var name: String
|
|
||||||
|
|
||||||
override var order: Int = 0
|
|
||||||
|
|
||||||
override var flags: Int = 0
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
|
||||||
|
|
||||||
val category = other as Category
|
|
||||||
return name == category.name
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return name.hashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
interface Chapter : SChapter, Serializable {
|
|
||||||
|
|
||||||
var id: Long?
|
|
||||||
|
|
||||||
var manga_id: Long?
|
|
||||||
|
|
||||||
var read: Boolean
|
|
||||||
|
|
||||||
var bookmark: Boolean
|
|
||||||
|
|
||||||
var last_page_read: Int
|
|
||||||
|
|
||||||
var date_fetch: Long
|
|
||||||
|
|
||||||
var source_order: Int
|
|
||||||
|
|
||||||
val isRecognizedNumber: Boolean
|
|
||||||
get() = chapter_number >= 0f
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun create(): Chapter = ChapterImpl().apply {
|
|
||||||
chapter_number = -1f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
|
||||||
|
|
||||||
class ChapterImpl : Chapter {
|
|
||||||
|
|
||||||
override var id: Long? = null
|
|
||||||
|
|
||||||
override var manga_id: Long? = null
|
|
||||||
|
|
||||||
override lateinit var url: String
|
|
||||||
|
|
||||||
override lateinit var name: String
|
|
||||||
|
|
||||||
override var scanlator: String? = null
|
|
||||||
|
|
||||||
override var read: Boolean = false
|
|
||||||
|
|
||||||
override var bookmark: Boolean = false
|
|
||||||
|
|
||||||
override var last_page_read: Int = 0
|
|
||||||
|
|
||||||
override var date_fetch: Long = 0
|
|
||||||
|
|
||||||
override var date_upload: Long = 0
|
|
||||||
|
|
||||||
override var chapter_number: Float = 0f
|
|
||||||
|
|
||||||
override var source_order: Int = 0
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
|
||||||
|
|
||||||
val chapter = other as Chapter
|
|
||||||
if (url != chapter.url) return false
|
|
||||||
return id == chapter.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return url.hashCode() + id.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tachidesk -->
|
|
||||||
companion object {
|
|
||||||
fun fromQuery(chapterRecord: ResultRow): ChapterImpl {
|
|
||||||
return ChapterImpl().apply {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Tachidesk <--
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Object containing the history statistics of a chapter
|
|
||||||
*/
|
|
||||||
interface History : Serializable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Id of history object.
|
|
||||||
*/
|
|
||||||
var id: Long?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter id of history object.
|
|
||||||
*/
|
|
||||||
var chapter_id: Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Last time chapter was read in time long format
|
|
||||||
*/
|
|
||||||
var last_read: Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total time chapter was read - todo not yet implemented
|
|
||||||
*/
|
|
||||||
var time_read: Long
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* History constructor
|
|
||||||
*
|
|
||||||
* @param chapter chapter object
|
|
||||||
* @return history object
|
|
||||||
*/
|
|
||||||
fun create(chapter: Chapter): History = HistoryImpl().apply {
|
|
||||||
this.chapter_id = chapter.id!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Object containing the history statistics of a chapter
|
|
||||||
*/
|
|
||||||
class HistoryImpl : History {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Id of history object.
|
|
||||||
*/
|
|
||||||
override var id: Long? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chapter id of history object.
|
|
||||||
*/
|
|
||||||
override var chapter_id: Long = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Last time chapter was read in time long format
|
|
||||||
*/
|
|
||||||
override var last_read: Long = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Total time chapter was read - todo not yet implemented
|
|
||||||
*/
|
|
||||||
override var time_read: Long = 0
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
class LibraryManga : MangaImpl() {
|
|
||||||
|
|
||||||
var unread: Int = 0
|
|
||||||
|
|
||||||
var category: Int = 0
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
// import tachiyomi.source.model.MangaInfo
|
|
||||||
|
|
||||||
interface Manga : SManga {
|
|
||||||
|
|
||||||
var id: Long?
|
|
||||||
|
|
||||||
var source: Long
|
|
||||||
|
|
||||||
/** is in library */
|
|
||||||
var favorite: Boolean
|
|
||||||
|
|
||||||
var last_update: Long
|
|
||||||
|
|
||||||
var date_added: Long
|
|
||||||
|
|
||||||
var viewer: Int
|
|
||||||
|
|
||||||
var chapter_flags: Int
|
|
||||||
|
|
||||||
var cover_last_modified: Long
|
|
||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
|
||||||
setFlags(order, SORT_MASK)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sortDescending(): Boolean {
|
|
||||||
return chapter_flags and SORT_MASK == SORT_DESC
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGenres(): List<String>? {
|
|
||||||
return genre?.split(", ")?.map { it.trim() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setFlags(flag: Int, mask: Int) {
|
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
|
||||||
var displayMode: Int
|
|
||||||
get() = chapter_flags and DISPLAY_MASK
|
|
||||||
set(mode) = setFlags(mode, DISPLAY_MASK)
|
|
||||||
|
|
||||||
var readFilter: Int
|
|
||||||
get() = chapter_flags and READ_MASK
|
|
||||||
set(filter) = setFlags(filter, READ_MASK)
|
|
||||||
|
|
||||||
var downloadedFilter: Int
|
|
||||||
get() = chapter_flags and DOWNLOADED_MASK
|
|
||||||
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
|
||||||
|
|
||||||
var bookmarkedFilter: Int
|
|
||||||
get() = chapter_flags and BOOKMARKED_MASK
|
|
||||||
set(filter) = setFlags(filter, BOOKMARKED_MASK)
|
|
||||||
|
|
||||||
var sorting: Int
|
|
||||||
get() = chapter_flags and SORTING_MASK
|
|
||||||
set(sort) = setFlags(sort, SORTING_MASK)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val SORT_DESC = 0x00000000
|
|
||||||
const val SORT_ASC = 0x00000001
|
|
||||||
const val SORT_MASK = 0x00000001
|
|
||||||
|
|
||||||
// Generic filter that does not filter anything
|
|
||||||
const val SHOW_ALL = 0x00000000
|
|
||||||
|
|
||||||
const val SHOW_UNREAD = 0x00000002
|
|
||||||
const val SHOW_READ = 0x00000004
|
|
||||||
const val READ_MASK = 0x00000006
|
|
||||||
|
|
||||||
const val SHOW_DOWNLOADED = 0x00000008
|
|
||||||
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
|
||||||
const val DOWNLOADED_MASK = 0x00000018
|
|
||||||
|
|
||||||
const val SHOW_BOOKMARKED = 0x00000020
|
|
||||||
const val SHOW_NOT_BOOKMARKED = 0x00000040
|
|
||||||
const val BOOKMARKED_MASK = 0x00000060
|
|
||||||
|
|
||||||
const val SORTING_SOURCE = 0x00000000
|
|
||||||
const val SORTING_NUMBER = 0x00000100
|
|
||||||
const val SORTING_UPLOAD_DATE = 0x00000200
|
|
||||||
const val SORTING_MASK = 0x00000300
|
|
||||||
|
|
||||||
const val DISPLAY_NAME = 0x00000000
|
|
||||||
const val DISPLAY_NUMBER = 0x00100000
|
|
||||||
const val DISPLAY_MASK = 0x00100000
|
|
||||||
|
|
||||||
fun create(source: Long): Manga = MangaImpl().apply {
|
|
||||||
this.source = source
|
|
||||||
}
|
|
||||||
|
|
||||||
fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
|
|
||||||
url = pathUrl
|
|
||||||
this.title = title
|
|
||||||
this.source = source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fun Manga.toMangaInfo(): MangaInfo {
|
|
||||||
// return MangaInfo(
|
|
||||||
// artist = this.artist ?: "",
|
|
||||||
// author = this.author ?: "",
|
|
||||||
// cover = this.thumbnail_url ?: "",
|
|
||||||
// description = this.description ?: "",
|
|
||||||
// genres = this.getGenres() ?: emptyList(),
|
|
||||||
// key = this.url,
|
|
||||||
// status = this.status,
|
|
||||||
// title = this.title
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
class MangaCategory {
|
|
||||||
|
|
||||||
var id: Long? = null
|
|
||||||
|
|
||||||
var manga_id: Long = 0
|
|
||||||
|
|
||||||
var category_id: Int = 0
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun create(manga: Manga, category: Category): MangaCategory {
|
|
||||||
val mc = MangaCategory()
|
|
||||||
mc.manga_id = manga.id!!
|
|
||||||
mc.category_id = category.id!!
|
|
||||||
return mc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
class MangaChapter(val manga: Manga, val chapter: Chapter)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Object containing manga, chapter and history
|
|
||||||
*
|
|
||||||
* @param manga object containing manga
|
|
||||||
* @param chapter object containing chater
|
|
||||||
* @param history object containing history
|
|
||||||
*/
|
|
||||||
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user