Compare commits
227 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84206a7074 | |||
| d1500baae1 | |||
| 045801dd1a | |||
| 14a2cbc793 | |||
| fd385017df | |||
| 9b05954cf2 | |||
| 6aaf636069 | |||
| d30e89e5ec | |||
| 7acc745478 | |||
| 5a9a2d816e | |||
| 105f11ed02 | |||
| 3021437a05 | |||
| 439602fc03 | |||
| 34e13b9589 | |||
| 2aab4ae918 | |||
| 7ef67671a4 | |||
| e8df84416c | |||
| be930bb68b | |||
| db52948865 | |||
| d2a72526f6 | |||
| 0a9f57b32b | |||
| 180f210536 | |||
| c1baa31eed | |||
| cacc97cec7 | |||
| d5691fd81c | |||
| 49dc9fe5f6 | |||
| c0b49c7428 | |||
| fa345af42d | |||
| db3cc786a1 | |||
| fe879ae51d | |||
| 2f55460ffb | |||
| fbc5bd4642 | |||
| 5e0c7d3c9d | |||
| 083996a48d | |||
| 9d38f478e3 | |||
| 57274a0a01 | |||
| b3b56b7fc8 | |||
| 0b690577da | |||
| e9683a3a37 | |||
| f8f67b3eba | |||
| 7b16b082d8 | |||
| 2a783f0d8e | |||
| 42ae32de33 | |||
| cec7ddc486 | |||
| 9c55fc3868 | |||
| 104c5a8d83 | |||
| 7450b16742 | |||
| 3ecd0931a1 | |||
| 2f2a52ae2f | |||
| f464087c30 | |||
| 2364960388 | |||
| 76be4d64cd | |||
| 7d98e8ce47 | |||
| 40831fc681 | |||
| e38e7ccf26 | |||
| 98b9e2f2cf | |||
| 4bf3c12f76 | |||
| bab25f9ad9 | |||
| a62ee8f8c3 | |||
| 5f23691e20 | |||
| 3de9ccc62f | |||
| 1896f7f37b | |||
| 490643dc02 | |||
| 9808976088 | |||
| 5a73068a10 | |||
| 01d5c2540d | |||
| 866b01f865 | |||
| da6a953099 | |||
| bce8d58845 | |||
| 3cfce2db04 | |||
| 327aae5dd9 | |||
| 1bdfde7032 | |||
| 295a0817b0 | |||
| a02dc02d52 | |||
| dc012edf7d | |||
| 1e2eb11c13 | |||
| 3a825f4f25 | |||
| b9ea8c5f74 | |||
| 320d7e2536 | |||
| c200785479 | |||
| 8abb132ad6 | |||
| 8bb2269f36 | |||
| 9d17b26283 | |||
| 5909f15db7 | |||
| 11672ca576 | |||
| e09773def3 | |||
| f6d4432e6f | |||
| 45a6abc5c2 | |||
| dc5e677a38 | |||
| a82549dc17 | |||
| a002e19d9d | |||
| cdf1f98d28 | |||
| 0ff1ebdeb7 | |||
| 17f4a396f8 | |||
| 8aa3cf4368 | |||
| 0136c5e493 | |||
| 8b94b9ee80 | |||
| bed63f19f2 | |||
| e2a6545a84 | |||
| e3d3ec6895 | |||
| 7ba476bd79 | |||
| 2dd41ebd27 | |||
| 038df78171 | |||
| 6e5ff2b508 | |||
| ec8d1e8680 | |||
| 1f0f0c33b7 | |||
| 825940fcac | |||
| 4618834af2 | |||
| 55d968df5e | |||
| 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 |
+2
-1
@@ -24,4 +24,5 @@
|
|||||||
*.woff binary
|
*.woff binary
|
||||||
*.pyc binary
|
*.pyc binary
|
||||||
*.swp binary
|
*.swp binary
|
||||||
*.pdf binary
|
*.pdf binary
|
||||||
|
*.exe binary
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
rm -rf preview/*.jar preview/*.zip
|
||||||
|
|
||||||
|
cp master/server/build/Tachidesk-*.jar preview
|
||||||
|
cp master/server/build/Tachidesk-*.zip preview
|
||||||
|
|
||||||
|
cd preview
|
||||||
|
|
||||||
|
new_jar_build=$(ls Tachidesk-*.jar)
|
||||||
|
echo "last jar build file name: $new_jar_build"
|
||||||
|
|
||||||
|
latest=$(echo $new_jar_build | sed -e's/Tachidesk-\|.jar//g')
|
||||||
|
echo "{ \"latest\": \"$latest\" }" > index.json
|
||||||
|
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git status
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
git add .
|
||||||
|
git commit -m "Update preview repository"
|
||||||
|
git push
|
||||||
|
else
|
||||||
|
echo "No changes to commit"
|
||||||
|
fi
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
git lfs install
|
|
||||||
#git lfs track "*.zip"
|
|
||||||
|
|
||||||
cp ../master/repo/* .
|
|
||||||
new_jar_build=$(ls *.jar| tail -1)
|
|
||||||
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_win32_build Tachidesk-latest-win32.zip
|
|
||||||
|
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git status
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
|
||||||
git add .
|
|
||||||
git commit -m "Update repo"
|
|
||||||
git push
|
|
||||||
else
|
|
||||||
echo "No changes to commit"
|
|
||||||
fi
|
|
||||||
@@ -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
|
||||||
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:shadowJar --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,88 @@
|
|||||||
|
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 artifacts and deploy preview
|
||||||
|
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
|
||||||
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
with:
|
||||||
|
build-root-directory: master
|
||||||
|
wrapper-directory: master
|
||||||
|
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||||
|
wrapper-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
- name: make windows packages
|
||||||
|
run: |
|
||||||
|
cd master/scripts
|
||||||
|
./windows32-bundler.sh
|
||||||
|
./windows64-bundler.sh
|
||||||
|
|
||||||
|
- name: Checkout preview branch
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: 'Suwayomi/Tachidesk-preview'
|
||||||
|
ref: main
|
||||||
|
path: preview
|
||||||
|
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||||
|
|
||||||
|
- name: Deploy preview
|
||||||
|
run: |
|
||||||
|
./master/.github/scripts/commit-preview.sh
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Publish
|
name: CI Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -56,60 +56,30 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||||
|
|
||||||
- name: Build Jar and launch4j
|
- name: Build and copy webUI, Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
arguments: :server:windowsPackage --stacktrace
|
arguments: :webUI:copyBuild :server:shadowJar --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: make windows packages
|
||||||
- name: Create repo artifacts
|
|
||||||
run: |
|
run: |
|
||||||
cd master
|
cd master/scripts
|
||||||
./.github/scripts/create-repo.sh
|
./windows32-bundler.sh
|
||||||
|
./windows64-bundler.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/*.zip"
|
||||||
tags: true
|
tags: true
|
||||||
draft: true
|
draft: true
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
# - name: Create Release
|
|
||||||
# id: create_release
|
|
||||||
# uses: actions/create-release@v1
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# with:
|
|
||||||
# tag_name: ${{ github.ref }}
|
|
||||||
# release_name: Release ${{ github.ref }}
|
|
||||||
# body: |
|
|
||||||
# Release body
|
|
||||||
# draft: false
|
|
||||||
# prerelease: true
|
|
||||||
#
|
|
||||||
# - name: Get the Ref
|
|
||||||
# id: get-ref
|
|
||||||
# uses: ankitvgupta/ref-to-tag-action@master
|
|
||||||
# with:
|
|
||||||
# ref: ${{ github.ref }}
|
|
||||||
# head_ref: ${{ github.head_ref }}
|
|
||||||
#
|
|
||||||
# - name: Get the tag
|
|
||||||
# run: echo "The tag was ${{ steps.get-ref.outputs.tag }}"
|
|
||||||
#
|
|
||||||
# - name: Upload Release
|
|
||||||
# uses: AButler/upload-release-assets@v2.0
|
|
||||||
# with:
|
|
||||||
# files: 'master/repo/*'
|
|
||||||
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# release-tag: ${{ steps.get-ref.outputs.tag }}
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
# 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/
|
||||||
|
|
||||||
|
# OpenJDK downlaods
|
||||||
|
OpenJDK*
|
||||||
@@ -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,8 +46,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
val config = ConfigFactory.empty()
|
val config = ConfigFactory.empty()
|
||||||
@@ -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"
|
||||||
|
|
||||||
@@ -87,7 +87,6 @@ function Dedupe($path)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Dedupe "AndroidCompat/src/main/java"
|
Dedupe "AndroidCompat/src/main/java"
|
||||||
Dedupe "server/src/main/java"
|
|
||||||
Dedupe "server/src/main/kotlin"
|
Dedupe "server/src/main/kotlin"
|
||||||
|
|
||||||
Write-Output "Copying Android.jar to library folder..."
|
Write-Output "Copying Android.jar to library folder..."
|
||||||
|
|||||||
@@ -8,8 +8,19 @@
|
|||||||
|
|
||||||
# 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 ..
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -48,7 +59,7 @@ zip --delete android.jar javax/*
|
|||||||
echo "Removing java..."
|
echo "Removing java..."
|
||||||
zip --delete android.jar java/*
|
zip --delete android.jar java/*
|
||||||
|
|
||||||
echo "Removing overriden classes..."
|
echo "Removing overridden classes..."
|
||||||
zip --delete android.jar android/app/Application.class
|
zip --delete android.jar android/app/Application.class
|
||||||
zip --delete android.jar android/app/Service.class
|
zip --delete android.jar android/app/Service.class
|
||||||
zip --delete android.jar android/net/Uri.class
|
zip --delete android.jar android/net/Uri.class
|
||||||
@@ -57,12 +68,12 @@ zip --delete android.jar android/os/Environment.class
|
|||||||
zip --delete android.jar android/text/format/Formatter.class
|
zip --delete android.jar android/text/format/Formatter.class
|
||||||
zip --delete android.jar android/text/Html.class
|
zip --delete android.jar android/text/Html.class
|
||||||
|
|
||||||
# Dedup overriden Android classes
|
# Dedup overridden Android classes
|
||||||
ABS_JAR="$(realpath android.jar)"
|
ABS_JAR="$(realpath android.jar)"
|
||||||
function dedup() {
|
function dedup() {
|
||||||
pushd "$1"
|
pushd "$1"
|
||||||
CLASSES="$(find * -type f)"
|
CLASSES="$(find ./* -type f)"
|
||||||
echo "$CLASSES" | while read class
|
echo "$CLASSES" | while read -r class
|
||||||
do
|
do
|
||||||
NAME="${class%.*}"
|
NAME="${class%.*}"
|
||||||
echo "Processing class: $NAME"
|
echo "Processing class: $NAME"
|
||||||
@@ -71,13 +82,10 @@ function dedup() {
|
|||||||
popd
|
popd
|
||||||
}
|
}
|
||||||
|
|
||||||
pushd ..
|
popd
|
||||||
dedup AndroidCompat/src/main/java
|
dedup AndroidCompat/src/main/java
|
||||||
dedup server/src/main/java
|
|
||||||
dedup server/src/main/kotlin
|
dedup server/src/main/kotlin
|
||||||
popd
|
|
||||||
|
|
||||||
popd
|
|
||||||
echo "Copying Android.jar to library folder..."
|
echo "Copying Android.jar to library folder..."
|
||||||
mv tmp/android.jar AndroidCompat/lib
|
mv tmp/android.jar AndroidCompat/lib
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
val out = mutableListOf<Node>()
|
fun NodeList.toList(): List<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()
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Code Of Conduct
|
||||||
|
- Don't be a dick.
|
||||||
|
|
||||||
|
# expanding the code of conduct!
|
||||||
|
The contents of this document is up for debate and improvement! Discussions on discord.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Contributing
|
||||||
|
## Where should I start?
|
||||||
|
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
|
||||||
|
|
||||||
|
**Note to potential contributors:** Notify the developers on Suwayomi discord (#programming channel) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
|
||||||
|
|
||||||
|
## How does Tachidesk work?
|
||||||
|
This project has two components:
|
||||||
|
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
||||||
|
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation.
|
||||||
|
|
||||||
|
## Why a web app?
|
||||||
|
This structure is chosen to
|
||||||
|
- Achieve the maximum multi-platform-ness
|
||||||
|
- Gives the ability to acces Tachidesk from a remote web browser e.g. your phone, tablet or smart TV
|
||||||
|
- Eaise development of alternative user intefaces for Tachidesk
|
||||||
|
|
||||||
|
## User Interfaces for Tachidesk server
|
||||||
|
Currently, there are three known interfaces for Tachidesk:
|
||||||
|
1. [webUI](https://github.com/Suwayomi/Tachidesk/tree/master/webUI/react): The react SPA that Tachidesk is traditionally shipped with.
|
||||||
|
2. [TachideskJUI](https://github.com/Suwayomi/TachideskJUI): A Jetbrains Compose Native app, re-uses components made for the upcoming Tachiyomi 1.x
|
||||||
|
3. [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stages of development.
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
### Prerequisites
|
||||||
|
You need these software packages installed in order to build the project
|
||||||
|
### Server
|
||||||
|
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
||||||
|
- Android stubs jar
|
||||||
|
- Manual download: Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||||
|
- Automated download: Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
|
||||||
|
### webUI
|
||||||
|
- Nodejs LTS or latest
|
||||||
|
- Yarn
|
||||||
|
- Git
|
||||||
|
### building the full-blown jar
|
||||||
|
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||||
|
### building without `webUI` bundled(server only)
|
||||||
|
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||||
|
### building the Windows package
|
||||||
|
First Build the jar, then cd into the `scripts` directory and run `./windows<bits>-bundler.sh` (or `./windows<bits>-bundler.ps1` if you are on windows), the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win64.zip`.
|
||||||
|
## Running in development mode
|
||||||
|
First satisfy [the prerequisites](#prerequisites)
|
||||||
|
### server
|
||||||
|
run `./gradlew :server:run --stacktrace` to run the server
|
||||||
|
### webUI
|
||||||
|
How to do it is described in `webUI/react/README.md` but for short,
|
||||||
|
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
||||||
|
then `yarn start` to start the development server, if a new browser window doesn't get opened automatically,
|
||||||
|
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
||||||
|
and supports HMR and all the other goodies you'll need.
|
||||||
|
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
|
|
||||||

|
| Build | Stable | Preview | Support Server |
|
||||||
|
|-------|----------|---------|---------|
|
||||||
|
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk/tree/preview/) | [](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 as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
|
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
|
||||||
|
|
||||||
|
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
|
||||||
|
|
||||||
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
||||||
|
|
||||||
|
**Tachidesk needs serious front-end dev help for it's reader and other parts, if you like the app and want to see it become better please don't hesitate to contribute some code!**
|
||||||
|
|
||||||
## Is this application usable? Should I test it?
|
## Is this application usable? Should I test it?
|
||||||
Here is a list of current features:
|
Here is a list of current features:
|
||||||
|
|
||||||
@@ -15,23 +24,22 @@ 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. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting) if it happens.
|
||||||
|
|
||||||
Anyways, for more info checkout [finished milestone #1](https://github.com/Suwayomi/Tachidesk/issues/2) and [milestone #2](https://github.com/Suwayomi/Tachidesk/projects/1) to see what's implemented in more detail.
|
|
||||||
|
|
||||||
## Downloading and Running the app
|
## Downloading and Running the app
|
||||||
### All Operating Systems
|
### All Operating Systems
|
||||||
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
|
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed(Google is your friend for seeking assitance). 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 or win64 (depending on your system, usually you want win64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
|
||||||
|
|
||||||
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`. The rest works like the previous section.
|
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win64.zip` and run `Tachidesk Launcher.exe` or `Tachidesk Launcher.bat`. The rest works like the previous section.
|
||||||
|
|
||||||
### Arch Linux
|
### Arch Linux
|
||||||
You can install Tachidesk from the AUR
|
You can install Tachidesk from the AUR
|
||||||
@@ -42,51 +50,14 @@ yay -S tachidesk
|
|||||||
### Docker
|
### Docker
|
||||||
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
|
### Using Tachidesk Remotely
|
||||||
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.
|
You can run Tachidesk on your computer or a server and connect to it remotely through the web interface with a web browser on any device including a mobile or tablet or even your smart TV!, this method of using Tachidesk is only recommended if you are a power user and know what you are doing.
|
||||||
|
|
||||||
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
## Troubleshooting and Support
|
||||||
|
See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting).
|
||||||
|
|
||||||
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
|
## Contributing and Technical info
|
||||||
|
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||||
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
|
|
||||||
|
|
||||||
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
|
||||||
|
|
||||||
## Support and help
|
|
||||||
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support and help.
|
|
||||||
|
|
||||||
## How does it work?
|
|
||||||
This project has two components:
|
|
||||||
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
|
||||||
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
|
||||||
|
|
||||||
## Building from source
|
|
||||||
### Prerequisite: Get Android stubs jar
|
|
||||||
#### Manual download
|
|
||||||
Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
|
||||||
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
|
|
||||||
Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
|
|
||||||
### Prerequisite: Software dependencies
|
|
||||||
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)
|
|
||||||
- Nodejs LTS or latest
|
|
||||||
- Yarn
|
|
||||||
### building the full-blown jar
|
|
||||||
Run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
|
||||||
### building without `webUI` bundled
|
|
||||||
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`.
|
|
||||||
### building the Windows package
|
|
||||||
Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
|
||||||
## Running for development purposes
|
|
||||||
### `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
|
|
||||||
### `webUI` module
|
|
||||||
How to do it is described in `webUI/react/README.md` but for short,
|
|
||||||
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
|
||||||
then `yarn start` to start the client if a new browser window doesn't start automatically,
|
|
||||||
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
|
||||||
and supports HMR and all the other goodies you'll need.
|
|
||||||
|
|
||||||
## Credit
|
## Credit
|
||||||
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
|
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
jre\bin\java -Dir.armor.tachidesk.debugLogsEnabled=true -jar Tachidesk.jar
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
start "" jre/bin/javaw -jar Tachidesk.jar
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
system("start jre\\bin\\javaw -jar Tachidesk.jar");
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Building `Tachidesk Launcher.exe`
|
||||||
|
1. compile `Tachidesk Launcher.c` statically using GCC MinGW: `gcc -o "Tachidesk Launcher.exe" "Tachidesk Launcher.c"`
|
||||||
|
2. Add `server/src/main/resources/icon/faviconlogo.ico` into the exe with `rcedit` from the electron project: `rcedit "Tachidesk Launcher.exe" --set-icon faviconlogo.ico`
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# 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/.
|
||||||
|
|
||||||
|
Write-Output "Downloading jre..."
|
||||||
|
|
||||||
|
$jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||||
|
if (!(Test-Path $jre)) {
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "creating windows bundle"
|
||||||
|
|
||||||
|
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
|
||||||
|
$release_name=$jar.BaseName + "-win32"
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
New-Item -ItemType Directory $release_name
|
||||||
|
|
||||||
|
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# move jre
|
||||||
|
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
|
||||||
|
|
||||||
|
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
|
||||||
|
|
||||||
|
Copy-Item "resources/Tachidesk Launcher-win32.exe" $release_name
|
||||||
|
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
|
||||||
|
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
|
||||||
|
$zip_name="$release_name.zip"
|
||||||
|
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Remove-Item -Force -Recurse $release_name
|
||||||
|
|
||||||
|
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright (C) Contributors to the Suwayomi project
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
echo "Downloading jre..."
|
||||||
|
|
||||||
|
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||||
|
if [ ! -f $jre ]; then
|
||||||
|
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -o $jre
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "creating windows bundle"
|
||||||
|
|
||||||
|
jar=$(ls ../server/build/Tachidesk-*.jar)
|
||||||
|
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||||
|
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win32
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
mkdir $release_name
|
||||||
|
|
||||||
|
unzip $jre
|
||||||
|
|
||||||
|
# move jre
|
||||||
|
mv jdk8u292-b10-jre $release_name/jre
|
||||||
|
|
||||||
|
cp $jar $release_name/Tachidesk.jar
|
||||||
|
|
||||||
|
cp "resources/Tachidesk Launcher-win32.exe" "$release_name/Tachidesk Launcher.exe"
|
||||||
|
cp "resources/Tachidesk Launcher.bat" $release_name
|
||||||
|
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
|
||||||
|
zip_name=$release_name.zip
|
||||||
|
zip -9 -r $zip_name $release_name
|
||||||
|
|
||||||
|
rm -rf $release_name
|
||||||
|
|
||||||
|
mv $zip_name ../server/build/
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# 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/.
|
||||||
|
|
||||||
|
Write-Output "Downloading jre..."
|
||||||
|
|
||||||
|
$jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||||
|
if (!(Test-Path $jre)) {
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "creating windows bundle"
|
||||||
|
|
||||||
|
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
|
||||||
|
$release_name=$jar.BaseName + "-win64"
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
New-Item -ItemType Directory $release_name
|
||||||
|
|
||||||
|
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# move jre
|
||||||
|
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
|
||||||
|
|
||||||
|
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
|
||||||
|
|
||||||
|
Copy-Item "resources/Tachidesk Launcher-win64.exe" $release_name
|
||||||
|
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
|
||||||
|
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
|
||||||
|
$zip_name="$release_name.zip"
|
||||||
|
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Remove-Item -Force -Recurse $release_name
|
||||||
|
|
||||||
|
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright (C) Contributors to the Suwayomi project
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
echo "Downloading jre..."
|
||||||
|
|
||||||
|
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||||
|
if [ ! -f $jre ]; then
|
||||||
|
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -o $jre
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "creating windows bundle"
|
||||||
|
|
||||||
|
jar=$(ls ../server/build/Tachidesk-*.jar)
|
||||||
|
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||||
|
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win64
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
mkdir $release_name
|
||||||
|
|
||||||
|
unzip $jre
|
||||||
|
|
||||||
|
# move jre
|
||||||
|
mv jdk8u292-b10-jre $release_name/jre
|
||||||
|
|
||||||
|
cp $jar $release_name/Tachidesk.jar
|
||||||
|
|
||||||
|
cp "resources/Tachidesk Launcher-win64.exe" "$release_name/Tachidesk Launcher.exe"
|
||||||
|
cp "resources/Tachidesk Launcher.bat" $release_name
|
||||||
|
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
|
||||||
|
zip_name=$release_name.zip
|
||||||
|
zip -9 -r $zip_name $release_name
|
||||||
|
|
||||||
|
rm -rf $release_name
|
||||||
|
|
||||||
|
mv $zip_name ../server/build/
|
||||||
+98
-129
@@ -1,57 +1,49 @@
|
|||||||
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("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 +51,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 +77,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.MainKt"
|
||||||
application {
|
application {
|
||||||
mainClass.set(name)
|
mainClass.set(MainClass)
|
||||||
|
|
||||||
// Required by ShadowJar.
|
|
||||||
mainClassName = name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -105,7 +95,11 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val TachideskRevision = Runtime
|
// should be bumped with each stable release
|
||||||
|
val tachideskVersion = "v0.3.7"
|
||||||
|
|
||||||
|
// 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 +112,69 @@ 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())
|
||||||
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
jar {
|
shadowJar {
|
||||||
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
archiveBaseName.set(rootProject.name)
|
||||||
|
archiveVersion.set(tachideskVersion)
|
||||||
|
archiveClassifier.set(tachideskRevision)
|
||||||
|
}
|
||||||
|
withType<KotlinCompile> {
|
||||||
|
kotlinOptions {
|
||||||
|
freeCompilerArgs = listOf(
|
||||||
|
"-Xopt-in=kotlin.RequiresOptIn",
|
||||||
|
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
|
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shadowJar {
|
|
||||||
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
test {
|
||||||
archiveBaseName.set("Tachidesk")
|
useJUnit()
|
||||||
archiveVersion.set(TachideskVersion)
|
}
|
||||||
archiveClassifier.set(TachideskRevision)
|
|
||||||
|
withType<ShadowJar> {
|
||||||
|
destinationDirectory.set(File("$rootDir/server/build"))
|
||||||
|
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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
launch4j { //used for windows
|
|
||||||
mainClassName = name
|
|
||||||
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"))
|
|
||||||
archiveFileName.set("Tachidesk-$TachideskVersion-$TachideskRevision-win32.zip")
|
|
||||||
dependsOn("windowsPackageWorkaround2")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Delete>("windowsPackageWorkaround2") {
|
|
||||||
delete(
|
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jre",
|
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/lib",
|
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/server.exe",
|
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
|
||||||
)
|
|
||||||
dependsOn("windowsPackageWorkaround")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Copy>("windowsPackageWorkaround") {
|
|
||||||
from("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
|
||||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
|
||||||
dependsOn("deleteUnwantedJreDir")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Delete>("deleteUnwantedJreDir") {
|
|
||||||
delete(
|
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jdk8u282-b08-jre"
|
|
||||||
)
|
|
||||||
dependsOn("addJreToDistributable")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Copy>("addJreToDistributable") {
|
|
||||||
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
|
|
||||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
|
||||||
eachFile {
|
|
||||||
path = path.replace(".*-jre".toRegex(),"jre")
|
|
||||||
}
|
|
||||||
dependsOn("downloadJre")
|
|
||||||
dependsOn("createExe")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.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")
|
|
||||||
dest(buildDir)
|
|
||||||
overwrite(false)
|
|
||||||
onlyIfModified(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<ShadowJar> {
|
|
||||||
destinationDir = File("$rootDir/server/build")
|
|
||||||
dependsOn("lintKotlin")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("processResources") {
|
|
||||||
dependsOn(":webUI:copyBuild")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("run") {
|
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,16 +7,10 @@ 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 {
|
fun main() {
|
||||||
companion object {
|
applicationSetup()
|
||||||
|
javalinSetup()
|
||||||
@JvmStatic
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
applicationSetup()
|
|
||||||
javalinSetup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,21 +0,0 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
data class ChapterDataClass(
|
|
||||||
val id: Int,
|
|
||||||
val url: String,
|
|
||||||
val name: String,
|
|
||||||
val date_upload: Long,
|
|
||||||
val chapter_number: Float,
|
|
||||||
val scanlator: String?,
|
|
||||||
val mangaId: Int,
|
|
||||||
val chapterIndex: Int,
|
|
||||||
val chapterCount: Int,
|
|
||||||
val pageCount: Int? = null,
|
|
||||||
)
|
|
||||||
@@ -1,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
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
|
||||||
|
|
||||||
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 chapterIndex = integer("number_in_list")
|
|
||||||
|
|
||||||
val manga = reference("manga", MangaTable)
|
|
||||||
}
|
|
||||||
@@ -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,51 +20,59 @@ 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 {
|
||||||
transaction {
|
/**
|
||||||
val count = CategoryTable.selectAll().count()
|
* The new category will be placed at the end of the list
|
||||||
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
*/
|
||||||
CategoryTable.insert {
|
fun createCategory(name: String) {
|
||||||
it[CategoryTable.name] = name
|
transaction {
|
||||||
it[CategoryTable.order] = count.toInt() + 1
|
val count = CategoryTable.selectAll().count()
|
||||||
}
|
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
||||||
}
|
CategoryTable.insert {
|
||||||
}
|
it[CategoryTable.name] = name
|
||||||
|
it[CategoryTable.order] = count.toInt() + 1
|
||||||
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
}
|
||||||
transaction {
|
|
||||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
|
||||||
if (name != null) it[CategoryTable.name] = name
|
|
||||||
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
||||||
transaction {
|
transaction {
|
||||||
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||||
categories.add(to - 1, categories.removeAt(from - 1))
|
if (name != null) it[CategoryTable.name] = name
|
||||||
categories.forEachIndexed { index, cat ->
|
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
||||||
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
|
}
|
||||||
it[CategoryTable.order] = index + 1
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the category from position `from` to `to`
|
||||||
|
*/
|
||||||
|
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
||||||
|
transaction {
|
||||||
|
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||||
|
categories.add(to - 1, categories.removeAt(from - 1))
|
||||||
|
categories.forEachIndexed { index, cat ->
|
||||||
|
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
|
||||||
|
it[CategoryTable.order] = index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCategory(categoryId: Int) {
|
||||||
|
transaction {
|
||||||
|
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
|
||||||
|
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
|
||||||
|
}
|
||||||
|
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCategoryList(): List<CategoryDataClass> {
|
||||||
|
return transaction {
|
||||||
|
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||||
|
CategoryTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeCategory(categoryId: Int) {
|
|
||||||
transaction {
|
|
||||||
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
|
|
||||||
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
|
|
||||||
}
|
|
||||||
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCategoryList(): List<CategoryDataClass> {
|
|
||||||
return transaction {
|
|
||||||
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
|
||||||
CategoryTable.toDataClass(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,44 +21,52 @@ 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 {
|
||||||
transaction {
|
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||||
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
|
transaction {
|
||||||
CategoryMangaTable.insert {
|
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
|
||||||
it[CategoryMangaTable.category] = categoryId
|
CategoryMangaTable.insert {
|
||||||
it[CategoryMangaTable.manga] = mangaId
|
it[CategoryMangaTable.category] = categoryId
|
||||||
}
|
it[CategoryMangaTable.manga] = mangaId
|
||||||
|
}
|
||||||
|
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
it[MangaTable.defaultCategory] = false
|
it[MangaTable.defaultCategory] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
||||||
|
transaction {
|
||||||
|
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
||||||
|
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
it[MangaTable.defaultCategory] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* list of mangas that belong to a category
|
||||||
|
*/
|
||||||
|
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
||||||
|
return transaction {
|
||||||
|
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
||||||
|
MangaTable.toDataClass(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* list of categories that a manga belongs to
|
||||||
|
*/
|
||||||
|
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
|
||||||
|
return transaction {
|
||||||
|
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||||
|
CategoryTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
|
||||||
transaction {
|
|
||||||
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
|
||||||
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
|
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
|
||||||
it[MangaTable.defaultCategory] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
|
||||||
return transaction {
|
|
||||||
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
|
||||||
MangaTable.toDataClass(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
|
|
||||||
return transaction {
|
|
||||||
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
|
||||||
CategoryTable.toDataClass(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,86 +9,134 @@ 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.database.table.toDataClass
|
||||||
|
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder.DESC
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
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.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 {
|
||||||
val mangaDetails = getManga(mangaId)
|
/** get chapter list when showing a manga */
|
||||||
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean?): List<ChapterDataClass> {
|
||||||
|
return if (onlineFetch == true) {
|
||||||
val chapterList = source.fetchChapterList(
|
getSourceChapters(mangaId)
|
||||||
SManga.create().apply {
|
} else {
|
||||||
title = mangaDetails.title
|
transaction {
|
||||||
url = mangaDetails.url
|
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC)
|
||||||
|
.map {
|
||||||
|
ChapterTable.toDataClass(it)
|
||||||
|
}
|
||||||
|
}.ifEmpty {
|
||||||
|
// If it was explicitly set to offline dont grab chapters
|
||||||
|
if (onlineFetch == null) {
|
||||||
|
getSourceChapters(mangaId)
|
||||||
|
} else emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
).toBlocking().first()
|
}
|
||||||
|
|
||||||
val chapterCount = chapterList.count()
|
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
|
||||||
|
val mangaDetails = getManga(mangaId)
|
||||||
|
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
||||||
|
val chapterList = source.fetchChapterList(
|
||||||
|
SManga.create().apply {
|
||||||
|
title = mangaDetails.title
|
||||||
|
url = mangaDetails.url
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
return transaction {
|
val chapterCount = chapterList.count()
|
||||||
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
|
||||||
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
|
||||||
if (chapterEntry == null) {
|
|
||||||
ChapterTable.insertAndGetId {
|
|
||||||
it[url] = fetchedChapter.url
|
|
||||||
it[name] = fetchedChapter.name
|
|
||||||
it[date_upload] = fetchedChapter.date_upload
|
|
||||||
it[chapter_number] = fetchedChapter.chapter_number
|
|
||||||
it[scanlator] = fetchedChapter.scanlator
|
|
||||||
|
|
||||||
it[chapterIndex] = index + 1
|
transaction {
|
||||||
it[manga] = mangaId
|
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
||||||
}
|
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
||||||
} else {
|
if (chapterEntry == null) {
|
||||||
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
ChapterTable.insert {
|
||||||
it[name] = fetchedChapter.name
|
it[url] = fetchedChapter.url
|
||||||
it[date_upload] = fetchedChapter.date_upload
|
it[name] = fetchedChapter.name
|
||||||
it[chapter_number] = fetchedChapter.chapter_number
|
it[date_upload] = fetchedChapter.date_upload
|
||||||
it[scanlator] = fetchedChapter.scanlator
|
it[chapter_number] = fetchedChapter.chapter_number
|
||||||
|
it[scanlator] = fetchedChapter.scanlator
|
||||||
|
|
||||||
it[chapterIndex] = index + 1
|
it[chapterIndex] = index + 1
|
||||||
it[manga] = mangaId
|
it[manga] = mangaId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
||||||
|
it[name] = fetchedChapter.name
|
||||||
|
it[date_upload] = fetchedChapter.date_upload
|
||||||
|
it[chapter_number] = fetchedChapter.chapter_number
|
||||||
|
it[scanlator] = fetchedChapter.scanlator
|
||||||
|
|
||||||
|
it[chapterIndex] = index + 1
|
||||||
|
it[manga] = mangaId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear any orphaned chapters
|
// clear any orphaned chapters that are in the db but not in `chapterList`
|
||||||
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
|
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
||||||
if (dbChapterCount > chapterCount) { // we got some clean up due
|
if (dbChapterCount > chapterCount) { // we got some clean up due
|
||||||
// TODO
|
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } }
|
||||||
|
|
||||||
|
dbChapterList.forEach {
|
||||||
|
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
|
||||||
|
chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url]
|
||||||
|
) {
|
||||||
|
transaction {
|
||||||
|
PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] }
|
||||||
|
ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return@transaction chapterList.mapIndexed { index, it ->
|
val dbChapterMap = transaction {
|
||||||
|
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||||
|
.associateBy({ it[ChapterTable.url] }, { it })
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapterList.mapIndexed { index, it ->
|
||||||
|
|
||||||
|
val dbChapter = dbChapterMap.getValue(it.url)
|
||||||
|
|
||||||
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,
|
||||||
it.chapter_number,
|
it.chapter_number,
|
||||||
it.scanlator,
|
it.scanlator,
|
||||||
mangaId,
|
mangaId,
|
||||||
|
|
||||||
|
dbChapter[ChapterTable.isRead],
|
||||||
|
dbChapter[ChapterTable.isBookmarked],
|
||||||
|
dbChapter[ChapterTable.lastPageRead],
|
||||||
|
|
||||||
chapterCount - index,
|
chapterCount - index,
|
||||||
chapterCount
|
chapterList.size
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
}.firstOrNull()!!
|
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||||
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
}.first()
|
||||||
|
}
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
val pageList = source.fetchPageList(
|
val pageList = source.fetchPageList(
|
||||||
@@ -96,38 +144,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.select { ChapterTable.manga eq mangaId }.count() }
|
||||||
|
|
||||||
val chapter = ChapterDataClass(
|
// update page list for this chapter
|
||||||
chapterId,
|
transaction {
|
||||||
chapterEntry[ChapterTable.url],
|
pageList.forEach { page ->
|
||||||
chapterEntry[ChapterTable.name],
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||||
chapterEntry[ChapterTable.date_upload],
|
if (pageEntry == null) {
|
||||||
chapterEntry[ChapterTable.chapter_number],
|
|
||||||
chapterEntry[ChapterTable.scanlator],
|
|
||||||
mangaId,
|
|
||||||
chapterEntry[ChapterTable.chapterIndex],
|
|
||||||
chapterCount.toInt(),
|
|
||||||
|
|
||||||
pageList.count()
|
|
||||||
)
|
|
||||||
|
|
||||||
pageList.forEach { page ->
|
|
||||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
|
||||||
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 +169,44 @@ 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.isRead],
|
||||||
|
chapterEntry[ChapterTable.isBookmarked],
|
||||||
|
chapterEntry[ChapterTable.lastPageRead],
|
||||||
|
|
||||||
|
chapterEntry[ChapterTable.chapterIndex],
|
||||||
|
chapterCount.toInt(),
|
||||||
|
pageList.count()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
|
||||||
|
transaction {
|
||||||
|
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
|
||||||
|
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) { update ->
|
||||||
|
isRead?.also {
|
||||||
|
update[ChapterTable.isRead] = it
|
||||||
|
}
|
||||||
|
isBookmarked?.also {
|
||||||
|
update[ChapterTable.isBookmarked] = it
|
||||||
|
}
|
||||||
|
lastPageRead?.also {
|
||||||
|
update[ChapterTable.lastPageRead] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markPrevRead?.let {
|
||||||
|
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex less chapterIndex) }) {
|
||||||
|
it[ChapterTable.isRead] = markPrevRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,179 +39,213 @@ 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()
|
suspend fun installExtension(pkgName: String): Int {
|
||||||
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
|
logger.debug("Installing $pkgName")
|
||||||
val handler = BaksmaliBaseDexExceptionHandler()
|
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
|
||||||
Dex2jar
|
|
||||||
.from(reader)
|
return installAPK {
|
||||||
.withExceptionHandler(handler)
|
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
|
||||||
.reUseReg(false)
|
val apkName = Uri.parse(apkURL).lastPathSegment!!
|
||||||
.topoLogicalSort()
|
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
||||||
.skipDebug(true)
|
// download apk file
|
||||||
.optimizeSynchronized(false)
|
downloadAPKFile(apkURL, apkSavePath)
|
||||||
.printIR(false)
|
|
||||||
.noCode(false)
|
apkSavePath
|
||||||
.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>())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
|
||||||
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
|
||||||
|
|
||||||
// check if we don't have the dex file already downloaded
|
// check if we don't have the extension already installed
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
// if it's installed and we want to update, it first has to be uninstalled
|
||||||
if (!File(jarPath).exists()) {
|
val isInstalled = transaction {
|
||||||
runBlocking {
|
ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()
|
||||||
val api = ExtensionGithubApi()
|
}?.get(ExtensionTable.isInstalled) ?: false
|
||||||
val apkToDownload = api.getApkUrl(extensionRecord)
|
|
||||||
|
|
||||||
val apkFilePath = "$dirPathWithoutType.apk"
|
if (!isInstalled) {
|
||||||
|
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||||
|
|
||||||
|
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionId = ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.first()[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
|
||||||
return 201 // we downloaded successfully
|
} else {
|
||||||
} else {
|
return 302 // extension was already installed
|
||||||
return 302
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val networkHelper: NetworkHelper by injectLazy()
|
|
||||||
|
|
||||||
private fun downloadAPKFile(url: String, apkPath: String) {
|
|
||||||
val request = Request.Builder().url(url).build()
|
|
||||||
val response = networkHelper.client.newCall(request).execute()
|
|
||||||
|
|
||||||
val downloadedFile = File(apkPath)
|
|
||||||
val sink = downloadedFile.sink().buffer()
|
|
||||||
sink.writeAll(response.body!!.source())
|
|
||||||
sink.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeExtension(apkName: String) {
|
|
||||||
logger.debug("Uninstalling $apkName")
|
|
||||||
|
|
||||||
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
|
|
||||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
|
||||||
transaction {
|
|
||||||
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
|
||||||
|
|
||||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
|
||||||
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
|
|
||||||
it[ExtensionTable.installed] = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (File(jarPath).exists()) {
|
private val network: NetworkHelper by injectLazy()
|
||||||
File(jarPath).delete()
|
|
||||||
|
private suspend fun downloadAPKFile(url: String, savePath: String) {
|
||||||
|
val request = Request.Builder().url(url).build()
|
||||||
|
val response = network.client.newCall(request).await()
|
||||||
|
|
||||||
|
val downloadedFile = File(savePath)
|
||||||
|
downloadedFile.sink().buffer().use { sink ->
|
||||||
|
response.body!!.source().use { source ->
|
||||||
|
sink.writeAll(source)
|
||||||
|
sink.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallExtension(pkgName: String) {
|
||||||
|
logger.debug("Uninstalling $pkgName")
|
||||||
|
|
||||||
|
val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.first() }
|
||||||
|
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
|
||||||
|
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
|
transaction {
|
||||||
|
val extensionId = extensionRecord[ExtensionTable.id].value
|
||||||
|
|
||||||
|
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||||
|
if (extensionRecord[ExtensionTable.isObsolete])
|
||||||
|
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq pkgName }
|
||||||
|
else
|
||||||
|
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||||
|
it[isInstalled] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File(jarPath).exists()) {
|
||||||
|
File(jarPath).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateExtension(pkgName: String): Int {
|
||||||
|
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
|
||||||
|
uninstallExtension(pkgName)
|
||||||
|
transaction {
|
||||||
|
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||||
|
it[name] = targetExtension.name
|
||||||
|
it[versionName] = targetExtension.versionName
|
||||||
|
it[versionCode] = targetExtension.versionCode
|
||||||
|
it[lang] = targetExtension.lang
|
||||||
|
it[isNsfw] = targetExtension.isNsfw
|
||||||
|
it[apkName] = targetExtension.apkName
|
||||||
|
it[iconUrl] = targetExtension.iconUrl
|
||||||
|
it[hasUpdate] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return installExtension(pkgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||||
|
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl]
|
||||||
|
|
||||||
|
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||||
|
|
||||||
|
return getCachedImageResponse(saveDir, apkName) {
|
||||||
|
network.client.newCall(
|
||||||
|
GET(iconUrl)
|
||||||
|
).await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtensionIconUrl(apkName: String): String {
|
||||||
|
return "/api/v1/extension/icon/$apkName"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val network: NetworkHelper by injectLazy()
|
|
||||||
|
|
||||||
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
|
||||||
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
|
||||||
|
|
||||||
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, apkName) {
|
|
||||||
network.client.newCall(
|
|
||||||
GET(iconUrl)
|
|
||||||
).execute()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getExtensionIconUrl(apkName: String): String {
|
|
||||||
return "/api/v1/extension/icon/$apkName"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,57 +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
|
|
||||||
|
suspend fun getExtensionList(): List<ExtensionDataClass> {
|
||||||
|
// update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty
|
||||||
|
if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) {
|
||||||
|
logger.debug("Getting extensions list from the internet")
|
||||||
|
lastUpdateCheck = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val foundExtensions = ExtensionGithubApi.findExtensions()
|
||||||
|
updateExtensionDatabase(foundExtensions)
|
||||||
|
} else {
|
||||||
|
logger.debug("used cached extension list")
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensionTableAsDataClass()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
fun extensionTableAsDataClass() = transaction {
|
||||||
// update if 60 seconds has passed or requested offline and database is empty
|
ExtensionTable.selectAll().map {
|
||||||
if (Data.lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) {
|
ExtensionDataClass(
|
||||||
logger.debug("Getting extensions list from the internet")
|
it[ExtensionTable.apkName],
|
||||||
Data.lastExtensionCheck = System.currentTimeMillis()
|
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
||||||
var foundExtensions: List<Extension.Available>
|
it[ExtensionTable.name],
|
||||||
runBlocking {
|
it[ExtensionTable.pkgName],
|
||||||
val api = ExtensionGithubApi()
|
it[ExtensionTable.versionName],
|
||||||
foundExtensions = api.findExtensions()
|
it[ExtensionTable.versionCode],
|
||||||
transaction {
|
it[ExtensionTable.lang],
|
||||||
foundExtensions.forEach { foundExtension ->
|
it[ExtensionTable.isNsfw],
|
||||||
val extensionRecord = ExtensionTable.select { ExtensionTable.name eq foundExtension.name }.firstOrNull()
|
it[ExtensionTable.isInstalled],
|
||||||
if (extensionRecord != null) {
|
it[ExtensionTable.hasUpdate],
|
||||||
// update the record
|
it[ExtensionTable.isObsolete],
|
||||||
ExtensionTable.update({ ExtensionTable.name eq foundExtension.name }) {
|
)
|
||||||
it[name] = foundExtension.name
|
}
|
||||||
it[pkgName] = foundExtension.pkgName
|
}
|
||||||
it[versionName] = foundExtension.versionName
|
|
||||||
it[versionCode] = foundExtension.versionCode
|
private fun updateExtensionDatabase(foundExtensions: List<Extension.Available>) {
|
||||||
it[lang] = foundExtension.lang
|
transaction {
|
||||||
it[isNsfw] = foundExtension.isNsfw
|
foundExtensions.forEach { foundExtension ->
|
||||||
it[apkName] = foundExtension.apkName
|
val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
|
||||||
it[iconUrl] = foundExtension.iconUrl
|
if (extensionRecord != null) {
|
||||||
|
if (extensionRecord[ExtensionTable.isInstalled]) {
|
||||||
|
when {
|
||||||
|
foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode] -> {
|
||||||
|
// there is an update
|
||||||
|
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
|
||||||
|
it[hasUpdate] = true
|
||||||
|
}
|
||||||
|
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
|
||||||
|
}
|
||||||
|
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> {
|
||||||
|
// some how the user installed an invalid version
|
||||||
|
ExtensionTable.update({ ExtensionTable.pkgName eq foundExtension.pkgName }) {
|
||||||
|
it[isObsolete] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// insert new record
|
// extension is not installed so we can overwrite the data without a care
|
||||||
ExtensionTable.insert {
|
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
|
||||||
@@ -68,27 +96,37 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
|||||||
it[iconUrl] = foundExtension.iconUrl
|
it[iconUrl] = foundExtension.iconUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// insert new record
|
||||||
|
ExtensionTable.insert {
|
||||||
|
it[name] = foundExtension.name
|
||||||
|
it[pkgName] = foundExtension.pkgName
|
||||||
|
it[versionName] = foundExtension.versionName
|
||||||
|
it[versionCode] = foundExtension.versionCode
|
||||||
|
it[lang] = foundExtension.lang
|
||||||
|
it[isNsfw] = foundExtension.isNsfw
|
||||||
|
it[apkName] = foundExtension.apkName
|
||||||
|
it[iconUrl] = foundExtension.iconUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deal with obsolete extensions
|
||||||
|
ExtensionTable.selectAll().forEach { extensionRecord ->
|
||||||
|
val foundExtension = foundExtensions.find { it.pkgName == extensionRecord[ExtensionTable.pkgName] }
|
||||||
|
if (foundExtension == null) {
|
||||||
|
// not in the repo, so this extensions is obsolete
|
||||||
|
if (extensionRecord[ExtensionTable.isInstalled]) {
|
||||||
|
// is installed so we should mark it as obsolete
|
||||||
|
ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }) {
|
||||||
|
it[isObsolete] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// is not installed so we can remove the record without a care
|
||||||
|
ExtensionTable.deleteWhere { ExtensionTable.pkgName eq extensionRecord[ExtensionTable.pkgName] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logger.debug("used cached extension list")
|
|
||||||
}
|
|
||||||
|
|
||||||
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,44 +7,50 @@ 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 {
|
||||||
val manga = getManga(mangaId)
|
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
|
||||||
if (!manga.inLibrary) {
|
// ..implement that shit at some time...
|
||||||
transaction {
|
// ..also Consider to rename it to `isDefault`
|
||||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
suspend fun addMangaToLibrary(mangaId: Int) {
|
||||||
it[inLibrary] = true
|
val manga = getManga(mangaId)
|
||||||
|
if (!manga.inLibrary) {
|
||||||
|
transaction {
|
||||||
|
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||||
|
it[inLibrary] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removeMangaFromLibrary(mangaId: Int) {
|
||||||
|
val manga = getManga(mangaId)
|
||||||
|
if (manga.inLibrary) {
|
||||||
|
transaction {
|
||||||
|
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||||
|
it[inLibrary] = false
|
||||||
|
it[defaultCategory] = true
|
||||||
|
}
|
||||||
|
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLibraryMangas(): List<MangaDataClass> {
|
||||||
|
return transaction {
|
||||||
|
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
||||||
|
MangaTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMangaFromLibrary(mangaId: Int) {
|
|
||||||
val manga = getManga(mangaId)
|
|
||||||
if (manga.inLibrary) {
|
|
||||||
transaction {
|
|
||||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
|
||||||
it[inLibrary] = false
|
|
||||||
it[defaultCategory] = true
|
|
||||||
}
|
|
||||||
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLibraryMangas(): List<MangaDataClass> {
|
|
||||||
return transaction {
|
|
||||||
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
|
||||||
MangaTable.toDataClass(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,100 +9,131 @@ 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.clearCachedImage
|
||||||
import ir.armor.tachidesk.server.applicationDirs
|
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
||||||
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import ir.armor.tachidesk.impl.util.await
|
||||||
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
|
import ir.armor.tachidesk.model.database.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 {
|
||||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
private fun truncate(text: String?, maxLength: Int): String? {
|
||||||
|
return if (text?.length ?: 0 > maxLength)
|
||||||
|
text?.take(maxLength - 3) + "..."
|
||||||
|
else
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
return if (mangaEntry[MangaTable.initialized]) {
|
suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
|
||||||
MangaDataClass(
|
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
mangaId,
|
|
||||||
mangaEntry[MangaTable.sourceReference].toString(),
|
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
|
||||||
mangaEntry[MangaTable.title],
|
MangaDataClass(
|
||||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
mangaId,
|
||||||
|
mangaEntry[MangaTable.sourceReference].toString(),
|
||||||
|
|
||||||
true,
|
mangaEntry[MangaTable.url],
|
||||||
|
mangaEntry[MangaTable.title],
|
||||||
|
proxyThumbnailUrl(mangaId),
|
||||||
|
|
||||||
mangaEntry[MangaTable.artist],
|
true,
|
||||||
mangaEntry[MangaTable.author],
|
|
||||||
mangaEntry[MangaTable.description],
|
mangaEntry[MangaTable.artist],
|
||||||
mangaEntry[MangaTable.genre],
|
mangaEntry[MangaTable.author],
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
mangaEntry[MangaTable.description],
|
||||||
mangaEntry[MangaTable.inLibrary],
|
mangaEntry[MangaTable.genre],
|
||||||
getSource(mangaEntry[MangaTable.sourceReference])
|
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||||
)
|
mangaEntry[MangaTable.inLibrary],
|
||||||
} else { // initialize manga
|
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
false
|
||||||
val fetchedManga = source.fetchMangaDetails(
|
)
|
||||||
SManga.create().apply {
|
} else { // initialize manga
|
||||||
url = mangaEntry[MangaTable.url]
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
title = mangaEntry[MangaTable.title]
|
val fetchedManga = source.fetchMangaDetails(
|
||||||
|
SManga.create().apply {
|
||||||
|
url = mangaEntry[MangaTable.url]
|
||||||
|
title = mangaEntry[MangaTable.title]
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
|
||||||
|
it[MangaTable.initialized] = true
|
||||||
|
|
||||||
|
it[MangaTable.artist] = fetchedManga.artist
|
||||||
|
it[MangaTable.author] = fetchedManga.author
|
||||||
|
it[MangaTable.description] = truncate(fetchedManga.description, 4096)
|
||||||
|
it[MangaTable.genre] = fetchedManga.genre
|
||||||
|
it[MangaTable.status] = fetchedManga.status
|
||||||
|
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
|
||||||
|
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||||
|
}
|
||||||
}
|
}
|
||||||
).toBlocking().first()
|
|
||||||
|
|
||||||
transaction {
|
clearMangaThumbnail(mangaId)
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
|
||||||
|
|
||||||
it[MangaTable.initialized] = true
|
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
it[MangaTable.artist] = fetchedManga.artist
|
MangaDataClass(
|
||||||
it[MangaTable.author] = fetchedManga.author
|
mangaId,
|
||||||
it[MangaTable.description] = fetchedManga.description
|
mangaEntry[MangaTable.sourceReference].toString(),
|
||||||
it[MangaTable.genre] = fetchedManga.genre
|
|
||||||
it[MangaTable.status] = fetchedManga.status
|
mangaEntry[MangaTable.url],
|
||||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
mangaEntry[MangaTable.title],
|
||||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
proxyThumbnailUrl(mangaId),
|
||||||
}
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
fetchedManga.artist,
|
||||||
|
fetchedManga.author,
|
||||||
|
fetchedManga.description,
|
||||||
|
fetchedManga.genre,
|
||||||
|
MangaStatus.valueOf(fetchedManga.status).name,
|
||||||
|
false,
|
||||||
|
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||||
|
true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
|
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||||
|
val saveDir = applicationDirs.thumbnailsRoot
|
||||||
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
MangaDataClass(
|
return getCachedImageResponse(saveDir, fileName) {
|
||||||
mangaId,
|
getManga(mangaId) // make sure is initialized
|
||||||
mangaEntry[MangaTable.sourceReference].toString(),
|
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
mangaEntry[MangaTable.title],
|
|
||||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
|
|
||||||
|
|
||||||
true,
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
|
val source = getHttpSource(sourceId)
|
||||||
|
|
||||||
fetchedManga.artist,
|
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]!!
|
||||||
fetchedManga.author,
|
|
||||||
fetchedManga.description,
|
source.client.newCall(
|
||||||
fetchedManga.genre,
|
GET(thumbnailUrl, source.headers)
|
||||||
MangaStatus.valueOf(fetchedManga.status).name,
|
).await()
|
||||||
false,
|
}
|
||||||
getSource(mangaEntry[MangaTable.sourceReference])
|
}
|
||||||
)
|
|
||||||
}
|
private fun clearMangaThumbnail(mangaId: Int) {
|
||||||
}
|
val saveDir = applicationDirs.thumbnailsRoot
|
||||||
|
val fileName = mangaId.toString()
|
||||||
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
clearCachedImage(saveDir, fileName)
|
||||||
val saveDir = applicationDirs.thumbnailsRoot
|
|
||||||
val fileName = mangaId.toString()
|
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, fileName) {
|
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
|
||||||
val source = getHttpSource(sourceId)
|
|
||||||
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
|
||||||
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
|
||||||
}
|
|
||||||
|
|
||||||
source.client.newCall(
|
|
||||||
GET(thumbnailUrl, source.headers)
|
|
||||||
).execute()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,91 +8,95 @@ 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 {
|
||||||
return "/api/v1/manga/$mangaId/thumbnail"
|
fun proxyThumbnailUrl(mangaId: Int): String {
|
||||||
}
|
return "/api/v1/manga/$mangaId/thumbnail"
|
||||||
|
|
||||||
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
|
||||||
val source = getHttpSource(sourceId.toLong())
|
|
||||||
val mangasPage = if (popular) {
|
|
||||||
source.fetchPopularManga(pageNum).toBlocking().first()
|
|
||||||
} else {
|
|
||||||
if (source.supportsLatest)
|
|
||||||
source.fetchLatestUpdates(pageNum).toBlocking().first()
|
|
||||||
else
|
|
||||||
throw Exception("Source $source doesn't support latest")
|
|
||||||
}
|
}
|
||||||
return mangasPage.processEntries(sourceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
||||||
val mangasPage = this
|
val source = getHttpSource(sourceId)
|
||||||
val mangaList = transaction {
|
val mangasPage = if (popular) {
|
||||||
return@transaction mangasPage.mangas.map { manga ->
|
source.fetchPopularManga(pageNum).awaitSingle()
|
||||||
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
} else {
|
||||||
if (mangaEntry == null) { // create manga entry
|
if (source.supportsLatest)
|
||||||
val mangaId = MangaTable.insertAndGetId {
|
source.fetchLatestUpdates(pageNum).awaitSingle()
|
||||||
it[url] = manga.url
|
else
|
||||||
it[title] = manga.title
|
throw Exception("Source $source doesn't support latest")
|
||||||
|
}
|
||||||
|
return mangasPage.processEntries(sourceId)
|
||||||
|
}
|
||||||
|
|
||||||
it[artist] = manga.artist
|
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
||||||
it[author] = manga.author
|
val mangasPage = this
|
||||||
it[description] = manga.description
|
val mangaList = transaction {
|
||||||
it[genre] = manga.genre
|
return@transaction mangasPage.mangas.map { manga ->
|
||||||
it[status] = manga.status
|
val mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
||||||
it[thumbnail_url] = manga.thumbnail_url
|
if (mangaEntry == null) { // create manga entry
|
||||||
|
val mangaId = MangaTable.insertAndGetId {
|
||||||
|
it[url] = manga.url
|
||||||
|
it[title] = manga.title
|
||||||
|
|
||||||
it[sourceReference] = sourceId
|
it[artist] = manga.artist
|
||||||
}.value
|
it[author] = manga.author
|
||||||
|
it[description] = manga.description
|
||||||
|
it[genre] = manga.genre
|
||||||
|
it[status] = manga.status
|
||||||
|
it[thumbnail_url] = manga.thumbnail_url
|
||||||
|
|
||||||
MangaDataClass(
|
it[sourceReference] = sourceId
|
||||||
mangaId,
|
}.value
|
||||||
sourceId.toString(),
|
|
||||||
|
|
||||||
manga.url,
|
MangaDataClass(
|
||||||
manga.title,
|
mangaId,
|
||||||
proxyThumbnailUrl(mangaId),
|
sourceId.toString(),
|
||||||
|
|
||||||
manga.initialized,
|
manga.url,
|
||||||
|
manga.title,
|
||||||
|
proxyThumbnailUrl(mangaId),
|
||||||
|
|
||||||
manga.artist,
|
manga.initialized,
|
||||||
manga.author,
|
|
||||||
manga.description,
|
|
||||||
manga.genre,
|
|
||||||
MangaStatus.valueOf(manga.status).name
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val mangaId = mangaEntry[MangaTable.id].value
|
|
||||||
MangaDataClass(
|
|
||||||
mangaId,
|
|
||||||
sourceId.toString(),
|
|
||||||
|
|
||||||
manga.url,
|
manga.artist,
|
||||||
manga.title,
|
manga.author,
|
||||||
proxyThumbnailUrl(mangaId),
|
manga.description,
|
||||||
|
manga.genre,
|
||||||
|
MangaStatus.valueOf(manga.status).name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val mangaId = mangaEntry[MangaTable.id].value
|
||||||
|
MangaDataClass(
|
||||||
|
mangaId,
|
||||||
|
sourceId.toString(),
|
||||||
|
|
||||||
true,
|
manga.url,
|
||||||
|
manga.title,
|
||||||
|
proxyThumbnailUrl(mangaId),
|
||||||
|
|
||||||
mangaEntry[MangaTable.artist],
|
true,
|
||||||
mangaEntry[MangaTable.author],
|
|
||||||
mangaEntry[MangaTable.description],
|
mangaEntry[MangaTable.artist],
|
||||||
mangaEntry[MangaTable.genre],
|
mangaEntry[MangaTable.author],
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
mangaEntry[MangaTable.description],
|
||||||
mangaEntry[MangaTable.inLibrary]
|
mangaEntry[MangaTable.genre],
|
||||||
)
|
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||||
|
mangaEntry[MangaTable.inLibrary]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return PagedMangaListDataClass(
|
||||||
|
mangaList,
|
||||||
|
mangasPage.hasNextPage
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return PagedMangaListDataClass(
|
|
||||||
mangaList,
|
|
||||||
mangasPage.hasNextPage
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,77 +9,92 @@ 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 {
|
||||||
if (page.imageUrl == null) {
|
/**
|
||||||
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!!
|
* A page might have a imageUrl ready from the get go, or we might need to
|
||||||
|
* go an extra step and call fetchImageUrl to get it.
|
||||||
|
*/
|
||||||
|
suspend fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||||
|
if (page.imageUrl == null) {
|
||||||
|
page.imageUrl = source.fetchImageUrl(page).awaitSingle()
|
||||||
|
}
|
||||||
|
return page.imageUrl!!
|
||||||
}
|
}
|
||||||
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 }.first() }
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
val chapterEntry = transaction {
|
val chapterEntry = transaction {
|
||||||
ChapterTable.select {
|
ChapterTable.select {
|
||||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||||
}.firstOrNull()!!
|
}.first()
|
||||||
}
|
}
|
||||||
val chapterId = chapterEntry[ChapterTable.id].value
|
val chapterId = chapterEntry[ChapterTable.id].value
|
||||||
|
|
||||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.first() }
|
||||||
|
|
||||||
val tachiPage = Page(
|
val tachiPage = Page(
|
||||||
pageEntry[PageTable.index],
|
pageEntry[PageTable.index],
|
||||||
pageEntry[PageTable.url],
|
pageEntry[PageTable.url],
|
||||||
pageEntry[PageTable.imageUrl]
|
pageEntry[PageTable.imageUrl]
|
||||||
)
|
)
|
||||||
|
|
||||||
if (pageEntry[PageTable.imageUrl] == null) {
|
if (pageEntry[PageTable.imageUrl] == null) {
|
||||||
transaction {
|
val trueImageUrl = getTrueImageUrl(tachiPage, source)
|
||||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
|
transaction {
|
||||||
it[imageUrl] = getTrueImageUrl(tachiPage, source)
|
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
|
||||||
|
it[imageUrl] = trueImageUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val saveDir = getChapterDir(mangaId, chapterId)
|
||||||
|
File(saveDir).mkdirs()
|
||||||
|
val fileName = index.toString()
|
||||||
|
|
||||||
|
return getCachedImageResponse(saveDir, fileName) {
|
||||||
|
source.fetchImage(tachiPage).awaitSingle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val saveDir = getChapterDir(mangaId, chapterId)
|
// TODO: rewrite this to match tachiyomi
|
||||||
File(saveDir).mkdirs()
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
val fileName = index.toString()
|
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
|
val source = getHttpSource(sourceId)
|
||||||
|
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.first() }
|
||||||
|
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, fileName) {
|
val chapterDir = when {
|
||||||
source.fetchImage(tachiPage).toBlocking().first()
|
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||||
|
else -> chapterEntry[ChapterTable.name]
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaTitle = mangaEntry[MangaTable.title]
|
||||||
|
val sourceName = source.toString()
|
||||||
|
|
||||||
|
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
||||||
|
// make sure dirs exist
|
||||||
|
File(mangaDir).mkdirs()
|
||||||
|
return mangaDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
|
||||||
val source = getHttpSource(sourceId)
|
|
||||||
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
|
||||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
|
||||||
|
|
||||||
val chapterDir = when {
|
|
||||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
|
||||||
else -> chapterEntry[ChapterTable.name]
|
|
||||||
}
|
|
||||||
|
|
||||||
val mangaTitle = mangaEntry[MangaTable.title]
|
|
||||||
val sourceName = source.toString()
|
|
||||||
|
|
||||||
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
|
||||||
// make sure dirs exist
|
|
||||||
File(mangaDir).mkdirs()
|
|
||||||
return mangaDir
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
val source = getHttpSource(sourceId)
|
// TODO
|
||||||
// source.getFilterList().toItems()
|
fun sourceFilters(sourceId: Long) {
|
||||||
}
|
val source = getHttpSource(sourceId)
|
||||||
|
// 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) {
|
||||||
|
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||||
|
val dbCategories = getCategoryList()
|
||||||
|
|
||||||
|
// Iterate over them and create missing categories
|
||||||
|
backupCategories.forEach { category ->
|
||||||
|
if (dbCategories.none { it.name == category.name }) {
|
||||||
|
createCategory(category.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||||
|
val manga = parser.fromJson<MangaImpl>(
|
||||||
|
mangaJson.get(
|
||||||
|
Backup.MANGA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val chapters = parser.fromJson<List<ChapterImpl>>(
|
||||||
|
mangaJson.get(Backup.CHAPTERS)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val categories = parser.fromJson<List<String>>(
|
||||||
|
mangaJson.get(Backup.CATEGORIES)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val history = parser.fromJson<List<DHistory>>(
|
||||||
|
mangaJson.get(Backup.HISTORY)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val tracks = parser.fromJson<List<TrackImpl>>(
|
||||||
|
mangaJson.get(Backup.TRACK)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
val source = try {
|
||||||
|
getHttpSource(manga.source)
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
|
||||||
|
logger.debug("Restoring Manga: ${manga.title} from $sourceName")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (source != null) {
|
||||||
|
restoreMangaData(manga, source, chapters, categories, history, tracks)
|
||||||
|
} else {
|
||||||
|
errors.add(Date() to "${manga.title} [$sourceName]: Source not found: $sourceName (${manga.source})")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param manga manga data from json
|
||||||
|
* @param source source to get manga data from
|
||||||
|
* @param chapters chapters data from json
|
||||||
|
* @param categories categories data from json
|
||||||
|
* @param history history data from json
|
||||||
|
* @param tracks tracking data from json
|
||||||
|
*/
|
||||||
|
private suspend fun restoreMangaData(
|
||||||
|
manga: Manga,
|
||||||
|
source: Source,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
val fetchedManga = fetchManga(source, manga)
|
||||||
|
|
||||||
|
updateChapters(source, fetchedManga, chapters)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// backupManager.restoreCategoriesForManga(manga, categories)
|
||||||
|
|
||||||
|
// backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// backupManager.restoreTrackForManga(manga, tracks)
|
||||||
|
|
||||||
|
// updateTracking(fetchedManga, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches manga information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return Updated manga.
|
||||||
|
*/
|
||||||
|
private suspend fun fetchManga(source: Source, manga: Manga): SManga {
|
||||||
|
// make sure we have the manga record in library
|
||||||
|
transaction {
|
||||||
|
if (MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }.firstOrNull() == null) {
|
||||||
|
MangaTable.insert {
|
||||||
|
it[url] = manga.url
|
||||||
|
it[title] = manga.title
|
||||||
|
|
||||||
|
it[sourceReference] = manga.source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
|
||||||
|
it[MangaTable.inLibrary] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update manga details
|
||||||
|
val fetchedManga = source.fetchMangaDetails(manga).awaitSingle()
|
||||||
|
transaction {
|
||||||
|
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
|
||||||
|
|
||||||
|
it[artist] = fetchedManga.artist
|
||||||
|
it[author] = fetchedManga.author
|
||||||
|
it[description] = fetchedManga.description
|
||||||
|
it[genre] = fetchedManga.genre
|
||||||
|
it[status] = fetchedManga.status
|
||||||
|
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
|
||||||
|
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchedManga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateChapters(source: Source, fetchedManga: SManga, chapters: List<Chapter>) {
|
||||||
|
// TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,71 @@
|
|||||||
|
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.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 FileInputStream(path).buffered()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
||||||
|
val target = "$fileName."
|
||||||
|
File(directoryPath).listFiles().orEmpty().forEach { file ->
|
||||||
|
if (file.name.startsWith(target))
|
||||||
|
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 pathToInputStream(fullPath) to contentType
|
||||||
|
} else {
|
||||||
|
throw Exception("request error! ${response.code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearCachedImage(saveDir: String, fileName: String) {
|
||||||
|
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
||||||
|
cachedFile?.also {
|
||||||
|
File(it).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionId = sourceRecord[SourceTable.extension]
|
||||||
|
val extensionRecord = transaction {
|
||||||
|
ExtensionTable.select { ExtensionTable.id eq extensionId }.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
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,51 @@
|
|||||||
|
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 okhttp3.internal.closeQuietly
|
||||||
|
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) {
|
||||||
|
response.body?.closeQuietly()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,146 @@
|
|||||||
|
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
|
||||||
|
Please report this file to one of following link if possible (any one).
|
||||||
|
https://sourceforge.net/p/dex2jar/tickets/
|
||||||
|
https://bitbucket.org/pxb1988/dex2jar/issues
|
||||||
|
https://github.com/pxb1988/dex2jar/issues
|
||||||
|
dex2jar@googlegroups.com
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
.orEmpty()
|
||||||
|
.asSequence()
|
||||||
|
.filter {
|
||||||
|
it.nodeType == Node.ELEMENT_NODE
|
||||||
|
}.map {
|
||||||
|
it as Element
|
||||||
|
}.filter {
|
||||||
|
it.tagName == "meta-data"
|
||||||
|
}.forEach {
|
||||||
|
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 DBManager {
|
||||||
|
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 = DBManager.db
|
||||||
|
db.useNestedTransactions = true
|
||||||
|
|
||||||
|
val migrations = loadMigrationsFrom("ir.armor.tachidesk.model.database.migration")
|
||||||
|
runMigrations(migrations)
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import 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
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0001_Initial : Migration() {
|
||||||
|
private class ExtensionTable : IntIdTable() {
|
||||||
|
init {
|
||||||
|
varchar("apk_name", 1024)
|
||||||
|
// default is the local source icon from tachiyomi
|
||||||
|
varchar("icon_url", 2048)
|
||||||
|
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||||
|
varchar("name", 128)
|
||||||
|
varchar("pkg_name", 128)
|
||||||
|
varchar("version_name", 16)
|
||||||
|
integer("version_code")
|
||||||
|
varchar("lang", 10)
|
||||||
|
bool("is_nsfw")
|
||||||
|
|
||||||
|
bool("is_installed").default(false)
|
||||||
|
bool("has_update").default(false)
|
||||||
|
bool("is_obsolete").default(false)
|
||||||
|
|
||||||
|
varchar("class_name", 1024).default("") // fully qualified name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SourceTable(extensionTable: ExtensionTable) : IdTable<Long>() {
|
||||||
|
override val id = long("id").entityId()
|
||||||
|
init {
|
||||||
|
varchar("name", 128)
|
||||||
|
varchar("lang", 10)
|
||||||
|
reference("extension", extensionTable)
|
||||||
|
bool("part_of_factory_source").default(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MangaTable : IntIdTable() {
|
||||||
|
init {
|
||||||
|
varchar("url", 2048)
|
||||||
|
varchar("title", 512)
|
||||||
|
bool("initialized").default(false)
|
||||||
|
|
||||||
|
varchar("artist", 64).nullable()
|
||||||
|
varchar("author", 64).nullable()
|
||||||
|
varchar("description", 4096).nullable()
|
||||||
|
varchar("genre", 1024).nullable()
|
||||||
|
|
||||||
|
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
|
||||||
|
integer("status").default(SManga.UNKNOWN)
|
||||||
|
varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
|
bool("in_library").default(false)
|
||||||
|
bool("default_category").default(true)
|
||||||
|
|
||||||
|
// source is used by some ancestor of IntIdTable
|
||||||
|
long("source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ChapterTable(mangaTable: MangaTable) : IntIdTable() {
|
||||||
|
init {
|
||||||
|
varchar("url", 2048)
|
||||||
|
varchar("name", 512)
|
||||||
|
long("date_upload").default(0)
|
||||||
|
float("chapter_number").default(-1f)
|
||||||
|
varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
|
bool("read").default(false)
|
||||||
|
bool("bookmark").default(false)
|
||||||
|
integer("last_page_read").default(0)
|
||||||
|
|
||||||
|
integer("number_in_list")
|
||||||
|
reference("manga", mangaTable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PageTable(chapterTable: ChapterTable) : IntIdTable() {
|
||||||
|
init {
|
||||||
|
integer("index")
|
||||||
|
varchar("url", 2048)
|
||||||
|
varchar("imageUrl", 2048).nullable()
|
||||||
|
reference("chapter", chapterTable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CategoryTable : IntIdTable() {
|
||||||
|
init {
|
||||||
|
varchar("name", 64)
|
||||||
|
bool("is_landing").default(false)
|
||||||
|
integer("order").default(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CategoryMangaTable : IntIdTable() {
|
||||||
|
init {
|
||||||
|
reference("category", ir.armor.tachidesk.model.database.table.CategoryTable)
|
||||||
|
reference("manga", ir.armor.tachidesk.model.database.table.MangaTable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** initial migration, create all tables */
|
||||||
|
override fun run() {
|
||||||
|
transaction {
|
||||||
|
val extensionTable = ExtensionTable()
|
||||||
|
val sourceTable = SourceTable(extensionTable)
|
||||||
|
val mangaTable = MangaTable()
|
||||||
|
val chapterTable = ChapterTable(mangaTable)
|
||||||
|
val pageTable = PageTable(chapterTable)
|
||||||
|
val categoryTable = CategoryTable()
|
||||||
|
val categoryMangaTable = CategoryMangaTable()
|
||||||
|
SchemaUtils.create(
|
||||||
|
extensionTable,
|
||||||
|
sourceTable,
|
||||||
|
mangaTable,
|
||||||
|
chapterTable,
|
||||||
|
pageTable,
|
||||||
|
categoryTable,
|
||||||
|
categoryMangaTable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.model.database.migration.lib.Migration
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0002_ChapterTableIndexRename : Migration() {
|
||||||
|
/** this migration renamed ChapterTable.NUMBER_IN_LIST to ChapterTable.INDEX */
|
||||||
|
override fun run() {
|
||||||
|
with(TransactionManager.current()) {
|
||||||
|
exec("ALTER TABLE CHAPTER ALTER COLUMN NUMBER_IN_LIST RENAME TO INDEX")
|
||||||
|
commit()
|
||||||
|
currentDialect.resetCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
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" }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
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
|
||||||
+6
-6
@@ -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
|
||||||
|
|
||||||
@@ -18,8 +18,8 @@ object CategoryTable : IntIdTable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
||||||
categoryEntry[CategoryTable.id].value,
|
categoryEntry[this.id].value,
|
||||||
categoryEntry[CategoryTable.order],
|
categoryEntry[this.order],
|
||||||
categoryEntry[CategoryTable.name],
|
categoryEntry[this.name],
|
||||||
categoryEntry[CategoryTable.isLanding],
|
categoryEntry[this.isLanding],
|
||||||
)
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user