Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63a078cf7d | |||
| 5304917e53 | |||
| 831b74d2ec | |||
| 1bad9dcd69 | |||
| dd43716851 | |||
| f2e55e95a2 | |||
| 14658a0c4d | |||
| 4195e7056b | |||
| 1d29e8b248 | |||
| b718c718df | |||
| a3601cf1b5 | |||
| 0236a9639b | |||
| 5f4c7454ee | |||
| 773120c96a | |||
| 4b273c6bf9 | |||
| b626aa66ba | |||
| 1dd029559e | |||
| 59cbe5d5bc | |||
| 40d1173653 | |||
| bf6a0aba5d | |||
| 34d8feacdd | |||
| 1ea821584c | |||
| 3d2fee19bb | |||
| 449d12779a | |||
| 6fb6a251e7 | |||
| 4d6220f894 | |||
| fe747bfc52 | |||
| 0c2d038870 | |||
| 4e3f73af75 | |||
| 63e5e1b45f | |||
| 2e1558bd96 | |||
| 0671dee8b2 | |||
| 8f91b8089a | |||
| 009b45f676 | |||
| 8f7d5eb311 | |||
| f3de835ef3 | |||
| fd6662f428 | |||
| fde137b3ed | |||
| a1349aa0e3 | |||
| c9ef5f9b9d | |||
| 8fbf564177 | |||
| ae0b1a818c | |||
| c04cc780b7 | |||
| 71ad1bb6e3 | |||
| c1be77ee9b | |||
| d1fa857ffb | |||
| 93fd81b38b | |||
| 2f116b40b2 | |||
| b884d34bdf | |||
| 309803368b | |||
| 19fc5be8f3 | |||
| c28fac14c0 | |||
| 66e38de29f | |||
| 282cb1d3be | |||
| b741ded595 | |||
| 6b290695fc | |||
| 4e43c554c0 | |||
| 090a72b35f | |||
| 3fcc269df3 | |||
| 9958e0eb34 | |||
| c5269002a2 | |||
| 455a35f8ae | |||
| 0c79f207c3 | |||
| cd16d32a35 | |||
| 1989c1eb48 | |||
| f56856529f | |||
| 52e27a3e39 | |||
| 177c971b52 | |||
| 7a52e19235 | |||
| 5171e509a5 | |||
| 975a3b1828 | |||
| c11887fada | |||
| e043cb5690 | |||
| b2d5354798 | |||
| a211a4143b | |||
| c0df7d314b | |||
| c8a8ce07e2 | |||
| e0e474dfce | |||
| 7591748811 | |||
| 884308690f | |||
| 15bd5b4b7a | |||
| abc3a16ee3 | |||
| bb09ccf3c0 | |||
| ad2ea8095b | |||
| 760d1116a1 | |||
| 47fcf7eb97 | |||
| b0e90c2f63 | |||
| f502884fdd | |||
| 5ed79523d2 | |||
| da5dd70969 | |||
| 68e69085df | |||
| 640ce8f5d7 | |||
| c960cc1ee5 | |||
| 2b2601aa4a | |||
| 99a10ec7db | |||
| 035105adf0 | |||
| f983f0e359 | |||
| 769472b24c | |||
| 8c80ad7575 | |||
| 63db2e6695 | |||
| d6d5e97fbd | |||
| 1ae0a8326e | |||
| 57693fef7b | |||
| 5656016700 | |||
| 90ae180b3e | |||
| 2a3c78d43e | |||
| 11000af718 | |||
| b808121f1d | |||
| addadefeb1 | |||
| 838cd20e57 | |||
| 5b9219522d | |||
| caeb4d273d | |||
| 77cf87c989 | |||
| 50c2dbed5d | |||
| 71a9396952 | |||
| bc3ad75328 | |||
| 077bbc3c38 | |||
| b1b1abad1d |
@@ -1,24 +1,25 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
git lfs install
|
cp master/server/build/Tachidesk-*.jar preview
|
||||||
#git lfs track "*.zip"
|
cd preview
|
||||||
|
|
||||||
cp ../master/repo/* .
|
|
||||||
new_jar_build=$(ls *.jar| tail -1)
|
new_jar_build=$(ls *.jar| tail -1)
|
||||||
echo "last jar build file name: $new_jar_build"
|
echo "last jar build file name: $new_jar_build"
|
||||||
|
|
||||||
new_win32_build=$(ls *.zip| tail -1)
|
|
||||||
echo "last win32 build file name: $new_win32_build"
|
|
||||||
|
|
||||||
cp -f $new_jar_build Tachidesk-latest.jar
|
cp -f $new_jar_build Tachidesk-latest.jar
|
||||||
cp -f $new_win32_build Tachidesk-latest-win32.zip
|
|
||||||
|
rm -rf latest_pointer/*
|
||||||
|
cp $new_jar_build latest_pointer
|
||||||
|
|
||||||
|
latest=$(ls *.jar | tail -n1 | cut -d"-" -f3 | cut -d"." -f1)
|
||||||
|
echo "{ \"latest\": \"$latest\" }" > index.json
|
||||||
|
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.name "github-actions[bot]"
|
||||||
git status
|
git status
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Update repo"
|
git commit -m "Update preview repository"
|
||||||
git push
|
git push
|
||||||
else
|
else
|
||||||
echo "No changes to commit"
|
echo "No changes to commit"
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Get last commit message
|
|
||||||
#last_commit_log=$(git log -1 --pretty=format:"%s")
|
|
||||||
#echo "last commit log: $last_commit_log"
|
|
||||||
#
|
|
||||||
#filter_count=$(echo "$last_commit_log" | grep -e '\[RELEASE CI\]' -e '\[CI RELEASE\]' | wc -c)
|
|
||||||
#echo "count is: $filter_count"
|
|
||||||
|
|
||||||
mkdir -p repo/
|
|
||||||
cp server/build/Tachidesk-*.jar repo/
|
|
||||||
cp server/build/Tachidesk-*.zip repo/
|
|
||||||
|
|
||||||
ls repo
|
|
||||||
pwd
|
|
||||||
|
|
||||||
#if [ "$filter_count" -gt 0 ]; then
|
|
||||||
# cp server/build/Tachidesk-*.jar repo/
|
|
||||||
# cp server/build/Tachidesk-*.zip repo/
|
|
||||||
#fi
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
name: CI
|
name: CI Pull Request
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -33,7 +30,7 @@ jobs:
|
|||||||
- name: Checkout master branch
|
- name: Checkout master branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
path: master
|
path: master
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -60,12 +57,12 @@ jobs:
|
|||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||||
|
|
||||||
- name: Build Jar and launch4j
|
- name: Build and copy webUI, 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: :server:windowsPackage --stacktrace
|
arguments: :webUI:copyBuild :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
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
name: CI build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_wrapper:
|
||||||
|
name: Validate Gradle Wrapper
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Clone repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Validate Gradle Wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build FatJar
|
||||||
|
needs: check_wrapper
|
||||||
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Cancel previous runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.5.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Checkout master branch
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
path: master
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up JDK 1.8
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
|
||||||
|
- name: Copy CI gradle.properties
|
||||||
|
run: |
|
||||||
|
cd master
|
||||||
|
mkdir -p ~/.gradle
|
||||||
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
- name: Download android.jar
|
||||||
|
run: |
|
||||||
|
cd master
|
||||||
|
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||||
|
|
||||||
|
- name: Cache node_modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
**/react/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||||
|
|
||||||
|
- name: Build and copy webUI, Build Jar and launch4j
|
||||||
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
with:
|
||||||
|
build-root-directory: master
|
||||||
|
wrapper-directory: master
|
||||||
|
arguments: :webUI:copyBuild :server:windowsPackage --stacktrace
|
||||||
|
wrapper-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
- name: Checkout preview branch
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: preview
|
||||||
|
path: preview
|
||||||
|
|
||||||
|
- name: Deploy preview
|
||||||
|
run: |
|
||||||
|
./master/.github/scripts/commit-repo.sh
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Publish
|
name: CI Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -58,28 +58,22 @@ jobs:
|
|||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
|
||||||
- name: Build Jar and launch4j
|
- name: Build and copy webUI, 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: :server:windowsPackage --stacktrace
|
arguments: :webUI:copyBuild :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
|
||||||
|
|
||||||
|
|
||||||
- name: Create repo artifacts
|
|
||||||
run: |
|
|
||||||
cd master
|
|
||||||
./.github/scripts/create-repo.sh
|
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: xresloader/upload-to-github-release@master
|
uses: xresloader/upload-to-github-release@master
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
file: "master/repo/*"
|
file: "master/server/build/*.jar;master/server/build/*-win32.zip"
|
||||||
tags: true
|
tags: true
|
||||||
draft: true
|
draft: true
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
# 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/
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,15 +11,12 @@ 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() }
|
||||||
|
|
||||||
@@ -27,8 +24,6 @@ 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 {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +46,7 @@ open class ConfigManager {
|
|||||||
|
|
||||||
//Load user config
|
//Load user config
|
||||||
val userConfig =
|
val userConfig =
|
||||||
File(appConfigFile).let{
|
File(ApplicationRootDir, "server.conf").let {
|
||||||
ConfigFactory.parseFile(it)
|
ConfigFactory.parseFile(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +64,7 @@ open class ConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun registerModule(module: ConfigModule) {
|
fun registerModule(module: ConfigModule) {
|
||||||
generatedModules.put(module.javaClass, module)
|
generatedModules[module.javaClass] = module
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerModules(vararg modules: ConfigModule) {
|
fun registerModules(vararg modules: ConfigModule) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ plugins {
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://jitpack.io")
|
url = uri("https://jitpack.io")
|
||||||
}
|
}
|
||||||
@@ -30,23 +29,12 @@ 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: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
|
||||||
compileOnly("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
|
||||||
|
|
||||||
// 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")
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Write-Output "Getting required Android.jar..."
|
|||||||
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
|
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
|
||||||
New-Item -ItemType Directory -Force -Path "tmp" | 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
|
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" -UseBasicParsing).content
|
||||||
|
|
||||||
$android_jar = (Get-Location).Path + "\tmp\android.jar"
|
$android_jar = (Get-Location).Path + "\tmp\android.jar"
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,17 @@
|
|||||||
|
|
||||||
# This is a bash script to create android.jar stubs
|
# This is a bash script to create android.jar stubs
|
||||||
|
|
||||||
|
for dep in "curl" "base64" "zip"
|
||||||
|
do
|
||||||
|
which $dep >/dev/null 2>&1 || { echo >&2 "Error: This script needs $dep installed."; abort=yes; }
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $abort = yes ]; then
|
||||||
|
echo "Some of the dependencies didn't exist. Aborting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
# 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 ..
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -64,18 +73,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,13 +1,6 @@
|
|||||||
package com.squareup.duktape;
|
package com.squareup.duktape;
|
||||||
|
|
||||||
/*
|
/* part of tachiyomi-extensions which is licensed under Apache License Version 2.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/. */
|
|
||||||
// part of tachiyomi-extensions which was originally licensed under Apache License Version 2.0
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|||||||
@@ -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(root, apk).also {
|
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(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,12 +82,14 @@ data class InstalledPackage(val root: File) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NodeList.toList(): List<Node> {
|
companion object {
|
||||||
|
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(root: File, apk: File): PackageInfo {
|
fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
|
||||||
return PackageInfo().also {
|
return PackageInfo().also {
|
||||||
it.packageName = packageName
|
it.packageName = packageName
|
||||||
it.versionCode = versionCode.toInt()
|
it.versionCode = versionCode.toInt()
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
|
||||||

|
| Build | Stable | Preview | Support Server |
|
||||||
|
|-------|----------|---------|---------|
|
||||||
|
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk/tree/preview/latest_pointer) | [](https://discord.gg/DDZdqZWaHA) |
|
||||||
|
|
||||||
# Tachidesk
|
# Tachidesk
|
||||||
|
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
||||||
|
|
||||||
A free and open source manga reader 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/).
|
||||||
|
|
||||||
|
Tachidesk is an independent Tachiyomi compatible software made by [@AriaMoradi AKA ArMor](https://github.com/AriaMoradi) and contributors and is **not a Fork of** Tachiyomi.
|
||||||
|
|
||||||
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
|
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
|
||||||
|
|
||||||
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
||||||
@@ -15,6 +22,7 @@ Here is a list of current features:
|
|||||||
- Searching and browsing installed sources.
|
- Searching and browsing installed sources.
|
||||||
- A decent chapter reader.
|
- A decent chapter reader.
|
||||||
- Ability to download Mangas for offline read(This partially works)
|
- Ability to download Mangas for offline read(This partially works)
|
||||||
|
- Backup and restore support powered by Tachiyomi Legacy Backups
|
||||||
|
|
||||||
**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.
|
||||||
|
|
||||||
@@ -24,9 +32,9 @@ Anyways, for more info checkout [finished milestone #1](https://github.com/Suway
|
|||||||
### 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 "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview branch](https://github.com/Suwayomi/Tachidesk/tree/preview).
|
||||||
|
|
||||||
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` (or `java -jar Tachidesk-latest.jar` if you have the latest preview) 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/Suwayomi/Tachidesk/releases).
|
||||||
@@ -43,7 +51,7 @@ yay -S tachidesk
|
|||||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
||||||
|
|
||||||
## General troubleshooting
|
## General troubleshooting
|
||||||
If the app breaks try deleting the directory below and re-running the app (**This will delete all your data!**) and if the problem persists open an issue.
|
If the app breaks, make sure that it's not running(right click on tray icon and quit or kill it through the way your Operating System provides), delete the directory below and re-run the app (**This procedure will delete all your data!**) and if the problem persists open an issue or ask for help on discord.
|
||||||
|
|
||||||
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
||||||
|
|
||||||
@@ -54,7 +62,7 @@ On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
|
|||||||
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
||||||
|
|
||||||
## Support and help
|
## Support and help
|
||||||
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support and help.
|
Join Tachidesk's [discord server](https://discord.gg/DDZdqZWaHA) to hang out with the community and to receive support and help.
|
||||||
|
|
||||||
## How does it work?
|
## How does it work?
|
||||||
This project has two components:
|
This project has two components:
|
||||||
@@ -65,26 +73,27 @@ This project has two components:
|
|||||||
### Prerequisite: Get Android stubs jar
|
### Prerequisite: 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/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||||
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
|
#### Automated download
|
||||||
Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
|
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.
|
||||||
### Prerequisite: Software dependencies
|
### Prerequisite: Software dependencies
|
||||||
You need this software packages installed in order to build this project:
|
You need this software packages installed in order to build this project:
|
||||||
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
||||||
- Nodejs LTS or latest
|
- Nodejs LTS or latest
|
||||||
- Yarn
|
- Yarn
|
||||||
|
- Git
|
||||||
### building the full-blown jar
|
### building the full-blown jar
|
||||||
Run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.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
|
### building without `webUI` bundled(server only)
|
||||||
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar -x :webUI:copyBuild`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
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 windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
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`.
|
||||||
## 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 -x :webUI:copyBuild --stacktrace` to run the server
|
Follow [Get Android stubs jar](#prerequisite-get-android-stubs-jar) then run `./gradlew :server:run --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 client if a new browser window doesn't start automatically,
|
then `yarn start` to start the development server, if a new browser window doesn't get opned 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.
|
||||||
|
|
||||||
|
|||||||
+27
-29
@@ -1,33 +1,30 @@
|
|||||||
import org.jetbrains.kotlin.config.KotlinCompilerVersion
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.jetbrains.kotlin.jvm") version "1.4.21" apply false // Also in buildSrc Config.kt
|
kotlin("jvm") version "1.4.32"
|
||||||
id("java")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "xyz.nulldev.ts"
|
group = "ir.armor.tachidesk"
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
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.google.com/dl/android/maven2/")
|
maven("https://dl.google.com/dl/android/maven2/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val javaProjects = listOf(
|
val projects = listOf(
|
||||||
project(":AndroidCompat"),
|
project(":AndroidCompat"),
|
||||||
project(":AndroidCompat:Config"),
|
project(":AndroidCompat:Config"),
|
||||||
project(":server")
|
project(":server")
|
||||||
)
|
)
|
||||||
|
|
||||||
configure(javaProjects) {
|
configure(projects) {
|
||||||
apply(plugin = "java")
|
|
||||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -35,34 +32,32 @@ configure(javaProjects) {
|
|||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Kotlin
|
// Kotlin
|
||||||
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
implementation(kotlin("reflect"))
|
||||||
testImplementation(kotlin("test", version = "1.4.21"))
|
testImplementation(kotlin("test"))
|
||||||
}
|
|
||||||
}
|
// coroutines
|
||||||
|
val coroutinesVersion = "1.4.3"
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
||||||
|
|
||||||
configure(listOf(
|
|
||||||
project(":AndroidCompat"),
|
|
||||||
project(":server"),
|
|
||||||
project(":AndroidCompat:Config")
|
|
||||||
|
|
||||||
)) {
|
|
||||||
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.5.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("ch.qos.logback:logback-classic:1.2.3")
|
||||||
implementation("io.github.microutils:kotlin-logging:2.0.3")
|
implementation("io.github.microutils:kotlin-logging:2.0.6")
|
||||||
|
|
||||||
// RxJava
|
// RxJava
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
@@ -71,15 +66,18 @@ configure(listOf(
|
|||||||
// 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.1")
|
||||||
implementation("io.github.config4k:config4k:0.4.2")
|
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.1")
|
||||||
|
|
||||||
|
// 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
+134
-100
@@ -1,57 +1,50 @@
|
|||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
||||||
|
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
// id("org.jetbrains.kotlin.jvm") version "1.4.21"
|
|
||||||
application
|
application
|
||||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||||
id("org.jmailen.kotlinter") version "3.3.0"
|
id("org.jmailen.kotlinter") version "3.4.3"
|
||||||
id("edu.sc.seis.launch4j") version "2.4.9"
|
id("edu.sc.seis.launch4j") version "2.5.0"
|
||||||
|
id("de.fuerstenau.buildconfig") version "1.1.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
val TachideskVersion = "v0.2.7"
|
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://jitpack.io")
|
url = uri("https://jitpack.io")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
||||||
// implementation("tachiyomi.sourceapi:source-api:1.1")
|
// implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||||
|
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
val okhttp_version = "4.10.0-RC1"
|
val okhttpVersion = "4.10.0-RC1"
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttp_version")
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttp_version")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
implementation("com.squareup.okio:okio:2.9.0")
|
implementation("com.squareup.okio:okio:2.10.0")
|
||||||
|
|
||||||
|
|
||||||
// retrofit
|
// Retrofit
|
||||||
val retrofit_version = "2.9.0"
|
val retrofitVersion = "2.9.0"
|
||||||
implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
|
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
|
||||||
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:$retrofit_version")
|
implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion")
|
||||||
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofit_version")
|
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofitVersion")
|
||||||
|
|
||||||
|
|
||||||
// reactivex
|
// Reactivex
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
// implementation("io.reactivex:rxandroid:1.2.1")
|
|
||||||
// implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
|
||||||
// implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
|
||||||
implementation("com.google.code.gson:gson:2.8.6")
|
implementation("com.google.code.gson:gson:2.8.6")
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
|
||||||
@@ -59,27 +52,25 @@ dependencies {
|
|||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.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("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
|
||||||
|
|
||||||
|
|
||||||
// api
|
// api
|
||||||
implementation("io.javalin:javalin:3.12.0")
|
implementation("io.javalin:javalin:3.13.6")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.3")
|
||||||
|
|
||||||
// Exposed ORM
|
// Exposed ORM
|
||||||
val exposed_version = "0.28.1"
|
val exposedVersion = "0.31.1"
|
||||||
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
||||||
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||||
implementation("com.h2database:h2:1.4.199")
|
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
|
||||||
|
|
||||||
|
// current database driver
|
||||||
|
implementation("com.h2database:h2:1.4.200")
|
||||||
|
|
||||||
// tray icon
|
// tray icon
|
||||||
implementation("com.dorkbox:SystemTray:3.17")
|
implementation("com.dorkbox:SystemTray:4.1")
|
||||||
|
implementation("com.dorkbox:Utilities:1.9")
|
||||||
|
|
||||||
|
implementation("com.google.guava:guava:30.1.1-jre")
|
||||||
|
|
||||||
// AndroidCompat
|
// AndroidCompat
|
||||||
implementation(project(":AndroidCompat"))
|
implementation(project(":AndroidCompat"))
|
||||||
@@ -87,14 +78,14 @@ dependencies {
|
|||||||
|
|
||||||
// uncomment to test extensions directly
|
// uncomment to test extensions directly
|
||||||
// implementation(fileTree("lib/"))
|
// implementation(fileTree("lib/"))
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
testImplementation(kotlin("test-junit5"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val name = "ir.armor.tachidesk.Main"
|
val MainClass = "ir.armor.tachidesk.Main"
|
||||||
application {
|
application {
|
||||||
mainClass.set(name)
|
mainClass.set(MainClass)
|
||||||
|
|
||||||
// Required by ShadowJar.
|
|
||||||
mainClassName = name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -105,7 +96,11 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val TachideskRevision = Runtime
|
// should be bumped with each stable release
|
||||||
|
val tachideskVersion = "v0.3.0"
|
||||||
|
|
||||||
|
// counts commit count on master
|
||||||
|
val tachideskRevision = Runtime
|
||||||
.getRuntime()
|
.getRuntime()
|
||||||
.exec("git rev-list master --count")
|
.exec("git rev-list master --count")
|
||||||
.let { process ->
|
.let { process ->
|
||||||
@@ -118,94 +113,133 @@ val TachideskRevision = Runtime
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildConfig {
|
||||||
|
appName = rootProject.name
|
||||||
|
clsName = "BuildConfig"
|
||||||
|
packageName = "ir.armor.tachidesk.server"
|
||||||
|
version = tachideskVersion
|
||||||
|
|
||||||
|
|
||||||
|
buildConfigField("String", "name", rootProject.name) // alias for BuildConfig.NAME
|
||||||
|
buildConfigField("String", "version", tachideskVersion) // alias for BuildConfig.VERSION
|
||||||
|
buildConfigField("String", "revision", tachideskRevision)
|
||||||
|
buildConfigField("boolean", "debug", project.hasProperty("debugApp").toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
launch4j { //used for windows
|
||||||
|
mainClassName = MainClass
|
||||||
|
bundledJrePath = "jre"
|
||||||
|
bundledJre64Bit = true
|
||||||
|
jreMinVersion = "8"
|
||||||
|
outputDir = "${rootProject.name}-$tachideskVersion-$tachideskRevision-win32"
|
||||||
|
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
||||||
|
jar = "${projectDir}/build/${rootProject.name}-$tachideskVersion-$tachideskRevision.jar"
|
||||||
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes(
|
attributes(
|
||||||
mapOf(
|
mapOf(
|
||||||
"Main-Class" to "com.example.MainKt", //will make your jar (produced by jar task) runnable
|
"Main-Class" to MainClass,
|
||||||
"ImplementationTitle" to project.name,
|
"Implementation-Title" to rootProject.name,
|
||||||
"Implementation-Version" to project.version)
|
"Implementation-Vendor" to "The Suwayomi Project",
|
||||||
|
"Specification-Version" to tachideskVersion,
|
||||||
|
"Implementation-Version" to tachideskRevision
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shadowJar {
|
shadowJar {
|
||||||
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
||||||
archiveBaseName.set("Tachidesk")
|
archiveBaseName.set(rootProject.name)
|
||||||
archiveVersion.set(TachideskVersion)
|
archiveVersion.set(tachideskVersion)
|
||||||
archiveClassifier.set(TachideskRevision)
|
archiveClassifier.set(tachideskRevision)
|
||||||
|
}
|
||||||
|
withType<KotlinCompile> {
|
||||||
|
kotlinOptions {
|
||||||
|
freeCompilerArgs = listOf(
|
||||||
|
"-Xopt-in=kotlin.RequiresOptIn",
|
||||||
|
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test {
|
||||||
|
useJUnit()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
launch4j { //used for windows
|
register<Zip>("windowsPackage") {
|
||||||
mainClassName = name
|
from(fileTree("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32"))
|
||||||
bundledJrePath = "jre"
|
|
||||||
bundledJre64Bit = true
|
|
||||||
jreMinVersion = "8"
|
|
||||||
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
|
||||||
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
|
||||||
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Zip>("windowsPackage") {
|
|
||||||
from(fileTree("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32"))
|
|
||||||
destinationDirectory.set(File("$buildDir"))
|
destinationDirectory.set(File("$buildDir"))
|
||||||
archiveFileName.set("Tachidesk-$TachideskVersion-$TachideskRevision-win32.zip")
|
archiveFileName.set("${rootProject.name}-$tachideskVersion-$tachideskRevision-win32.zip")
|
||||||
dependsOn("windowsPackageWorkaround2")
|
dependsOn("windowsPackageWorkaround2")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Delete>("windowsPackageWorkaround2") {
|
register<Delete>("windowsPackageWorkaround2") {
|
||||||
delete(
|
delete(
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jre",
|
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/jre",
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/lib",
|
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/lib",
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/server.exe",
|
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/server.exe",
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/Tachidesk-$tachideskVersion-$tachideskRevision-win32/Tachidesk-$tachideskVersion-$tachideskRevision-win32"
|
||||||
)
|
)
|
||||||
dependsOn("windowsPackageWorkaround")
|
dependsOn("windowsPackageWorkaround")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Copy>("windowsPackageWorkaround") {
|
register<Copy>("windowsPackageWorkaround") {
|
||||||
from("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
from("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
||||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
into("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
||||||
dependsOn("deleteUnwantedJreDir")
|
dependsOn("deleteUnwantedJreDir")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Delete>("deleteUnwantedJreDir") {
|
register<Delete>("deleteUnwantedJreDir") {
|
||||||
delete(
|
delete(
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jdk8u282-b08-jre"
|
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/jdk8u282-b08-jre"
|
||||||
)
|
)
|
||||||
dependsOn("addJreToDistributable")
|
dependsOn("addJreToDistributable")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Copy>("addJreToDistributable") {
|
register<Copy>("addJreToDistributable") {
|
||||||
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
|
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
|
||||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
into("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
||||||
eachFile {
|
eachFile {
|
||||||
path = path.replace(".*-jre".toRegex(),"jre")
|
path = path.replace(".*-jre".toRegex(), "jre")
|
||||||
}
|
}
|
||||||
dependsOn("downloadJre")
|
dependsOn("downloadJre")
|
||||||
dependsOn("createExe")
|
dependsOn("createExe")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
|
named("createExe") {
|
||||||
|
dependsOn("shadowJar")
|
||||||
|
}
|
||||||
|
|
||||||
|
register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
|
||||||
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
||||||
dest(buildDir)
|
dest("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
||||||
overwrite(false)
|
overwrite(false)
|
||||||
onlyIfModified(true)
|
onlyIfModified(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<ShadowJar> {
|
withType<ShadowJar> {
|
||||||
destinationDir = File("$rootDir/server/build")
|
destinationDirectory.set(File("$rootDir/server/build"))
|
||||||
dependsOn("lintKotlin")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("processResources") {
|
|
||||||
dependsOn(":webUI:copyBuild")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("run") {
|
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
dependsOn("formatKotlin", "lintKotlin")
|
||||||
|
}
|
||||||
|
|
||||||
|
named("run") {
|
||||||
|
dependsOn("formatKotlin", "lintKotlin")
|
||||||
|
}
|
||||||
|
|
||||||
|
named<Copy>("processResources") {
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
|
mustRunAfter(":webUI:copyBuild")
|
||||||
|
}
|
||||||
|
|
||||||
|
withType<LintTask> {
|
||||||
|
source(files("src"))
|
||||||
|
}
|
||||||
|
|
||||||
|
withType<FormatTask> {
|
||||||
|
source(files("src"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,52 +7,17 @@ package eu.kanade.tachiyomi.extension.api
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
// import android.content.Context
|
|
||||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
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.database.dataclass.ExtensionDataClass
|
import ir.armor.tachidesk.model.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
|
|
||||||
|
|
||||||
internal class ExtensionGithubApi {
|
object ExtensionGithubApi {
|
||||||
|
const val BASE_URL = "https://raw.githubusercontent.com"
|
||||||
// private val preferences: PreferencesHelper by injectLazy()
|
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
|
||||||
|
|
||||||
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
|
||||||
@@ -75,16 +40,14 @@ internal class ExtensionGithubApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApkUrl(extension: Extension.Available): String {
|
suspend fun findExtensions(): List<Extension.Available> {
|
||||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
val service: ExtensionGithubService = ExtensionGithubService.create()
|
||||||
|
|
||||||
|
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,6 +2,7 @@ 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
|
||||||
@@ -9,8 +10,6 @@ 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.
|
||||||
*/
|
*/
|
||||||
@@ -30,6 +29,7 @@ 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)
|
||||||
|
|||||||
@@ -0,0 +1,358 @@
|
|||||||
|
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()
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,8 @@ package ir.armor.tachidesk
|
|||||||
* 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 ir.armor.tachidesk.server.applicationSetup
|
import ir.armor.tachidesk.server.applicationSetup
|
||||||
import ir.armor.tachidesk.server.javalinSetup
|
|
||||||
|
|
||||||
class Main {
|
class Main {
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package ir.armor.tachidesk.database
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.database.table.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.database.table.CategoryTable
|
|
||||||
import ir.armor.tachidesk.database.table.ChapterTable
|
|
||||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
|
||||||
import ir.armor.tachidesk.database.table.PageTable
|
|
||||||
import ir.armor.tachidesk.database.table.SourceTable
|
|
||||||
import ir.armor.tachidesk.server.applicationDirs
|
|
||||||
import org.jetbrains.exposed.sql.Database
|
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
|
|
||||||
object DBMangaer {
|
|
||||||
val db by lazy {
|
|
||||||
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeDataBaseTables() {
|
|
||||||
// must mention db object so the lazy block executes
|
|
||||||
val db = DBMangaer.db
|
|
||||||
db.useNestedTransactions = true
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
SchemaUtils.createMissingTablesAndColumns(
|
|
||||||
ExtensionTable,
|
|
||||||
SourceTable,
|
|
||||||
MangaTable,
|
|
||||||
ChapterTable,
|
|
||||||
PageTable,
|
|
||||||
CategoryTable,
|
|
||||||
CategoryMangaTable,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package ir.armor.tachidesk.database.entity
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.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
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package ir.armor.tachidesk.database.entity
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.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
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package ir.armor.tachidesk.database.entity
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.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
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,11 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
||||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
||||||
import ir.armor.tachidesk.database.table.CategoryTable
|
import ir.armor.tachidesk.model.database.table.CategoryTable
|
||||||
import ir.armor.tachidesk.database.table.toDataClass
|
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||||
|
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
@@ -19,7 +20,11 @@ import org.jetbrains.exposed.sql.selectAll
|
|||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
|
||||||
fun createCategory(name: String) {
|
object Category {
|
||||||
|
/**
|
||||||
|
* The new category will be placed at the end of the list
|
||||||
|
*/
|
||||||
|
fun createCategory(name: String) {
|
||||||
transaction {
|
transaction {
|
||||||
val count = CategoryTable.selectAll().count()
|
val count = CategoryTable.selectAll().count()
|
||||||
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
||||||
@@ -28,18 +33,21 @@ fun createCategory(name: String) {
|
|||||||
it[CategoryTable.order] = count.toInt() + 1
|
it[CategoryTable.order] = count.toInt() + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
||||||
transaction {
|
transaction {
|
||||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||||
if (name != null) it[CategoryTable.name] = name
|
if (name != null) it[CategoryTable.name] = name
|
||||||
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
/**
|
||||||
|
* Move the category from position `from` to `to`
|
||||||
|
*/
|
||||||
|
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
||||||
transaction {
|
transaction {
|
||||||
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||||
categories.add(to - 1, categories.removeAt(from - 1))
|
categories.add(to - 1, categories.removeAt(from - 1))
|
||||||
@@ -49,21 +57,22 @@ fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeCategory(categoryId: Int) {
|
fun removeCategory(categoryId: Int) {
|
||||||
transaction {
|
transaction {
|
||||||
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
|
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
|
||||||
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
|
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
|
||||||
}
|
}
|
||||||
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCategoryList(): List<CategoryDataClass> {
|
fun getCategoryList(): List<CategoryDataClass> {
|
||||||
return transaction {
|
return transaction {
|
||||||
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||||
CategoryTable.toDataClass(it)
|
CategoryTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
||||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
import ir.armor.tachidesk.model.database.table.CategoryTable
|
||||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.database.table.CategoryTable
|
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
||||||
import ir.armor.tachidesk.database.table.toDataClass
|
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
@@ -21,7 +21,8 @@ import org.jetbrains.exposed.sql.select
|
|||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
|
||||||
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
object CategoryManga {
|
||||||
|
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||||
transaction {
|
transaction {
|
||||||
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
|
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
|
||||||
CategoryMangaTable.insert {
|
CategoryMangaTable.insert {
|
||||||
@@ -34,9 +35,9 @@ fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
||||||
transaction {
|
transaction {
|
||||||
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
||||||
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
|
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
|
||||||
@@ -45,20 +46,27 @@ fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
/**
|
||||||
|
* list of mangas that belong to a category
|
||||||
|
*/
|
||||||
|
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
||||||
return transaction {
|
return transaction {
|
||||||
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
||||||
MangaTable.toDataClass(it)
|
MangaTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
|
/**
|
||||||
|
* list of categories that a manga belongs to
|
||||||
|
*/
|
||||||
|
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
|
||||||
return transaction {
|
return transaction {
|
||||||
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||||
CategoryTable.toDataClass(it)
|
CategoryTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,23 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.database.dataclass.ChapterDataClass
|
import ir.armor.tachidesk.impl.Manga.getManga
|
||||||
import ir.armor.tachidesk.database.table.ChapterTable
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
import ir.armor.tachidesk.database.table.PageTable
|
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||||
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.model.database.table.PageTable
|
||||||
|
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
|
||||||
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
object Chapter {
|
||||||
|
/** get chapter list when showing a manga */
|
||||||
|
suspend fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||||
val mangaDetails = getManga(mangaId)
|
val mangaDetails = getManga(mangaId)
|
||||||
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
||||||
|
|
||||||
@@ -30,7 +34,7 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
|||||||
title = mangaDetails.title
|
title = mangaDetails.title
|
||||||
url = mangaDetails.url
|
url = mangaDetails.url
|
||||||
}
|
}
|
||||||
).toBlocking().first()
|
).awaitSingle()
|
||||||
|
|
||||||
val chapterCount = chapterList.count()
|
val chapterCount = chapterList.count()
|
||||||
|
|
||||||
@@ -38,7 +42,7 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
|||||||
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
||||||
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
||||||
if (chapterEntry == null) {
|
if (chapterEntry == null) {
|
||||||
ChapterTable.insertAndGetId {
|
ChapterTable.insert {
|
||||||
it[url] = fetchedChapter.url
|
it[url] = fetchedChapter.url
|
||||||
it[name] = fetchedChapter.name
|
it[name] = fetchedChapter.name
|
||||||
it[date_upload] = fetchedChapter.date_upload
|
it[date_upload] = fetchedChapter.date_upload
|
||||||
@@ -61,15 +65,14 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear any orphaned chapters
|
// clear any orphaned chapters that are in the db but not in `chapterList`
|
||||||
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
|
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
|
||||||
if (dbChapterCount > chapterCount) { // we got some clean up due
|
if (dbChapterCount > chapterCount) { // we got some clean up due
|
||||||
// TODO
|
// TODO: delete orphan chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
return@transaction chapterList.mapIndexed { index, it ->
|
chapterList.mapIndexed { index, it ->
|
||||||
ChapterDataClass(
|
ChapterDataClass(
|
||||||
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
|
|
||||||
it.url,
|
it.url,
|
||||||
it.name,
|
it.name,
|
||||||
it.date_upload,
|
it.date_upload,
|
||||||
@@ -77,18 +80,19 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
|||||||
it.scanlator,
|
it.scanlator,
|
||||||
mangaId,
|
mangaId,
|
||||||
chapterCount - index,
|
chapterCount - index,
|
||||||
chapterCount
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
/** used to display a chapter, get a chapter in order to show it's pages */
|
||||||
return transaction {
|
suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
||||||
val chapterEntry = ChapterTable.select {
|
val chapterEntry = transaction {
|
||||||
ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId)
|
ChapterTable.select {
|
||||||
|
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||||
}.firstOrNull()!!
|
}.firstOrNull()!!
|
||||||
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
}
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
val pageList = source.fetchPageList(
|
val pageList = source.fetchPageList(
|
||||||
@@ -96,38 +100,23 @@ fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
|||||||
url = chapterEntry[ChapterTable.url]
|
url = chapterEntry[ChapterTable.url]
|
||||||
name = chapterEntry[ChapterTable.name]
|
name = chapterEntry[ChapterTable.name]
|
||||||
}
|
}
|
||||||
).toBlocking().first()
|
).awaitSingle()
|
||||||
|
|
||||||
val chapterId = chapterEntry[ChapterTable.id].value
|
val chapterId = chapterEntry[ChapterTable.id].value
|
||||||
val chapterCount = transaction { ChapterTable.selectAll().count() }
|
val chapterCount = transaction { ChapterTable.selectAll().count() }
|
||||||
|
|
||||||
val chapter = ChapterDataClass(
|
// update page list for this chapter
|
||||||
chapterId,
|
transaction {
|
||||||
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
pageList.forEach { page ->
|
pageList.forEach { page ->
|
||||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||||
if (pageEntry == null) {
|
if (pageEntry == null) {
|
||||||
transaction {
|
|
||||||
PageTable.insert {
|
PageTable.insert {
|
||||||
it[index] = page.index
|
it[index] = page.index
|
||||||
it[url] = page.url
|
it[url] = page.url
|
||||||
it[imageUrl] = page.imageUrl
|
it[imageUrl] = page.imageUrl
|
||||||
it[this.chapter] = chapterId
|
it[chapter] = chapterId
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
transaction {
|
|
||||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
|
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
|
||||||
it[url] = page.url
|
it[url] = page.url
|
||||||
it[imageUrl] = page.imageUrl
|
it[imageUrl] = page.imageUrl
|
||||||
@@ -136,6 +125,16 @@ fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return@transaction chapter
|
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()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,19 +7,29 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import com.googlecode.d2j.dex.Dex2jar
|
import android.net.Uri
|
||||||
import com.googlecode.d2j.reader.MultiDexFileReader
|
|
||||||
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
|
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
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 eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass
|
||||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
||||||
import ir.armor.tachidesk.database.table.SourceTable
|
import ir.armor.tachidesk.impl.util.PackageTools.EXTENSION_FEATURE
|
||||||
import ir.armor.tachidesk.impl.util.APKExtractor
|
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MAX
|
||||||
import ir.armor.tachidesk.server.applicationDirs
|
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MIN
|
||||||
import kotlinx.coroutines.runBlocking
|
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.table.ExtensionTable
|
||||||
|
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||||
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
@@ -29,168 +39,201 @@ import org.jetbrains.exposed.sql.insert
|
|||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.URL
|
|
||||||
import java.net.URLClassLoader
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
object Extension {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
private fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
|
data class InstallableAPK(
|
||||||
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
|
val apkFilePath: String,
|
||||||
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
|
val pkgName: String
|
||||||
|
|
||||||
val jarFilePath = File(jarFile).toPath()
|
|
||||||
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
|
|
||||||
val handler = BaksmaliBaseDexExceptionHandler()
|
|
||||||
Dex2jar
|
|
||||||
.from(reader)
|
|
||||||
.withExceptionHandler(handler)
|
|
||||||
.reUseReg(false)
|
|
||||||
.topoLogicalSort()
|
|
||||||
.skipDebug(true)
|
|
||||||
.optimizeSynchronized(false)
|
|
||||||
.printIR(false)
|
|
||||||
.noCode(false)
|
|
||||||
.skipExceptions(false)
|
|
||||||
.to(jarFilePath)
|
|
||||||
if (handler.hasException()) {
|
|
||||||
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
|
|
||||||
logger.error(
|
|
||||||
"Detail Error Information in File $errorFile\n" +
|
|
||||||
"Please report this file to one of following link if possible (any one).\n" +
|
|
||||||
" https://sourceforge.net/p/dex2jar/tickets/\n" +
|
|
||||||
" https://bitbucket.org/pxb1988/dex2jar/issues\n" +
|
|
||||||
" https://github.com/pxb1988/dex2jar/issues\n" +
|
|
||||||
" dex2jar@googlegroups.com"
|
|
||||||
)
|
)
|
||||||
handler.dump(errorFile, emptyArray<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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun installAPK(apkName: String): Int {
|
suspend fun installAPK(fetcher: suspend () -> String): Int {
|
||||||
logger.debug("Installing $apkName")
|
val apkFilePath = fetcher()
|
||||||
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
|
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 fileNameWithoutType = apkName.substringBefore(".apk")
|
||||||
|
|
||||||
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||||
|
|
||||||
// check if we don't have the dex file already downloaded
|
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
|
||||||
if (!File(jarPath).exists()) {
|
|
||||||
runBlocking {
|
|
||||||
val api = ExtensionGithubApi()
|
|
||||||
val apkToDownload = api.getApkUrl(extensionRecord)
|
|
||||||
|
|
||||||
val apkFilePath = "$dirPathWithoutType.apk"
|
|
||||||
val jarFilePath = "$dirPathWithoutType.jar"
|
val jarFilePath = "$dirPathWithoutType.jar"
|
||||||
val dexFilePath = "$dirPathWithoutType.dex"
|
val dexFilePath = "$dirPathWithoutType.dex"
|
||||||
|
|
||||||
// download apk file
|
val packageInfo = getPackageInfo(apkFilePath)
|
||||||
downloadAPKFile(apkToDownload, apkFilePath)
|
val pkgName = packageInfo.packageName
|
||||||
|
|
||||||
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
|
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
|
||||||
logger.debug(className)
|
throw Exception("This apk is not a Tachiyomi extension")
|
||||||
// dex -> jar
|
}
|
||||||
dex2jar(dexFilePath, jarFilePath, fileNameWithoutType)
|
|
||||||
|
// 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
|
// clean up
|
||||||
File(apkFilePath).delete()
|
// File(apkFilePath).delete()
|
||||||
File(dexFilePath).delete()
|
File(dexFilePath).delete()
|
||||||
|
|
||||||
// update sources of the extension
|
// collect sources from the extension
|
||||||
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarFilePath")), this::class.java.classLoader)
|
val sources: List<CatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) {
|
||||||
val classToLoad = Class.forName(className, true, child)
|
is Source -> listOf(instance)
|
||||||
val instance = classToLoad.newInstance()
|
is SourceFactory -> instance.createSources()
|
||||||
|
else -> throw RuntimeException("Unknown source class type! ${instance.javaClass}")
|
||||||
|
}.map { it as CatalogueSource }
|
||||||
|
|
||||||
val extensionId = transaction {
|
val langs = sources.map { it.lang }.toSet()
|
||||||
return@transaction ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
val extensionLang = when (langs.size) {
|
||||||
|
0 -> ""
|
||||||
|
1 -> langs.first()
|
||||||
|
else -> "all"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance is HttpSource) { // single source
|
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
|
||||||
val httpSource = instance as HttpSource
|
|
||||||
transaction {
|
|
||||||
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
|
|
||||||
SourceTable.insert {
|
|
||||||
it[this.id] = httpSource.id
|
|
||||||
it[name] = httpSource.name
|
|
||||||
it[this.lang] = httpSource.lang
|
|
||||||
it[extension] = extensionId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.debug("Installed source ${httpSource.name} with id ${httpSource.id}")
|
|
||||||
}
|
|
||||||
} else { // multi source
|
|
||||||
val sourceFactory = instance as SourceFactory
|
|
||||||
transaction {
|
|
||||||
sourceFactory.createSources().forEachIndexed { index, source ->
|
|
||||||
val httpSource = source as HttpSource
|
|
||||||
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
|
|
||||||
SourceTable.insert {
|
|
||||||
it[this.id] = httpSource.id
|
|
||||||
it[name] = httpSource.name
|
|
||||||
it[this.lang] = httpSource.lang
|
|
||||||
it[extension] = extensionId
|
|
||||||
it[partOfFactorySource] = true
|
|
||||||
it[positionInFactorySource] = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update extension info
|
// update extension info
|
||||||
transaction {
|
transaction {
|
||||||
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
|
if (ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
|
||||||
it[installed] = true
|
ExtensionTable.insert {
|
||||||
it[classFQName] = className
|
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
|
||||||
}
|
}
|
||||||
return 201 // we downloaded successfully
|
|
||||||
|
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 {
|
} else {
|
||||||
return 302
|
return 302 // extension was already installed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val networkHelper: NetworkHelper by injectLazy()
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
private fun downloadAPKFile(url: String, apkPath: String) {
|
private suspend fun downloadAPKFile(url: String, savePath: String) {
|
||||||
val request = Request.Builder().url(url).build()
|
val request = Request.Builder().url(url).build()
|
||||||
val response = networkHelper.client.newCall(request).execute()
|
val response = network.client.newCall(request).await()
|
||||||
|
|
||||||
val downloadedFile = File(apkPath)
|
val downloadedFile = File(savePath)
|
||||||
val sink = downloadedFile.sink().buffer()
|
downloadedFile.sink().buffer().use { sink ->
|
||||||
sink.writeAll(response.body!!.source())
|
response.body!!.source().use { source ->
|
||||||
sink.close()
|
sink.writeAll(source)
|
||||||
}
|
sink.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun removeExtension(apkName: String) {
|
fun uninstallExtension(pkgName: String) {
|
||||||
logger.debug("Uninstalling $apkName")
|
logger.debug("Uninstalling $pkgName")
|
||||||
|
|
||||||
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
|
val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!! }
|
||||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
transaction {
|
transaction {
|
||||||
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
val extensionId = extensionRecord[ExtensionTable.id].value
|
||||||
|
|
||||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||||
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
|
if (extensionRecord[ExtensionTable.isObsolete])
|
||||||
it[ExtensionTable.installed] = false
|
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
|
||||||
|
else
|
||||||
|
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||||
|
it[isInstalled] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File(jarPath).exists()) {
|
if (File(jarPath).exists()) {
|
||||||
File(jarPath).delete()
|
File(jarPath).delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val network: NetworkHelper by injectLazy()
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||||
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
||||||
|
|
||||||
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||||
@@ -198,10 +241,11 @@ fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
|||||||
return getCachedImageResponse(saveDir, apkName) {
|
return getCachedImageResponse(saveDir, apkName) {
|
||||||
network.client.newCall(
|
network.client.newCall(
|
||||||
GET(iconUrl)
|
GET(iconUrl)
|
||||||
).execute()
|
).await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtensionIconUrl(apkName: String): String {
|
||||||
|
return "/api/v1/extension/icon/$apkName"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExtensionIconUrl(apkName: String): String {
|
|
||||||
return "/api/v1/extension/icon/$apkName"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,45 +9,85 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
|
||||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||||
import kotlinx.coroutines.runBlocking
|
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
object ExtensionsList {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
private object Data {
|
var lastUpdateCheck: Long = 0
|
||||||
var lastExtensionCheck: Long = 0
|
var updateMap = ConcurrentHashMap<String, Extension.Available>()
|
||||||
}
|
|
||||||
|
|
||||||
private fun extensionDatabaseIsEmtpy(): Boolean {
|
/** 60,000 milliseconds = 60 seconds */
|
||||||
return transaction {
|
private const val ExtensionUpdateDelayTime = 60 * 1000
|
||||||
return@transaction ExtensionTable.selectAll().count() == 0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
suspend fun getExtensionList(): List<ExtensionDataClass> {
|
||||||
// update if 60 seconds has passed or requested offline and database is empty
|
// update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty
|
||||||
if (Data.lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) {
|
if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) {
|
||||||
logger.debug("Getting extensions list from the internet")
|
logger.debug("Getting extensions list from the internet")
|
||||||
Data.lastExtensionCheck = System.currentTimeMillis()
|
lastUpdateCheck = System.currentTimeMillis()
|
||||||
var foundExtensions: List<Extension.Available>
|
|
||||||
runBlocking {
|
val foundExtensions = ExtensionGithubApi.findExtensions()
|
||||||
val api = ExtensionGithubApi()
|
updateExtensionDatabase(foundExtensions)
|
||||||
foundExtensions = api.findExtensions()
|
} 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 {
|
transaction {
|
||||||
foundExtensions.forEach { foundExtension ->
|
foundExtensions.forEach { foundExtension ->
|
||||||
val extensionRecord = ExtensionTable.select { ExtensionTable.name eq foundExtension.name }.firstOrNull()
|
val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
|
||||||
if (extensionRecord != null) {
|
if (extensionRecord != null) {
|
||||||
// update the record
|
if (extensionRecord[ExtensionTable.isInstalled]) {
|
||||||
ExtensionTable.update({ ExtensionTable.name eq foundExtension.name }) {
|
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[name] = foundExtension.name
|
||||||
it[pkgName] = foundExtension.pkgName
|
|
||||||
it[versionName] = foundExtension.versionName
|
it[versionName] = foundExtension.versionName
|
||||||
it[versionCode] = foundExtension.versionCode
|
it[versionCode] = foundExtension.versionCode
|
||||||
it[lang] = foundExtension.lang
|
it[lang] = foundExtension.lang
|
||||||
@@ -55,6 +95,7 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
|||||||
it[apkName] = foundExtension.apkName
|
it[apkName] = foundExtension.apkName
|
||||||
it[iconUrl] = foundExtension.iconUrl
|
it[iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// insert new record
|
// insert new record
|
||||||
ExtensionTable.insert {
|
ExtensionTable.insert {
|
||||||
@@ -69,26 +110,23 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// 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 {
|
} else {
|
||||||
logger.debug("used cached extension list")
|
// is not installed so we can remove the record without a care
|
||||||
|
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return transaction {
|
|
||||||
return@transaction ExtensionTable.selectAll().map {
|
|
||||||
ExtensionDataClass(
|
|
||||||
it[ExtensionTable.name],
|
|
||||||
it[ExtensionTable.pkgName],
|
|
||||||
it[ExtensionTable.versionName],
|
|
||||||
it[ExtensionTable.versionCode],
|
|
||||||
it[ExtensionTable.lang],
|
|
||||||
it[ExtensionTable.isNsfw],
|
|
||||||
it[ExtensionTable.apkName],
|
|
||||||
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
|
||||||
it[ExtensionTable.installed],
|
|
||||||
it[ExtensionTable.classFQName]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,22 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
import ir.armor.tachidesk.impl.Manga.getManga
|
||||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.database.table.toDataClass
|
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||||
|
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
|
||||||
fun addMangaToLibrary(mangaId: Int) {
|
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)
|
val manga = getManga(mangaId)
|
||||||
if (!manga.inLibrary) {
|
if (!manga.inLibrary) {
|
||||||
transaction {
|
transaction {
|
||||||
@@ -26,9 +31,9 @@ fun addMangaToLibrary(mangaId: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMangaFromLibrary(mangaId: Int) {
|
suspend fun removeMangaFromLibrary(mangaId: Int) {
|
||||||
val manga = getManga(mangaId)
|
val manga = getManga(mangaId)
|
||||||
if (manga.inLibrary) {
|
if (manga.inLibrary) {
|
||||||
transaction {
|
transaction {
|
||||||
@@ -39,12 +44,13 @@ fun removeMangaFromLibrary(mangaId: Int) {
|
|||||||
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLibraryMangas(): List<MangaDataClass> {
|
fun getLibraryMangas(): List<MangaDataClass> {
|
||||||
return transaction {
|
return transaction {
|
||||||
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
||||||
MangaTable.toDataClass(it)
|
MangaTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,33 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||||
import ir.armor.tachidesk.database.table.MangaStatus
|
import ir.armor.tachidesk.impl.Source.getSource
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
||||||
import ir.armor.tachidesk.server.applicationDirs
|
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.table.MangaStatus
|
||||||
|
import ir.armor.tachidesk.model.database.table.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.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
object Manga {
|
||||||
|
private fun truncate(text: String?, maxLength: Int): String? {
|
||||||
|
return if (text?.length ?: 0 > maxLength)
|
||||||
|
text?.take(maxLength - 3) + "..."
|
||||||
|
else
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
|
|
||||||
return if (mangaEntry[MangaTable.initialized]) {
|
return if (mangaEntry[MangaTable.initialized]) {
|
||||||
@@ -47,7 +64,7 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
|||||||
url = mangaEntry[MangaTable.url]
|
url = mangaEntry[MangaTable.url]
|
||||||
title = mangaEntry[MangaTable.title]
|
title = mangaEntry[MangaTable.title]
|
||||||
}
|
}
|
||||||
).toBlocking().first()
|
).awaitSingle()
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
@@ -56,7 +73,7 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
|||||||
|
|
||||||
it[MangaTable.artist] = fetchedManga.artist
|
it[MangaTable.artist] = fetchedManga.artist
|
||||||
it[MangaTable.author] = fetchedManga.author
|
it[MangaTable.author] = fetchedManga.author
|
||||||
it[MangaTable.description] = fetchedManga.description
|
it[MangaTable.description] = truncate(fetchedManga.description, 4096)
|
||||||
it[MangaTable.genre] = fetchedManga.genre
|
it[MangaTable.genre] = fetchedManga.genre
|
||||||
it[MangaTable.status] = fetchedManga.status
|
it[MangaTable.status] = fetchedManga.status
|
||||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
||||||
@@ -86,9 +103,10 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
|||||||
getSource(mangaEntry[MangaTable.sourceReference])
|
getSource(mangaEntry[MangaTable.sourceReference])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
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 mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
val saveDir = applicationDirs.thumbnailsRoot
|
val saveDir = applicationDirs.thumbnailsRoot
|
||||||
val fileName = mangaId.toString()
|
val fileName = mangaId.toString()
|
||||||
@@ -103,6 +121,7 @@ fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
|||||||
|
|
||||||
source.client.newCall(
|
source.client.newCall(
|
||||||
GET(thumbnailUrl, source.headers)
|
GET(thumbnailUrl, source.headers)
|
||||||
).execute()
|
).await()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,32 +8,35 @@ package ir.armor.tachidesk.impl
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
import ir.armor.tachidesk.database.table.MangaStatus
|
import ir.armor.tachidesk.model.database.table.MangaStatus
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.model.database.table.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.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
fun proxyThumbnailUrl(mangaId: Int): String {
|
object MangaList {
|
||||||
|
fun proxyThumbnailUrl(mangaId: Int): String {
|
||||||
return "/api/v1/manga/$mangaId/thumbnail"
|
return "/api/v1/manga/$mangaId/thumbnail"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
||||||
val source = getHttpSource(sourceId.toLong())
|
val source = getHttpSource(sourceId)
|
||||||
val mangasPage = if (popular) {
|
val mangasPage = if (popular) {
|
||||||
source.fetchPopularManga(pageNum).toBlocking().first()
|
source.fetchPopularManga(pageNum).awaitSingle()
|
||||||
} else {
|
} else {
|
||||||
if (source.supportsLatest)
|
if (source.supportsLatest)
|
||||||
source.fetchLatestUpdates(pageNum).toBlocking().first()
|
source.fetchLatestUpdates(pageNum).awaitSingle()
|
||||||
else
|
else
|
||||||
throw Exception("Source $source doesn't support latest")
|
throw Exception("Source $source doesn't support latest")
|
||||||
}
|
}
|
||||||
return mangasPage.processEntries(sourceId)
|
return mangasPage.processEntries(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
||||||
val mangasPage = this
|
val mangasPage = this
|
||||||
val mangaList = transaction {
|
val mangaList = transaction {
|
||||||
return@transaction mangasPage.mangas.map { manga ->
|
return@transaction mangasPage.mangas.map { manga ->
|
||||||
@@ -95,4 +98,5 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
|||||||
mangaList,
|
mangaList,
|
||||||
mangasPage.hasNextPage
|
mangasPage.hasNextPage
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,26 +9,37 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import ir.armor.tachidesk.database.table.ChapterTable
|
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.database.table.PageTable
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
import ir.armor.tachidesk.database.table.SourceTable
|
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.server.applicationDirs
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.model.database.table.PageTable
|
||||||
|
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||||
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
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) {
|
if (page.imageUrl == null) {
|
||||||
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!!
|
page.imageUrl = source.fetchImageUrl(page).awaitSingle()
|
||||||
}
|
}
|
||||||
return page.imageUrl!!
|
return page.imageUrl!!
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
|
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
val chapterEntry = transaction {
|
val chapterEntry = transaction {
|
||||||
@@ -47,9 +58,10 @@ fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream,
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (pageEntry[PageTable.imageUrl] == null) {
|
if (pageEntry[PageTable.imageUrl] == null) {
|
||||||
|
val trueImageUrl = getTrueImageUrl(tachiPage, source)
|
||||||
transaction {
|
transaction {
|
||||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
|
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
|
||||||
it[imageUrl] = getTrueImageUrl(tachiPage, source)
|
it[imageUrl] = trueImageUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,11 +71,13 @@ fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream,
|
|||||||
val fileName = index.toString()
|
val fileName = index.toString()
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, fileName) {
|
return getCachedImageResponse(saveDir, fileName) {
|
||||||
source.fetchImage(tachiPage).toBlocking().first()
|
source.fetchImage(tachiPage).awaitSingle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
// 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 mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
@@ -82,4 +96,5 @@ fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
|||||||
// make sure dirs exist
|
// make sure dirs exist
|
||||||
File(mangaDir).mkdirs()
|
File(mangaDir).mkdirs()
|
||||||
return mangaDir
|
return mangaDir
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,28 +7,36 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
|
import ir.armor.tachidesk.impl.MangaList.processEntries
|
||||||
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
|
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
|
||||||
|
|
||||||
fun sourceFilters(sourceId: Long) {
|
object Search {
|
||||||
|
// TODO
|
||||||
|
fun sourceFilters(sourceId: Long) {
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
// source.getFilterList().toItems()
|
// source.getFilterList().toItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
|
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
|
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).awaitSingle()
|
||||||
return searchManga.processEntries(sourceId)
|
return searchManga.processEntries(sourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sourceGlobalSearch(searchTerm: String) {
|
fun sourceGlobalSearch(searchTerm: String) {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FilterWrapper(
|
data class FilterWrapper(
|
||||||
val type: String,
|
val type: String,
|
||||||
val filter: Any
|
val filter: Any
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note: Exhentai had a filter serializer (now in SY) that we might be able to steal
|
||||||
|
*/
|
||||||
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
|
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
|
||||||
// return mapNotNull { filter ->
|
// return mapNotNull { filter ->
|
||||||
// when (filter) {
|
// when (filter) {
|
||||||
@@ -64,3 +72,4 @@ data class FilterWrapper(
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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.table.ExtensionTable
|
||||||
|
import ir.armor.tachidesk.model.database.table.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,107 +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.SourceFactory
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import ir.armor.tachidesk.database.dataclass.SourceDataClass
|
|
||||||
import ir.armor.tachidesk.database.entity.ExtensionEntity
|
|
||||||
import ir.armor.tachidesk.database.entity.SourceEntity
|
|
||||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
|
||||||
import ir.armor.tachidesk.database.table.SourceTable
|
|
||||||
import ir.armor.tachidesk.server.applicationDirs
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import java.lang.NullPointerException
|
|
||||||
import java.net.URL
|
|
||||||
import java.net.URLClassLoader
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
|
|
||||||
private val extensionCache = mutableListOf<Pair<String, Any>>()
|
|
||||||
|
|
||||||
fun getHttpSource(sourceId: Long): HttpSource {
|
|
||||||
val sourceRecord = transaction {
|
|
||||||
SourceEntity.findById(sourceId)
|
|
||||||
} ?: throw NullPointerException("Source with id $sourceId is not installed")
|
|
||||||
|
|
||||||
val cachedResult: Pair<Long, HttpSource>? = sourceCache.firstOrNull { it.first == sourceId }
|
|
||||||
if (cachedResult != null) {
|
|
||||||
logger.debug("used cached HttpSource: ${cachedResult.second.name}")
|
|
||||||
return cachedResult.second
|
|
||||||
}
|
|
||||||
|
|
||||||
val result: HttpSource = transaction {
|
|
||||||
val extensionId = sourceRecord.extension.id.value
|
|
||||||
val extensionRecord = ExtensionEntity.findById(extensionId)!!
|
|
||||||
val apkName = extensionRecord.apkName
|
|
||||||
val className = extensionRecord.classFQName
|
|
||||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
|
||||||
|
|
||||||
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
|
|
||||||
var usedCached = false
|
|
||||||
val instance =
|
|
||||||
if (cachedExtensionPair != null) {
|
|
||||||
usedCached = true
|
|
||||||
logger.debug("Used cached Extension")
|
|
||||||
cachedExtensionPair.second
|
|
||||||
} else {
|
|
||||||
logger.debug("No Extension cache")
|
|
||||||
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
|
|
||||||
val classToLoad = Class.forName(className, true, child)
|
|
||||||
classToLoad.newInstance()
|
|
||||||
}
|
|
||||||
if (sourceRecord.partOfFactorySource) {
|
|
||||||
return@transaction if (usedCached) {
|
|
||||||
(instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!]
|
|
||||||
} else {
|
|
||||||
val list = (instance as SourceFactory).createSources()
|
|
||||||
extensionCache.add(Pair(jarPath, list))
|
|
||||||
list[sourceRecord.positionInFactorySource!!] as HttpSource
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!usedCached)
|
|
||||||
extensionCache.add(Pair(jarPath, instance))
|
|
||||||
return@transaction instance as HttpSource
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sourceCache.add(Pair(sourceId, result))
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSourceList(): List<SourceDataClass> {
|
|
||||||
return transaction {
|
|
||||||
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()
|
|
||||||
|
|
||||||
return@transaction 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 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
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.Category.getCategoryList
|
||||||
|
import ir.armor.tachidesk.impl.CategoryManga.getMangaCategories
|
||||||
|
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.CategoryImpl
|
||||||
|
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.table.ChapterTable
|
||||||
|
import ir.armor.tachidesk.model.database.table.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)
|
||||||
|
val mangaId = manga.id!!.toInt()
|
||||||
|
|
||||||
|
// Check if user wants chapter information in backup
|
||||||
|
if (options.includeChapters) {
|
||||||
|
// Backup all the chapters
|
||||||
|
val chapters = ChapterTable.select { ChapterTable.manga eq mangaId }.map { ChapterImpl.fromQuery(it) }
|
||||||
|
if (chapters.count() > 0) {
|
||||||
|
val chaptersJson = parser.toJsonTree(chapters)
|
||||||
|
if (chaptersJson.asJsonArray.size() > 0) {
|
||||||
|
entry[Backup.CHAPTERS] = chaptersJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants category information in backup
|
||||||
|
if (options.includeCategories) {
|
||||||
|
// Backup categories for this manga
|
||||||
|
val categoriesForManga = getMangaCategories(mangaId)
|
||||||
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
|
val categoriesNames = categoriesForManga.map { it.name }
|
||||||
|
entry[Backup.CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants track information in backup
|
||||||
|
if (options.includeTracking) { // TODO
|
||||||
|
// val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
// if (tracks.isNotEmpty()) {
|
||||||
|
// entry[TRACK] = parser.toJsonTree(tracks)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// // Check if user wants history information in backup
|
||||||
|
if (options.includeHistory) { // TODO
|
||||||
|
// val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||||
|
// if (historyForManga.isNotEmpty()) {
|
||||||
|
// val historyData = historyForManga.mapNotNull { history ->
|
||||||
|
// val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||||
|
// url?.let { DHistory(url, history.last_read) }
|
||||||
|
// }
|
||||||
|
// val historyJson = parser.toJsonTree(historyData)
|
||||||
|
// if (historyJson.asJsonArray.size() > 0) {
|
||||||
|
// entry[HISTORY] = historyJson
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupCategories(root: JsonArray) {
|
||||||
|
val categories = getCategoryList().map {
|
||||||
|
CategoryImpl().apply {
|
||||||
|
name = it.name
|
||||||
|
order = it.order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories.forEach { root.add(parser.toJsonTree(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
||||||
|
extensions.sorted().forEach {
|
||||||
|
root.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
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.Category.createCategory
|
||||||
|
import ir.armor.tachidesk.impl.Category.getCategoryList
|
||||||
|
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupValidator.ValidationResult
|
||||||
|
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupValidator.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.CategoryImpl
|
||||||
|
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.table.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 = LegacyBackupValidator.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(jsonCategories: JsonElement) { // TODO
|
||||||
|
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||||
|
val dbCategories = getCategoryList()
|
||||||
|
|
||||||
|
// Iterate over them
|
||||||
|
backupCategories.forEach { category ->
|
||||||
|
if (dbCategories.none { it.name == category.name }) {
|
||||||
|
createCategory(category.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||||
|
val manga = parser.fromJson<MangaImpl>(
|
||||||
|
mangaJson.get(
|
||||||
|
Backup.MANGA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val chapters = parser.fromJson<List<ChapterImpl>>(
|
||||||
|
mangaJson.get(Backup.CHAPTERS)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val categories = parser.fromJson<List<String>>(
|
||||||
|
mangaJson.get(Backup.CATEGORIES)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val history = parser.fromJson<List<DHistory>>(
|
||||||
|
mangaJson.get(Backup.HISTORY)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val tracks = parser.fromJson<List<TrackImpl>>(
|
||||||
|
mangaJson.get(Backup.TRACK)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
val source = try {
|
||||||
|
getHttpSource(manga.source)
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
|
||||||
|
logger.debug("Restoring Manga: ${manga.title} from $sourceName")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (source != null) {
|
||||||
|
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||||
|
} else {
|
||||||
|
errors.add(Date() to "${manga.title} [$sourceName]: Source not found: $sourceName (${manga.source})")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param manga manga data from json
|
||||||
|
* @param source source to get manga data from
|
||||||
|
* @param chapters chapters data from json
|
||||||
|
* @param categories categories data from json
|
||||||
|
* @param history history data from json
|
||||||
|
* @param tracks tracking data from json
|
||||||
|
*/
|
||||||
|
private suspend fun restoreMangaData(
|
||||||
|
manga: Manga,
|
||||||
|
source: Source,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
val fetchedManga = fetchManga(source, manga)
|
||||||
|
|
||||||
|
updateChapters(source, fetchedManga, chapters)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// backupManager.restoreCategoriesForManga(manga, categories)
|
||||||
|
|
||||||
|
// backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// backupManager.restoreTrackForManga(manga, tracks)
|
||||||
|
|
||||||
|
// updateTracking(fetchedManga, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches manga information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return Updated manga.
|
||||||
|
*/
|
||||||
|
private suspend fun fetchManga(source: Source, manga: Manga): SManga {
|
||||||
|
// make sure we have the manga record in library
|
||||||
|
transaction {
|
||||||
|
if (MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }.firstOrNull() == null) {
|
||||||
|
MangaTable.insert {
|
||||||
|
it[url] = manga.url
|
||||||
|
it[title] = manga.title
|
||||||
|
|
||||||
|
it[sourceReference] = manga.source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
|
||||||
|
it[MangaTable.inLibrary] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update manga details
|
||||||
|
val fetchedManga = source.fetchMangaDetails(manga).awaitSingle()
|
||||||
|
transaction {
|
||||||
|
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
|
||||||
|
|
||||||
|
it[artist] = fetchedManga.artist
|
||||||
|
it[author] = fetchedManga.author
|
||||||
|
it[description] = fetchedManga.description
|
||||||
|
it[genre] = fetchedManga.genre
|
||||||
|
it[status] = fetchedManga.status
|
||||||
|
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
||||||
|
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchedManga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateChapters(source: Source, fetchedManga: SManga, chapters: List<Chapter>) {
|
||||||
|
// TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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.table.SourceTable
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
object LegacyBackupValidator {
|
||||||
|
data class ValidationResult(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for critical backup file data.
|
||||||
|
*
|
||||||
|
* @throws Exception if version or manga cannot be found.
|
||||||
|
* @return List of missing sources or missing trackers.
|
||||||
|
*/
|
||||||
|
fun validate(json: JsonObject): ValidationResult {
|
||||||
|
val version = json.get(Backup.VERSION)
|
||||||
|
val mangasJson = json.get(Backup.MANGAS)
|
||||||
|
if (version == null || mangasJson == null) {
|
||||||
|
throw Exception("File is missing data.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangas = mangasJson.asJsonArray
|
||||||
|
if (mangas.size() == 0) {
|
||||||
|
throw Exception("Backup does not contain any manga.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val sources = getSourceMapping(json)
|
||||||
|
val missingSources = transaction {
|
||||||
|
sources
|
||||||
|
.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
|
||||||
|
.map { "${it.value} (${it.key})" }
|
||||||
|
.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
val trackers = mangas
|
||||||
|
.filter { it.asJsonObject.has("track") }
|
||||||
|
.flatMap { it.asJsonObject["track"].asJsonArray }
|
||||||
|
.map { it.asJsonObject["s"].asInt }
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
val missingTrackers = listOf("")
|
||||||
|
// val missingTrackers = trackers
|
||||||
|
// .mapNotNull { trackManager.getService(it) }
|
||||||
|
// .filter { !it.isLogged }
|
||||||
|
// .map { context.getString(it.nameRes()) }
|
||||||
|
// .sorted()
|
||||||
|
|
||||||
|
return ValidationResult(missingSources, missingTrackers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||||
|
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||||
|
|
||||||
|
return extensionsMapping.asJsonArray
|
||||||
|
.map {
|
||||||
|
val items = it.asString.split(":")
|
||||||
|
items[0].toLong() to items[1]
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ir.armor.tachidesk.impl.backup.legacy.models
|
||||||
|
|
||||||
|
data class DHistory(val url: String, val lastRead: Long)
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package ir.armor.tachidesk.impl.backup.models
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||||
|
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 {
|
||||||
|
url = chapterRecord[ChapterTable.url]
|
||||||
|
read = chapterRecord[ChapterTable.isRead]
|
||||||
|
bookmark = chapterRecord[ChapterTable.isBookmarked]
|
||||||
|
last_page_read = chapterRecord[ChapterTable.lastPageRead]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tachidesk <--
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ir.armor.tachidesk.impl.backup.models
|
||||||
|
|
||||||
|
class LibraryManga : MangaImpl() {
|
||||||
|
|
||||||
|
var unread: Int = 0
|
||||||
|
|
||||||
|
var category: Int = 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
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
|
||||||
|
// )
|
||||||
|
// }
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ir.armor.tachidesk.impl.backup.models
|
||||||
|
|
||||||
|
class MangaChapter(val manga: Manga, val chapter: Chapter)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package ir.armor.tachidesk.impl.backup.models
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
|
||||||
|
open class MangaImpl : Manga {
|
||||||
|
|
||||||
|
override var id: Long? = 0
|
||||||
|
|
||||||
|
override var source: Long = -1
|
||||||
|
|
||||||
|
override lateinit var url: String
|
||||||
|
|
||||||
|
override lateinit var title: String
|
||||||
|
|
||||||
|
override var artist: String? = null
|
||||||
|
|
||||||
|
override var author: String? = null
|
||||||
|
|
||||||
|
override var description: String? = null
|
||||||
|
|
||||||
|
override var genre: String? = null
|
||||||
|
|
||||||
|
override var status: Int = 0
|
||||||
|
|
||||||
|
override var thumbnail_url: String? = null
|
||||||
|
|
||||||
|
override var favorite: Boolean = false
|
||||||
|
|
||||||
|
override var last_update: Long = 0
|
||||||
|
|
||||||
|
override var date_added: Long = 0
|
||||||
|
|
||||||
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
|
/** Reader mode value
|
||||||
|
* ref: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt#L8
|
||||||
|
* 0 -> Default
|
||||||
|
* 1 -> Left to Right
|
||||||
|
* 2 -> Right to Left
|
||||||
|
* 3 -> Vertical
|
||||||
|
* 4 -> Webtoon
|
||||||
|
* 5 -> Continues Vertical
|
||||||
|
*/
|
||||||
|
override var viewer: Int = 0
|
||||||
|
|
||||||
|
/** Contains some useful info about
|
||||||
|
*/
|
||||||
|
override var chapter_flags: Int = 0
|
||||||
|
|
||||||
|
override var cover_last_modified: Long = 0
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
|
val manga = other as Manga
|
||||||
|
if (url != manga.url) return false
|
||||||
|
return id == manga.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return url.hashCode() + id.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tachidesk -->
|
||||||
|
companion object {
|
||||||
|
fun fromQuery(mangaRecord: ResultRow): MangaImpl {
|
||||||
|
return MangaImpl().apply {
|
||||||
|
url = mangaRecord[MangaTable.url]
|
||||||
|
title = mangaRecord[MangaTable.title]
|
||||||
|
source = mangaRecord[MangaTable.sourceReference]
|
||||||
|
viewer = 0 // TODO: implement
|
||||||
|
chapter_flags = 0 // TODO: implement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tachidesk <--
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package ir.armor.tachidesk.impl.backup.models
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface Track : Serializable {
|
||||||
|
|
||||||
|
var id: Long?
|
||||||
|
|
||||||
|
var manga_id: Long
|
||||||
|
|
||||||
|
var sync_id: Int
|
||||||
|
|
||||||
|
var media_id: Int
|
||||||
|
|
||||||
|
var library_id: Long?
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
|
||||||
|
var last_chapter_read: Int
|
||||||
|
|
||||||
|
var total_chapters: Int
|
||||||
|
|
||||||
|
var score: Float
|
||||||
|
|
||||||
|
var status: Int
|
||||||
|
|
||||||
|
var started_reading_date: Long
|
||||||
|
|
||||||
|
var finished_reading_date: Long
|
||||||
|
|
||||||
|
var tracking_url: String
|
||||||
|
|
||||||
|
fun copyPersonalFrom(other: Track) {
|
||||||
|
last_chapter_read = other.last_chapter_read
|
||||||
|
score = other.score
|
||||||
|
status = other.status
|
||||||
|
started_reading_date = other.started_reading_date
|
||||||
|
finished_reading_date = other.finished_reading_date
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(serviceId: Int): Track = TrackImpl().apply {
|
||||||
|
sync_id = serviceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package ir.armor.tachidesk.impl.backup.models
|
||||||
|
|
||||||
|
class TrackImpl : Track {
|
||||||
|
|
||||||
|
override var id: Long? = null
|
||||||
|
|
||||||
|
override var manga_id: Long = 0
|
||||||
|
|
||||||
|
override var sync_id: Int = 0
|
||||||
|
|
||||||
|
override var media_id: Int = 0
|
||||||
|
|
||||||
|
override var library_id: Long? = null
|
||||||
|
|
||||||
|
override lateinit var title: String
|
||||||
|
|
||||||
|
override var last_chapter_read: Int = 0
|
||||||
|
|
||||||
|
override var total_chapters: Int = 0
|
||||||
|
|
||||||
|
override var score: Float = 0f
|
||||||
|
|
||||||
|
override var status: Int = 0
|
||||||
|
|
||||||
|
override var started_reading_date: Long = 0
|
||||||
|
|
||||||
|
override var finished_reading_date: Long = 0
|
||||||
|
|
||||||
|
override var tracking_url: String = ""
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
|
||||||
|
other as Track
|
||||||
|
|
||||||
|
if (manga_id != other.manga_id) return false
|
||||||
|
if (sync_id != other.sync_id) return false
|
||||||
|
return media_id == other.media_id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = (manga_id xor manga_id.ushr(32)).toInt()
|
||||||
|
result = 31 * result + sync_id
|
||||||
|
result = 31 * result + media_id
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.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 org.w3c.dom.Document
|
|
||||||
import org.xml.sax.InputSource
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.StringReader
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.util.zip.ZipFile
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
|
||||||
|
|
||||||
object APKExtractor {
|
|
||||||
// decompressXML -- Parse the 'compressed' binary form of Android XML docs
|
|
||||||
// such as for AndroidManifest.xml in .apk files
|
|
||||||
var endDocTag = 0x00100101
|
|
||||||
var startTag = 0x00100102
|
|
||||||
var endTag = 0x00100103
|
|
||||||
fun prt(str: String?) {
|
|
||||||
// System.err.print(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decompressXML(xml: ByteArray): String {
|
|
||||||
val finalXML = 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).
|
|
||||||
val 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.
|
|
||||||
val 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.
|
|
||||||
val 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.
|
|
||||||
var 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)
|
|
||||||
var ii = xmlTagOff
|
|
||||||
while (ii < xml.size - 4) {
|
|
||||||
if (LEW(xml, ii) == startTag) {
|
|
||||||
xmlTagOff = ii
|
|
||||||
break
|
|
||||||
}
|
|
||||||
ii += 4
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
var off = xmlTagOff
|
|
||||||
var indent = 0
|
|
||||||
var startTagLineNo = -2
|
|
||||||
while (off < xml.size) {
|
|
||||||
val tag0 = LEW(xml, off)
|
|
||||||
// int tag1 = LEW(xml, off+1*4);
|
|
||||||
val lineNo = LEW(xml, off + 2 * 4)
|
|
||||||
// int tag3 = LEW(xml, off+3*4);
|
|
||||||
val nameNsSi = LEW(xml, off + 4 * 4)
|
|
||||||
val nameSi = LEW(xml, off + 5 * 4)
|
|
||||||
if (tag0 == startTag) { // XML START TAG
|
|
||||||
val tag6 = LEW(xml, off + 6 * 4) // Expected to be 14001400
|
|
||||||
val 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
|
|
||||||
val name = compXmlString(xml, sitOff, stOff, nameSi)
|
|
||||||
// tr.addSelect(name, null);
|
|
||||||
startTagLineNo = lineNo
|
|
||||||
|
|
||||||
// Look for the Attributes
|
|
||||||
val sb = StringBuffer()
|
|
||||||
for (ii in 0 until numbAttrs) {
|
|
||||||
val attrNameNsSi = LEW(xml, off) // AttrName Namespace Str
|
|
||||||
// Ind, or FFFFFFFF
|
|
||||||
val attrNameSi = LEW(xml, off + 1 * 4) // AttrName String
|
|
||||||
// Index
|
|
||||||
val attrValueSi = LEW(xml, off + 2 * 4) // AttrValue Str
|
|
||||||
// Ind, or
|
|
||||||
// FFFFFFFF
|
|
||||||
val attrFlags = LEW(xml, off + 3 * 4)
|
|
||||||
val attrResId = LEW(xml, off + 4 * 4) // AttrValue
|
|
||||||
// ResourceId or dup
|
|
||||||
// AttrValue StrInd
|
|
||||||
off += 5 * 4 // Skip over the 5 words of an attribute
|
|
||||||
val attrName = compXmlString(
|
|
||||||
xml, sitOff, stOff,
|
|
||||||
attrNameSi
|
|
||||||
)
|
|
||||||
val attrValue = if (attrValueSi != -1) compXmlString(xml, sitOff, stOff, attrValueSi)
|
|
||||||
else "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
|
|
||||||
val 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
|
|
||||||
|
|
||||||
fun compXmlString(xml: ByteArray, sitOff: Int, stOff: Int, strInd: Int): String? {
|
|
||||||
if (strInd < 0) return null
|
|
||||||
val strOff = stOff + LEW(xml, sitOff + strInd * 4)
|
|
||||||
return compXmlStringAt(xml, strOff)
|
|
||||||
}
|
|
||||||
|
|
||||||
var spaces = " "
|
|
||||||
fun prtIndent(indent: Int, str: String) {
|
|
||||||
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.
|
|
||||||
fun compXmlStringAt(arr: ByteArray, strOff: Int): String {
|
|
||||||
val strLen: Int = arr[strOff + 1].toInt() shl 8 and 0xff00 or arr[strOff].toInt() and 0xff
|
|
||||||
val chars = ByteArray(strLen)
|
|
||||||
for (ii in 0 until strLen) {
|
|
||||||
chars[ii] = arr[strOff + 2 + ii * 2]
|
|
||||||
}
|
|
||||||
return 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.
|
|
||||||
fun LEW(arr: ByteArray, off: Int): Int {
|
|
||||||
|
|
||||||
return (arr[off + 3].toInt() shl 24) and -0x1000000 or
|
|
||||||
(arr[off + 2].toInt() shl 16 and 0xff0000) or
|
|
||||||
(arr[off + 1].toInt() shl 8 and 0xff00) or
|
|
||||||
(arr[off].toInt() and 0xFF)
|
|
||||||
} // end of LEW
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun loadXMLFromString(xml: String?): Document {
|
|
||||||
val docBuilderFactory = DocumentBuilderFactory.newInstance()
|
|
||||||
val docBuilder = docBuilderFactory.newDocumentBuilder()
|
|
||||||
return docBuilder.parse(InputSource(StringReader(xml)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun extract_dex_and_read_className(filePath: String?, dexPath: String?): String {
|
|
||||||
var zip: ZipFile? = null
|
|
||||||
zip = ZipFile(filePath)
|
|
||||||
val androidManifest = zip.getEntry("AndroidManifest.xml")
|
|
||||||
val classesDex = zip.getEntry("classes.dex")
|
|
||||||
|
|
||||||
// write dex file
|
|
||||||
val dexStream = zip.getInputStream(classesDex)
|
|
||||||
Files.newOutputStream(Paths.get(dexPath)).use { os ->
|
|
||||||
val buffer = ByteArray(1024)
|
|
||||||
var len: Int
|
|
||||||
while (dexStream.read(buffer).also { len = it } > 0) {
|
|
||||||
os.write(buffer, 0, len)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// read xml file
|
|
||||||
val `is` = zip.getInputStream(androidManifest)
|
|
||||||
val buf = ByteArray(1024000) // 100 kb
|
|
||||||
`is`.read(buf)
|
|
||||||
`is`.close()
|
|
||||||
zip.close()
|
|
||||||
val xml = decompressXML(buf)
|
|
||||||
try {
|
|
||||||
val xmlDoc = loadXMLFromString(xml)
|
|
||||||
val pkg = xmlDoc.documentElement.getAttribute("package")
|
|
||||||
val nodes = xmlDoc.getElementsByTagName("meta-data")
|
|
||||||
for (i in 0 until nodes.length) {
|
|
||||||
val attributes = nodes.item(i).attributes
|
|
||||||
println(attributes.getNamedItem("name").nodeValue)
|
|
||||||
if (attributes.getNamedItem("name").nodeValue == "tachiyomi.extension.class") return pkg + attributes.getNamedItem("value").nodeValue
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// original Java code
|
|
||||||
|
|
||||||
// package ir.armor.tachidesk.impl.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 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;
|
|
||||||
//
|
|
||||||
// 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 "";
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package ir.armor.tachidesk.impl.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 okhttp3.Response
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
object CachedImageResponse {
|
||||||
|
private fun pathToInputStream(path: String): InputStream {
|
||||||
|
return BufferedInputStream(FileInputStream(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
||||||
|
File(directoryPath).listFiles().forEach { file ->
|
||||||
|
if (file.name.startsWith(fileName))
|
||||||
|
return "$directoryPath/${file.name}"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** fetch a cached image response, calls `fetcher` if cache fails */
|
||||||
|
suspend fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: suspend () -> Response): Pair<InputStream, String> {
|
||||||
|
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
||||||
|
val filePath = "$saveDir/$fileName"
|
||||||
|
if (cachedFile != null) {
|
||||||
|
val fileType = cachedFile.substringAfter(filePath)
|
||||||
|
return Pair(
|
||||||
|
pathToInputStream(cachedFile),
|
||||||
|
"image/$fileType"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = fetcher()
|
||||||
|
|
||||||
|
if (response.code == 200) {
|
||||||
|
val contentType = response.headers["content-type"]!!
|
||||||
|
val fullPath = filePath + "." + contentType.substringAfter("image/")
|
||||||
|
|
||||||
|
Files.newOutputStream(Paths.get(fullPath)).use { output ->
|
||||||
|
response.body!!.source().use { input ->
|
||||||
|
output.sink().buffer().use {
|
||||||
|
it.writeAll(input)
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pair(
|
||||||
|
pathToInputStream(fullPath),
|
||||||
|
contentType
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw Exception("request error! ${response.code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,85 +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 okhttp3.Response
|
|
||||||
import okio.BufferedSource
|
|
||||||
import okio.buffer
|
|
||||||
import okio.sink
|
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
// fun writeStream(fileStream: InputStream, path: String) {
|
|
||||||
// Files.newOutputStream(Paths.get(path)).use { os ->
|
|
||||||
// val buffer = ByteArray(128 * 1024)
|
|
||||||
// var len: Int
|
|
||||||
// while (fileStream.read(buffer).also { len = it } > 0) {
|
|
||||||
// os.write(buffer, 0, len)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun pathToInputStream(path: String): InputStream {
|
|
||||||
return BufferedInputStream(FileInputStream(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
|
||||||
File(directoryPath).listFiles().forEach { file ->
|
|
||||||
if (file.name.startsWith(fileName))
|
|
||||||
return "$directoryPath/${file.name}"
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the given source to an output stream and closes both resources.
|
|
||||||
*
|
|
||||||
* @param stream the stream where the source is copied.
|
|
||||||
*/
|
|
||||||
private fun BufferedSource.saveTo(stream: OutputStream) {
|
|
||||||
use { input ->
|
|
||||||
stream.sink().buffer().use {
|
|
||||||
it.writeAll(input)
|
|
||||||
it.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
|
|
||||||
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
|
||||||
val filePath = "$saveDir/$fileName"
|
|
||||||
if (cachedFile != null) {
|
|
||||||
val fileType = cachedFile.substringAfter(filePath)
|
|
||||||
return Pair(
|
|
||||||
pathToInputStream(cachedFile),
|
|
||||||
"image/$fileType"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = fetcher()
|
|
||||||
|
|
||||||
if (response.code == 200) {
|
|
||||||
val contentType = response.headers["content-type"]!!
|
|
||||||
val fullPath = filePath + "." + contentType.substringAfter("image/")
|
|
||||||
|
|
||||||
Files.newOutputStream(Paths.get(fullPath)).use { os ->
|
|
||||||
response.body!!.source().saveTo(os)
|
|
||||||
}
|
|
||||||
return Pair(
|
|
||||||
pathToInputStream(fullPath),
|
|
||||||
contentType
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw Exception("request error! ${response.code}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package ir.armor.tachidesk.impl.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 eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
|
||||||
|
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||||
|
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||||
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
object GetHttpSource {
|
||||||
|
private val sourceCache = ConcurrentHashMap<Long, HttpSource>()
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
|
fun getHttpSource(sourceId: Long): HttpSource {
|
||||||
|
val cachedResult: HttpSource? = sourceCache[sourceId]
|
||||||
|
if (cachedResult != null) {
|
||||||
|
return cachedResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceRecord = transaction {
|
||||||
|
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionId = sourceRecord[SourceTable.extension]
|
||||||
|
val extensionRecord = transaction {
|
||||||
|
ExtensionTable.select { ExtensionTable.id eq extensionId }.firstOrNull()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val apkName = extensionRecord[ExtensionTable.apkName]
|
||||||
|
val className = extensionRecord[ExtensionTable.classFQName]
|
||||||
|
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||||
|
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||||
|
|
||||||
|
when (val instance = loadExtensionSources(jarPath, className)) {
|
||||||
|
is Source -> listOf(instance)
|
||||||
|
is SourceFactory -> instance.createSources()
|
||||||
|
else -> throw Exception("Unknown source class type! ${instance.javaClass}")
|
||||||
|
}.forEach {
|
||||||
|
sourceCache[it.id] = it as HttpSource
|
||||||
|
}
|
||||||
|
return sourceCache[sourceId]!!
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package ir.armor.tachidesk.impl.util
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -13,6 +13,7 @@ import okhttp3.FormBody
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
// TODO: finish MangaDex support
|
||||||
class MangaDexHelper(private val mangaDexSource: HttpSource) {
|
class MangaDexHelper(private val mangaDexSource: HttpSource) {
|
||||||
|
|
||||||
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
|
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package ir.armor.tachidesk.impl.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 kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.Response
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
|
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||||
|
suspend fun Call.await(): Response {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
enqueue(
|
||||||
|
object : Callback {
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.resume(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
// Don't bother with resuming the continuation if it is already cancelled.
|
||||||
|
if (continuation.isCancelled) return
|
||||||
|
continuation.resumeWithException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
try {
|
||||||
|
cancel()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
// Ignore cancel exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package ir.armor.tachidesk.impl.util
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.Signature
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.googlecode.d2j.dex.Dex2jar
|
||||||
|
import com.googlecode.d2j.reader.MultiDexFileReader
|
||||||
|
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
|
||||||
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import net.dongliu.apk.parser.ApkFile
|
||||||
|
import net.dongliu.apk.parser.ApkParsers
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.Node
|
||||||
|
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
|
||||||
|
import xyz.nulldev.androidcompat.pm.toPackageInfo
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLClassLoader
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
object PackageTools {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
|
const val EXTENSION_FEATURE = "tachiyomi.extension"
|
||||||
|
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
||||||
|
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||||
|
const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||||
|
const val LIB_VERSION_MIN = 1.2
|
||||||
|
const val LIB_VERSION_MAX = 1.2
|
||||||
|
|
||||||
|
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" // inorichi's key
|
||||||
|
private const val unofficialSignature = "64feb21075ba97ebc9cc981243645b331595c111cef1b0d084236a0403b00581" // ArMor's key
|
||||||
|
var trustedSignatures = mutableSetOf<String>() + officialSignature + unofficialSignature
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert dex to jar, a wrapper for the dex2jar library
|
||||||
|
*/
|
||||||
|
fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
|
||||||
|
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
|
||||||
|
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
|
||||||
|
|
||||||
|
val jarFilePath = File(jarFile).toPath()
|
||||||
|
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
|
||||||
|
val handler = BaksmaliBaseDexExceptionHandler()
|
||||||
|
Dex2jar
|
||||||
|
.from(reader)
|
||||||
|
.withExceptionHandler(handler)
|
||||||
|
.reUseReg(false)
|
||||||
|
.topoLogicalSort()
|
||||||
|
.skipDebug(true)
|
||||||
|
.optimizeSynchronized(false)
|
||||||
|
.printIR(false)
|
||||||
|
.noCode(false)
|
||||||
|
.skipExceptions(false)
|
||||||
|
.to(jarFilePath)
|
||||||
|
if (handler.hasException()) {
|
||||||
|
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
|
||||||
|
logger.error(
|
||||||
|
"Detail Error Information in File $errorFile\n" +
|
||||||
|
"Please report this file to one of following link if possible (any one).\n" +
|
||||||
|
" https://sourceforge.net/p/dex2jar/tickets/\n" +
|
||||||
|
" https://bitbucket.org/pxb1988/dex2jar/issues\n" +
|
||||||
|
" https://github.com/pxb1988/dex2jar/issues\n" +
|
||||||
|
" dex2jar@googlegroups.com"
|
||||||
|
)
|
||||||
|
handler.dump(errorFile, emptyArray<String>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A modified version of `xyz.nulldev.androidcompat.pm.InstalledPackage.info` */
|
||||||
|
fun getPackageInfo(apkFilePath: String): PackageInfo {
|
||||||
|
val apk = File(apkFilePath)
|
||||||
|
return ApkParsers.getMetaInfo(apk).toPackageInfo(apk).apply {
|
||||||
|
val parsed = ApkFile(apk)
|
||||||
|
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||||
|
val dBuilder = dbFactory.newDocumentBuilder()
|
||||||
|
val doc = parsed.manifestXml.byteInputStream().use {
|
||||||
|
dBuilder.parse(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(parsed.manifestXml)
|
||||||
|
|
||||||
|
applicationInfo.metaData = Bundle().apply {
|
||||||
|
val appTag = doc.getElementsByTagName("application").item(0)
|
||||||
|
|
||||||
|
appTag?.childNodes?.toList()?.filter {
|
||||||
|
it.nodeType == Node.ELEMENT_NODE
|
||||||
|
}?.map {
|
||||||
|
it as Element
|
||||||
|
}?.filter {
|
||||||
|
it.tagName == "meta-data"
|
||||||
|
}?.map {
|
||||||
|
putString(
|
||||||
|
it.attributes.getNamedItem("android:name").nodeValue,
|
||||||
|
it.attributes.getNamedItem("android:value").nodeValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signatures = (
|
||||||
|
parsed.apkSingers.flatMap { it.certificateMetas }
|
||||||
|
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
|
||||||
|
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
|
||||||
|
.map { Signature(it.data) }.toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
||||||
|
val signatures = pkgInfo.signatures
|
||||||
|
return if (signatures != null && signatures.isNotEmpty()) {
|
||||||
|
Hash.sha256(signatures.first().toByteArray())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads the extension main class called $className from the jar located at $jarPath
|
||||||
|
* It may return an instance of HttpSource or SourceFactory depending on the extension.
|
||||||
|
*/
|
||||||
|
fun loadExtensionSources(jarPath: String, className: String): Any {
|
||||||
|
val classLoader = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")))
|
||||||
|
val classToLoad = Class.forName(className, false, classLoader)
|
||||||
|
return classToLoad.getDeclaredConstructor().newInstance()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package ir.armor.tachidesk.impl.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 kotlinx.coroutines.CancellableContinuation
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscriber
|
||||||
|
import rx.Subscription
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
|
// source: https://github.com/jobobby04/TachiyomiSY/blob/9320221a4e8b118ef68deb60d8c4c32bcbb9e06f/app/src/main/java/eu/kanade/tachiyomi/util/lang/RxCoroutineBridge.kt
|
||||||
|
/*
|
||||||
|
* Util functions for bridging RxJava and coroutines. Taken from TachiyomiEH/SY.
|
||||||
|
*/
|
||||||
|
|
||||||
|
suspend fun <T> Observable<T>.awaitSingle(): T = single().awaitOne()
|
||||||
|
|
||||||
|
private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
|
||||||
|
cont.unsubscribeOnCancellation(
|
||||||
|
subscribe(
|
||||||
|
object : Subscriber<T>() {
|
||||||
|
override fun onStart() {
|
||||||
|
request(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNext(t: T) {
|
||||||
|
cont.resume(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCompleted() {
|
||||||
|
if (cont.isActive) cont.resumeWithException(
|
||||||
|
IllegalStateException(
|
||||||
|
"Should have invoked onNext"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Throwable) {
|
||||||
|
/*
|
||||||
|
* Rx1 observable throws NoSuchElementException if cancellation happened before
|
||||||
|
* element emission. To mitigate this we try to atomically resume continuation with exception:
|
||||||
|
* if resume failed, then we know that continuation successfully cancelled itself
|
||||||
|
*/
|
||||||
|
val token = cont.tryResumeWithException(e)
|
||||||
|
if (token != null) {
|
||||||
|
cont.completeResume(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subscription) =
|
||||||
|
invokeOnCancellation { sub.unsubscribe() }
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package ir.armor.tachidesk.model.database
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.migration.lib.loadMigrationsFrom
|
||||||
|
import ir.armor.tachidesk.model.database.migration.lib.runMigrations
|
||||||
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
|
||||||
|
object DBMangaer {
|
||||||
|
val db by lazy {
|
||||||
|
val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun databaseUp() {
|
||||||
|
// must mention db object so the lazy block executes
|
||||||
|
val db = DBMangaer.db
|
||||||
|
db.useNestedTransactions = true
|
||||||
|
|
||||||
|
val migrations = loadMigrationsFrom("ir.armor.tachidesk.model.database.migration")
|
||||||
|
runMigrations(migrations)
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import ir.armor.tachidesk.model.database.migration.lib.Migration
|
||||||
|
import org.jetbrains.exposed.dao.id.IdTable
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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/. */
|
||||||
|
|
||||||
|
class M0001_Initial : Migration() {
|
||||||
|
private object ExtensionTable : IntIdTable() {
|
||||||
|
val apkName = varchar("apk_name", 1024)
|
||||||
|
|
||||||
|
// default is the local source icon from tachiyomi
|
||||||
|
val iconUrl = varchar("icon_url", 2048)
|
||||||
|
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||||
|
|
||||||
|
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 isInstalled = bool("is_installed").default(false)
|
||||||
|
val hasUpdate = bool("has_update").default(false)
|
||||||
|
val isObsolete = bool("is_obsolete").default(false)
|
||||||
|
|
||||||
|
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
|
||||||
|
}
|
||||||
|
|
||||||
|
private object SourceTable : IdTable<Long>() {
|
||||||
|
override val id = long("id").entityId()
|
||||||
|
val name = varchar("name", 128)
|
||||||
|
val lang = varchar("lang", 10)
|
||||||
|
val extension = reference("extension", ExtensionTable)
|
||||||
|
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object MangaTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val title = varchar("title", 512)
|
||||||
|
val initialized = bool("initialized").default(false)
|
||||||
|
|
||||||
|
val artist = varchar("artist", 64).nullable()
|
||||||
|
val author = varchar("author", 64).nullable()
|
||||||
|
val description = varchar("description", 4096).nullable()
|
||||||
|
val genre = varchar("genre", 1024).nullable()
|
||||||
|
|
||||||
|
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
|
||||||
|
val status = integer("status").default(SManga.UNKNOWN)
|
||||||
|
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
|
val inLibrary = bool("in_library").default(false)
|
||||||
|
val defaultCategory = bool("default_category").default(true)
|
||||||
|
|
||||||
|
// source is used by some ancestor of IntIdTable
|
||||||
|
val sourceReference = long("source")
|
||||||
|
}
|
||||||
|
|
||||||
|
private object ChapterTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val name = varchar("name", 512)
|
||||||
|
val date_upload = long("date_upload").default(0)
|
||||||
|
val chapter_number = float("chapter_number").default(-1f)
|
||||||
|
val scanlator = varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
|
val isRead = bool("read").default(false)
|
||||||
|
val isBookmarked = bool("bookmark").default(false)
|
||||||
|
val lastPageRead = integer("last_page_read").default(0)
|
||||||
|
|
||||||
|
val chapterIndex = integer("number_in_list")
|
||||||
|
|
||||||
|
val manga = reference("manga", MangaTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object PageTable : IntIdTable() {
|
||||||
|
val index = integer("index")
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val imageUrl = varchar("imageUrl", 2048).nullable()
|
||||||
|
|
||||||
|
val chapter = reference("chapter", ChapterTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object CategoryTable : IntIdTable() {
|
||||||
|
val name = varchar("name", 64)
|
||||||
|
val isLanding = bool("is_landing").default(false)
|
||||||
|
val order = integer("order").default(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object CategoryMangaTable : IntIdTable() {
|
||||||
|
val category = reference("category", ir.armor.tachidesk.model.database.table.CategoryTable)
|
||||||
|
val manga = reference("manga", ir.armor.tachidesk.model.database.table.MangaTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(
|
||||||
|
ExtensionTable,
|
||||||
|
ExtensionTable,
|
||||||
|
SourceTable,
|
||||||
|
MangaTable,
|
||||||
|
ChapterTable,
|
||||||
|
PageTable,
|
||||||
|
CategoryTable,
|
||||||
|
CategoryMangaTable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Andreas Mausch
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration.lib
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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/. */
|
||||||
|
|
||||||
|
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
|
||||||
|
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
||||||
|
|
||||||
|
abstract class Migration {
|
||||||
|
val name: String
|
||||||
|
val version: Int
|
||||||
|
|
||||||
|
init {
|
||||||
|
val groups = Regex("^M(\\d+)_(.*)$").matchEntire(this::class.simpleName!!)?.groupValues
|
||||||
|
?: throw IllegalArgumentException("Migration class name doesn't match convention")
|
||||||
|
version = groups[1].toInt()
|
||||||
|
name = groups[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun run()
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration.lib
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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/. */
|
||||||
|
|
||||||
|
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
|
||||||
|
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.IntEntity
|
||||||
|
import org.jetbrains.exposed.dao.IntEntityClass
|
||||||
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
import org.jetbrains.exposed.dao.id.IdTable
|
||||||
|
import org.jetbrains.exposed.sql.`java-time`.timestamp
|
||||||
|
|
||||||
|
object MigrationsTable : IdTable<Int>() {
|
||||||
|
override val id = integer("version").entityId()
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
|
val name = varchar("name", length = 400)
|
||||||
|
val executedAt = timestamp("executed_at")
|
||||||
|
|
||||||
|
init {
|
||||||
|
index(true, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MigrationEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||||
|
companion object : IntEntityClass<MigrationEntity>(MigrationsTable)
|
||||||
|
|
||||||
|
var version by MigrationsTable.id
|
||||||
|
var name by MigrationsTable.name
|
||||||
|
var executedAt by MigrationsTable.executedAt
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration.lib
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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/. */
|
||||||
|
|
||||||
|
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
|
||||||
|
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
||||||
|
|
||||||
|
import com.google.common.reflect.ClassPath
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils.create
|
||||||
|
import org.jetbrains.exposed.sql.exists
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import java.time.Clock
|
||||||
|
import java.time.Instant.now
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
fun runMigrations(migrations: List<Migration>, database: Database = TransactionManager.defaultDatabase!!, clock: Clock = Clock.systemUTC()) {
|
||||||
|
checkVersions(migrations)
|
||||||
|
|
||||||
|
logger.info { "Running migrations on database ${database.url}" }
|
||||||
|
|
||||||
|
val latestVersion = transaction(database) {
|
||||||
|
createTableIfNotExists(database)
|
||||||
|
MigrationEntity.all().maxByOrNull { it.version }?.version?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info { "Database version before migrations: $latestVersion" }
|
||||||
|
|
||||||
|
migrations
|
||||||
|
.sortedBy { it.version }
|
||||||
|
.filter { shouldRun(latestVersion, it) }
|
||||||
|
.forEach {
|
||||||
|
logger.info { "Running migration version ${it.version}: ${it.name}" }
|
||||||
|
transaction(database) {
|
||||||
|
it.run()
|
||||||
|
|
||||||
|
MigrationEntity.new {
|
||||||
|
version = EntityID(it.version, MigrationsTable)
|
||||||
|
name = it.name
|
||||||
|
executedAt = now(clock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info { "Migrations finished successfully" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMigrationsFrom(classPath: String): List<Migration> {
|
||||||
|
return ClassPath.from(Thread.currentThread().contextClassLoader)
|
||||||
|
.getTopLevelClasses(classPath)
|
||||||
|
.map {
|
||||||
|
logger.debug("found Migration class ${it.name}")
|
||||||
|
val clazz = it.load().getDeclaredConstructor().newInstance()
|
||||||
|
if (clazz is Migration)
|
||||||
|
clazz
|
||||||
|
else
|
||||||
|
throw RuntimeException("found a class that's not a Migration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkVersions(migrations: List<Migration>) {
|
||||||
|
val sorted = migrations.map { it.version }.sorted()
|
||||||
|
if ((1..migrations.size).toList() != sorted) {
|
||||||
|
throw IllegalStateException("List of migrations version is not consecutive: $sorted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTableIfNotExists(database: Database) {
|
||||||
|
if (MigrationsTable.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val tableNames = database.dialect.allTablesNames()
|
||||||
|
when (tableNames.isEmpty()) {
|
||||||
|
true -> {
|
||||||
|
logger.info { "Empty database found, creating table for migrations" }
|
||||||
|
create(MigrationsTable)
|
||||||
|
}
|
||||||
|
false -> throw IllegalStateException("Tried to run migrations against a non-empty database without a Migrations table. This is not supported.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldRun(latestVersion: Int?, migration: Migration): Boolean {
|
||||||
|
val run = latestVersion?.let { migration.version > it } ?: true
|
||||||
|
if (!run) {
|
||||||
|
logger.debug { "Skipping migration version ${migration.version}: ${migration.name}" }
|
||||||
|
}
|
||||||
|
return run
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,7 +7,7 @@ package ir.armor.tachidesk.database.table
|
|||||||
* 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.database.dataclass.CategoryDataClass
|
import ir.armor.tachidesk.model.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
|
||||||
|
|
||||||
+5
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -16,6 +16,10 @@ object ChapterTable : IntIdTable() {
|
|||||||
val chapter_number = float("chapter_number").default(-1f)
|
val chapter_number = float("chapter_number").default(-1f)
|
||||||
val scanlator = varchar("scanlator", 128).nullable()
|
val scanlator = varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
|
val isRead = bool("read").default(false)
|
||||||
|
val isBookmarked = bool("bookmark").default(false)
|
||||||
|
val lastPageRead = integer("last_page_read").default(0)
|
||||||
|
|
||||||
val chapterIndex = integer("number_in_list")
|
val chapterIndex = integer("number_in_list")
|
||||||
|
|
||||||
val manga = reference("manga", MangaTable)
|
val manga = reference("manga", MangaTable)
|
||||||
+12
-5
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -10,15 +10,22 @@ package ir.armor.tachidesk.database.table
|
|||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
object ExtensionTable : IntIdTable() {
|
object ExtensionTable : IntIdTable() {
|
||||||
|
val apkName = varchar("apk_name", 1024)
|
||||||
|
|
||||||
|
// default is the local source icon from tachiyomi
|
||||||
|
val iconUrl = varchar("icon_url", 2048)
|
||||||
|
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||||
|
|
||||||
val name = varchar("name", 128)
|
val name = varchar("name", 128)
|
||||||
val pkgName = varchar("pkg_name", 128)
|
val pkgName = varchar("pkg_name", 128)
|
||||||
val versionName = varchar("version_name", 16)
|
val versionName = varchar("version_name", 16)
|
||||||
val versionCode = integer("version_code")
|
val versionCode = integer("version_code")
|
||||||
val lang = varchar("lang", 10)
|
val lang = varchar("lang", 10)
|
||||||
val isNsfw = bool("is_nsfw")
|
val isNsfw = bool("is_nsfw")
|
||||||
val apkName = varchar("apk_name", 1024)
|
|
||||||
val iconUrl = varchar("icon_url", 2048)
|
|
||||||
|
|
||||||
val installed = bool("installed").default(false)
|
val isInstalled = bool("is_installed").default(false)
|
||||||
val classFQName = varchar("class_name", 256).default("") // fully qualified name
|
val hasUpdate = bool("has_update").default(false)
|
||||||
|
val isObsolete = bool("is_obsolete").default(false)
|
||||||
|
|
||||||
|
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
|
||||||
}
|
}
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,8 +8,8 @@ package ir.armor.tachidesk.database.table
|
|||||||
* 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.database.dataclass.MangaDataClass
|
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||||
import ir.armor.tachidesk.impl.proxyThumbnailUrl
|
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||||
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
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ object MangaTable : IntIdTable() {
|
|||||||
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaEntry[MangaTable.id].value,
|
mangaEntry[MangaTable.id].value,
|
||||||
mangaEntry[sourceReference].toString(),
|
mangaEntry[MangaTable.sourceReference].toString(),
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[MangaTable.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[MangaTable.title],
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+1
-2
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -15,5 +15,4 @@ 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
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
package ir.armor.tachidesk.model.dataclass
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+9
-4
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
package ir.armor.tachidesk.model.dataclass
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,14 +8,19 @@ package ir.armor.tachidesk.database.dataclass
|
|||||||
* 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 ChapterDataClass(
|
data class ChapterDataClass(
|
||||||
val id: Int,
|
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val date_upload: Long,
|
val date_upload: Long,
|
||||||
val chapter_number: Float,
|
val chapter_number: Float,
|
||||||
val scanlator: String?,
|
val scanlator: String?,
|
||||||
val mangaId: Int,
|
val mangaId: Int,
|
||||||
val chapterIndex: Int,
|
|
||||||
val chapterCount: Int,
|
/** this chapter's index */
|
||||||
|
val chapterIndex: Int? = null,
|
||||||
|
|
||||||
|
/** total chapter count, used to calculate if there's a next and prev chapter */
|
||||||
|
val chapterCount: Int? = null,
|
||||||
|
|
||||||
|
/** used to construct pages in the front-end */
|
||||||
val pageCount: Int? = null,
|
val pageCount: Int? = null,
|
||||||
)
|
)
|
||||||
+7
-4
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
package ir.armor.tachidesk.model.dataclass
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,14 +8,17 @@ package ir.armor.tachidesk.database.dataclass
|
|||||||
* 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 classFQName: String,
|
val hasUpdate: Boolean,
|
||||||
|
val obsolete: Boolean,
|
||||||
)
|
)
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
package ir.armor.tachidesk.model.dataclass
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,7 +7,7 @@ package ir.armor.tachidesk.database.dataclass
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.database.table.MangaStatus
|
import ir.armor.tachidesk.model.database.table.MangaStatus
|
||||||
|
|
||||||
data class MangaDataClass(
|
data class MangaDataClass(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
package ir.armor.tachidesk.model.dataclass
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
package ir.armor.tachidesk.model.dataclass
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -2,35 +2,49 @@ package ir.armor.tachidesk.server
|
|||||||
|
|
||||||
import io.javalin.Javalin
|
import io.javalin.Javalin
|
||||||
import ir.armor.tachidesk.Main
|
import ir.armor.tachidesk.Main
|
||||||
import ir.armor.tachidesk.impl.addMangaToCategory
|
import ir.armor.tachidesk.impl.Category.createCategory
|
||||||
import ir.armor.tachidesk.impl.addMangaToLibrary
|
import ir.armor.tachidesk.impl.Category.getCategoryList
|
||||||
import ir.armor.tachidesk.impl.createCategory
|
import ir.armor.tachidesk.impl.Category.removeCategory
|
||||||
import ir.armor.tachidesk.impl.getCategoryList
|
import ir.armor.tachidesk.impl.Category.reorderCategory
|
||||||
import ir.armor.tachidesk.impl.getCategoryMangaList
|
import ir.armor.tachidesk.impl.Category.updateCategory
|
||||||
import ir.armor.tachidesk.impl.getChapter
|
import ir.armor.tachidesk.impl.CategoryManga.addMangaToCategory
|
||||||
import ir.armor.tachidesk.impl.getChapterList
|
import ir.armor.tachidesk.impl.CategoryManga.getCategoryMangaList
|
||||||
import ir.armor.tachidesk.impl.getExtensionIcon
|
import ir.armor.tachidesk.impl.CategoryManga.getMangaCategories
|
||||||
import ir.armor.tachidesk.impl.getExtensionList
|
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
||||||
import ir.armor.tachidesk.impl.getLibraryMangas
|
import ir.armor.tachidesk.impl.Chapter.getChapter
|
||||||
import ir.armor.tachidesk.impl.getManga
|
import ir.armor.tachidesk.impl.Chapter.getChapterList
|
||||||
import ir.armor.tachidesk.impl.getMangaCategories
|
import ir.armor.tachidesk.impl.Extension.getExtensionIcon
|
||||||
import ir.armor.tachidesk.impl.getMangaList
|
import ir.armor.tachidesk.impl.Extension.installExtension
|
||||||
import ir.armor.tachidesk.impl.getPageImage
|
import ir.armor.tachidesk.impl.Extension.uninstallExtension
|
||||||
import ir.armor.tachidesk.impl.getSource
|
import ir.armor.tachidesk.impl.Extension.updateExtension
|
||||||
import ir.armor.tachidesk.impl.getSourceList
|
import ir.armor.tachidesk.impl.ExtensionsList.getExtensionList
|
||||||
import ir.armor.tachidesk.impl.getThumbnail
|
import ir.armor.tachidesk.impl.Library.addMangaToLibrary
|
||||||
import ir.armor.tachidesk.impl.installAPK
|
import ir.armor.tachidesk.impl.Library.getLibraryMangas
|
||||||
import ir.armor.tachidesk.impl.removeCategory
|
import ir.armor.tachidesk.impl.Library.removeMangaFromLibrary
|
||||||
import ir.armor.tachidesk.impl.removeExtension
|
import ir.armor.tachidesk.impl.Manga.getManga
|
||||||
import ir.armor.tachidesk.impl.removeMangaFromCategory
|
import ir.armor.tachidesk.impl.Manga.getMangaThumbnail
|
||||||
import ir.armor.tachidesk.impl.removeMangaFromLibrary
|
import ir.armor.tachidesk.impl.MangaList.getMangaList
|
||||||
import ir.armor.tachidesk.impl.reorderCategory
|
import ir.armor.tachidesk.impl.Page.getPageImage
|
||||||
import ir.armor.tachidesk.impl.sourceFilters
|
import ir.armor.tachidesk.impl.Search.sourceFilters
|
||||||
import ir.armor.tachidesk.impl.sourceGlobalSearch
|
import ir.armor.tachidesk.impl.Search.sourceGlobalSearch
|
||||||
import ir.armor.tachidesk.impl.sourceSearch
|
import ir.armor.tachidesk.impl.Search.sourceSearch
|
||||||
import ir.armor.tachidesk.impl.updateCategory
|
import ir.armor.tachidesk.impl.Source.getSource
|
||||||
|
import ir.armor.tachidesk.impl.Source.getSourceList
|
||||||
|
import ir.armor.tachidesk.impl.backup.BackupFlags
|
||||||
|
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
||||||
|
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
|
||||||
|
import ir.armor.tachidesk.server.internal.About.getAbout
|
||||||
import ir.armor.tachidesk.server.util.openInBrowser
|
import ir.armor.tachidesk.server.util.openInBrowser
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.future.future
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import java.io.IOException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -39,14 +53,23 @@ import mu.KotlinLogging
|
|||||||
* 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/. */
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
object JavalinSetup {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
fun javalinSetup() {
|
private fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
|
||||||
|
return scope.future(block = block)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun javalinSetup() {
|
||||||
var hasWebUiBundled = false
|
var hasWebUiBundled = false
|
||||||
|
|
||||||
val app = Javalin.create { config ->
|
val app = Javalin.create { config ->
|
||||||
try {
|
try {
|
||||||
|
// if the bellow line throws an exception then webUI is not bundled
|
||||||
Main::class.java.getResource("/react/index.html")
|
Main::class.java.getResource("/react/index.html")
|
||||||
|
|
||||||
|
// no exception so we can tell javalin to serve webUI
|
||||||
hasWebUiBundled = true
|
hasWebUiBundled = true
|
||||||
config.addStaticFiles("/react")
|
config.addStaticFiles("/react")
|
||||||
config.addSinglePageRoot("/", "/react/index.html")
|
config.addSinglePageRoot("/", "/react/index.html")
|
||||||
@@ -56,6 +79,14 @@ fun javalinSetup() {
|
|||||||
}
|
}
|
||||||
config.enableCorsForAllOrigins()
|
config.enableCorsForAllOrigins()
|
||||||
}.start(serverConfig.ip, serverConfig.port)
|
}.start(serverConfig.ip, serverConfig.port)
|
||||||
|
|
||||||
|
// when JVM is prompted to shutdown, stop javalin gracefully
|
||||||
|
Runtime.getRuntime().addShutdownHook(
|
||||||
|
thread(start = false) {
|
||||||
|
app.stop()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
||||||
openInBrowser()
|
openInBrowser()
|
||||||
}
|
}
|
||||||
@@ -65,32 +96,58 @@ fun javalinSetup() {
|
|||||||
ctx.status(404)
|
ctx.status(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/api/v1/extension/list") { ctx ->
|
app.exception(IOException::class.java) { e, ctx ->
|
||||||
ctx.json(getExtensionList())
|
logger.error("IOException while handling the request", e)
|
||||||
|
ctx.status(500)
|
||||||
|
ctx.result(e.message ?: "Internal Server Error")
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
app.get("/api/v1/extension/list") { ctx ->
|
||||||
val apkName = ctx.pathParam("apkName")
|
ctx.json(
|
||||||
|
future {
|
||||||
ctx.status(
|
getExtensionList()
|
||||||
installAPK(apkName)
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
|
app.get("/api/v1/extension/install/:pkgName") { ctx ->
|
||||||
val apkName = ctx.pathParam("apkName")
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
removeExtension(apkName)
|
ctx.json(
|
||||||
|
future {
|
||||||
|
installExtension(pkgName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/extension/update/:pkgName") { ctx ->
|
||||||
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
updateExtension(pkgName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/extension/uninstall/:pkgName") { ctx ->
|
||||||
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
|
uninstallExtension(pkgName)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// icon for extension named `apkName`
|
// icon for extension named `apkName`
|
||||||
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
||||||
val apkName = ctx.pathParam("apkName")
|
val apkName = ctx.pathParam("apkName")
|
||||||
val result = getExtensionIcon(apkName)
|
|
||||||
|
|
||||||
ctx.result(result.first)
|
ctx.result(
|
||||||
ctx.header("content-type", result.second)
|
future { getExtensionIcon(apkName) }
|
||||||
|
.thenApply {
|
||||||
|
ctx.header("content-type", it.second)
|
||||||
|
it.first
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// list of sources
|
// list of sources
|
||||||
@@ -108,43 +165,63 @@ fun javalinSetup() {
|
|||||||
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
ctx.json(getMangaList(sourceId, pageNum, popular = true))
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getMangaList(sourceId, pageNum, popular = true)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// latest mangas from source with id `sourceId`
|
// latest mangas from source with id `sourceId`
|
||||||
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
||||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
ctx.json(getMangaList(sourceId, pageNum, popular = false))
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getMangaList(sourceId, pageNum, popular = false)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get manga info
|
// get manga info
|
||||||
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(getManga(mangaId))
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getManga(mangaId)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// manga thumbnail
|
// manga thumbnail
|
||||||
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
val result = getThumbnail(mangaId)
|
|
||||||
|
|
||||||
ctx.result(result.first)
|
ctx.result(
|
||||||
ctx.header("content-type", result.second)
|
future { getMangaThumbnail(mangaId) }
|
||||||
|
.thenApply {
|
||||||
|
ctx.header("content-type", it.second)
|
||||||
|
it.first
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// adds the manga to library
|
// adds the manga to library
|
||||||
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
addMangaToLibrary(mangaId)
|
|
||||||
ctx.status(200)
|
ctx.result(
|
||||||
|
future { addMangaToLibrary(mangaId) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// removes the manga from the library
|
// removes the manga from the library
|
||||||
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
removeMangaFromLibrary(mangaId)
|
|
||||||
ctx.status(200)
|
ctx.result(
|
||||||
|
future { removeMangaFromLibrary(mangaId) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// list manga's categories
|
// list manga's categories
|
||||||
@@ -169,25 +246,31 @@ fun javalinSetup() {
|
|||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get chapter list when showing a manga
|
||||||
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(getChapterList(mangaId))
|
ctx.json(future { getChapterList(mangaId) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// used to display a chapter, get a chapter in order to show it's pages
|
||||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(getChapter(chapterIndex, mangaId))
|
ctx.json(future { getChapter(chapterIndex, mangaId) })
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
val index = ctx.pathParam("index").toInt()
|
val index = ctx.pathParam("index").toInt()
|
||||||
val result = getPageImage(mangaId, chapterIndex, index)
|
|
||||||
|
|
||||||
ctx.result(result.first)
|
ctx.result(
|
||||||
ctx.header("content-type", result.second)
|
future { getPageImage(mangaId, chapterIndex, index) }
|
||||||
|
.thenApply {
|
||||||
|
ctx.header("content-type", it.second)
|
||||||
|
it.first
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// global search
|
// global search
|
||||||
@@ -201,7 +284,7 @@ fun javalinSetup() {
|
|||||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
val searchTerm = ctx.pathParam("searchTerm")
|
val searchTerm = ctx.pathParam("searchTerm")
|
||||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
|
ctx.json(future { sourceSearch(sourceId, searchTerm, pageNum) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// source filter list
|
// source filter list
|
||||||
@@ -227,6 +310,11 @@ fun javalinSetup() {
|
|||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns some static info of the current app build
|
||||||
|
app.get("/api/v1/about/") { ctx ->
|
||||||
|
ctx.json(getAbout())
|
||||||
|
}
|
||||||
|
|
||||||
// category modification
|
// category modification
|
||||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
@@ -257,4 +345,63 @@ fun javalinSetup() {
|
|||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
ctx.json(getCategoryMangaList(categoryId))
|
ctx.json(getCategoryMangaList(categoryId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// expects a Tachiyomi legacy backup json in the body
|
||||||
|
app.post("/api/v1/backup/legacy/import") { ctx ->
|
||||||
|
ctx.result(
|
||||||
|
future {
|
||||||
|
restoreLegacyBackup(ctx.bodyAsInputStream())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json"
|
||||||
|
app.post("/api/v1/backup/legacy/import/file") { ctx ->
|
||||||
|
ctx.result(
|
||||||
|
future {
|
||||||
|
restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a Tachiyomi legacy backup json created from the current database as a json body
|
||||||
|
app.get("/api/v1/backup/legacy/export") { ctx ->
|
||||||
|
ctx.contentType("application/json")
|
||||||
|
ctx.result(
|
||||||
|
future {
|
||||||
|
createLegacyBackup(
|
||||||
|
BackupFlags(
|
||||||
|
includeManga = true,
|
||||||
|
includeCategories = true,
|
||||||
|
includeChapters = true,
|
||||||
|
includeTracking = true,
|
||||||
|
includeHistory = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a Tachiyomi legacy backup json created from the current database as a file
|
||||||
|
app.get("/api/v1/backup/legacy/export/file") { ctx ->
|
||||||
|
ctx.contentType("application/json")
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm")
|
||||||
|
val currentDate = sdf.format(Date())
|
||||||
|
|
||||||
|
ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
|
||||||
|
ctx.result(
|
||||||
|
future {
|
||||||
|
createLegacyBackup(
|
||||||
|
BackupFlags(
|
||||||
|
includeManga = true,
|
||||||
|
includeCategories = true,
|
||||||
|
includeChapters = true,
|
||||||
|
includeTracking = true,
|
||||||
|
includeHistory = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ServerConfig(config: Config) : ConfigModule(config) {
|
|||||||
val port: Int by config
|
val port: Int by config
|
||||||
|
|
||||||
// proxy
|
// proxy
|
||||||
val socksProxy: Boolean by config
|
val socksProxyEnabled: Boolean by config
|
||||||
val socksProxyHost: String by config
|
val socksProxyHost: String by config
|
||||||
val socksProxyPort: String by config
|
val socksProxyPort: String by config
|
||||||
|
|
||||||
|
|||||||
@@ -10,22 +10,26 @@ package ir.armor.tachidesk.server
|
|||||||
import ch.qos.logback.classic.Level
|
import ch.qos.logback.classic.Level
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import ir.armor.tachidesk.Main
|
import ir.armor.tachidesk.Main
|
||||||
import ir.armor.tachidesk.database.makeDataBaseTables
|
import ir.armor.tachidesk.model.database.databaseUp
|
||||||
import ir.armor.tachidesk.server.util.systemTray
|
import ir.armor.tachidesk.server.util.systemTray
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import net.harawata.appdirs.AppDirsFactory
|
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.bind
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.singleton
|
||||||
|
import org.slf4j.Logger
|
||||||
import xyz.nulldev.androidcompat.AndroidCompat
|
import xyz.nulldev.androidcompat.AndroidCompat
|
||||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||||
|
import xyz.nulldev.ts.config.ApplicationRootDir
|
||||||
import xyz.nulldev.ts.config.ConfigKodeinModule
|
import xyz.nulldev.ts.config.ConfigKodeinModule
|
||||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
object applicationDirs {
|
class ApplicationDirs(
|
||||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
|
val dataRoot: String = ApplicationRootDir
|
||||||
|
) {
|
||||||
val extensionsRoot = "$dataRoot/extensions"
|
val extensionsRoot = "$dataRoot/extensions"
|
||||||
val thumbnailsRoot = "$dataRoot/thumbnails"
|
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||||
val mangaRoot = "$dataRoot/manga"
|
val mangaRoot = "$dataRoot/manga"
|
||||||
@@ -38,26 +42,41 @@ val systemTray by lazy { systemTray() }
|
|||||||
val androidCompat by lazy { AndroidCompat() }
|
val androidCompat by lazy { AndroidCompat() }
|
||||||
|
|
||||||
fun applicationSetup() {
|
fun applicationSetup() {
|
||||||
// register server config
|
// Application dirs
|
||||||
GlobalConfigManager.registerModule(
|
val applicationDirs = ApplicationDirs()
|
||||||
ServerConfig.register(GlobalConfigManager.config)
|
DI.global.addImport(
|
||||||
)
|
DI.Module("Server") {
|
||||||
|
bind<ApplicationDirs>() with singleton { applicationDirs }
|
||||||
// set application wide logging level
|
|
||||||
if (serverConfig.debugLogsEnabled) {
|
|
||||||
(mu.KotlinLogging.logger(org.slf4j.Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// make dirs we need
|
// make dirs we need
|
||||||
listOf(
|
listOf(
|
||||||
applicationDirs.dataRoot,
|
applicationDirs.dataRoot,
|
||||||
applicationDirs.extensionsRoot,
|
applicationDirs.extensionsRoot,
|
||||||
"${applicationDirs.extensionsRoot}/icon",
|
applicationDirs.extensionsRoot + "/icon",
|
||||||
applicationDirs.thumbnailsRoot
|
applicationDirs.thumbnailsRoot
|
||||||
).forEach {
|
).forEach {
|
||||||
File(it).mkdirs()
|
File(it).mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// register Tachidesk's config which is dubbed "ServerConfig"
|
||||||
|
GlobalConfigManager.registerModule(
|
||||||
|
ServerConfig.register(GlobalConfigManager.config)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load config API
|
||||||
|
DI.global.addImport(ConfigKodeinModule().create())
|
||||||
|
// Load Android compatibility dependencies
|
||||||
|
AndroidCompatInitializer().init()
|
||||||
|
// start app
|
||||||
|
androidCompat.startApp(App())
|
||||||
|
|
||||||
|
// set application wide logging level
|
||||||
|
if (serverConfig.debugLogsEnabled) {
|
||||||
|
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
|
||||||
|
}
|
||||||
|
|
||||||
// create conf file if doesn't exist
|
// create conf file if doesn't exist
|
||||||
try {
|
try {
|
||||||
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
||||||
@@ -72,22 +91,16 @@ fun applicationSetup() {
|
|||||||
logger.error("Exception while creating initial server.conf:\n", e)
|
logger.error("Exception while creating initial server.conf:\n", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
makeDataBaseTables()
|
databaseUp()
|
||||||
|
|
||||||
// create system tray
|
// create system tray
|
||||||
if (serverConfig.systemTrayEnabled)
|
if (serverConfig.systemTrayEnabled) {
|
||||||
try {
|
try {
|
||||||
systemTray
|
systemTray
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Load config API
|
|
||||||
DI.global.addImport(ConfigKodeinModule().create())
|
|
||||||
// Load Android compatibility dependencies
|
|
||||||
AndroidCompatInitializer().init()
|
|
||||||
// start app
|
|
||||||
androidCompat.startApp(App())
|
|
||||||
|
|
||||||
// Disable jetty's logging
|
// Disable jetty's logging
|
||||||
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
|
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
|
||||||
@@ -95,7 +108,10 @@ fun applicationSetup() {
|
|||||||
System.setProperty("org.eclipse.jetty.LEVEL", "OFF")
|
System.setProperty("org.eclipse.jetty.LEVEL", "OFF")
|
||||||
|
|
||||||
// socks proxy settings
|
// socks proxy settings
|
||||||
System.getProperties()["proxySet"] = serverConfig.socksProxy.toString()
|
if (serverConfig.socksProxyEnabled) {
|
||||||
|
// System.getProperties()["proxySet"] = "true"
|
||||||
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
|
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
|
||||||
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
|
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
|
||||||
|
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package ir.armor.tachidesk.server.internal
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.server.BuildConfig
|
||||||
|
|
||||||
|
data class AboutDataClass(
|
||||||
|
val version: String,
|
||||||
|
val revision: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
object About {
|
||||||
|
fun getAbout(): AboutDataClass {
|
||||||
|
return AboutDataClass(
|
||||||
|
BuildConfig.version,
|
||||||
|
BuildConfig.revision,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,15 +13,15 @@ import dorkbox.systemTray.SystemTray.TrayType
|
|||||||
import dorkbox.util.CacheUtil
|
import dorkbox.util.CacheUtil
|
||||||
import dorkbox.util.Desktop
|
import dorkbox.util.Desktop
|
||||||
import ir.armor.tachidesk.Main
|
import ir.armor.tachidesk.Main
|
||||||
|
import ir.armor.tachidesk.server.BuildConfig
|
||||||
import ir.armor.tachidesk.server.serverConfig
|
import ir.armor.tachidesk.server.serverConfig
|
||||||
import java.awt.event.ActionListener
|
import kotlin.system.exitProcess
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
fun openInBrowser() {
|
fun openInBrowser() {
|
||||||
try {
|
try {
|
||||||
Desktop.browseURL("http://127.0.0.1:4567")
|
Desktop.browseURL("http://127.0.0.1:4567")
|
||||||
} catch (e1: IOException) {
|
} catch (e: Exception) {
|
||||||
e1.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,22 +32,17 @@ fun systemTray(): SystemTray? {
|
|||||||
if (System.getProperty("os.name").startsWith("Windows"))
|
if (System.getProperty("os.name").startsWith("Windows"))
|
||||||
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
||||||
|
|
||||||
CacheUtil.clear()
|
CacheUtil.clear(BuildConfig.name)
|
||||||
|
|
||||||
val systemTray = SystemTray.get() ?: return null
|
val systemTray = SystemTray.get(BuildConfig.name) ?: return null
|
||||||
val mainMenu = systemTray.menu
|
val mainMenu = systemTray.menu
|
||||||
|
|
||||||
mainMenu.add(
|
mainMenu.add(
|
||||||
MenuItem(
|
MenuItem(
|
||||||
"Open Tachidesk",
|
"Open Tachidesk"
|
||||||
ActionListener {
|
) {
|
||||||
try {
|
openInBrowser()
|
||||||
Desktop.browseURL("http://127.0.0.1:4567")
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
|
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
|
||||||
@@ -56,13 +51,15 @@ fun systemTray(): SystemTray? {
|
|||||||
systemTray.setImage(icon)
|
systemTray.setImage(icon)
|
||||||
// systemTray.status = "No Mail"
|
// systemTray.status = "No Mail"
|
||||||
|
|
||||||
systemTray.getMenu().add(
|
mainMenu.add(
|
||||||
MenuItem("Quit") {
|
MenuItem("Quit") {
|
||||||
systemTray.shutdown()
|
systemTray.shutdown()
|
||||||
System.exit(0)
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
systemTray.installShutdownHook()
|
||||||
|
|
||||||
return systemTray
|
return systemTray
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ server.ip = "0.0.0.0"
|
|||||||
server.port = 4567
|
server.port = 4567
|
||||||
|
|
||||||
# Socks5 proxy
|
# Socks5 proxy
|
||||||
server.socksProxy = false
|
server.socksProxyEnabled = false
|
||||||
server.socksProxyHost = ""
|
server.socksProxyHost = ""
|
||||||
server.socksProxyPort = ""
|
server.socksProxyPort = ""
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user