Compare commits
215 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfa59a1691 | |||
| 5023e96301 | |||
| 224c24ee9f | |||
| e3b154cf9e | |||
| d249867c4c | |||
| b56045e984 | |||
| 3777cc646e | |||
| aa5a1083d0 | |||
| 2ae5e0742e | |||
| e5e875c54a | |||
| 1a99ec76e4 | |||
| 1b122d1157 | |||
| 77f2f8cc18 | |||
| f0a99980b6 | |||
| b0d43ffe69 | |||
| 16cb0184a4 | |||
| f211a33ea3 | |||
| 440c815189 | |||
| 25829aacfd | |||
| 700a739f95 | |||
| d9620bec05 | |||
| 4b6c51b1f8 | |||
| bd02edf0b1 | |||
| 5c7123a997 | |||
| c17e3bd04f | |||
| 994ae97256 | |||
| 781428a690 | |||
| c23ac5faa8 | |||
| e8d41f83c2 | |||
| 921a0a3361 | |||
| dda5a2df93 | |||
| 155f9f107d | |||
| 24f68b8f1a | |||
| 0ffbe194fa | |||
| 0b41e2b72b | |||
| ef07b9b4ce | |||
| f3999cf2d9 | |||
| 1729847937 | |||
| 37bff6c76c | |||
| 4ef32d8037 | |||
| d2f6a33f0a | |||
| 31d9903251 | |||
| e97642d92a | |||
| c49fc0ff5f | |||
| deb2ab1ff4 | |||
| 23466cf853 | |||
| 16b34f874d | |||
| 0e0d08ae5a | |||
| 986b4c2c27 | |||
| 0bf9ccfcbd | |||
| 5e8c47928d | |||
| ffae7f911f | |||
| e37fdf6d79 | |||
| b359116745 | |||
| 60073aace3 | |||
| 874b13fa14 | |||
| b146d1024b | |||
| 332e95c021 | |||
| 1f68141df5 | |||
| dd731cd306 | |||
| 38d8d03cae | |||
| ec7d840f37 | |||
| 2813dbb897 | |||
| 77d1402b8a | |||
| 08e8a9d105 | |||
| 71661f70b6 | |||
| ac1e79ba83 | |||
| d082809776 | |||
| a458a696db | |||
| 75786a91b0 | |||
| 6ddb5db57b | |||
| 4f70cc9283 | |||
| 23b643d637 | |||
| fdfc256c4d | |||
| fba56c1b75 | |||
| 4743bfacf7 | |||
| 2356537f7c | |||
| fa071aee84 | |||
| c00ca23a8b | |||
| 733b017936 | |||
| 4147f2e368 | |||
| 154b9992eb | |||
| 88b881b043 | |||
| 5d1491fb8c | |||
| 3a33196cf1 | |||
| fa8e0478da | |||
| 7e7e069244 | |||
| 18e0d34af0 | |||
| 3fe3f35483 | |||
| cf8e274883 | |||
| 10dee8b345 | |||
| ae8d30593f | |||
| 9cde46b5da | |||
| 8e61632155 | |||
| e2c4b4cb57 | |||
| 326da504ea | |||
| c5874a3f10 | |||
| 02802fab97 | |||
| 29dea10be2 | |||
| 6bc36193dc | |||
| 81e123388e | |||
| 8ebd7869a5 | |||
| 7a2f5f13f1 | |||
| 25d7dad39f | |||
| c8f8795920 | |||
| 84206a7074 | |||
| 6fd8b36dca | |||
| 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 |
+2
-1
@@ -9,7 +9,7 @@
|
|||||||
# Gradle wrapper
|
# Gradle wrapper
|
||||||
*.jar binary
|
*.jar binary
|
||||||
|
|
||||||
# Images
|
# Binary files types
|
||||||
*.webp binary
|
*.webp binary
|
||||||
*.png binary
|
*.png binary
|
||||||
*.jpg binary
|
*.jpg binary
|
||||||
@@ -25,3 +25,4 @@
|
|||||||
*.pyc binary
|
*.pyc binary
|
||||||
*.swp binary
|
*.swp binary
|
||||||
*.pdf binary
|
*.pdf binary
|
||||||
|
*.exe binary
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
cp master/server/build/Tachidesk-*.jar preview
|
|
||||||
cd preview
|
|
||||||
|
|
||||||
new_jar_build=$(ls *.jar| tail -1)
|
|
||||||
echo "last jar build file name: $new_jar_build"
|
|
||||||
|
|
||||||
cp -f $new_jar_build Tachidesk-latest.jar
|
|
||||||
|
|
||||||
rm -rf latest_pointer/*
|
|
||||||
cp $new_jar_build latest_pointer
|
|
||||||
|
|
||||||
latest=$(ls *.jar | tail -n1 | cut -d"-" -f3 | cut -d"." -f1)
|
|
||||||
echo "{ \"latest\": \"$latest\" }" > index.json
|
|
||||||
|
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --global user.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
|
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build FatJar
|
name: Build pull request
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
access_token: ${{ github.token }}
|
access_token: ${{ github.token }}
|
||||||
|
|
||||||
- name: Checkout master branch
|
- name: Checkout pull request
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
@@ -57,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 and copy webUI, 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: :webUI:copyBuild :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
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build FatJar
|
name: Build artifacts and deploy preview
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -59,22 +59,71 @@ jobs:
|
|||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||||
|
|
||||||
- name: Build and copy webUI, Build Jar and launch4j
|
- name: Build and copy webUI, Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
env:
|
||||||
|
TachideskBuildType: "Preview"
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
arguments: :webUI:copyBuild :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: Mock Build and copy webUI, Build Jar
|
||||||
|
# run: |
|
||||||
|
# mkdir -p master/server/build
|
||||||
|
# cd master/server/build
|
||||||
|
# echo "test" > Tachidesk-v0.3.8-r583.jar
|
||||||
|
|
||||||
|
- name: Generate Tag Name
|
||||||
|
id: GenTagName
|
||||||
|
run: |
|
||||||
|
cd master/server/build
|
||||||
|
genTag=$(ls *.jar | sed -e's/Tachidesk-\|.jar//g')
|
||||||
|
echo "$genTag"
|
||||||
|
echo "::set-output name=value::$genTag"
|
||||||
|
|
||||||
|
- name: make windows packages
|
||||||
|
run: |
|
||||||
|
cd master/scripts
|
||||||
|
./windows-bundler.sh win32
|
||||||
|
./windows-bundler.sh win64
|
||||||
|
|
||||||
|
# - name: Mock make windows packages
|
||||||
|
# run: |
|
||||||
|
# cd master/server/build
|
||||||
|
# echo test > Tachidesk-v0.3.8-r580-win32.zip
|
||||||
|
|
||||||
- name: Checkout preview branch
|
- name: Checkout preview branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: preview
|
repository: 'Suwayomi/Tachidesk-preview'
|
||||||
|
ref: main
|
||||||
path: preview
|
path: preview
|
||||||
|
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||||
|
|
||||||
- name: Deploy preview
|
- name: Create Tag
|
||||||
run: |
|
run: |
|
||||||
./master/.github/scripts/commit-repo.sh
|
TAG="${{ steps.GenTagName.outputs.value }}"
|
||||||
|
echo "tag: $TAG"
|
||||||
|
cd preview
|
||||||
|
echo "{ \"latest\": \"$TAG\" }" > index.json
|
||||||
|
git add index.json
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git commit -m "Updated to $TAG"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
git tag $TAG
|
||||||
|
git push origin $TAG
|
||||||
|
|
||||||
|
- name: Upload Preview Release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||||
|
artifacts: "master/server/build/*.jar,master/server/build/*.zip"
|
||||||
|
owner: "Suwayomi"
|
||||||
|
repo: "Tachidesk-preview"
|
||||||
|
tag: ${{ steps.GenTagName.outputs.value }}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build FatJar
|
name: Build artifacts and release
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -28,10 +28,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
access_token: ${{ github.token }}
|
access_token: ${{ github.token }}
|
||||||
|
|
||||||
- name: Checkout master branch
|
- name: Checkout ${{ github.ref }}
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: ${{ github.ref }}
|
||||||
path: master
|
path: master
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -56,54 +56,32 @@ 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 and copy webUI, Build Jar and launch4j
|
- name: Build and copy webUI, Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
env:
|
||||||
|
TachideskBuildType: "Stable"
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
arguments: :webUI:copyBuild :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
|
||||||
|
run: |
|
||||||
|
cd master/scripts
|
||||||
|
./windows-bundler.sh win32
|
||||||
|
./windows-bundler.sh win64
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: xresloader/upload-to-github-release@master
|
uses: xresloader/upload-to-github-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
file: "master/server/build/*.jar;master/server/build/*-win32.zip"
|
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 }}
|
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ build
|
|||||||
server/src/main/resources/react
|
server/src/main/resources/react
|
||||||
server/tmp/
|
server/tmp/
|
||||||
server/tachiserver-data/
|
server/tachiserver-data/
|
||||||
|
|
||||||
|
# bundle asset downlaods
|
||||||
|
OpenJDK*.zip
|
||||||
|
electron-*.zip
|
||||||
|
rcedit-*
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import net.harawata.appdirs.AppDirsFactory
|
|||||||
val ApplicationRootDir: String
|
val ApplicationRootDir: String
|
||||||
get(): String {
|
get(): String {
|
||||||
return System.getProperty(
|
return System.getProperty(
|
||||||
"ir.armor.tachidesk.rootDir",
|
"suwayomi.server.rootDir",
|
||||||
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ package xyz.nulldev.ts.config
|
|||||||
* 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 ch.qos.logback.classic.Level
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import com.typesafe.config.ConfigRenderOptions
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
@@ -43,6 +44,12 @@ open class ConfigManager {
|
|||||||
//Load reference configs
|
//Load reference configs
|
||||||
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
|
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
|
||||||
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
||||||
|
val baseConfig =
|
||||||
|
ConfigFactory.parseMap(
|
||||||
|
mapOf(
|
||||||
|
"ts.server.rootDir" to ApplicationRootDir
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
//Load user config
|
//Load user config
|
||||||
val userConfig =
|
val userConfig =
|
||||||
@@ -50,12 +57,19 @@ open class ConfigManager {
|
|||||||
ConfigFactory.parseFile(it)
|
ConfigFactory.parseFile(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val config = ConfigFactory.empty()
|
val config = ConfigFactory.empty()
|
||||||
|
.withFallback(baseConfig)
|
||||||
.withFallback(userConfig)
|
.withFallback(userConfig)
|
||||||
.withFallback(compatConfig)
|
.withFallback(compatConfig)
|
||||||
.withFallback(serverConfig)
|
.withFallback(serverConfig)
|
||||||
.resolve()
|
.resolve()
|
||||||
|
|
||||||
|
// set log level early
|
||||||
|
if (debugLogsEnabled(config)) {
|
||||||
|
setLogLevel(Level.DEBUG)
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug {
|
logger.debug {
|
||||||
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
|
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package xyz.nulldev.ts.config
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Level
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.slf4j.Logger
|
||||||
|
|
||||||
|
fun setLogLevel(level: Level) {
|
||||||
|
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level
|
||||||
|
}
|
||||||
|
|
||||||
|
fun debugLogsEnabled(config: Config)
|
||||||
|
= System.getProperty("suwayomi.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
|
||||||
@@ -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..."
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ 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
|
||||||
|
|
||||||
@@ -59,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
|
||||||
@@ -68,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"
|
||||||
@@ -82,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
|
||||||
|
|
||||||
|
|||||||
@@ -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,19 +1,21 @@
|
|||||||
|
|
||||||
| Build | Stable | Preview | Support Server |
|
| Build | Stable | Preview | Support Server |
|
||||||
|-------|----------|---------|---------|
|
|-------|----------|---------|---------|
|
||||||
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk/tree/preview/latest_pointer) | [](https://discord.gg/DDZdqZWaHA) |
|
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [](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"/>
|
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
||||||
|
|
||||||
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||||
|
|
||||||
Tachidesk is an independent Tachiyomi compatible software made by [@AriaMoradi AKA ArMor](https://github.com/AriaMoradi) and contributors and is **not a Fork of** Tachiyomi.
|
Tachidesk is 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.
|
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:
|
||||||
|
|
||||||
@@ -24,22 +26,24 @@ Here is a list of current features:
|
|||||||
- 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
|
- 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 "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).
|
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
|
||||||
|
|
||||||
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.
|
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 "Stable" win32 or win64 (depending on your system, usually you want win64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/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 one of the Launcher files depending on what you want(see bellow). The rest works like the previous section.
|
||||||
|
#### Windows Launchers
|
||||||
|
- `Tachidesk Electron Launcher.bat`: Launches Tachidesk inside Electron as a desktop applicaton
|
||||||
|
- `Tachidesk Browser Launcher.bat`: Launches Tachidesk in a browser window
|
||||||
|
- `Tachidesk Debug Launcher.bat`: Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why.
|
||||||
|
|
||||||
### Arch Linux
|
### Arch Linux
|
||||||
You can install Tachidesk from the AUR
|
You can install Tachidesk from the AUR
|
||||||
@@ -48,54 +52,25 @@ yay -S tachidesk
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
Check our Offical Docker release [Tachidesk Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) or use [arbuilder's](https://github.com/arbuilder/Tachidesk-docker) tachidesk docker repo for installation. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk). By default the server will be running on http://localhost:4567 open this url in your browser.
|
||||||
|
|
||||||
## General troubleshooting
|
Install from the command line:
|
||||||
If the app breaks, make sure that it's not running(right click on tray icon and quit or kill it through the way your Operating System provides), delete the directory below and re-run the app (**This procedure will delete all your data!**) and if the problem persists open an issue or ask for help on discord.
|
```
|
||||||
|
$ docker pull ghcr.io/suwayomi/tachidesk
|
||||||
|
```
|
||||||
|
Run Container from the command line:
|
||||||
|
```
|
||||||
|
$ docker run -p 4567:4567 ghcr.io/suwayomi/tachidesk
|
||||||
|
```
|
||||||
|
|
||||||
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
### Using Tachidesk Remotely
|
||||||
|
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 Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
|
## Troubleshooting and Support
|
||||||
|
See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting).
|
||||||
|
|
||||||
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
|
## Contributing and Technical info
|
||||||
|
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||||
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
|
||||||
|
|
||||||
## Support and help
|
|
||||||
Join Tachidesk's [discord server](https://discord.gg/DDZdqZWaHA) to hang out with the community and to receive support and help.
|
|
||||||
|
|
||||||
## How does it work?
|
|
||||||
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
|
|
||||||
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
|
|
||||||
- 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
|
|
||||||
Run `./gradlew :server:windowsPackage` to build a server only bundle and `./gradlew :webUI:copyBuild :server:windowsPackage` to get a full bundle , the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
|
||||||
## Running for development purposes
|
|
||||||
### `server` module
|
|
||||||
Follow [Get Android stubs jar](#prerequisite-get-android-stubs-jar) then run `./gradlew :server:run --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 development server, if a new browser window doesn't get opned automatically,
|
|
||||||
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
|
||||||
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.
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "ir.armor.tachidesk"
|
group = "suwayomi"
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
|
|
||||||
@@ -59,14 +59,14 @@ configure(projects) {
|
|||||||
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.6")
|
implementation("io.github.microutils:kotlin-logging:2.0.6")
|
||||||
|
|
||||||
// RxJava
|
// ReactiveX
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
implementation("io.reactivex:rxkotlin:1.0.0")
|
implementation("io.reactivex:rxkotlin:1.0.0")
|
||||||
|
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||||
|
|
||||||
// JSoup
|
// JSoup
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
implementation("org.jsoup:jsoup:1.13.1")
|
||||||
|
|
||||||
|
|
||||||
// dependency of :AndroidCompat:Config
|
// dependency of :AndroidCompat:Config
|
||||||
implementation("com.typesafe:config:1.4.1")
|
implementation("com.typesafe:config:1.4.1")
|
||||||
implementation("io.github.config4k:config4k:0.4.2")
|
implementation("io.github.config4k:config4k:0.4.2")
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
start "" jre/bin/javaw -jar Tachidesk.jar
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
jre\bin\java -Dsuwayomi.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
jre\bin\javaw "-Dsuwayomi.server.webInterface=electron" "-Dsuwayomi.server.electronPath=electron/electron.exe" -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 with MSVC compiler.
|
||||||
|
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`
|
||||||
Executable
+83
@@ -0,0 +1,83 @@
|
|||||||
|
#!/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/.
|
||||||
|
|
||||||
|
electron_version="v12.0.9"
|
||||||
|
|
||||||
|
if [ $1 = "win32" ]; then
|
||||||
|
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||||
|
arch="win32"
|
||||||
|
electron="electron-$electron_version-win32-ia32.zip"
|
||||||
|
else
|
||||||
|
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||||
|
arch="win64"
|
||||||
|
electron="electron-$electron_version-win32-x64.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
jre_dir="jdk8u292-b10-jre"
|
||||||
|
|
||||||
|
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)-$arch
|
||||||
|
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
mkdir $release_name
|
||||||
|
|
||||||
|
|
||||||
|
echo "Dealing with jre..."
|
||||||
|
if [ ! -f $jre ]; then
|
||||||
|
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/$jre" -o $jre
|
||||||
|
fi
|
||||||
|
unzip $jre
|
||||||
|
mv $jre_dir $release_name/jre
|
||||||
|
|
||||||
|
echo "Dealing with electron"
|
||||||
|
if [ ! -f $electron ]; then
|
||||||
|
curl -L "https://github.com/electron/electron/releases/download/$electron_version/$electron" -o $electron
|
||||||
|
fi
|
||||||
|
unzip $electron -d $release_name/electron
|
||||||
|
|
||||||
|
# change electron's icon
|
||||||
|
rcedit="rcedit-x86.exe"
|
||||||
|
if [ ! -f $rcedit ]; then
|
||||||
|
curl -L "https://github.com/electron/rcedit/releases/download/v1.1.1/$rcedit" -o $rcedit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check if running under github actions
|
||||||
|
if [ $CI = true ]; then
|
||||||
|
# change electron executable's icon
|
||||||
|
sudo dpkg --add-architecture i386
|
||||||
|
wget -qO - https://dl.winehq.org/wine-builds/winehq.key | sudo apt-key add -
|
||||||
|
sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport
|
||||||
|
sudo apt-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu $(lsb_release -cs) main"
|
||||||
|
sudo apt install --install-recommends winehq-stable
|
||||||
|
fi
|
||||||
|
# this script assumes that wine is installed here on out
|
||||||
|
|
||||||
|
WINEARCH=win32 wine $rcedit $release_name/electron/electron.exe --set-icon ../server/src/main/resources/icon/faviconlogo.ico
|
||||||
|
|
||||||
|
# copy artifacts
|
||||||
|
cp $jar $release_name/Tachidesk.jar
|
||||||
|
#cp "resources/Tachidesk Launcher-$arch.exe" "$release_name/Tachidesk Launcher.exe"
|
||||||
|
cp "resources/Tachidesk Browser Launcher.bat" $release_name
|
||||||
|
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
cp "resources/Tachidesk Electron Launcher.bat" $release_name
|
||||||
|
|
||||||
|
zip_name=$release_name.zip
|
||||||
|
zip -9 -r $zip_name $release_name
|
||||||
|
|
||||||
|
rm -rf $release_name
|
||||||
|
|
||||||
|
# clean up from possible previous runs
|
||||||
|
if [ -f ../server/build/$zip_name ]; then
|
||||||
|
rm ../server/build/$zip_name
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv $zip_name ../server/build/
|
||||||
+47
-111
@@ -3,58 +3,37 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|||||||
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
||||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
application
|
application
|
||||||
id("com.github.johnrengelman.shadow") version "7.0.0"
|
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||||
id("org.jmailen.kotlinter") version "3.4.3"
|
id("org.jmailen.kotlinter") version "3.4.3"
|
||||||
id("edu.sc.seis.launch4j") version "2.5.0"
|
|
||||||
id("de.fuerstenau.buildconfig") version "1.1.8"
|
id("de.fuerstenau.buildconfig") version "1.1.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
maven {
|
||||||
|
url = uri("https://repo1.maven.org/maven2/")
|
||||||
|
}
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://jitpack.io")
|
url = uri("https://jitpack.io")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// okhttp
|
||||||
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
val okhttpVersion = "4.9.1" // version is locked by Tachiyomi extensions
|
||||||
// implementation("tachiyomi.sourceapi:source-api:1.1")
|
|
||||||
|
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
|
||||||
|
|
||||||
val okhttpVersion = "4.10.0-RC1"
|
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
implementation("com.squareup.okio:okio:2.10.0")
|
implementation("com.squareup.okio:okio:2.10.0")
|
||||||
|
|
||||||
|
// Javalin api
|
||||||
// Retrofit
|
|
||||||
val retrofitVersion = "2.9.0"
|
|
||||||
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
|
|
||||||
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
|
|
||||||
implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion")
|
|
||||||
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofitVersion")
|
|
||||||
|
|
||||||
|
|
||||||
// Reactivex
|
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
|
|
||||||
implementation("com.google.code.gson:gson:2.8.6")
|
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
|
||||||
|
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
|
||||||
|
|
||||||
|
|
||||||
// api
|
|
||||||
implementation("io.javalin:javalin:3.13.6")
|
implementation("io.javalin:javalin:3.13.6")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.3")
|
// jackson version is tied to javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||||
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.3")
|
||||||
|
|
||||||
// Exposed ORM
|
// Exposed ORM
|
||||||
val exposedVersion = "0.31.1"
|
val exposedVersion = "0.31.1"
|
||||||
@@ -62,7 +41,6 @@ dependencies {
|
|||||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||||
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
|
||||||
|
|
||||||
// current database driver
|
// current database driver
|
||||||
implementation("com.h2database:h2:1.4.200")
|
implementation("com.h2database:h2:1.4.200")
|
||||||
|
|
||||||
@@ -70,7 +48,19 @@ dependencies {
|
|||||||
implementation("com.dorkbox:SystemTray:4.1")
|
implementation("com.dorkbox:SystemTray:4.1")
|
||||||
implementation("com.dorkbox:Utilities:1.9")
|
implementation("com.dorkbox:Utilities:1.9")
|
||||||
|
|
||||||
implementation("com.google.guava:guava:30.1.1-jre")
|
|
||||||
|
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
||||||
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:4.9.1")
|
||||||
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
|
implementation("org.jsoup:jsoup:1.13.1")
|
||||||
|
implementation("com.google.code.gson:gson:2.8.6")
|
||||||
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
|
||||||
|
|
||||||
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
|
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
||||||
|
// implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||||
|
|
||||||
// AndroidCompat
|
// AndroidCompat
|
||||||
implementation(project(":AndroidCompat"))
|
implementation(project(":AndroidCompat"))
|
||||||
@@ -83,9 +73,15 @@ dependencies {
|
|||||||
testImplementation(kotlin("test-junit5"))
|
testImplementation(kotlin("test-junit5"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val MainClass = "ir.armor.tachidesk.Main"
|
val MainClass = "suwayomi.MainKt"
|
||||||
application {
|
application {
|
||||||
mainClass.set(MainClass)
|
mainClass.set(MainClass)
|
||||||
|
|
||||||
|
// for testing electron
|
||||||
|
// applicationDefaultJvmArgs = listOf(
|
||||||
|
// "-Dsuwayomi.tachidesk.webInterface=electron",
|
||||||
|
// "-Dsuwayomi.tachidesk.electronPath=/usr/bin/electron"
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -97,12 +93,13 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should be bumped with each stable release
|
// should be bumped with each stable release
|
||||||
val tachideskVersion = "v0.3.0"
|
val tachideskVersion = "v0.4.2"
|
||||||
|
|
||||||
// counts commit count on master
|
// counts commit count on master
|
||||||
val tachideskRevision = Runtime
|
val tachideskRevision = runCatching {
|
||||||
|
Runtime
|
||||||
.getRuntime()
|
.getRuntime()
|
||||||
.exec("git rev-list master --count")
|
.exec("git rev-list HEAD --count")
|
||||||
.let { process ->
|
.let { process ->
|
||||||
process.waitFor()
|
process.waitFor()
|
||||||
val output = process.inputStream.use {
|
val output = process.inputStream.use {
|
||||||
@@ -112,32 +109,25 @@ val tachideskRevision = Runtime
|
|||||||
"r" + output.trim()
|
"r" + output.trim()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}.getOrDefault("r0")
|
||||||
|
|
||||||
buildConfig {
|
buildConfig {
|
||||||
appName = rootProject.name
|
|
||||||
clsName = "BuildConfig"
|
clsName = "BuildConfig"
|
||||||
packageName = "ir.armor.tachidesk.server"
|
packageName = "suwayomi.server"
|
||||||
version = tachideskVersion
|
|
||||||
|
|
||||||
|
|
||||||
buildConfigField("String", "name", rootProject.name) // alias for BuildConfig.NAME
|
buildConfigField("String", "NAME", rootProject.name)
|
||||||
buildConfigField("String", "version", tachideskVersion) // alias for BuildConfig.VERSION
|
buildConfigField("String", "VERSION", tachideskVersion)
|
||||||
buildConfigField("String", "revision", tachideskRevision)
|
buildConfigField("String", "REVISION", tachideskRevision)
|
||||||
buildConfigField("boolean", "debug", project.hasProperty("debugApp").toString())
|
buildConfigField("String", "BUILD_TYPE", if (System.getenv("TachideskBuildType") == "Stable") "Stable" else "Preview")
|
||||||
}
|
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
|
||||||
|
|
||||||
launch4j { //used for windows
|
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk")
|
||||||
mainClassName = MainClass
|
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA")
|
||||||
bundledJrePath = "jre"
|
|
||||||
bundledJre64Bit = true
|
|
||||||
jreMinVersion = "8"
|
|
||||||
outputDir = "${rootProject.name}-$tachideskVersion-$tachideskRevision-win32"
|
|
||||||
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
|
||||||
jar = "${projectDir}/build/${rootProject.name}-$tachideskVersion-$tachideskRevision.jar"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
jar {
|
shadowJar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes(
|
attributes(
|
||||||
mapOf(
|
mapOf(
|
||||||
@@ -149,9 +139,6 @@ tasks {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
shadowJar {
|
|
||||||
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
|
||||||
archiveBaseName.set(rootProject.name)
|
archiveBaseName.set(rootProject.name)
|
||||||
archiveVersion.set(tachideskVersion)
|
archiveVersion.set(tachideskVersion)
|
||||||
archiveClassifier.set(tachideskRevision)
|
archiveClassifier.set(tachideskRevision)
|
||||||
@@ -165,61 +152,11 @@ tasks {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnit()
|
useJUnit()
|
||||||
}
|
}
|
||||||
|
|
||||||
register<Zip>("windowsPackage") {
|
|
||||||
from(fileTree("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32"))
|
|
||||||
destinationDirectory.set(File("$buildDir"))
|
|
||||||
archiveFileName.set("${rootProject.name}-$tachideskVersion-$tachideskRevision-win32.zip")
|
|
||||||
dependsOn("windowsPackageWorkaround2")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Delete>("windowsPackageWorkaround2") {
|
|
||||||
delete(
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/jre",
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/lib",
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/server.exe",
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/Tachidesk-$tachideskVersion-$tachideskRevision-win32/Tachidesk-$tachideskVersion-$tachideskRevision-win32"
|
|
||||||
)
|
|
||||||
dependsOn("windowsPackageWorkaround")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Copy>("windowsPackageWorkaround") {
|
|
||||||
from("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
|
||||||
into("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
|
||||||
dependsOn("deleteUnwantedJreDir")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Delete>("deleteUnwantedJreDir") {
|
|
||||||
delete(
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/jdk8u282-b08-jre"
|
|
||||||
)
|
|
||||||
dependsOn("addJreToDistributable")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Copy>("addJreToDistributable") {
|
|
||||||
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
|
|
||||||
into("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
|
||||||
eachFile {
|
|
||||||
path = path.replace(".*-jre".toRegex(), "jre")
|
|
||||||
}
|
|
||||||
dependsOn("downloadJre")
|
|
||||||
dependsOn("createExe")
|
|
||||||
}
|
|
||||||
|
|
||||||
named("createExe") {
|
|
||||||
dependsOn("shadowJar")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
|
|
||||||
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
|
||||||
dest("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
|
||||||
overwrite(false)
|
|
||||||
onlyIfModified(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
withType<ShadowJar> {
|
withType<ShadowJar> {
|
||||||
destinationDirectory.set(File("$rootDir/server/build"))
|
destinationDirectory.set(File("$rootDir/server/build"))
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
dependsOn("formatKotlin", "lintKotlin")
|
||||||
@@ -235,11 +172,10 @@ tasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withType<LintTask> {
|
withType<LintTask> {
|
||||||
source(files("src"))
|
source(files("src/kotlin"))
|
||||||
}
|
}
|
||||||
|
|
||||||
withType<FormatTask> {
|
withType<FormatTask> {
|
||||||
source(files("src"))
|
source(files("src/kotlin"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
interface AnimeCatalogueSource : AnimeSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||||
|
*/
|
||||||
|
val lang: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the source has support for latest updates.
|
||||||
|
*/
|
||||||
|
val supportsLatest: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of anime.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
fun fetchPopularAnime(page: Int): Observable<AnimesPage>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of anime.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
* @param query the search query.
|
||||||
|
* @param filters the list of filters to apply.
|
||||||
|
*/
|
||||||
|
fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of latest anime updates.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
fun fetchLatestUpdates(page: Int): Observable<AnimesPage>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of filters for the source.
|
||||||
|
*/
|
||||||
|
fun getFilterList(): AnimeFilterList
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
||||||
|
*/
|
||||||
|
interface AnimeSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id for the source. Must be unique.
|
||||||
|
*/
|
||||||
|
val id: Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the source.
|
||||||
|
*/
|
||||||
|
val name: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the updated details for a anime.
|
||||||
|
*
|
||||||
|
* @param anime the anime to update.
|
||||||
|
*/
|
||||||
|
// @Deprecated("Use getAnimeDetails instead")
|
||||||
|
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with all the available episodes for an anime.
|
||||||
|
*
|
||||||
|
* @param anime the anime to update.
|
||||||
|
*/
|
||||||
|
// @Deprecated("Use getEpisodeList instead")
|
||||||
|
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with a link for the episode of an anime.
|
||||||
|
*
|
||||||
|
* @param episode the episode to get the link for.
|
||||||
|
*/
|
||||||
|
// @Deprecated("Use getEpisodeList instead")
|
||||||
|
fun fetchEpisodeLink(episode: SEpisode): Observable<String>
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * [1.x API] Get the updated details for a anime.
|
||||||
|
// */
|
||||||
|
// @Suppress("DEPRECATION")
|
||||||
|
// override suspend fun getAnimeDetails(anime: AnimeInfo): AnimeInfo {
|
||||||
|
// val sAnime = anime.toSAnime()
|
||||||
|
// val networkAnime = fetchAnimeDetails(sAnime).awaitSingle()
|
||||||
|
// sAnime.copyFrom(networkAnime)
|
||||||
|
// return sAnime.toAnimeInfo()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * [1.x API] Get all the available episodes for a anime.
|
||||||
|
// */
|
||||||
|
// @Suppress("DEPRECATION")
|
||||||
|
// override suspend fun getEpisodeList(anime: AnimeInfo): List<EpisodeInfo> {
|
||||||
|
// return fetchEpisodeList(anime.toSAnime()).awaitSingle()
|
||||||
|
// .map { it.toEpisodeInfo() }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * [1.x API] Get a link for the episode of an anime.
|
||||||
|
// */
|
||||||
|
// @Suppress("DEPRECATION")
|
||||||
|
// override suspend fun getEpisodeLink(episode: EpisodeInfo): String {
|
||||||
|
// return fetchEpisodeLink(episode.toSEpisode()).awaitSingle()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIconForSource(this)
|
||||||
|
|
||||||
|
// fun AnimeSource.getPreferenceKey(): String = "source_$id"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A factory for creating sources at runtime.
|
||||||
|
*/
|
||||||
|
interface AnimeSourceFactory {
|
||||||
|
/**
|
||||||
|
* Create a new copy of the sources
|
||||||
|
* @return The created sources
|
||||||
|
*/
|
||||||
|
fun createSources(): List<AnimeSource>
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
open class AnimeSourceManager(private val context: Context) {
|
||||||
|
|
||||||
|
private val sourcesMap = mutableMapOf<Long, AnimeSource>()
|
||||||
|
|
||||||
|
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
createInternalSources().forEach { registerSource(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun get(sourceKey: Long): AnimeSource? {
|
||||||
|
return sourcesMap[sourceKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOrStub(sourceKey: Long): AnimeSource {
|
||||||
|
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
|
||||||
|
StubSource(sourceKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOnlineSources() = sourcesMap.values.filterIsInstance<AnimeHttpSource>()
|
||||||
|
|
||||||
|
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<AnimeCatalogueSource>()
|
||||||
|
|
||||||
|
internal fun registerSource(source: AnimeSource) {
|
||||||
|
if (!sourcesMap.containsKey(source.id)) {
|
||||||
|
sourcesMap[source.id] = source
|
||||||
|
}
|
||||||
|
if (!stubSourcesMap.containsKey(source.id)) {
|
||||||
|
stubSourcesMap[source.id] = StubSource(source.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun unregisterSource(source: AnimeSource) {
|
||||||
|
sourcesMap.remove(source.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createInternalSources(): List<AnimeSource> = listOf(
|
||||||
|
// LocalAnimeSource(context)
|
||||||
|
)
|
||||||
|
|
||||||
|
inner class StubSource(override val id: Long) : AnimeSource {
|
||||||
|
|
||||||
|
override val name: String
|
||||||
|
get() = id.toString()
|
||||||
|
|
||||||
|
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
||||||
|
return Observable.error(getSourceNotInstalledException())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
|
||||||
|
return Observable.error(getSourceNotInstalledException())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchEpisodeLink(episode: SEpisode): Observable<String> {
|
||||||
|
return Observable.error(getSourceNotInstalledException())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSourceNotInstalledException(): Exception {
|
||||||
|
// return Exception(context.getString(R.string.source_not_installed, id.toString()))
|
||||||
|
return Exception("source not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource
|
||||||
|
|
||||||
|
import android.support.v7.preference.PreferenceScreen
|
||||||
|
|
||||||
|
interface ConfigurableAnimeSource : AnimeSource {
|
||||||
|
|
||||||
|
fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
|
sealed class AnimeFilter<T>(val name: String, var state: T) {
|
||||||
|
open class Header(name: String) : AnimeFilter<Any>(name, 0)
|
||||||
|
open class Separator(name: String = "") : AnimeFilter<Any>(name, 0)
|
||||||
|
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : AnimeFilter<Int>(name, state)
|
||||||
|
abstract class Text(name: String, state: String = "") : AnimeFilter<String>(name, state)
|
||||||
|
abstract class CheckBox(name: String, state: Boolean = false) : AnimeFilter<Boolean>(name, state)
|
||||||
|
abstract class TriState(name: String, state: Int = STATE_IGNORE) : AnimeFilter<Int>(name, state) {
|
||||||
|
fun isIgnored() = state == STATE_IGNORE
|
||||||
|
fun isIncluded() = state == STATE_INCLUDE
|
||||||
|
fun isExcluded() = state == STATE_EXCLUDE
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val STATE_IGNORE = 0
|
||||||
|
const val STATE_INCLUDE = 1
|
||||||
|
const val STATE_EXCLUDE = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Group<V>(name: String, state: List<V>) : AnimeFilter<List<V>>(name, state)
|
||||||
|
|
||||||
|
abstract class Sort(name: String, val values: Array<String>, state: Selection? = null) :
|
||||||
|
AnimeFilter<Sort.Selection?>(name, state) {
|
||||||
|
data class Selection(val index: Int, val ascending: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is AnimeFilter<*>) return false
|
||||||
|
|
||||||
|
return name == other.name && state == other.state
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = name.hashCode()
|
||||||
|
result = 31 * result + (state?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
|
data class AnimeFilterList(val list: List<AnimeFilter<*>>) : List<AnimeFilter<*>> by list {
|
||||||
|
|
||||||
|
constructor(vararg fs: AnimeFilter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
|
data class AnimesPage(val animes: List<SAnime>, val hasNextPage: Boolean)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface SAnime : Serializable {
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
|
||||||
|
var artist: String?
|
||||||
|
|
||||||
|
var author: String?
|
||||||
|
|
||||||
|
var description: String?
|
||||||
|
|
||||||
|
var genre: String?
|
||||||
|
|
||||||
|
var status: Int
|
||||||
|
|
||||||
|
var thumbnail_url: String?
|
||||||
|
|
||||||
|
var initialized: Boolean
|
||||||
|
|
||||||
|
fun copyFrom(other: SAnime) {
|
||||||
|
if (other.title != null) {
|
||||||
|
title = other.title
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other.author != null) {
|
||||||
|
author = other.author
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other.artist != null) {
|
||||||
|
artist = other.artist
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other.description != null) {
|
||||||
|
description = other.description
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other.genre != null) {
|
||||||
|
genre = other.genre
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other.thumbnail_url != null) {
|
||||||
|
thumbnail_url = other.thumbnail_url
|
||||||
|
}
|
||||||
|
|
||||||
|
status = other.status
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
initialized = other.initialized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val UNKNOWN = 0
|
||||||
|
const val ONGOING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val LICENSED = 3
|
||||||
|
|
||||||
|
fun create(): SAnime {
|
||||||
|
return SAnimeImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
|
class SAnimeImpl : SAnime {
|
||||||
|
|
||||||
|
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 initialized: Boolean = false
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface SEpisode : Serializable {
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
var date_upload: Long
|
||||||
|
|
||||||
|
var episode_number: Float
|
||||||
|
|
||||||
|
var scanlator: String?
|
||||||
|
|
||||||
|
fun copyFrom(other: SEpisode) {
|
||||||
|
name = other.name
|
||||||
|
url = other.url
|
||||||
|
date_upload = other.date_upload
|
||||||
|
episode_number = other.episode_number
|
||||||
|
scanlator = other.scanlator
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(): SEpisode {
|
||||||
|
return SEpisodeImpl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
|
class SEpisodeImpl : SEpisode {
|
||||||
|
|
||||||
|
override lateinit var url: String
|
||||||
|
|
||||||
|
override lateinit var name: String
|
||||||
|
|
||||||
|
override var date_upload: Long = 0
|
||||||
|
|
||||||
|
override var episode_number: Float = -1f
|
||||||
|
|
||||||
|
override var scanlator: String? = null
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource.online
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.newCallWithProgress
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple implementation for sources from a website.
|
||||||
|
*/
|
||||||
|
abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network service.
|
||||||
|
*/
|
||||||
|
protected val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Preferences that a source may need.
|
||||||
|
// */
|
||||||
|
// val preferences: SharedPreferences by lazy {
|
||||||
|
// Injekt.get<Application>().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base url of the website without the trailing slash, like: http://mysite.com
|
||||||
|
*/
|
||||||
|
abstract val baseUrl: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version id used to generate the source id. If the site completely changes and urls are
|
||||||
|
* incompatible, you may increase this value and it'll be considered as a new source.
|
||||||
|
*/
|
||||||
|
open val versionId = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||||
|
* of the MD5 of the string: sourcename/language/versionId
|
||||||
|
* Note the generated id sets the sign bit to 0.
|
||||||
|
*/
|
||||||
|
override val id by lazy {
|
||||||
|
val key = "${name.toLowerCase()}/$lang/$versionId"
|
||||||
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
|
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headers used for requests.
|
||||||
|
*/
|
||||||
|
val headers: Headers by lazy { headersBuilder().build() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default network client for doing requests.
|
||||||
|
*/
|
||||||
|
open val client: OkHttpClient
|
||||||
|
get() = network.client
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||||
|
*/
|
||||||
|
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||||
|
add("User-Agent", DEFAULT_USER_AGENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visible name of the source.
|
||||||
|
*/
|
||||||
|
override fun toString() = "$name (${lang.toUpperCase()})"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of anime. Normally it's not needed to
|
||||||
|
* override this method.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
override fun fetchPopularAnime(page: Int): Observable<AnimesPage> {
|
||||||
|
return client.newCall(popularAnimeRequest(page))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
popularAnimeParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for the popular anime given the page.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
protected abstract fun popularAnimeRequest(page: Int): Request
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [AnimesPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
protected abstract fun popularAnimeParse(response: Response): AnimesPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of anime. Normally it's not needed to
|
||||||
|
* override this method.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
* @param query the search query.
|
||||||
|
* @param filters the list of filters to apply.
|
||||||
|
*/
|
||||||
|
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||||
|
return client.newCall(searchAnimeRequest(page, query, filters))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
searchAnimeParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for the search anime given the page.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
* @param query the search query.
|
||||||
|
* @param filters the list of filters to apply.
|
||||||
|
*/
|
||||||
|
protected abstract fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [AnimesPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
protected abstract fun searchAnimeParse(response: Response): AnimesPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable containing a page with a list of latest anime updates.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
override fun fetchLatestUpdates(page: Int): Observable<AnimesPage> {
|
||||||
|
return client.newCall(latestUpdatesRequest(page))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
latestUpdatesParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for latest anime given the page.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
protected abstract fun latestUpdatesRequest(page: Int): Request
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [AnimesPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
protected abstract fun latestUpdatesParse(response: Response): AnimesPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the updated details for a anime. Normally it's not needed to
|
||||||
|
* override this method.
|
||||||
|
*
|
||||||
|
* @param anime the anime to be updated.
|
||||||
|
*/
|
||||||
|
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
||||||
|
return client.newCall(animeDetailsRequest(anime))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
animeDetailsParse(response).apply { initialized = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for the details of a anime. Override only if it's needed to change the
|
||||||
|
* url, send different headers or request method like POST.
|
||||||
|
*
|
||||||
|
* @param anime the anime to be updated.
|
||||||
|
*/
|
||||||
|
open fun animeDetailsRequest(anime: SAnime): Request {
|
||||||
|
return GET(baseUrl + anime.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns the details of a anime.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
protected abstract fun animeDetailsParse(response: Response): SAnime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the updated episode list for a anime. Normally it's not needed to
|
||||||
|
* override this method. If a anime is licensed an empty episode list observable is returned
|
||||||
|
*
|
||||||
|
* @param anime the anime to look for episodes.
|
||||||
|
*/
|
||||||
|
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
|
||||||
|
return if (anime.status != SAnime.LICENSED) {
|
||||||
|
client.newCall(episodeListRequest(anime))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
episodeListParse(response)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Observable.error(Exception("Licensed - No episodes to show"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchEpisodeLink(episode: SEpisode): Observable<String> {
|
||||||
|
return client.newCall(episodeLinkRequest(episode))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { response ->
|
||||||
|
episodeLinkParse(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for updating the episode list. Override only if it's needed to override
|
||||||
|
* the url, send different headers or request method like POST.
|
||||||
|
*
|
||||||
|
* @param anime the anime to look for episodes.
|
||||||
|
*/
|
||||||
|
protected open fun episodeListRequest(anime: SAnime): Request {
|
||||||
|
return GET(baseUrl + anime.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for getting the episode link. Override only if it's needed to override
|
||||||
|
* the url, send different headers or request method like POST.
|
||||||
|
*
|
||||||
|
* @param episode the episode to look for links.
|
||||||
|
*/
|
||||||
|
protected open fun episodeLinkRequest(episode: SEpisode): Request {
|
||||||
|
return GET(baseUrl + episode.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a list of episodes.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
protected abstract fun episodeListParse(response: Response): List<SEpisode>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a list of episodes.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
protected abstract fun episodeLinkParse(response: Response): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for getting the page list. Override only if it's needed to override the
|
||||||
|
* url, send different headers or request method like POST.
|
||||||
|
*
|
||||||
|
* @param episode the episode whose page list has to be fetched.
|
||||||
|
*/
|
||||||
|
protected open fun pageListRequest(episode: SEpisode): Request {
|
||||||
|
return GET(baseUrl + episode.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a list of pages.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
protected abstract fun pageListParse(response: Response): List<Page>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the page containing the source url of the image. If there's any
|
||||||
|
* error, it will return null instead of throwing an exception.
|
||||||
|
*
|
||||||
|
* @param page the page whose source image has to be fetched.
|
||||||
|
*/
|
||||||
|
open fun fetchImageUrl(page: Page): Observable<String> {
|
||||||
|
return client.newCall(imageUrlRequest(page))
|
||||||
|
.asObservableSuccess()
|
||||||
|
.map { imageUrlParse(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for getting the url to the source image. Override only if it's needed to
|
||||||
|
* override the url, send different headers or request method like POST.
|
||||||
|
*
|
||||||
|
* @param page the episode whose page list has to be fetched
|
||||||
|
*/
|
||||||
|
protected open fun imageUrlRequest(page: Page): Request {
|
||||||
|
return GET(page.url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns the absolute url to the source image.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
protected abstract fun imageUrlParse(response: Response): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable with the response of the source image.
|
||||||
|
*
|
||||||
|
* @param page the page whose source image has to be downloaded.
|
||||||
|
*/
|
||||||
|
fun fetchImage(page: Page): Observable<Response> {
|
||||||
|
return client.newCallWithProgress(imageRequest(page), page)
|
||||||
|
.asObservableSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request for getting the source image. Override only if it's needed to override
|
||||||
|
* the url, send different headers or request method like POST.
|
||||||
|
*
|
||||||
|
* @param page the episode whose page list has to be fetched
|
||||||
|
*/
|
||||||
|
protected open fun imageRequest(page: Page): Request {
|
||||||
|
return GET(page.imageUrl!!, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns the url of the episode without the scheme and domain. It saves some redundancy from
|
||||||
|
* database and the urls could still work after a domain change.
|
||||||
|
*
|
||||||
|
* @param url the full url to the episode.
|
||||||
|
*/
|
||||||
|
fun SEpisode.setUrlWithoutDomain(url: String) {
|
||||||
|
this.url = getUrlWithoutDomain(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns the url of the anime without the scheme and domain. It saves some redundancy from
|
||||||
|
* database and the urls could still work after a domain change.
|
||||||
|
*
|
||||||
|
* @param url the full url to the anime.
|
||||||
|
*/
|
||||||
|
fun SAnime.setUrlWithoutDomain(url: String) {
|
||||||
|
this.url = getUrlWithoutDomain(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the url of the given string without the scheme and domain.
|
||||||
|
*
|
||||||
|
* @param orig the full url.
|
||||||
|
*/
|
||||||
|
private fun getUrlWithoutDomain(orig: String): String {
|
||||||
|
return try {
|
||||||
|
val uri = URI(orig)
|
||||||
|
var out = uri.path
|
||||||
|
if (uri.query != null) {
|
||||||
|
out += "?" + uri.query
|
||||||
|
}
|
||||||
|
if (uri.fragment != null) {
|
||||||
|
out += "#" + uri.fragment
|
||||||
|
}
|
||||||
|
out
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
orig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before inserting a new episode into database. Use it if you need to override episode
|
||||||
|
* fields, like the title or the episode number. Do not change anything to [anime].
|
||||||
|
*
|
||||||
|
* @param episode the episode to be added.
|
||||||
|
* @param anime the anime of the episode.
|
||||||
|
*/
|
||||||
|
open fun prepareNewEpisode(episode: SEpisode, anime: SAnime) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of filters for the source.
|
||||||
|
*/
|
||||||
|
override fun getFilterList() = AnimeFilterList()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
fun AnimeHttpSource.getImageUrl(page: Page): Observable<Page> {
|
||||||
|
page.status = Page.LOAD_PAGE
|
||||||
|
return fetchImageUrl(page)
|
||||||
|
.doOnError { page.status = Page.ERROR }
|
||||||
|
.onErrorReturn { null }
|
||||||
|
.doOnNext { page.imageUrl = it }
|
||||||
|
.map { page }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AnimeHttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||||
|
return Observable.from(pages)
|
||||||
|
.filter { !it.imageUrl.isNullOrEmpty() }
|
||||||
|
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AnimeHttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||||
|
return Observable.from(pages)
|
||||||
|
.filter { it.imageUrl.isNullOrEmpty() }
|
||||||
|
.concatMap { getImageUrl(it) }
|
||||||
|
}
|
||||||
+222
@@ -0,0 +1,222 @@
|
|||||||
|
package eu.kanade.tachiyomi.animesource.online
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||||
|
*/
|
||||||
|
abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [AnimesPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val animes = document.select(popularAnimeSelector()).map { element ->
|
||||||
|
popularAnimeFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = popularAnimeNextPageSelector()?.let { selector ->
|
||||||
|
document.select(selector).first()
|
||||||
|
} != null
|
||||||
|
|
||||||
|
return AnimesPage(animes, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
|
||||||
|
*/
|
||||||
|
protected abstract fun popularAnimeSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
|
||||||
|
* totally fine to fill only those two values.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [popularAnimeSelector].
|
||||||
|
*/
|
||||||
|
protected abstract fun popularAnimeFromElement(element: Element): SAnime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||||
|
* there's no next page.
|
||||||
|
*/
|
||||||
|
protected abstract fun popularAnimeNextPageSelector(): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [AnimesPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val animes = document.select(searchAnimeSelector()).map { element ->
|
||||||
|
searchAnimeFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = searchAnimeNextPageSelector()?.let { selector ->
|
||||||
|
document.select(selector).first()
|
||||||
|
} != null
|
||||||
|
|
||||||
|
return AnimesPage(animes, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
|
||||||
|
*/
|
||||||
|
protected abstract fun searchAnimeSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
|
||||||
|
* totally fine to fill only those two values.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [searchAnimeSelector].
|
||||||
|
*/
|
||||||
|
protected abstract fun searchAnimeFromElement(element: Element): SAnime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||||
|
* there's no next page.
|
||||||
|
*/
|
||||||
|
protected abstract fun searchAnimeNextPageSelector(): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a [AnimesPage] object.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val animes = document.select(latestUpdatesSelector()).map { element ->
|
||||||
|
latestUpdatesFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
|
||||||
|
document.select(selector).first()
|
||||||
|
} != null
|
||||||
|
|
||||||
|
return AnimesPage(animes, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each anime.
|
||||||
|
*/
|
||||||
|
protected abstract fun latestUpdatesSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a anime from the given [element]. Most sites only show the title and the url, it's
|
||||||
|
* totally fine to fill only those two values.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [latestUpdatesSelector].
|
||||||
|
*/
|
||||||
|
protected abstract fun latestUpdatesFromElement(element: Element): SAnime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
|
||||||
|
* there's no next page.
|
||||||
|
*/
|
||||||
|
protected abstract fun latestUpdatesNextPageSelector(): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns the details of a anime.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun animeDetailsParse(response: Response): SAnime {
|
||||||
|
return animeDetailsParse(response.asJsoup())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the details of the anime from the given [document].
|
||||||
|
*
|
||||||
|
* @param document the parsed document.
|
||||||
|
*/
|
||||||
|
protected abstract fun animeDetailsParse(document: Document): SAnime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a list of episodes.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return document.select(episodeListSelector()).map { episodeFromElement(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each episode.
|
||||||
|
*/
|
||||||
|
protected abstract fun episodeListSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns a list of episodes.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun episodeLinkParse(response: Response): String {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return linkFromElement(document.select(episodeLinkSelector()).first())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Jsoup selector that returns a list of [Element] corresponding to each episode.
|
||||||
|
*/
|
||||||
|
protected abstract fun episodeLinkSelector(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a episode from the given element.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [episodeListSelector].
|
||||||
|
*/
|
||||||
|
protected abstract fun episodeFromElement(element: Element): SEpisode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a episode from the given element.
|
||||||
|
*
|
||||||
|
* @param element an element obtained from [episodeListSelector].
|
||||||
|
*/
|
||||||
|
protected abstract fun linkFromElement(element: Element): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response from the site and returns the page list.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
return pageListParse(response.asJsoup())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a page list from the given document.
|
||||||
|
*
|
||||||
|
* @param document the parsed document.
|
||||||
|
*/
|
||||||
|
protected abstract fun pageListParse(document: Document): List<Page>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the response from the site and returns the absolute url to the source image.
|
||||||
|
*
|
||||||
|
* @param response the response from the site.
|
||||||
|
*/
|
||||||
|
override fun imageUrlParse(response: Response): String {
|
||||||
|
return imageUrlParse(response.asJsoup())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the absolute url to the source image from the document.
|
||||||
|
*
|
||||||
|
* @param document the parsed document.
|
||||||
|
*/
|
||||||
|
protected abstract fun imageUrlParse(document: Document): String
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
|
||||||
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
|
|
||||||
object ExtensionGithubApi {
|
|
||||||
const val BASE_URL = "https://raw.githubusercontent.com"
|
|
||||||
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
|
|
||||||
|
|
||||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
|
||||||
return json
|
|
||||||
.filter { element ->
|
|
||||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
|
||||||
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
|
||||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
|
||||||
}
|
|
||||||
.map { element ->
|
|
||||||
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
|
|
||||||
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
|
|
||||||
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
|
|
||||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
|
||||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
|
||||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
|
||||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
|
||||||
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
|
||||||
|
|
||||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
|
||||||
val service: ExtensionGithubService = ExtensionGithubService.create()
|
|
||||||
|
|
||||||
val response = service.getRepo()
|
|
||||||
return parseResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getApkUrl(extension: ExtensionDataClass): String {
|
|
||||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
|
||||||
|
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to get the extension repo listing from GitHub.
|
|
||||||
*/
|
|
||||||
interface ExtensionGithubService {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val client by lazy {
|
|
||||||
val network: NetworkHelper by injectLazy()
|
|
||||||
network.client.newBuilder()
|
|
||||||
.addNetworkInterceptor { chain ->
|
|
||||||
val originalResponse = chain.proceed(chain.request())
|
|
||||||
originalResponse.newBuilder()
|
|
||||||
.header("Content-Encoding", "gzip")
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalSerializationApi
|
|
||||||
fun create(): ExtensionGithubService {
|
|
||||||
val adapter = Retrofit.Builder()
|
|
||||||
.baseUrl(ExtensionGithubApi.BASE_URL)
|
|
||||||
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
|
|
||||||
.client(client)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return adapter.create(ExtensionGithubService::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}/index.json.gz")
|
|
||||||
suspend fun getRepo(): JsonArray
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.model
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
|
|
||||||
sealed class Extension {
|
|
||||||
|
|
||||||
abstract val name: String
|
|
||||||
abstract val pkgName: String
|
|
||||||
abstract val versionName: String
|
|
||||||
abstract val versionCode: Int
|
|
||||||
abstract val lang: String?
|
|
||||||
abstract val isNsfw: Boolean
|
|
||||||
|
|
||||||
data class Installed(
|
|
||||||
override val name: String,
|
|
||||||
override val pkgName: String,
|
|
||||||
override val versionName: String,
|
|
||||||
override val versionCode: Int,
|
|
||||||
override val lang: String,
|
|
||||||
override val isNsfw: Boolean,
|
|
||||||
val sources: List<Source>,
|
|
||||||
val hasUpdate: Boolean = false,
|
|
||||||
val isObsolete: Boolean = false,
|
|
||||||
val isUnofficial: Boolean = false
|
|
||||||
) : Extension()
|
|
||||||
|
|
||||||
data class Available(
|
|
||||||
override val name: String,
|
|
||||||
override val pkgName: String,
|
|
||||||
override val versionName: String,
|
|
||||||
override val versionCode: Int,
|
|
||||||
override val lang: String,
|
|
||||||
override val isNsfw: Boolean,
|
|
||||||
val apkName: String,
|
|
||||||
val iconUrl: String
|
|
||||||
) : Extension()
|
|
||||||
|
|
||||||
data class Untrusted(
|
|
||||||
override val name: String,
|
|
||||||
override val pkgName: String,
|
|
||||||
override val versionName: String,
|
|
||||||
override val versionCode: Int,
|
|
||||||
val signatureHash: String,
|
|
||||||
override val lang: String? = null,
|
|
||||||
override val isNsfw: Boolean = false
|
|
||||||
) : Extension()
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.model
|
|
||||||
|
|
||||||
enum class InstallStep {
|
|
||||||
Pending, Downloading, Installing, Installed, Error;
|
|
||||||
|
|
||||||
fun isCompleted(): Boolean {
|
|
||||||
return this == Installed || this == Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.model
|
|
||||||
|
|
||||||
sealed class LoadResult {
|
|
||||||
|
|
||||||
class Success(val extension: Extension.Installed) : LoadResult()
|
|
||||||
class Untrusted(val extension: Extension.Untrusted) : LoadResult()
|
|
||||||
class Error(val message: String? = null) : LoadResult() {
|
|
||||||
constructor(exception: Throwable) : this(exception.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.util
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
// import android.annotation.SuppressLint
|
|
||||||
// import android.content.Context
|
|
||||||
// import android.content.pm.PackageInfo
|
|
||||||
// import android.content.pm.PackageManager
|
|
||||||
// import dalvik.system.PathClassLoader
|
|
||||||
// import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
|
||||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
// import eu.kanade.tachiyomi.util.lang.Hash
|
|
||||||
// import kotlinx.coroutines.async
|
|
||||||
// import kotlinx.coroutines.runBlocking
|
|
||||||
// import timber.log.Timber
|
|
||||||
// import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that handles the loading of the extensions installed in the system.
|
|
||||||
*/
|
|
||||||
// @SuppressLint("PackageManagerGetSignatures")
|
|
||||||
internal object ExtensionLoader {
|
|
||||||
|
|
||||||
// private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
// private val allowNsfwSource by lazy {
|
|
||||||
// preferences.allowNsfwSource().get()
|
|
||||||
// }
|
|
||||||
|
|
||||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
|
||||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
|
||||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
|
||||||
const val LIB_VERSION_MIN = 1.2
|
|
||||||
const val LIB_VERSION_MAX = 1.2
|
|
||||||
|
|
||||||
// private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
|
||||||
|
|
||||||
// inorichi's key
|
|
||||||
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
|
||||||
/**
|
|
||||||
* List of the trusted signatures.
|
|
||||||
*/
|
|
||||||
// var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a list of all the installed extensions initialized concurrently.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
*/
|
|
||||||
// fun loadExtensions(context: Context): List<LoadResult> {
|
|
||||||
// val pkgManager = context.packageManager
|
|
||||||
// val installedPkgs = pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
|
||||||
// val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
|
|
||||||
//
|
|
||||||
// if (extPkgs.isEmpty()) return emptyList()
|
|
||||||
//
|
|
||||||
// // Load each extension concurrently and wait for completion
|
|
||||||
// return runBlocking {
|
|
||||||
// val deferred = extPkgs.map {
|
|
||||||
// async { loadExtension(context, it.packageName, it) }
|
|
||||||
// }
|
|
||||||
// deferred.map { it.await() }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to load an extension from the given package name. It checks if the extension
|
|
||||||
* contains the required feature flag before trying to load it.
|
|
||||||
*/
|
|
||||||
// fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
|
|
||||||
// val pkgInfo = try {
|
|
||||||
// context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
|
||||||
// } catch (error: PackageManager.NameNotFoundException) {
|
|
||||||
// // Unlikely, but the package may have been uninstalled at this point
|
|
||||||
// return LoadResult.Error(error)
|
|
||||||
// }
|
|
||||||
// if (!isPackageAnExtension(pkgInfo)) {
|
|
||||||
// return LoadResult.Error("Tried to load a package that wasn't a extension")
|
|
||||||
// }
|
|
||||||
// return loadExtension(context, pkgName, pkgInfo)
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an extension given its package name.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
* @param pkgName The package name of the extension to load.
|
|
||||||
* @param pkgInfo The package info of the extension.
|
|
||||||
*/
|
|
||||||
// private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): LoadResult {
|
|
||||||
// val pkgManager = context.packageManager
|
|
||||||
//
|
|
||||||
// val appInfo = try {
|
|
||||||
// pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
|
||||||
// } catch (error: PackageManager.NameNotFoundException) {
|
|
||||||
// // Unlikely, but the package may have been uninstalled at this point
|
|
||||||
// return LoadResult.Error(error)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
|
||||||
// val versionName = pkgInfo.versionName
|
|
||||||
// val versionCode = pkgInfo.versionCode
|
|
||||||
//
|
|
||||||
// if (versionName.isNullOrEmpty()) {
|
|
||||||
// val exception = Exception("Missing versionName for extension $extName")
|
|
||||||
// Timber.w(exception)
|
|
||||||
// return LoadResult.Error(exception)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Validate lib version
|
|
||||||
// val libVersion = versionName.substringBeforeLast('.').toDouble()
|
|
||||||
// if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
|
||||||
// val exception = Exception(
|
|
||||||
// "Lib version is $libVersion, while only versions " +
|
|
||||||
// "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
|
||||||
// )
|
|
||||||
// Timber.w(exception)
|
|
||||||
// return LoadResult.Error(exception)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val signatureHash = getSignatureHash(pkgInfo)
|
|
||||||
//
|
|
||||||
// if (signatureHash == null) {
|
|
||||||
// return LoadResult.Error("Package $pkgName isn't signed")
|
|
||||||
// } else if (signatureHash !in trustedSignatures) {
|
|
||||||
// val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
|
|
||||||
// Timber.w("Extension $pkgName isn't trusted")
|
|
||||||
// return LoadResult.Untrusted(extension)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
|
||||||
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
|
|
||||||
// return LoadResult.Error("NSFW extension $pkgName not allowed")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
|
||||||
//
|
|
||||||
// val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
|
||||||
// .split(";")
|
|
||||||
// .map {
|
|
||||||
// val sourceClass = it.trim()
|
|
||||||
// if (sourceClass.startsWith(".")) {
|
|
||||||
// pkgInfo.packageName + sourceClass
|
|
||||||
// } else {
|
|
||||||
// sourceClass
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .flatMap {
|
|
||||||
// try {
|
|
||||||
// when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
|
||||||
// is Source -> listOf(obj)
|
|
||||||
// is SourceFactory -> {
|
|
||||||
// if (isSourceNsfw(obj)) {
|
|
||||||
// emptyList()
|
|
||||||
// } else {
|
|
||||||
// obj.createSources()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
|
||||||
// }
|
|
||||||
// } catch (e: Throwable) {
|
|
||||||
// Timber.e(e, "Extension load error: $extName.")
|
|
||||||
// return LoadResult.Error(e)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .filter { !isSourceNsfw(it) }
|
|
||||||
//
|
|
||||||
// val langs = sources.filterIsInstance<CatalogueSource>()
|
|
||||||
// .map { it.lang }
|
|
||||||
// .toSet()
|
|
||||||
// val lang = when (langs.size) {
|
|
||||||
// 0 -> ""
|
|
||||||
// 1 -> langs.first()
|
|
||||||
// else -> "all"
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val extension = Extension.Installed(
|
|
||||||
// extName,
|
|
||||||
// pkgName,
|
|
||||||
// versionName,
|
|
||||||
// versionCode,
|
|
||||||
// lang,
|
|
||||||
// isNsfw,
|
|
||||||
// sources,
|
|
||||||
// isUnofficial = signatureHash != officialSignature
|
|
||||||
// )
|
|
||||||
// return LoadResult.Success(extension)
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given package is an extension.
|
|
||||||
*
|
|
||||||
* @param pkgInfo The package info of the application.
|
|
||||||
*/
|
|
||||||
// private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
|
|
||||||
// return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the signature hash of the package or null if it's not signed.
|
|
||||||
*
|
|
||||||
* @param pkgInfo The package info of the application.
|
|
||||||
*/
|
|
||||||
// private fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
|
||||||
// val signatures = pkgInfo.signatures
|
|
||||||
// return if (signatures != null && signatures.isNotEmpty()) {
|
|
||||||
// Hash.sha256(signatures.first().toByteArray())
|
|
||||||
// } else {
|
|
||||||
// null
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
|
|
||||||
*/
|
|
||||||
// private fun isSourceNsfw(clazz: Any): Boolean {
|
|
||||||
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (clazz !is Source && clazz !is SourceFactory) {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Annotations are proxied, hence this janky way of checking for them
|
|
||||||
// return clazz.javaClass.annotations
|
|
||||||
// .flatMap { it.javaClass.interfaces.map { it.simpleName } }
|
|
||||||
// .firstOrNull { it == Nsfw::class.java.simpleName } != null
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
// import android.graphics.drawable.Drawable
|
|
||||||
// import eu.kanade.tachiyomi.extension.ExtensionManager
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
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 rx.Observable
|
import rx.Observable
|
||||||
// import uy.kohesive.injekt.Injekt
|
|
||||||
// import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
// import android.content.Context
|
import android.content.Context
|
||||||
// import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
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 eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
open class SourceManager() {
|
open class SourceManager(private val context: Context) {
|
||||||
|
|
||||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package ir.armor.tachidesk
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import ir.armor.tachidesk.server.JavalinSetup.javalinSetup
|
|
||||||
import ir.armor.tachidesk.server.applicationSetup
|
|
||||||
|
|
||||||
class Main {
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
applicationSetup()
|
|
||||||
javalinSetup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
|
||||||
import ir.armor.tachidesk.model.database.table.ChapterTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.PageTable
|
|
||||||
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
|
|
||||||
import org.jetbrains.exposed.sql.and
|
|
||||||
import org.jetbrains.exposed.sql.insert
|
|
||||||
import org.jetbrains.exposed.sql.select
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.sql.update
|
|
||||||
|
|
||||||
object Chapter {
|
|
||||||
/** get chapter list when showing a manga */
|
|
||||||
suspend fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
|
||||||
val mangaDetails = getManga(mangaId)
|
|
||||||
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
|
||||||
|
|
||||||
val chapterList = source.fetchChapterList(
|
|
||||||
SManga.create().apply {
|
|
||||||
title = mangaDetails.title
|
|
||||||
url = mangaDetails.url
|
|
||||||
}
|
|
||||||
).awaitSingle()
|
|
||||||
|
|
||||||
val chapterCount = chapterList.count()
|
|
||||||
|
|
||||||
return transaction {
|
|
||||||
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
|
||||||
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
|
||||||
if (chapterEntry == null) {
|
|
||||||
ChapterTable.insert {
|
|
||||||
it[url] = fetchedChapter.url
|
|
||||||
it[name] = fetchedChapter.name
|
|
||||||
it[date_upload] = fetchedChapter.date_upload
|
|
||||||
it[chapter_number] = fetchedChapter.chapter_number
|
|
||||||
it[scanlator] = fetchedChapter.scanlator
|
|
||||||
|
|
||||||
it[chapterIndex] = index + 1
|
|
||||||
it[manga] = mangaId
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
|
||||||
it[name] = fetchedChapter.name
|
|
||||||
it[date_upload] = fetchedChapter.date_upload
|
|
||||||
it[chapter_number] = fetchedChapter.chapter_number
|
|
||||||
it[scanlator] = fetchedChapter.scanlator
|
|
||||||
|
|
||||||
it[chapterIndex] = index + 1
|
|
||||||
it[manga] = mangaId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear any orphaned chapters that are in the db but not in `chapterList`
|
|
||||||
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
|
|
||||||
if (dbChapterCount > chapterCount) { // we got some clean up due
|
|
||||||
// TODO: delete orphan chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
chapterList.mapIndexed { index, it ->
|
|
||||||
ChapterDataClass(
|
|
||||||
it.url,
|
|
||||||
it.name,
|
|
||||||
it.date_upload,
|
|
||||||
it.chapter_number,
|
|
||||||
it.scanlator,
|
|
||||||
mangaId,
|
|
||||||
chapterCount - index,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** used to display a chapter, get a chapter in order to show it's pages */
|
|
||||||
suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
|
||||||
val chapterEntry = transaction {
|
|
||||||
ChapterTable.select {
|
|
||||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
|
||||||
}.firstOrNull()!!
|
|
||||||
}
|
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
|
||||||
|
|
||||||
val pageList = source.fetchPageList(
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = chapterEntry[ChapterTable.url]
|
|
||||||
name = chapterEntry[ChapterTable.name]
|
|
||||||
}
|
|
||||||
).awaitSingle()
|
|
||||||
|
|
||||||
val chapterId = chapterEntry[ChapterTable.id].value
|
|
||||||
val chapterCount = transaction { ChapterTable.selectAll().count() }
|
|
||||||
|
|
||||||
// update page list for this chapter
|
|
||||||
transaction {
|
|
||||||
pageList.forEach { page ->
|
|
||||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
|
||||||
if (pageEntry == null) {
|
|
||||||
PageTable.insert {
|
|
||||||
it[index] = page.index
|
|
||||||
it[url] = page.url
|
|
||||||
it[imageUrl] = page.imageUrl
|
|
||||||
it[chapter] = chapterId
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
|
|
||||||
it[url] = page.url
|
|
||||||
it[imageUrl] = page.imageUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ChapterDataClass(
|
|
||||||
chapterEntry[ChapterTable.url],
|
|
||||||
chapterEntry[ChapterTable.name],
|
|
||||||
chapterEntry[ChapterTable.date_upload],
|
|
||||||
chapterEntry[ChapterTable.chapter_number],
|
|
||||||
chapterEntry[ChapterTable.scanlator],
|
|
||||||
mangaId,
|
|
||||||
chapterEntry[ChapterTable.chapterIndex],
|
|
||||||
chapterCount.toInt(),
|
|
||||||
pageList.count()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +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 eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import java.net.URLEncoder
|
|
||||||
|
|
||||||
// TODO: finish MangaDex support
|
|
||||||
class MangaDexHelper(private val mangaDexSource: HttpSource) {
|
|
||||||
|
|
||||||
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
|
|
||||||
|
|
||||||
private fun clientBuilder(
|
|
||||||
r18Toggle: Int,
|
|
||||||
okHttpClient: OkHttpClient = mangaDexSource.network.client
|
|
||||||
): OkHttpClient = okHttpClient.newBuilder()
|
|
||||||
.addNetworkInterceptor { chain ->
|
|
||||||
val originalCookies = chain.request().header("Cookie") ?: ""
|
|
||||||
val newReq = chain
|
|
||||||
.request()
|
|
||||||
.newBuilder()
|
|
||||||
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
|
|
||||||
.build()
|
|
||||||
chain.proceed(newReq)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
private fun cookiesHeader(r18Toggle: Int): String {
|
|
||||||
val cookies = mutableMapOf<String, String>()
|
|
||||||
cookies["mangadex_h_toggle"] = r18Toggle.toString()
|
|
||||||
return buildCookies(cookies)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildCookies(cookies: Map<String, String>) =
|
|
||||||
cookies.entries.joinToString(separator = "; ", postfix = ";") {
|
|
||||||
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// fun isLogged(): Boolean {
|
|
||||||
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
|
|
||||||
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
|
|
||||||
val formBody = FormBody.Builder()
|
|
||||||
.add("login_username", username)
|
|
||||||
.add("login_password", password)
|
|
||||||
.add("no_js", "1")
|
|
||||||
.add("remember_me", "1")
|
|
||||||
|
|
||||||
twoFactorCode.let {
|
|
||||||
formBody.add("two_factor", it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = clientBuilder().newCall(
|
|
||||||
POST(
|
|
||||||
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
|
|
||||||
mangaDexSource.headers,
|
|
||||||
formBody.build()
|
|
||||||
)
|
|
||||||
).execute()
|
|
||||||
return response.body!!.string().isEmpty()
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// fun logout(): Boolean {
|
|
||||||
// return withContext(Dispatchers.IO) {
|
|
||||||
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
|
|
||||||
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
|
|
||||||
// val listOfDexCookies = network.cookieManager.get(httpUrl)
|
|
||||||
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
|
|
||||||
// val token = cookie?.value
|
|
||||||
// if (token.isNullOrEmpty()) {
|
|
||||||
// return@withContext true
|
|
||||||
// }
|
|
||||||
// val result = clientBuilder().newCall(
|
|
||||||
// POSTWithCookie(
|
|
||||||
// "$baseUrl/ajax/actions.ajax.php?function=logout",
|
|
||||||
// REMEMBER_ME,
|
|
||||||
// token,
|
|
||||||
// headers
|
|
||||||
// )
|
|
||||||
// ).execute()
|
|
||||||
// val resultStr = result.body!!.string()
|
|
||||||
// if (resultStr.contains("success", true)) {
|
|
||||||
// network.cookieManager.remove(httpUrl)
|
|
||||||
// return@withContext true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// false
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import ir.armor.tachidesk.model.database.migration.lib.Migration
|
|
||||||
import org.jetbrains.exposed.dao.id.IdTable
|
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
class M0001_Initial : Migration() {
|
|
||||||
private object ExtensionTable : IntIdTable() {
|
|
||||||
val apkName = varchar("apk_name", 1024)
|
|
||||||
|
|
||||||
// default is the local source icon from tachiyomi
|
|
||||||
val iconUrl = varchar("icon_url", 2048)
|
|
||||||
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
|
||||||
|
|
||||||
val name = varchar("name", 128)
|
|
||||||
val pkgName = varchar("pkg_name", 128)
|
|
||||||
val versionName = varchar("version_name", 16)
|
|
||||||
val versionCode = integer("version_code")
|
|
||||||
val lang = varchar("lang", 10)
|
|
||||||
val isNsfw = bool("is_nsfw")
|
|
||||||
|
|
||||||
val isInstalled = bool("is_installed").default(false)
|
|
||||||
val hasUpdate = bool("has_update").default(false)
|
|
||||||
val isObsolete = bool("is_obsolete").default(false)
|
|
||||||
|
|
||||||
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
|
|
||||||
}
|
|
||||||
|
|
||||||
private object SourceTable : IdTable<Long>() {
|
|
||||||
override val id = long("id").entityId()
|
|
||||||
val name = varchar("name", 128)
|
|
||||||
val lang = varchar("lang", 10)
|
|
||||||
val extension = reference("extension", ExtensionTable)
|
|
||||||
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private object MangaTable : IntIdTable() {
|
|
||||||
val url = varchar("url", 2048)
|
|
||||||
val title = varchar("title", 512)
|
|
||||||
val initialized = bool("initialized").default(false)
|
|
||||||
|
|
||||||
val artist = varchar("artist", 64).nullable()
|
|
||||||
val author = varchar("author", 64).nullable()
|
|
||||||
val description = varchar("description", 4096).nullable()
|
|
||||||
val genre = varchar("genre", 1024).nullable()
|
|
||||||
|
|
||||||
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
|
|
||||||
val status = integer("status").default(SManga.UNKNOWN)
|
|
||||||
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
|
||||||
|
|
||||||
val inLibrary = bool("in_library").default(false)
|
|
||||||
val defaultCategory = bool("default_category").default(true)
|
|
||||||
|
|
||||||
// source is used by some ancestor of IntIdTable
|
|
||||||
val sourceReference = long("source")
|
|
||||||
}
|
|
||||||
|
|
||||||
private object ChapterTable : IntIdTable() {
|
|
||||||
val url = varchar("url", 2048)
|
|
||||||
val name = varchar("name", 512)
|
|
||||||
val date_upload = long("date_upload").default(0)
|
|
||||||
val chapter_number = float("chapter_number").default(-1f)
|
|
||||||
val scanlator = varchar("scanlator", 128).nullable()
|
|
||||||
|
|
||||||
val isRead = bool("read").default(false)
|
|
||||||
val isBookmarked = bool("bookmark").default(false)
|
|
||||||
val lastPageRead = integer("last_page_read").default(0)
|
|
||||||
|
|
||||||
val chapterIndex = integer("number_in_list")
|
|
||||||
|
|
||||||
val manga = reference("manga", MangaTable)
|
|
||||||
}
|
|
||||||
|
|
||||||
private object PageTable : IntIdTable() {
|
|
||||||
val index = integer("index")
|
|
||||||
val url = varchar("url", 2048)
|
|
||||||
val imageUrl = varchar("imageUrl", 2048).nullable()
|
|
||||||
|
|
||||||
val chapter = reference("chapter", ChapterTable)
|
|
||||||
}
|
|
||||||
|
|
||||||
private object CategoryTable : IntIdTable() {
|
|
||||||
val name = varchar("name", 64)
|
|
||||||
val isLanding = bool("is_landing").default(false)
|
|
||||||
val order = integer("order").default(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private object CategoryMangaTable : IntIdTable() {
|
|
||||||
val category = reference("category", ir.armor.tachidesk.model.database.table.CategoryTable)
|
|
||||||
val manga = reference("manga", ir.armor.tachidesk.model.database.table.MangaTable)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
transaction {
|
|
||||||
SchemaUtils.create(
|
|
||||||
ExtensionTable,
|
|
||||||
ExtensionTable,
|
|
||||||
SourceTable,
|
|
||||||
MangaTable,
|
|
||||||
ChapterTable,
|
|
||||||
PageTable,
|
|
||||||
CategoryTable,
|
|
||||||
CategoryMangaTable,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package ir.armor.tachidesk.model.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 isRead = bool("read").default(false)
|
|
||||||
val isBookmarked = bool("bookmark").default(false)
|
|
||||||
val lastPageRead = integer("last_page_read").default(0)
|
|
||||||
|
|
||||||
val chapterIndex = integer("number_in_list")
|
|
||||||
|
|
||||||
val manga = reference("manga", MangaTable)
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package ir.armor.tachidesk.server.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 dorkbox.systemTray.MenuItem
|
|
||||||
import dorkbox.systemTray.SystemTray
|
|
||||||
import dorkbox.systemTray.SystemTray.TrayType
|
|
||||||
import dorkbox.util.CacheUtil
|
|
||||||
import dorkbox.util.Desktop
|
|
||||||
import ir.armor.tachidesk.Main
|
|
||||||
import ir.armor.tachidesk.server.BuildConfig
|
|
||||||
import ir.armor.tachidesk.server.serverConfig
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
fun openInBrowser() {
|
|
||||||
try {
|
|
||||||
Desktop.browseURL("http://127.0.0.1:4567")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun systemTray(): SystemTray? {
|
|
||||||
try {
|
|
||||||
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
|
||||||
SystemTray.DEBUG = serverConfig.debugLogsEnabled
|
|
||||||
if (System.getProperty("os.name").startsWith("Windows"))
|
|
||||||
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
|
||||||
|
|
||||||
CacheUtil.clear(BuildConfig.name)
|
|
||||||
|
|
||||||
val systemTray = SystemTray.get(BuildConfig.name) ?: return null
|
|
||||||
val mainMenu = systemTray.menu
|
|
||||||
|
|
||||||
mainMenu.add(
|
|
||||||
MenuItem(
|
|
||||||
"Open Tachidesk"
|
|
||||||
) {
|
|
||||||
openInBrowser()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
|
|
||||||
|
|
||||||
// systemTray.setTooltip("Tachidesk")
|
|
||||||
systemTray.setImage(icon)
|
|
||||||
// systemTray.status = "No Mail"
|
|
||||||
|
|
||||||
mainMenu.add(
|
|
||||||
MenuItem("Quit") {
|
|
||||||
systemTray.shutdown()
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
systemTray.installShutdownHook()
|
|
||||||
|
|
||||||
return systemTray
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package suwayomi
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import suwayomi.server.JavalinSetup.javalinSetup
|
||||||
|
import suwayomi.server.applicationSetup
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
applicationSetup()
|
||||||
|
javalinSetup()
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
package suwayomi.anime
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 io.javalin.Javalin
|
||||||
|
import suwayomi.anime.impl.Anime.getAnime
|
||||||
|
import suwayomi.anime.impl.Anime.getAnimeThumbnail
|
||||||
|
import suwayomi.anime.impl.AnimeList.getAnimeList
|
||||||
|
import suwayomi.anime.impl.Episode.getEpisode
|
||||||
|
import suwayomi.anime.impl.Episode.getEpisodeList
|
||||||
|
import suwayomi.anime.impl.Episode.modifyEpisode
|
||||||
|
import suwayomi.anime.impl.Source.getAnimeSource
|
||||||
|
import suwayomi.anime.impl.Source.getSourceList
|
||||||
|
import suwayomi.anime.impl.extension.Extension.getExtensionIcon
|
||||||
|
import suwayomi.anime.impl.extension.Extension.installExtension
|
||||||
|
import suwayomi.anime.impl.extension.Extension.uninstallExtension
|
||||||
|
import suwayomi.anime.impl.extension.Extension.updateExtension
|
||||||
|
import suwayomi.anime.impl.extension.ExtensionsList.getExtensionList
|
||||||
|
import suwayomi.server.JavalinSetup.future
|
||||||
|
|
||||||
|
object AnimeAPI {
|
||||||
|
fun defineEndpoints(app: Javalin) {
|
||||||
|
// list all extensions
|
||||||
|
app.get("/api/v1/anime/extension/list") { ctx ->
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getExtensionList()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// install extension identified with "pkgName"
|
||||||
|
app.get("/api/v1/anime/extension/install/:pkgName") { ctx ->
|
||||||
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
installExtension(pkgName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update extension identified with "pkgName"
|
||||||
|
app.get("/api/v1/anime/extension/update/:pkgName") { ctx ->
|
||||||
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
updateExtension(pkgName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// uninstall extension identified with "pkgName"
|
||||||
|
app.get("/api/v1/anime/extension/uninstall/:pkgName") { ctx ->
|
||||||
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
|
uninstallExtension(pkgName)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// icon for extension named `apkName`
|
||||||
|
app.get("/api/v1/anime/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
|
||||||
|
val apkName = ctx.pathParam("apkName")
|
||||||
|
|
||||||
|
ctx.result(
|
||||||
|
future { getExtensionIcon(apkName) }
|
||||||
|
.thenApply {
|
||||||
|
ctx.header("content-type", it.second)
|
||||||
|
it.first
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// list of sources
|
||||||
|
app.get("/api/v1/anime/source/list") { ctx ->
|
||||||
|
ctx.json(getSourceList())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch source with id `sourceId`
|
||||||
|
app.get("/api/v1/anime/source/:sourceId") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
ctx.json(getAnimeSource(sourceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// popular animes from source with id `sourceId`
|
||||||
|
app.get("/api/v1/anime/source/:sourceId/popular/:pageNum") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getAnimeList(sourceId, pageNum, popular = true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// latest animes from source with id `sourceId`
|
||||||
|
app.get("/api/v1/anime/source/:sourceId/latest/:pageNum") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getAnimeList(sourceId, pageNum, popular = false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get anime info
|
||||||
|
app.get("/api/v1/anime/anime/:animeId/") { ctx ->
|
||||||
|
val animeId = ctx.pathParam("animeId").toInt()
|
||||||
|
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
|
||||||
|
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getAnime(animeId, onlineFetch)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// anime thumbnail
|
||||||
|
app.get("api/v1/anime/anime/:animeId/thumbnail") { ctx ->
|
||||||
|
val animeId = ctx.pathParam("animeId").toInt()
|
||||||
|
|
||||||
|
ctx.result(
|
||||||
|
future { getAnimeThumbnail(animeId) }
|
||||||
|
.thenApply {
|
||||||
|
ctx.header("content-type", it.second)
|
||||||
|
it.first
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// // list manga's categories
|
||||||
|
// app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
// ctx.json(getMangaCategories(mangaId))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // adds the manga to category
|
||||||
|
// app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// addMangaToCategory(mangaId, categoryId)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // removes the manga from the category
|
||||||
|
// app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// removeMangaFromCategory(mangaId, categoryId)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// get episode list when showing a anime
|
||||||
|
app.get("/api/v1/anime/anime/:animeId/episodes") { ctx ->
|
||||||
|
val animeId = ctx.pathParam("animeId").toInt()
|
||||||
|
|
||||||
|
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean()
|
||||||
|
|
||||||
|
ctx.json(future { getEpisodeList(animeId, onlineFetch) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// used to display a episode, get a episode in order to show it's <Quality pending>
|
||||||
|
app.get("/api/v1/anime/anime/:animeId/episode/:episodeIndex") { ctx ->
|
||||||
|
val episodeIndex = ctx.pathParam("episodeIndex").toInt()
|
||||||
|
val animeId = ctx.pathParam("animeId").toInt()
|
||||||
|
ctx.json(future { getEpisode(episodeIndex, animeId) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// used to modify a episode's parameters
|
||||||
|
app.patch("/api/v1/anime/anime/:animeId/episode/:episodeIndex") { ctx ->
|
||||||
|
val episodeIndex = ctx.pathParam("episodeIndex").toInt()
|
||||||
|
val animeId = ctx.pathParam("animeId").toInt()
|
||||||
|
|
||||||
|
val read = ctx.formParam("read")?.toBoolean()
|
||||||
|
val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
|
||||||
|
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
|
||||||
|
val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
|
||||||
|
|
||||||
|
modifyEpisode(animeId, episodeIndex, read, bookmarked, markPrevRead, lastPageRead)
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// // get page at index "index"
|
||||||
|
// app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
// val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
|
// val index = ctx.pathParam("index").toInt()
|
||||||
|
//
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future { getPageImage(mangaId, chapterIndex, index) }
|
||||||
|
// .thenApply {
|
||||||
|
// ctx.header("content-type", it.second)
|
||||||
|
// it.first
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // submit a chapter for download
|
||||||
|
// app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||||
|
// // TODO
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // cancel a chapter download
|
||||||
|
// app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||||
|
// // TODO
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // global search, Not implemented yet
|
||||||
|
// app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||||
|
// val searchTerm = ctx.pathParam("searchTerm")
|
||||||
|
// ctx.json(sourceGlobalSearch(searchTerm))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // single source search
|
||||||
|
// app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
||||||
|
// val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
// val searchTerm = ctx.pathParam("searchTerm")
|
||||||
|
// val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
|
// ctx.json(JavalinSetup.future { sourceSearch(sourceId, searchTerm, pageNum) })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // source filter list
|
||||||
|
// app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||||
|
// val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
// ctx.json(sourceFilters(sourceId))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // adds the manga to library
|
||||||
|
// app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
//
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future { addMangaToLibrary(mangaId) }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // removes the manga from the library
|
||||||
|
// app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
//
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future { removeMangaFromLibrary(mangaId) }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // lists mangas that have no category assigned
|
||||||
|
// app.get("/api/v1/library/") { ctx ->
|
||||||
|
// ctx.json(getLibraryMangas())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // category list
|
||||||
|
// app.get("/api/v1/category/") { ctx ->
|
||||||
|
// ctx.json(Category.getCategoryList())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // category create
|
||||||
|
// app.post("/api/v1/category/") { ctx ->
|
||||||
|
// val name = ctx.formParam("name")!!
|
||||||
|
// Category.createCategory(name)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // returns some static info of the current app build
|
||||||
|
// app.get("/api/v1/about/") { ctx ->
|
||||||
|
// ctx.json(About.getAbout())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // category modification
|
||||||
|
// app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// val name = ctx.formParam("name")
|
||||||
|
// val isDefault = ctx.formParam("default")?.toBoolean()
|
||||||
|
// Category.updateCategory(categoryId, name, isDefault)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // category re-ordering
|
||||||
|
// app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// val from = ctx.formParam("from")!!.toInt()
|
||||||
|
// val to = ctx.formParam("to")!!.toInt()
|
||||||
|
// Category.reorderCategory(categoryId, from, to)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // category delete
|
||||||
|
// app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// Category.removeCategory(categoryId)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // returns the manga list associated with a category
|
||||||
|
// app.get("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// ctx.json(getCategoryMangaList(categoryId))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // expects a Tachiyomi legacy backup json in the body
|
||||||
|
// app.post("/api/v1/backup/legacy/import") { ctx ->
|
||||||
|
// ctx.result(
|
||||||
|
// future {
|
||||||
|
// restoreLegacyBackup(ctx.bodyAsInputStream())
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json"
|
||||||
|
// app.post("/api/v1/backup/legacy/import/file") { ctx ->
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future {
|
||||||
|
// restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // returns a Tachiyomi legacy backup json created from the current database as a json body
|
||||||
|
// app.get("/api/v1/backup/legacy/export") { ctx ->
|
||||||
|
// ctx.contentType("application/json")
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future {
|
||||||
|
// createLegacyBackup(
|
||||||
|
// BackupFlags(
|
||||||
|
// includeManga = true,
|
||||||
|
// includeCategories = true,
|
||||||
|
// includeChapters = true,
|
||||||
|
// includeTracking = true,
|
||||||
|
// includeHistory = true,
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // returns a Tachiyomi legacy backup json created from the current database as a file
|
||||||
|
// app.get("/api/v1/backup/legacy/export/file") { ctx ->
|
||||||
|
// ctx.contentType("application/json")
|
||||||
|
// val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm")
|
||||||
|
// val currentDate = sdf.format(Date())
|
||||||
|
//
|
||||||
|
// ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future {
|
||||||
|
// createLegacyBackup(
|
||||||
|
// BackupFlags(
|
||||||
|
// includeManga = true,
|
||||||
|
// includeCategories = true,
|
||||||
|
// includeChapters = true,
|
||||||
|
// includeTracking = true,
|
||||||
|
// includeHistory = true,
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Download queue stats
|
||||||
|
// app.ws("/api/v1/downloads") { ws ->
|
||||||
|
// ws.onConnect { ctx ->
|
||||||
|
// // TODO: send current stat
|
||||||
|
// // TODO: add to downlad subscribers
|
||||||
|
// }
|
||||||
|
// ws.onMessage {
|
||||||
|
// // TODO: send current stat
|
||||||
|
// }
|
||||||
|
// ws.onClose { ctx ->
|
||||||
|
// // TODO: remove from subscribers
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package suwayomi.anime.impl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import suwayomi.anime.impl.AnimeList.proxyThumbnailUrl
|
||||||
|
import suwayomi.anime.impl.Source.getAnimeSource
|
||||||
|
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||||
|
import suwayomi.anime.model.dataclass.AnimeDataClass
|
||||||
|
import suwayomi.anime.model.table.AnimeStatus
|
||||||
|
import suwayomi.anime.model.table.AnimeTable
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import suwayomi.tachidesk.impl.util.network.await
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
object Anime {
|
||||||
|
private fun truncate(text: String?, maxLength: Int): String? {
|
||||||
|
return if (text?.length ?: 0 > maxLength)
|
||||||
|
text?.take(maxLength - 3) + "..."
|
||||||
|
else
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getAnime(animeId: Int, onlineFetch: Boolean = false): AnimeDataClass {
|
||||||
|
var animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
|
||||||
|
|
||||||
|
return if (animeEntry[AnimeTable.initialized] && !onlineFetch) {
|
||||||
|
AnimeDataClass(
|
||||||
|
animeId,
|
||||||
|
animeEntry[AnimeTable.sourceReference].toString(),
|
||||||
|
|
||||||
|
animeEntry[AnimeTable.url],
|
||||||
|
animeEntry[AnimeTable.title],
|
||||||
|
proxyThumbnailUrl(animeId),
|
||||||
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
animeEntry[AnimeTable.artist],
|
||||||
|
animeEntry[AnimeTable.author],
|
||||||
|
animeEntry[AnimeTable.description],
|
||||||
|
animeEntry[AnimeTable.genre],
|
||||||
|
AnimeStatus.valueOf(animeEntry[AnimeTable.status]).name,
|
||||||
|
animeEntry[AnimeTable.inLibrary],
|
||||||
|
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
} else { // initialize anime
|
||||||
|
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
|
||||||
|
val fetchedAnime = source.fetchAnimeDetails(
|
||||||
|
SAnime.create().apply {
|
||||||
|
url = animeEntry[AnimeTable.url]
|
||||||
|
title = animeEntry[AnimeTable.title]
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
AnimeTable.update({ AnimeTable.id eq animeId }) {
|
||||||
|
|
||||||
|
it[AnimeTable.initialized] = true
|
||||||
|
|
||||||
|
it[AnimeTable.artist] = fetchedAnime.artist
|
||||||
|
it[AnimeTable.author] = fetchedAnime.author
|
||||||
|
it[AnimeTable.description] = truncate(fetchedAnime.description, 4096)
|
||||||
|
it[AnimeTable.genre] = fetchedAnime.genre
|
||||||
|
it[AnimeTable.status] = fetchedAnime.status
|
||||||
|
if (fetchedAnime.thumbnail_url != null && fetchedAnime.thumbnail_url.orEmpty().isNotEmpty())
|
||||||
|
it[AnimeTable.thumbnail_url] = fetchedAnime.thumbnail_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAnimeThumbnail(animeId)
|
||||||
|
|
||||||
|
animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
|
||||||
|
|
||||||
|
AnimeDataClass(
|
||||||
|
animeId,
|
||||||
|
animeEntry[AnimeTable.sourceReference].toString(),
|
||||||
|
|
||||||
|
animeEntry[AnimeTable.url],
|
||||||
|
animeEntry[AnimeTable.title],
|
||||||
|
proxyThumbnailUrl(animeId),
|
||||||
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
fetchedAnime.artist,
|
||||||
|
fetchedAnime.author,
|
||||||
|
fetchedAnime.description,
|
||||||
|
fetchedAnime.genre,
|
||||||
|
AnimeStatus.valueOf(fetchedAnime.status).name,
|
||||||
|
animeEntry[AnimeTable.inLibrary],
|
||||||
|
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
suspend fun getAnimeThumbnail(animeId: Int): Pair<InputStream, String> {
|
||||||
|
val saveDir = applicationDirs.animeThumbnailsRoot
|
||||||
|
val fileName = animeId.toString()
|
||||||
|
|
||||||
|
return getCachedImageResponse(saveDir, fileName) {
|
||||||
|
getAnime(animeId) // make sure is initialized
|
||||||
|
|
||||||
|
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
|
||||||
|
|
||||||
|
val sourceId = animeEntry[AnimeTable.sourceReference]
|
||||||
|
val source = getAnimeHttpSource(sourceId)
|
||||||
|
|
||||||
|
val thumbnailUrl = animeEntry[AnimeTable.thumbnail_url]!!
|
||||||
|
|
||||||
|
source.client.newCall(
|
||||||
|
GET(thumbnailUrl, source.headers)
|
||||||
|
).await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearAnimeThumbnail(animeId: Int) {
|
||||||
|
val saveDir = applicationDirs.animeThumbnailsRoot
|
||||||
|
val fileName = animeId.toString()
|
||||||
|
|
||||||
|
clearCachedImage(saveDir, fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package suwayomi.anime.impl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||||
|
import suwayomi.anime.model.dataclass.AnimeDataClass
|
||||||
|
import suwayomi.anime.model.dataclass.PagedAnimeListDataClass
|
||||||
|
import suwayomi.anime.model.table.AnimeStatus
|
||||||
|
import suwayomi.anime.model.table.AnimeTable
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
|
||||||
|
object AnimeList {
|
||||||
|
fun proxyThumbnailUrl(animeId: Int): String {
|
||||||
|
return "/api/v1/anime/anime/$animeId/thumbnail"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getAnimeList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedAnimeListDataClass {
|
||||||
|
val source = getAnimeHttpSource(sourceId)
|
||||||
|
val animesPage = if (popular) {
|
||||||
|
source.fetchPopularAnime(pageNum).awaitSingle()
|
||||||
|
} else {
|
||||||
|
if (source.supportsLatest)
|
||||||
|
source.fetchLatestUpdates(pageNum).awaitSingle()
|
||||||
|
else
|
||||||
|
throw Exception("Source $source doesn't support latest")
|
||||||
|
}
|
||||||
|
return animesPage.processEntries(sourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AnimesPage.processEntries(sourceId: Long): PagedAnimeListDataClass {
|
||||||
|
val animesPage = this
|
||||||
|
val animeList = transaction {
|
||||||
|
return@transaction animesPage.animes.map { anime ->
|
||||||
|
val animeEntry = AnimeTable.select { AnimeTable.url eq anime.url }.firstOrNull()
|
||||||
|
if (animeEntry == null) { // create anime entry
|
||||||
|
val animeId = AnimeTable.insertAndGetId {
|
||||||
|
it[url] = anime.url
|
||||||
|
it[title] = anime.title
|
||||||
|
|
||||||
|
it[artist] = anime.artist
|
||||||
|
it[author] = anime.author
|
||||||
|
it[description] = anime.description
|
||||||
|
it[genre] = anime.genre
|
||||||
|
it[status] = anime.status
|
||||||
|
it[thumbnail_url] = anime.thumbnail_url
|
||||||
|
|
||||||
|
it[sourceReference] = sourceId
|
||||||
|
}.value
|
||||||
|
|
||||||
|
AnimeDataClass(
|
||||||
|
animeId,
|
||||||
|
sourceId.toString(),
|
||||||
|
|
||||||
|
anime.url,
|
||||||
|
anime.title,
|
||||||
|
proxyThumbnailUrl(animeId),
|
||||||
|
|
||||||
|
anime.initialized,
|
||||||
|
|
||||||
|
anime.artist,
|
||||||
|
anime.author,
|
||||||
|
anime.description,
|
||||||
|
anime.genre,
|
||||||
|
AnimeStatus.valueOf(anime.status).name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val animeId = animeEntry[AnimeTable.id].value
|
||||||
|
AnimeDataClass(
|
||||||
|
animeId,
|
||||||
|
sourceId.toString(),
|
||||||
|
|
||||||
|
anime.url,
|
||||||
|
anime.title,
|
||||||
|
proxyThumbnailUrl(animeId),
|
||||||
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
animeEntry[AnimeTable.artist],
|
||||||
|
animeEntry[AnimeTable.author],
|
||||||
|
animeEntry[AnimeTable.description],
|
||||||
|
animeEntry[AnimeTable.genre],
|
||||||
|
AnimeStatus.valueOf(animeEntry[AnimeTable.status]).name,
|
||||||
|
animeEntry[AnimeTable.inLibrary]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PagedAnimeListDataClass(
|
||||||
|
animeList,
|
||||||
|
animesPage.hasNextPage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
package suwayomi.anime.impl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder.DESC
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.anime.impl.Anime.getAnime
|
||||||
|
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||||
|
import suwayomi.anime.model.dataclass.EpisodeDataClass
|
||||||
|
import suwayomi.anime.model.table.AnimeTable
|
||||||
|
import suwayomi.anime.model.table.EpisodeTable
|
||||||
|
import suwayomi.anime.model.table.toDataClass
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
|
||||||
|
object Episode {
|
||||||
|
/** get episode list when showing an anime */
|
||||||
|
suspend fun getEpisodeList(animeId: Int, onlineFetch: Boolean?): List<EpisodeDataClass> {
|
||||||
|
return if (onlineFetch == true) {
|
||||||
|
getSourceEpisodes(animeId)
|
||||||
|
} else {
|
||||||
|
transaction {
|
||||||
|
EpisodeTable.select { EpisodeTable.anime eq animeId }.orderBy(EpisodeTable.episodeIndex to DESC)
|
||||||
|
.map {
|
||||||
|
EpisodeTable.toDataClass(it)
|
||||||
|
}
|
||||||
|
}.ifEmpty {
|
||||||
|
// If it was explicitly set to offline dont grab episodes
|
||||||
|
if (onlineFetch == null) {
|
||||||
|
getSourceEpisodes(animeId)
|
||||||
|
} else emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getSourceEpisodes(animeId: Int): List<EpisodeDataClass> {
|
||||||
|
val animeDetails = getAnime(animeId)
|
||||||
|
val source = getAnimeHttpSource(animeDetails.sourceId.toLong())
|
||||||
|
val episodeList = source.fetchEpisodeList(
|
||||||
|
SAnime.create().apply {
|
||||||
|
title = animeDetails.title
|
||||||
|
url = animeDetails.url
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
|
val episodeCount = episodeList.count()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
episodeList.reversed().forEachIndexed { index, fetchedEpisode ->
|
||||||
|
val episodeEntry = EpisodeTable.select { EpisodeTable.url eq fetchedEpisode.url }.firstOrNull()
|
||||||
|
if (episodeEntry == null) {
|
||||||
|
EpisodeTable.insert {
|
||||||
|
it[url] = fetchedEpisode.url
|
||||||
|
it[name] = fetchedEpisode.name
|
||||||
|
it[date_upload] = fetchedEpisode.date_upload
|
||||||
|
it[episode_number] = fetchedEpisode.episode_number
|
||||||
|
it[scanlator] = fetchedEpisode.scanlator
|
||||||
|
|
||||||
|
it[episodeIndex] = index + 1
|
||||||
|
it[anime] = animeId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EpisodeTable.update({ EpisodeTable.url eq fetchedEpisode.url }) {
|
||||||
|
it[name] = fetchedEpisode.name
|
||||||
|
it[date_upload] = fetchedEpisode.date_upload
|
||||||
|
it[episode_number] = fetchedEpisode.episode_number
|
||||||
|
it[scanlator] = fetchedEpisode.scanlator
|
||||||
|
|
||||||
|
it[episodeIndex] = index + 1
|
||||||
|
it[anime] = animeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear any orphaned episodes that are in the db but not in `episodeList`
|
||||||
|
val dbEpisodeCount = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId }.count() }
|
||||||
|
if (dbEpisodeCount > episodeCount) { // we got some clean up due
|
||||||
|
val dbEpisodeList = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId } }
|
||||||
|
|
||||||
|
dbEpisodeList.forEach {
|
||||||
|
if (it[EpisodeTable.episodeIndex] >= episodeList.size ||
|
||||||
|
episodeList[it[EpisodeTable.episodeIndex] - 1].url != it[EpisodeTable.url]
|
||||||
|
) {
|
||||||
|
transaction {
|
||||||
|
// PageTable.deleteWhere { PageTable.episode eq it[EpisodeTable.id] }
|
||||||
|
EpisodeTable.deleteWhere { EpisodeTable.id eq it[EpisodeTable.id] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dbEpisodeMap = transaction {
|
||||||
|
EpisodeTable.select { EpisodeTable.anime eq animeId }
|
||||||
|
.associateBy({ it[EpisodeTable.url] }, { it })
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodeList.mapIndexed { index, it ->
|
||||||
|
|
||||||
|
val dbEpisode = dbEpisodeMap.getValue(it.url)
|
||||||
|
|
||||||
|
EpisodeDataClass(
|
||||||
|
it.url,
|
||||||
|
it.name,
|
||||||
|
it.date_upload,
|
||||||
|
it.episode_number,
|
||||||
|
it.scanlator,
|
||||||
|
animeId,
|
||||||
|
|
||||||
|
dbEpisode[EpisodeTable.isRead],
|
||||||
|
dbEpisode[EpisodeTable.isBookmarked],
|
||||||
|
dbEpisode[EpisodeTable.lastPageRead],
|
||||||
|
|
||||||
|
episodeCount - index,
|
||||||
|
episodeList.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** used to display a episode, get a episode in order to show it's video */
|
||||||
|
suspend fun getEpisode(episodeIndex: Int, animeId: Int): EpisodeDataClass {
|
||||||
|
val episode = getEpisodeList(animeId, false)
|
||||||
|
.first { it.index == episodeIndex }
|
||||||
|
|
||||||
|
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
|
||||||
|
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
|
||||||
|
val fetchedLinkUrl = source.fetchEpisodeLink(
|
||||||
|
SEpisode.create().also {
|
||||||
|
it.url = episode.url
|
||||||
|
it.name = episode.name
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
|
return EpisodeDataClass(
|
||||||
|
episode.url,
|
||||||
|
episode.name,
|
||||||
|
episode.uploadDate,
|
||||||
|
episode.episodeNumber,
|
||||||
|
episode.scanlator,
|
||||||
|
animeId,
|
||||||
|
episode.read,
|
||||||
|
episode.bookmarked,
|
||||||
|
episode.lastPageRead,
|
||||||
|
episode.index,
|
||||||
|
episode.episodeCount,
|
||||||
|
fetchedLinkUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// /** used to display a episode, get a episode in order to show it's pages */
|
||||||
|
// suspend fun getEpisode(episodeIndex: Int, animeId: Int): EpisodeDataClass {
|
||||||
|
// val episodeEntry = transaction {
|
||||||
|
// EpisodeTable.select {
|
||||||
|
// (EpisodeTable.episodeIndex eq episodeIndex) and (EpisodeTable.anime eq animeId)
|
||||||
|
// }.first()
|
||||||
|
// }
|
||||||
|
// val animeEntry = transaction { MangaTable.select { MangaTable.id eq animeId }.first() }
|
||||||
|
// val source = getAnimeHttpSource(animeEntry[MangaTable.sourceReference])
|
||||||
|
//
|
||||||
|
// val pageList = source.fetchPageList(
|
||||||
|
// SEpisode.create().apply {
|
||||||
|
// url = episodeEntry[EpisodeTable.url]
|
||||||
|
// name = episodeEntry[EpisodeTable.name]
|
||||||
|
// }
|
||||||
|
// ).awaitSingle()
|
||||||
|
//
|
||||||
|
// val episodeId = episodeEntry[EpisodeTable.id].value
|
||||||
|
// val episodeCount = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId }.count() }
|
||||||
|
//
|
||||||
|
// // update page list for this episode
|
||||||
|
// transaction {
|
||||||
|
// pageList.forEach { page ->
|
||||||
|
// val pageEntry = transaction { PageTable.select { (PageTable.episode eq episodeId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||||
|
// if (pageEntry == null) {
|
||||||
|
// PageTable.insert {
|
||||||
|
// it[index] = page.index
|
||||||
|
// it[url] = page.url
|
||||||
|
// it[imageUrl] = page.imageUrl
|
||||||
|
// it[episode] = episodeId
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// PageTable.update({ (PageTable.episode eq episodeId) and (PageTable.index eq page.index) }) {
|
||||||
|
// it[url] = page.url
|
||||||
|
// it[imageUrl] = page.imageUrl
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return EpisodeDataClass(
|
||||||
|
// episodeEntry[EpisodeTable.url],
|
||||||
|
// episodeEntry[EpisodeTable.name],
|
||||||
|
// episodeEntry[EpisodeTable.date_upload],
|
||||||
|
// episodeEntry[EpisodeTable.episode_number],
|
||||||
|
// episodeEntry[EpisodeTable.scanlator],
|
||||||
|
// animeId,
|
||||||
|
// episodeEntry[EpisodeTable.isRead],
|
||||||
|
// episodeEntry[EpisodeTable.isBookmarked],
|
||||||
|
// episodeEntry[EpisodeTable.lastPageRead],
|
||||||
|
//
|
||||||
|
// episodeEntry[EpisodeTable.episodeIndex],
|
||||||
|
// episodeCount.toInt(),
|
||||||
|
// pageList.count()
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun modifyEpisode(animeId: Int, episodeIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
|
||||||
|
transaction {
|
||||||
|
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
|
||||||
|
EpisodeTable.update({ (EpisodeTable.anime eq animeId) and (EpisodeTable.episodeIndex eq episodeIndex) }) { update ->
|
||||||
|
isRead?.also {
|
||||||
|
update[EpisodeTable.isRead] = it
|
||||||
|
}
|
||||||
|
isBookmarked?.also {
|
||||||
|
update[EpisodeTable.isBookmarked] = it
|
||||||
|
}
|
||||||
|
lastPageRead?.also {
|
||||||
|
update[EpisodeTable.lastPageRead] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markPrevRead?.let {
|
||||||
|
EpisodeTable.update({ (EpisodeTable.anime eq animeId) and (EpisodeTable.episodeIndex less episodeIndex) }) {
|
||||||
|
it[EpisodeTable.isRead] = markPrevRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package suwayomi.anime.impl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.anime.impl.extension.Extension.getExtensionIconUrl
|
||||||
|
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||||
|
import suwayomi.anime.model.dataclass.AnimeSourceDataClass
|
||||||
|
import suwayomi.anime.model.table.AnimeExtensionTable
|
||||||
|
import suwayomi.anime.model.table.AnimeSourceTable
|
||||||
|
|
||||||
|
object Source {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
fun getSourceList(): List<AnimeSourceDataClass> {
|
||||||
|
return transaction {
|
||||||
|
AnimeSourceTable.selectAll().map {
|
||||||
|
AnimeSourceDataClass(
|
||||||
|
it[AnimeSourceTable.id].value.toString(),
|
||||||
|
it[AnimeSourceTable.name],
|
||||||
|
it[AnimeSourceTable.lang],
|
||||||
|
getExtensionIconUrl(AnimeExtensionTable.select { AnimeExtensionTable.id eq it[AnimeSourceTable.extension] }.first()[AnimeExtensionTable.apkName]),
|
||||||
|
getAnimeHttpSource(it[AnimeSourceTable.id].value).supportsLatest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAnimeSource(sourceId: Long): AnimeSourceDataClass {
|
||||||
|
return transaction {
|
||||||
|
val source = AnimeSourceTable.select { AnimeSourceTable.id eq sourceId }.firstOrNull()
|
||||||
|
|
||||||
|
AnimeSourceDataClass(
|
||||||
|
sourceId.toString(),
|
||||||
|
source?.get(AnimeSourceTable.name),
|
||||||
|
source?.get(AnimeSourceTable.lang),
|
||||||
|
source?.let { AnimeExtensionTable.select { AnimeExtensionTable.id eq source[AnimeSourceTable.extension] }.first()[AnimeExtensionTable.iconUrl] },
|
||||||
|
source?.let { getAnimeHttpSource(sourceId).supportsLatest }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package suwayomi.anime.impl.extension
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import okhttp3.Request
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import suwayomi.anime.impl.extension.ExtensionsList.extensionTableAsDataClass
|
||||||
|
import suwayomi.anime.impl.extension.github.ExtensionGithubApi
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.EXTENSION_FEATURE
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MAX
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MIN
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.METADATA_NSFW
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.METADATA_SOURCE_CLASS
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.dex2jar
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.getPackageInfo
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.getSignatureHash
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.loadExtensionSources
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.trustedSignatures
|
||||||
|
import suwayomi.anime.model.table.AnimeExtensionTable
|
||||||
|
import suwayomi.anime.model.table.AnimeSourceTable
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import suwayomi.tachidesk.impl.util.network.await
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
object Extension {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
|
data class InstallableAPK(
|
||||||
|
val apkFilePath: String,
|
||||||
|
val pkgName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun installExtension(pkgName: String): Int {
|
||||||
|
logger.debug("Installing $pkgName")
|
||||||
|
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
|
||||||
|
|
||||||
|
return installAPK {
|
||||||
|
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
|
||||||
|
val apkName = Uri.parse(apkURL).lastPathSegment!!
|
||||||
|
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
||||||
|
// download apk file
|
||||||
|
downloadAPKFile(apkURL, apkSavePath)
|
||||||
|
|
||||||
|
apkSavePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun installAPK(fetcher: suspend () -> String): Int {
|
||||||
|
val apkFilePath = fetcher()
|
||||||
|
val apkName = File(apkFilePath).name
|
||||||
|
|
||||||
|
// check if we don't have the extension already installed
|
||||||
|
// if it's installed and we want to update, it first has to be uninstalled
|
||||||
|
val isInstalled = transaction {
|
||||||
|
AnimeExtensionTable.select { AnimeExtensionTable.apkName eq apkName }.firstOrNull()
|
||||||
|
}?.get(AnimeExtensionTable.isInstalled) ?: false
|
||||||
|
|
||||||
|
if (!isInstalled) {
|
||||||
|
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||||
|
|
||||||
|
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||||
|
val jarFilePath = "$dirPathWithoutType.jar"
|
||||||
|
val dexFilePath = "$dirPathWithoutType.dex"
|
||||||
|
|
||||||
|
val packageInfo = getPackageInfo(apkFilePath)
|
||||||
|
val pkgName = packageInfo.packageName
|
||||||
|
|
||||||
|
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
|
||||||
|
throw Exception("This apk is not a Tachiyomi extension")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate lib version
|
||||||
|
val libVersion = packageInfo.versionName.substringBeforeLast('.').toDouble()
|
||||||
|
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||||
|
throw Exception(
|
||||||
|
"Lib version is $libVersion, while only versions " +
|
||||||
|
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val signatureHash = getSignatureHash(packageInfo)
|
||||||
|
|
||||||
|
if (signatureHash == null) {
|
||||||
|
throw Exception("Package $pkgName isn't signed")
|
||||||
|
} else if (signatureHash !in trustedSignatures) {
|
||||||
|
// TODO: allow trusting keys
|
||||||
|
throw Exception("This apk is not a signed with the official tachiyomi signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
|
||||||
|
|
||||||
|
val className = packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||||
|
|
||||||
|
logger.debug("Main class for extension is $className")
|
||||||
|
|
||||||
|
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
// File(apkFilePath).delete()
|
||||||
|
File(dexFilePath).delete()
|
||||||
|
|
||||||
|
// collect sources from the extension
|
||||||
|
val sources: List<AnimeCatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) {
|
||||||
|
is AnimeSource -> listOf(instance)
|
||||||
|
is AnimeSourceFactory -> instance.createSources()
|
||||||
|
else -> throw RuntimeException("Unknown source class type! ${instance.javaClass}")
|
||||||
|
}.map { it as AnimeCatalogueSource }
|
||||||
|
|
||||||
|
val langs = sources.map { it.lang }.toSet()
|
||||||
|
val extensionLang = when (langs.size) {
|
||||||
|
0 -> ""
|
||||||
|
1 -> langs.first()
|
||||||
|
else -> "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
|
||||||
|
|
||||||
|
// update extension info
|
||||||
|
transaction {
|
||||||
|
if (AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
|
||||||
|
AnimeExtensionTable.insert {
|
||||||
|
it[this.apkName] = apkName
|
||||||
|
it[name] = extensionName
|
||||||
|
it[this.pkgName] = packageInfo.packageName
|
||||||
|
it[versionName] = packageInfo.versionName
|
||||||
|
it[versionCode] = packageInfo.versionCode
|
||||||
|
it[lang] = extensionLang
|
||||||
|
it[this.isNsfw] = isNsfw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq pkgName }) {
|
||||||
|
it[this.isInstalled] = true
|
||||||
|
it[this.classFQName] = className
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionId = AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.first()[AnimeExtensionTable.id].value
|
||||||
|
|
||||||
|
sources.forEach { httpSource ->
|
||||||
|
AnimeSourceTable.insert {
|
||||||
|
it[id] = httpSource.id
|
||||||
|
it[name] = httpSource.name
|
||||||
|
it[lang] = httpSource.lang
|
||||||
|
it[extension] = extensionId
|
||||||
|
}
|
||||||
|
logger.debug("Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 201 // we installed successfully
|
||||||
|
} else {
|
||||||
|
return 302 // extension was already installed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
private suspend fun downloadAPKFile(url: String, savePath: String) {
|
||||||
|
val request = Request.Builder().url(url).build()
|
||||||
|
val response = network.client.newCall(request).await()
|
||||||
|
|
||||||
|
val downloadedFile = File(savePath)
|
||||||
|
downloadedFile.sink().buffer().use { sink ->
|
||||||
|
response.body!!.source().use { source ->
|
||||||
|
sink.writeAll(source)
|
||||||
|
sink.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallExtension(pkgName: String) {
|
||||||
|
logger.debug("Uninstalling $pkgName")
|
||||||
|
|
||||||
|
val extensionRecord = transaction { AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.first() }
|
||||||
|
val fileNameWithoutType = extensionRecord[AnimeExtensionTable.apkName].substringBefore(".apk")
|
||||||
|
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
|
transaction {
|
||||||
|
val extensionId = extensionRecord[AnimeExtensionTable.id].value
|
||||||
|
|
||||||
|
AnimeSourceTable.deleteWhere { AnimeSourceTable.extension eq extensionId }
|
||||||
|
if (extensionRecord[AnimeExtensionTable.isObsolete])
|
||||||
|
AnimeExtensionTable.deleteWhere { AnimeExtensionTable.pkgName eq pkgName }
|
||||||
|
else
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.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 {
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.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 { AnimeExtensionTable.select { AnimeExtensionTable.apkName eq apkName }.first() }[AnimeExtensionTable.iconUrl]
|
||||||
|
|
||||||
|
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||||
|
|
||||||
|
return getCachedImageResponse(saveDir, apkName) {
|
||||||
|
network.client.newCall(
|
||||||
|
GET(iconUrl)
|
||||||
|
).await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtensionIconUrl(apkName: String): String {
|
||||||
|
return "/api/v1/anime/extension/icon/$apkName"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package suwayomi.anime.impl.extension
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.anime.impl.extension.Extension.getExtensionIconUrl
|
||||||
|
import suwayomi.anime.impl.extension.github.ExtensionGithubApi
|
||||||
|
import suwayomi.anime.impl.extension.github.OnlineExtension
|
||||||
|
import suwayomi.anime.model.dataclass.AnimeExtensionDataClass
|
||||||
|
import suwayomi.anime.model.table.AnimeExtensionTable
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
object ExtensionsList {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
var lastUpdateCheck: Long = 0
|
||||||
|
var updateMap = ConcurrentHashMap<String, OnlineExtension>()
|
||||||
|
|
||||||
|
/** 60,000 milliseconds = 60 seconds */
|
||||||
|
private const val ExtensionUpdateDelayTime = 60 * 1000
|
||||||
|
|
||||||
|
suspend fun getExtensionList(): List<AnimeExtensionDataClass> {
|
||||||
|
// update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty
|
||||||
|
if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) {
|
||||||
|
logger.debug("Getting extensions list from the internet")
|
||||||
|
lastUpdateCheck = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val foundExtensions = ExtensionGithubApi.findExtensions()
|
||||||
|
updateExtensionDatabase(foundExtensions)
|
||||||
|
} else {
|
||||||
|
logger.debug("used cached extension list")
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensionTableAsDataClass()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extensionTableAsDataClass() = transaction {
|
||||||
|
AnimeExtensionTable.selectAll().map {
|
||||||
|
AnimeExtensionDataClass(
|
||||||
|
it[AnimeExtensionTable.apkName],
|
||||||
|
getExtensionIconUrl(it[AnimeExtensionTable.apkName]),
|
||||||
|
it[AnimeExtensionTable.name],
|
||||||
|
it[AnimeExtensionTable.pkgName],
|
||||||
|
it[AnimeExtensionTable.versionName],
|
||||||
|
it[AnimeExtensionTable.versionCode],
|
||||||
|
it[AnimeExtensionTable.lang],
|
||||||
|
it[AnimeExtensionTable.isNsfw],
|
||||||
|
it[AnimeExtensionTable.isInstalled],
|
||||||
|
it[AnimeExtensionTable.hasUpdate],
|
||||||
|
it[AnimeExtensionTable.isObsolete],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
|
||||||
|
transaction {
|
||||||
|
foundExtensions.forEach { foundExtension ->
|
||||||
|
val extensionRecord = AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
|
||||||
|
if (extensionRecord != null) {
|
||||||
|
if (extensionRecord[AnimeExtensionTable.isInstalled]) {
|
||||||
|
when {
|
||||||
|
foundExtension.versionCode > extensionRecord[AnimeExtensionTable.versionCode] -> {
|
||||||
|
// there is an update
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
|
||||||
|
it[hasUpdate] = true
|
||||||
|
}
|
||||||
|
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
|
||||||
|
}
|
||||||
|
foundExtension.versionCode < extensionRecord[AnimeExtensionTable.versionCode] -> {
|
||||||
|
// some how the user installed an invalid version
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
|
||||||
|
it[isObsolete] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// extension is not installed so we can overwrite the data without a care
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
|
||||||
|
it[name] = foundExtension.name
|
||||||
|
it[versionName] = foundExtension.versionName
|
||||||
|
it[versionCode] = foundExtension.versionCode
|
||||||
|
it[lang] = foundExtension.lang
|
||||||
|
it[isNsfw] = foundExtension.isNsfw
|
||||||
|
it[apkName] = foundExtension.apkName
|
||||||
|
it[iconUrl] = foundExtension.iconUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// insert new record
|
||||||
|
AnimeExtensionTable.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
|
||||||
|
AnimeExtensionTable.selectAll().forEach { extensionRecord ->
|
||||||
|
val foundExtension = foundExtensions.find { it.pkgName == extensionRecord[AnimeExtensionTable.pkgName] }
|
||||||
|
if (foundExtension == null) {
|
||||||
|
// not in the repo, so this extensions is obsolete
|
||||||
|
if (extensionRecord[AnimeExtensionTable.isInstalled]) {
|
||||||
|
// is installed so we should mark it as obsolete
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq extensionRecord[AnimeExtensionTable.pkgName] }) {
|
||||||
|
it[isObsolete] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// is not installed so we can remove the record without a care
|
||||||
|
AnimeExtensionTable.deleteWhere { AnimeExtensionTable.pkgName eq extensionRecord[AnimeExtensionTable.pkgName] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package suwayomi.anime.impl.extension.github
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.int
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import okhttp3.Request
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MAX
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MIN
|
||||||
|
import suwayomi.anime.model.dataclass.AnimeExtensionDataClass
|
||||||
|
import suwayomi.tachidesk.impl.util.network.UnzippingInterceptor
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
object ExtensionGithubApi {
|
||||||
|
const val BASE_URL = "https://raw.githubusercontent.com"
|
||||||
|
const val REPO_URL_PREFIX = "$BASE_URL/jmir1/tachiyomi-extensions/repo"
|
||||||
|
|
||||||
|
private fun parseResponse(json: JsonArray): List<OnlineExtension> {
|
||||||
|
return json
|
||||||
|
.map { it.asJsonObject }
|
||||||
|
.filter { element ->
|
||||||
|
val versionName = element["version"].string
|
||||||
|
val libVersion = versionName.substringBeforeLast('.').toInt()
|
||||||
|
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
|
||||||
|
}
|
||||||
|
.map { element ->
|
||||||
|
val name = element["name"].string.substringAfter("Tachiyomi: ")
|
||||||
|
val pkgName = element["pkg"].string
|
||||||
|
val apkName = element["apk"].string
|
||||||
|
val versionName = element["version"].string
|
||||||
|
val versionCode = element["code"].int
|
||||||
|
val lang = element["lang"].string
|
||||||
|
val nsfw = element["nsfw"].int == 1
|
||||||
|
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
||||||
|
|
||||||
|
OnlineExtension(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findExtensions(): List<OnlineExtension> {
|
||||||
|
val response = getRepo()
|
||||||
|
return parseResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getApkUrl(extension: AnimeExtensionDataClass): String {
|
||||||
|
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val client by lazy {
|
||||||
|
val network: NetworkHelper by injectLazy()
|
||||||
|
network.client.newBuilder()
|
||||||
|
.addNetworkInterceptor { chain ->
|
||||||
|
val originalResponse = chain.proceed(chain.request())
|
||||||
|
originalResponse.newBuilder()
|
||||||
|
.header("Content-Encoding", "gzip")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.addInterceptor(UnzippingInterceptor())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRepo(): com.google.gson.JsonArray {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$REPO_URL_PREFIX/index.json.gz")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute().use { response -> response.body!!.string() }
|
||||||
|
return JsonParser.parseString(response).asJsonArray
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package suwayomi.anime.impl.extension.github
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 OnlineExtension(
|
||||||
|
val name: String,
|
||||||
|
val pkgName: String,
|
||||||
|
val versionName: String,
|
||||||
|
val versionCode: Int,
|
||||||
|
val lang: String,
|
||||||
|
val isNsfw: Boolean,
|
||||||
|
val apkName: String,
|
||||||
|
val iconUrl: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package suwayomi.anime.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.animesource.AnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
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 suwayomi.anime.impl.util.PackageTools.loadExtensionSources
|
||||||
|
import suwayomi.anime.model.table.AnimeExtensionTable
|
||||||
|
import suwayomi.anime.model.table.AnimeSourceTable
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
object GetAnimeHttpSource {
|
||||||
|
private val sourceCache = ConcurrentHashMap<Long, AnimeHttpSource>()
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
|
fun getAnimeHttpSource(sourceId: Long): AnimeHttpSource {
|
||||||
|
val cachedResult: AnimeHttpSource? = sourceCache[sourceId]
|
||||||
|
if (cachedResult != null) {
|
||||||
|
return cachedResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceRecord = transaction {
|
||||||
|
AnimeSourceTable.select { AnimeSourceTable.id eq sourceId }.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionId = sourceRecord[AnimeSourceTable.extension]
|
||||||
|
val extensionRecord = transaction {
|
||||||
|
AnimeExtensionTable.select { AnimeExtensionTable.id eq extensionId }.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
val apkName = extensionRecord[AnimeExtensionTable.apkName]
|
||||||
|
val className = extensionRecord[AnimeExtensionTable.classFQName]
|
||||||
|
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||||
|
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||||
|
|
||||||
|
when (val instance = loadExtensionSources(jarPath, className)) {
|
||||||
|
is AnimeSource -> listOf(instance)
|
||||||
|
is AnimeSourceFactory -> instance.createSources()
|
||||||
|
else -> throw Exception("Unknown source class type! ${instance.javaClass}")
|
||||||
|
}.forEach {
|
||||||
|
sourceCache[it.id] = it as AnimeHttpSource
|
||||||
|
}
|
||||||
|
return sourceCache[sourceId]!!
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package suwayomi.anime.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 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 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 suwayomi.server.ApplicationDirs
|
||||||
|
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
|
||||||
|
|
||||||
|
object PackageTools {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
|
const val EXTENSION_FEATURE = "tachiyomi.animeextension"
|
||||||
|
const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
|
||||||
|
const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
|
||||||
|
const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
|
||||||
|
const val LIB_VERSION_MIN = 10
|
||||||
|
const val LIB_VERSION_MAX = 10
|
||||||
|
|
||||||
|
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key
|
||||||
|
var trustedSignatures = mutableSetOf<String>() + officialSignature
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,36 @@
|
|||||||
|
package suwayomi.anime.model.dataclass
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import suwayomi.anime.model.table.AnimeStatus
|
||||||
|
|
||||||
|
data class AnimeDataClass(
|
||||||
|
val id: Int,
|
||||||
|
val sourceId: String,
|
||||||
|
|
||||||
|
val url: String,
|
||||||
|
val title: String,
|
||||||
|
val thumbnailUrl: String? = null,
|
||||||
|
|
||||||
|
val initialized: Boolean = false,
|
||||||
|
|
||||||
|
val artist: String? = null,
|
||||||
|
val author: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val genre: String? = null,
|
||||||
|
val status: String = AnimeStatus.UNKNOWN.name,
|
||||||
|
val inLibrary: Boolean = false,
|
||||||
|
val source: AnimeSourceDataClass? = null,
|
||||||
|
|
||||||
|
val freshData: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PagedAnimeListDataClass(
|
||||||
|
val mangaList: List<AnimeDataClass>,
|
||||||
|
val hasNextPage: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package suwayomi.anime.model.dataclass
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
data class AnimeExtensionDataClass(
|
||||||
|
val apkName: String,
|
||||||
|
val iconUrl: String,
|
||||||
|
|
||||||
|
val name: String,
|
||||||
|
val pkgName: String,
|
||||||
|
val versionName: String,
|
||||||
|
val versionCode: Int,
|
||||||
|
val lang: String,
|
||||||
|
val isNsfw: Boolean,
|
||||||
|
|
||||||
|
val installed: Boolean,
|
||||||
|
val hasUpdate: Boolean,
|
||||||
|
val obsolete: Boolean,
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package suwayomi.anime.model.dataclass
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
data class AnimeSourceDataClass(
|
||||||
|
val id: String,
|
||||||
|
val name: String?,
|
||||||
|
val lang: String?,
|
||||||
|
val iconUrl: String?,
|
||||||
|
val supportsLatest: Boolean?
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package suwayomi.anime.model.dataclass
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
data class EpisodeDataClass(
|
||||||
|
val url: String,
|
||||||
|
val name: String,
|
||||||
|
val uploadDate: Long,
|
||||||
|
val episodeNumber: Float,
|
||||||
|
val scanlator: String?,
|
||||||
|
val animeId: Int,
|
||||||
|
|
||||||
|
/** chapter is read */
|
||||||
|
val read: Boolean,
|
||||||
|
|
||||||
|
/** chapter is bookmarked */
|
||||||
|
val bookmarked: Boolean,
|
||||||
|
|
||||||
|
/** last read page, zero means not read/no data */
|
||||||
|
val lastPageRead: Int,
|
||||||
|
|
||||||
|
/** this chapter's index, starts with 1 */
|
||||||
|
val index: Int,
|
||||||
|
|
||||||
|
/** total episode count, used to calculate if there's a next and prev episode */
|
||||||
|
val episodeCount: Int? = null,
|
||||||
|
|
||||||
|
/** used to construct pages in the front-end */
|
||||||
|
val linkUrl: String? = null,
|
||||||
|
)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package suwayomi.anime.model.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 AnimeExtensionTable : IntIdTable() {
|
||||||
|
val apkName = varchar("apk_name", 1024)
|
||||||
|
|
||||||
|
// default is the local source icon from tachiyomi
|
||||||
|
val iconUrl = varchar("icon_url", 2048)
|
||||||
|
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||||
|
|
||||||
|
val name = varchar("name", 128)
|
||||||
|
val pkgName = varchar("pkg_name", 128)
|
||||||
|
val versionName = varchar("version_name", 16)
|
||||||
|
val versionCode = integer("version_code")
|
||||||
|
val lang = varchar("lang", 10)
|
||||||
|
val isNsfw = bool("is_nsfw")
|
||||||
|
|
||||||
|
val isInstalled = bool("is_installed").default(false)
|
||||||
|
val hasUpdate = bool("has_update").default(false)
|
||||||
|
val isObsolete = bool("is_obsolete").default(false)
|
||||||
|
|
||||||
|
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package suwayomi.anime.model.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.IdTable
|
||||||
|
|
||||||
|
object AnimeSourceTable : IdTable<Long>() {
|
||||||
|
override val id = long("id").entityId()
|
||||||
|
val name = varchar("name", 128)
|
||||||
|
val lang = varchar("lang", 10)
|
||||||
|
val extension = reference("extension", AnimeExtensionTable)
|
||||||
|
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package suwayomi.anime.model.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 eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
import suwayomi.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||||
|
import suwayomi.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.MangaStatus.Companion
|
||||||
|
|
||||||
|
object AnimeTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val title = varchar("title", 512)
|
||||||
|
val initialized = bool("initialized").default(false)
|
||||||
|
|
||||||
|
val artist = varchar("artist", 64).nullable()
|
||||||
|
val author = varchar("author", 64).nullable()
|
||||||
|
val description = varchar("description", 4096).nullable()
|
||||||
|
val genre = varchar("genre", 1024).nullable()
|
||||||
|
|
||||||
|
val status = integer("status").default(SAnime.UNKNOWN)
|
||||||
|
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
|
val inLibrary = bool("in_library").default(false)
|
||||||
|
val defaultCategory = bool("default_category").default(true)
|
||||||
|
|
||||||
|
// source is used by some ancestor of IntIdTable
|
||||||
|
val sourceReference = long("source")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AnimeTable.toDataClass(mangaEntry: ResultRow) =
|
||||||
|
MangaDataClass(
|
||||||
|
mangaEntry[this.id].value,
|
||||||
|
mangaEntry[sourceReference].toString(),
|
||||||
|
|
||||||
|
mangaEntry[url],
|
||||||
|
mangaEntry[title],
|
||||||
|
proxyThumbnailUrl(mangaEntry[this.id].value),
|
||||||
|
|
||||||
|
mangaEntry[initialized],
|
||||||
|
|
||||||
|
mangaEntry[artist],
|
||||||
|
mangaEntry[author],
|
||||||
|
mangaEntry[description],
|
||||||
|
mangaEntry[genre],
|
||||||
|
Companion.valueOf(mangaEntry[status]).name,
|
||||||
|
mangaEntry[inLibrary]
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class AnimeStatus(val status: Int) {
|
||||||
|
UNKNOWN(0),
|
||||||
|
ONGOING(1),
|
||||||
|
COMPLETED(2),
|
||||||
|
LICENSED(3);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun valueOf(value: Int): AnimeStatus = values().find { it.status == value } ?: UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package suwayomi.anime.model.table
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.anime.model.dataclass.EpisodeDataClass
|
||||||
|
|
||||||
|
object EpisodeTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val name = varchar("name", 512)
|
||||||
|
val date_upload = long("date_upload").default(0)
|
||||||
|
val episode_number = float("episode_number").default(-1f)
|
||||||
|
val scanlator = varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
|
val isRead = bool("read").default(false)
|
||||||
|
val isBookmarked = bool("bookmark").default(false)
|
||||||
|
val lastPageRead = integer("last_page_read").default(0)
|
||||||
|
|
||||||
|
// index is reserved by a function
|
||||||
|
val episodeIndex = integer("index")
|
||||||
|
|
||||||
|
val anime = reference("anime", AnimeTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EpisodeTable.toDataClass(episodeEntry: ResultRow) =
|
||||||
|
EpisodeDataClass(
|
||||||
|
episodeEntry[url],
|
||||||
|
episodeEntry[name],
|
||||||
|
episodeEntry[date_upload],
|
||||||
|
episodeEntry[episode_number],
|
||||||
|
episodeEntry[scanlator],
|
||||||
|
episodeEntry[anime].value,
|
||||||
|
episodeEntry[isRead],
|
||||||
|
episodeEntry[isBookmarked],
|
||||||
|
episodeEntry[lastPageRead],
|
||||||
|
episodeEntry[episodeIndex],
|
||||||
|
transaction { EpisodeTable.select { anime eq episodeEntry[anime] }.count().toInt() }
|
||||||
|
)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package suwayomi.server
|
||||||
|
|
||||||
|
import io.javalin.Javalin
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.future.future
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import suwayomi.anime.AnimeAPI
|
||||||
|
import suwayomi.server.util.Browser
|
||||||
|
import suwayomi.tachidesk.TachideskAPI
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 JavalinSetup {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
|
||||||
|
return scope.future(block = block)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun javalinSetup() {
|
||||||
|
var hasWebUiBundled = false
|
||||||
|
|
||||||
|
val app = Javalin.create { config ->
|
||||||
|
try {
|
||||||
|
// if the bellow line throws an exception then webUI is not bundled
|
||||||
|
this::class.java.getResource("/react/index.html")
|
||||||
|
|
||||||
|
// no exception so we can tell javalin to serve webUI
|
||||||
|
hasWebUiBundled = true
|
||||||
|
config.addStaticFiles("/react")
|
||||||
|
config.addSinglePageRoot("/", "/react/index.html")
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
logger.warn("react build files are missing.")
|
||||||
|
hasWebUiBundled = false
|
||||||
|
}
|
||||||
|
config.enableCorsForAllOrigins()
|
||||||
|
}.events { event ->
|
||||||
|
event.serverStarted {
|
||||||
|
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
||||||
|
Browser.openInBrowser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start(serverConfig.ip, serverConfig.port)
|
||||||
|
|
||||||
|
// when JVM is prompted to shutdown, stop javalin gracefully
|
||||||
|
Runtime.getRuntime().addShutdownHook(
|
||||||
|
thread(start = false) {
|
||||||
|
app.stop()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app.exception(NullPointerException::class.java) { e, ctx ->
|
||||||
|
logger.error("NullPointerException while handling the request", e)
|
||||||
|
ctx.status(404)
|
||||||
|
}
|
||||||
|
app.exception(NoSuchElementException::class.java) { e, ctx ->
|
||||||
|
logger.error("NoSuchElementException while handling the request", e)
|
||||||
|
ctx.status(404)
|
||||||
|
}
|
||||||
|
app.exception(IOException::class.java) { e, ctx ->
|
||||||
|
logger.error("IOException while handling the request", e)
|
||||||
|
ctx.status(500)
|
||||||
|
ctx.result(e.message ?: "Internal Server Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
TachideskAPI.defineEndpoints(app)
|
||||||
|
AnimeAPI.defineEndpoints(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.server
|
package suwayomi.server
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -10,6 +10,8 @@ package ir.armor.tachidesk.server
|
|||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import io.github.config4k.getValue
|
import io.github.config4k.getValue
|
||||||
import xyz.nulldev.ts.config.ConfigModule
|
import xyz.nulldev.ts.config.ConfigModule
|
||||||
|
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||||
|
import xyz.nulldev.ts.config.debugLogsEnabled
|
||||||
|
|
||||||
class ServerConfig(config: Config) : ConfigModule(config) {
|
class ServerConfig(config: Config) : ConfigModule(config) {
|
||||||
val ip: String by config
|
val ip: String by config
|
||||||
@@ -21,7 +23,7 @@ class ServerConfig(config: Config) : ConfigModule(config) {
|
|||||||
val socksProxyPort: String by config
|
val socksProxyPort: String by config
|
||||||
|
|
||||||
// misc
|
// misc
|
||||||
val debugLogsEnabled: Boolean by config
|
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
|
||||||
val systemTrayEnabled: Boolean by config
|
val systemTrayEnabled: Boolean by config
|
||||||
val initialOpenInBrowserEnabled: Boolean by config
|
val initialOpenInBrowserEnabled: Boolean by config
|
||||||
|
|
||||||
+17
-18
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.server
|
package suwayomi.server
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,17 +7,15 @@ package ir.armor.tachidesk.server
|
|||||||
* 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 ch.qos.logback.classic.Level
|
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import ir.armor.tachidesk.Main
|
|
||||||
import ir.armor.tachidesk.model.database.databaseUp
|
|
||||||
import ir.armor.tachidesk.server.util.systemTray
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.bind
|
import org.kodein.di.bind
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.singleton
|
import org.kodein.di.singleton
|
||||||
import org.slf4j.Logger
|
import suwayomi.server.database.databaseUp
|
||||||
|
import suwayomi.server.util.AppMutex.handleAppMutex
|
||||||
|
import suwayomi.server.util.SystemTray.systemTray
|
||||||
import xyz.nulldev.androidcompat.AndroidCompat
|
import xyz.nulldev.androidcompat.AndroidCompat
|
||||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||||
import xyz.nulldev.ts.config.ApplicationRootDir
|
import xyz.nulldev.ts.config.ApplicationRootDir
|
||||||
@@ -31,17 +29,20 @@ class ApplicationDirs(
|
|||||||
val dataRoot: String = ApplicationRootDir
|
val dataRoot: String = ApplicationRootDir
|
||||||
) {
|
) {
|
||||||
val extensionsRoot = "$dataRoot/extensions"
|
val extensionsRoot = "$dataRoot/extensions"
|
||||||
val thumbnailsRoot = "$dataRoot/thumbnails"
|
val mangaThumbnailsRoot = "$dataRoot/manga-thumbnails"
|
||||||
|
val animeThumbnailsRoot = "$dataRoot/anime-thumbnails"
|
||||||
val mangaRoot = "$dataRoot/manga"
|
val mangaRoot = "$dataRoot/manga"
|
||||||
}
|
}
|
||||||
|
|
||||||
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
||||||
|
|
||||||
val systemTray by lazy { systemTray() }
|
val systemTrayInstance by lazy { systemTray() }
|
||||||
|
|
||||||
val androidCompat by lazy { AndroidCompat() }
|
val androidCompat by lazy { AndroidCompat() }
|
||||||
|
|
||||||
fun applicationSetup() {
|
fun applicationSetup() {
|
||||||
|
logger.info("Running Tachidesk ${BuildConfig.VERSION} revision ${BuildConfig.REVISION}")
|
||||||
|
|
||||||
// Application dirs
|
// Application dirs
|
||||||
val applicationDirs = ApplicationDirs()
|
val applicationDirs = ApplicationDirs()
|
||||||
DI.global.addImport(
|
DI.global.addImport(
|
||||||
@@ -55,7 +56,8 @@ fun applicationSetup() {
|
|||||||
applicationDirs.dataRoot,
|
applicationDirs.dataRoot,
|
||||||
applicationDirs.extensionsRoot,
|
applicationDirs.extensionsRoot,
|
||||||
applicationDirs.extensionsRoot + "/icon",
|
applicationDirs.extensionsRoot + "/icon",
|
||||||
applicationDirs.thumbnailsRoot
|
applicationDirs.mangaThumbnailsRoot,
|
||||||
|
applicationDirs.animeThumbnailsRoot,
|
||||||
).forEach {
|
).forEach {
|
||||||
File(it).mkdirs()
|
File(it).mkdirs()
|
||||||
}
|
}
|
||||||
@@ -65,6 +67,9 @@ fun applicationSetup() {
|
|||||||
ServerConfig.register(GlobalConfigManager.config)
|
ServerConfig.register(GlobalConfigManager.config)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Make sure only one instance of the app is running
|
||||||
|
handleAppMutex()
|
||||||
|
|
||||||
// Load config API
|
// Load config API
|
||||||
DI.global.addImport(ConfigKodeinModule().create())
|
DI.global.addImport(ConfigKodeinModule().create())
|
||||||
// Load Android compatibility dependencies
|
// Load Android compatibility dependencies
|
||||||
@@ -72,16 +77,11 @@ fun applicationSetup() {
|
|||||||
// start app
|
// start app
|
||||||
androidCompat.startApp(App())
|
androidCompat.startApp(App())
|
||||||
|
|
||||||
// set application wide logging level
|
|
||||||
if (serverConfig.debugLogsEnabled) {
|
|
||||||
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
|
|
||||||
}
|
|
||||||
|
|
||||||
// create conf file if doesn't exist
|
// create conf file if doesn't exist
|
||||||
try {
|
try {
|
||||||
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
||||||
if (!dataConfFile.exists()) {
|
if (!dataConfFile.exists()) {
|
||||||
Main::class.java.getResourceAsStream("/server-reference.conf").use { input ->
|
JavalinSetup::class.java.getResourceAsStream("/server-reference.conf").use { input ->
|
||||||
dataConfFile.outputStream().use { output ->
|
dataConfFile.outputStream().use { output ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
@@ -96,8 +96,8 @@ fun applicationSetup() {
|
|||||||
// create system tray
|
// create system tray
|
||||||
if (serverConfig.systemTrayEnabled) {
|
if (serverConfig.systemTrayEnabled) {
|
||||||
try {
|
try {
|
||||||
systemTray
|
systemTrayInstance
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,6 @@ fun applicationSetup() {
|
|||||||
|
|
||||||
// socks proxy settings
|
// socks proxy settings
|
||||||
if (serverConfig.socksProxyEnabled) {
|
if (serverConfig.socksProxyEnabled) {
|
||||||
// System.getProperties()["proxySet"] = "true"
|
|
||||||
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
|
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
|
||||||
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
|
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
|
||||||
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
|
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
|
||||||
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package suwayomi.server.database
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,15 +7,15 @@ package ir.armor.tachidesk.model.database
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.model.database.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.jetbrains.exposed.sql.Database
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import suwayomi.server.database.migration.lib.loadMigrationsFrom
|
||||||
|
import suwayomi.server.database.migration.lib.runMigrations
|
||||||
|
|
||||||
object DBMangaer {
|
object DBManager {
|
||||||
val db by lazy {
|
val db by lazy {
|
||||||
val applicationDirs by DI.global.instance<ApplicationDirs>()
|
val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
||||||
@@ -24,9 +24,9 @@ object DBMangaer {
|
|||||||
|
|
||||||
fun databaseUp() {
|
fun databaseUp() {
|
||||||
// must mention db object so the lazy block executes
|
// must mention db object so the lazy block executes
|
||||||
val db = DBMangaer.db
|
val db = DBManager.db
|
||||||
db.useNestedTransactions = true
|
db.useNestedTransactions = true
|
||||||
|
|
||||||
val migrations = loadMigrationsFrom("ir.armor.tachidesk.model.database.migration")
|
val migrations = loadMigrationsFrom("suwayomi.server.database.migration")
|
||||||
runMigrations(migrations)
|
runMigrations(migrations)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
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
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
@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(categoryTable: CategoryTable, mangaTable: MangaTable) : IntIdTable() {
|
||||||
|
init {
|
||||||
|
reference("category", categoryTable)
|
||||||
|
reference("manga", 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(categoryTable, mangaTable)
|
||||||
|
SchemaUtils.create(
|
||||||
|
extensionTable,
|
||||||
|
sourceTable,
|
||||||
|
mangaTable,
|
||||||
|
chapterTable,
|
||||||
|
pageTable,
|
||||||
|
categoryTable,
|
||||||
|
categoryMangaTable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
@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,24 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0003_DefaultCategory : Migration() {
|
||||||
|
/** this migration renamed CategoryTable.IS_LANDING to ChapterTable.IS_DEFAULT */
|
||||||
|
override fun run() {
|
||||||
|
with(TransactionManager.current()) {
|
||||||
|
exec("ALTER TABLE CATEGORY ALTER COLUMN IS_LANDING RENAME TO IS_DEFAULT")
|
||||||
|
commit()
|
||||||
|
currentDialect.resetCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package suwayomi.server.database.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
|
||||||
|
import suwayomi.server.database.migration.lib.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/. */
|
||||||
|
|
||||||
|
class M0004_AnimeTablesBatch1 : Migration() {
|
||||||
|
private object AnimeExtensionTable : IntIdTable() {
|
||||||
|
val apkName = varchar("apk_name", 1024)
|
||||||
|
|
||||||
|
// default is the local source icon from tachiyomi
|
||||||
|
val iconUrl = varchar("icon_url", 2048)
|
||||||
|
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||||
|
|
||||||
|
val name = varchar("name", 128)
|
||||||
|
val pkgName = varchar("pkg_name", 128)
|
||||||
|
val versionName = varchar("version_name", 16)
|
||||||
|
val versionCode = integer("version_code")
|
||||||
|
val lang = varchar("lang", 10)
|
||||||
|
val isNsfw = bool("is_nsfw")
|
||||||
|
|
||||||
|
val isInstalled = bool("is_installed").default(false)
|
||||||
|
val hasUpdate = bool("has_update").default(false)
|
||||||
|
val isObsolete = bool("is_obsolete").default(false)
|
||||||
|
|
||||||
|
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
|
||||||
|
}
|
||||||
|
|
||||||
|
private object AnimeSourceTable : IdTable<Long>() {
|
||||||
|
override val id = long("id").entityId()
|
||||||
|
val name = varchar("name", 128)
|
||||||
|
val lang = varchar("lang", 10)
|
||||||
|
val extension = reference("extension", suwayomi.anime.model.table.AnimeExtensionTable)
|
||||||
|
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(
|
||||||
|
AnimeExtensionTable,
|
||||||
|
AnimeSourceTable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.server.database.migration.lib.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/. */
|
||||||
|
|
||||||
|
class M0005_AnimeTablesBatch2 : Migration() {
|
||||||
|
private object AnimeTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val title = varchar("title", 512)
|
||||||
|
val initialized = bool("initialized").default(false)
|
||||||
|
|
||||||
|
val artist = varchar("artist", 64).nullable()
|
||||||
|
val author = varchar("author", 64).nullable()
|
||||||
|
val description = varchar("description", 4096).nullable()
|
||||||
|
val genre = varchar("genre", 1024).nullable()
|
||||||
|
|
||||||
|
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
|
||||||
|
val status = integer("status").default(SAnime.UNKNOWN)
|
||||||
|
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
|
val inLibrary = bool("in_library").default(false)
|
||||||
|
val defaultCategory = bool("default_category").default(true)
|
||||||
|
|
||||||
|
// source is used by some ancestor of IntIdTable
|
||||||
|
val sourceReference = long("source")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(
|
||||||
|
AnimeTable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.anime.model.table.AnimeTable
|
||||||
|
import suwayomi.server.database.migration.lib.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/. */
|
||||||
|
|
||||||
|
class M0006_AnimeTablesBatch3 : Migration() {
|
||||||
|
private object EpisodeTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val name = varchar("name", 512)
|
||||||
|
val date_upload = long("date_upload").default(0)
|
||||||
|
val episode_number = float("episode_number").default(-1f)
|
||||||
|
val scanlator = varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
|
val isRead = bool("read").default(false)
|
||||||
|
val isBookmarked = bool("bookmark").default(false)
|
||||||
|
val lastPageRead = integer("last_page_read").default(0)
|
||||||
|
|
||||||
|
// index is reserved by a function
|
||||||
|
val animeIndex = integer("index")
|
||||||
|
|
||||||
|
val anime = reference("anime", AnimeTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(
|
||||||
|
EpisodeTable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0007_ChapterIsDownloaded : Migration() {
|
||||||
|
/** this migration added IS_DOWNLOADED to CHAPTER */
|
||||||
|
override fun run() {
|
||||||
|
with(TransactionManager.current()) {
|
||||||
|
exec("ALTER TABLE CHAPTER ADD COLUMN IS_DOWNLOADED BOOLEAN DEFAULT FALSE")
|
||||||
|
commit()
|
||||||
|
currentDialect.resetCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0008_ChapterPageCount : Migration() {
|
||||||
|
/** this migration added PAGE_COUNT to CHAPTER */
|
||||||
|
override fun run() {
|
||||||
|
with(TransactionManager.current()) {
|
||||||
|
exec("ALTER TABLE CHAPTER ADD COLUMN PAGE_COUNT INT DEFAULT -1")
|
||||||
|
commit()
|
||||||
|
currentDialect.resetCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0009_ChapterLastReadAt : Migration() {
|
||||||
|
/** this migration added PAGE_COUNT to CHAPTER */
|
||||||
|
override fun run() {
|
||||||
|
with(TransactionManager.current()) {
|
||||||
|
// BIGINT == Long
|
||||||
|
exec("ALTER TABLE CHAPTER ADD COLUMN LAST_READ_AT BIGINT DEFAULT 0")
|
||||||
|
commit()
|
||||||
|
currentDialect.resetCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration.lib
|
package suwayomi.server.database.migration.lib
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration.lib
|
package suwayomi.server.database.migration.lib
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+32
-6
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration.lib
|
package suwayomi.server.database.migration.lib
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -10,7 +10,6 @@ package ir.armor.tachidesk.model.database.migration.lib
|
|||||||
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 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
|
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
||||||
|
|
||||||
import com.google.common.reflect.ClassPath
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
@@ -18,8 +17,16 @@ import org.jetbrains.exposed.sql.SchemaUtils.create
|
|||||||
import org.jetbrains.exposed.sql.exists
|
import org.jetbrains.exposed.sql.exists
|
||||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.server.ServerConfig
|
||||||
|
import java.nio.file.FileSystems
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.Instant.now
|
import java.time.Instant.now
|
||||||
|
import kotlin.io.path.ExperimentalPathApi
|
||||||
|
import kotlin.io.path.isDirectory
|
||||||
|
import kotlin.io.path.name
|
||||||
|
import kotlin.streams.toList
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@@ -54,12 +61,31 @@ fun runMigrations(migrations: List<Migration>, database: Database = TransactionM
|
|||||||
logger.info { "Migrations finished successfully" }
|
logger.info { "Migrations finished successfully" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMigrationsFrom(classPath: String): List<Migration> {
|
@OptIn(ExperimentalPathApi::class)
|
||||||
return ClassPath.from(Thread.currentThread().contextClassLoader)
|
private fun getTopLevelClasses(packageName: String): List<Class<*>> {
|
||||||
.getTopLevelClasses(classPath)
|
ServerConfig::class.java.getResource("/" + "suwayomi.server.database.migration".replace('.', '/'))
|
||||||
|
val path = "/" + packageName.replace('.', '/')
|
||||||
|
val uri = ServerConfig::class.java.getResource(path).toURI()
|
||||||
|
|
||||||
|
return when (uri.scheme) {
|
||||||
|
"jar" -> {
|
||||||
|
val fileSystem = FileSystems.newFileSystem(uri, emptyMap<String, Any>())
|
||||||
|
fileSystem.getPath(path)
|
||||||
|
}
|
||||||
|
else -> Paths.get(uri)
|
||||||
|
}.let { Files.walk(it, 1) }
|
||||||
|
.toList()
|
||||||
|
.filterNot { it.isDirectory() || it.name.contains('$') } // '$' means it's not a top level class
|
||||||
|
.filter { it.name.endsWith(".class") }
|
||||||
|
.map { Class.forName("$packageName.${it.name.substringBefore(".class")}") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
fun loadMigrationsFrom(packageName: String): List<Migration> {
|
||||||
|
return getTopLevelClasses(packageName)
|
||||||
.map {
|
.map {
|
||||||
logger.debug("found Migration class ${it.name}")
|
logger.debug("found Migration class ${it.name}")
|
||||||
val clazz = it.load().getDeclaredConstructor().newInstance()
|
val clazz = it.getDeclaredConstructor().newInstance()
|
||||||
if (clazz is Migration)
|
if (clazz is Migration)
|
||||||
clazz
|
clazz
|
||||||
else
|
else
|
||||||
+14
-4
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.server.internal
|
package suwayomi.server.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,18 +7,28 @@ package ir.armor.tachidesk.server.internal
|
|||||||
* 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.BuildConfig
|
import suwayomi.server.BuildConfig
|
||||||
|
|
||||||
data class AboutDataClass(
|
data class AboutDataClass(
|
||||||
|
val name: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
val revision: String,
|
val revision: String,
|
||||||
|
val buildType: String,
|
||||||
|
val buildTime: Long,
|
||||||
|
val github: String,
|
||||||
|
val discord: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
object About {
|
object About {
|
||||||
fun getAbout(): AboutDataClass {
|
fun getAbout(): AboutDataClass {
|
||||||
return AboutDataClass(
|
return AboutDataClass(
|
||||||
BuildConfig.version,
|
BuildConfig.NAME,
|
||||||
BuildConfig.revision,
|
BuildConfig.VERSION,
|
||||||
|
BuildConfig.REVISION,
|
||||||
|
BuildConfig.BUILD_TYPE,
|
||||||
|
BuildConfig.BUILD_TIME,
|
||||||
|
BuildConfig.GITHUB,
|
||||||
|
BuildConfig.DISCORD,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package suwayomi.server.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 mu.KotlinLogging
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
enum class ExitCode(val code: Int) {
|
||||||
|
Success(0),
|
||||||
|
MutexCheckFailedTachideskRunning(1),
|
||||||
|
MutexCheckFailedAnotherAppRunning(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shutdownApp(exitCode: ExitCode) {
|
||||||
|
logger.info("Shutting Down Tachidesk. Goodbye!")
|
||||||
|
|
||||||
|
exitProcess(exitCode.code)
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package suwayomi.server.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 io.javalin.plugin.json.JavalinJackson
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request.Builder
|
||||||
|
import suwayomi.server.impl.AboutDataClass
|
||||||
|
import suwayomi.server.serverConfig
|
||||||
|
import suwayomi.server.util.AppMutex.AppMutexStat.Clear
|
||||||
|
import suwayomi.server.util.AppMutex.AppMutexStat.OtherApplicationRunning
|
||||||
|
import suwayomi.server.util.AppMutex.AppMutexStat.TachideskInstanceRunning
|
||||||
|
import suwayomi.server.util.Browser.openInBrowser
|
||||||
|
import suwayomi.server.util.ExitCode.MutexCheckFailedAnotherAppRunning
|
||||||
|
import suwayomi.server.util.ExitCode.MutexCheckFailedTachideskRunning
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object AppMutex {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
private enum class AppMutexStat(val stat: Int) {
|
||||||
|
Clear(0),
|
||||||
|
TachideskInstanceRunning(1),
|
||||||
|
OtherApplicationRunning(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
|
||||||
|
|
||||||
|
private fun checkAppMutex(): AppMutexStat {
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(200, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = Builder()
|
||||||
|
.url("http://$appIP:${serverConfig.port}/api/v1/about/")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = try {
|
||||||
|
client.newCall(request).execute().use { response -> response.body!!.string() }
|
||||||
|
} catch (e: IOException) {
|
||||||
|
return AppMutexStat.Clear
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
JavalinJackson.fromJson(response, AboutDataClass::class.java)
|
||||||
|
AppMutexStat.TachideskInstanceRunning
|
||||||
|
} catch (e: IOException) {
|
||||||
|
AppMutexStat.OtherApplicationRunning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleAppMutex() {
|
||||||
|
when (checkAppMutex()) {
|
||||||
|
Clear -> {
|
||||||
|
logger.info("Mutex status is clear, Resuming startup.")
|
||||||
|
}
|
||||||
|
TachideskInstanceRunning -> {
|
||||||
|
logger.info("Another instance of Tachidesk is running on $appIP:${serverConfig.port}")
|
||||||
|
|
||||||
|
logger.info("Probably user thought tachidesk is closed so, opening webUI in browser again.")
|
||||||
|
openInBrowser()
|
||||||
|
|
||||||
|
logger.info("Aborting startup.")
|
||||||
|
|
||||||
|
shutdownApp(MutexCheckFailedTachideskRunning)
|
||||||
|
}
|
||||||
|
OtherApplicationRunning -> {
|
||||||
|
logger.error("A non Tachidesk application is running on $appIP:${serverConfig.port}, aborting startup.")
|
||||||
|
shutdownApp(MutexCheckFailedAnotherAppRunning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package suwayomi.server.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 dorkbox.util.Desktop
|
||||||
|
import suwayomi.server.serverConfig
|
||||||
|
|
||||||
|
object Browser {
|
||||||
|
private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
|
||||||
|
private val appBaseUrl = "http://$appIP:${serverConfig.port}"
|
||||||
|
|
||||||
|
private val electronInstances = mutableListOf<Any>()
|
||||||
|
|
||||||
|
fun openInBrowser() {
|
||||||
|
|
||||||
|
val openInElectron = System.getProperty("suwayomi.server.webInterface")?.equals("electron")
|
||||||
|
|
||||||
|
if (openInElectron == true) {
|
||||||
|
try {
|
||||||
|
val electronPath = System.getProperty("suwayomi.server.electronPath")!!
|
||||||
|
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
|
||||||
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Desktop.browseURL(appBaseUrl)
|
||||||
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package suwayomi.server.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 dorkbox.systemTray.MenuItem
|
||||||
|
import dorkbox.systemTray.SystemTray
|
||||||
|
import dorkbox.util.CacheUtil
|
||||||
|
import suwayomi.server.BuildConfig
|
||||||
|
import suwayomi.server.ServerConfig
|
||||||
|
import suwayomi.server.serverConfig
|
||||||
|
import suwayomi.server.util.Browser.openInBrowser
|
||||||
|
import suwayomi.server.util.ExitCode.Success
|
||||||
|
|
||||||
|
object SystemTray {
|
||||||
|
fun systemTray(): SystemTray? {
|
||||||
|
try {
|
||||||
|
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
||||||
|
SystemTray.DEBUG = serverConfig.debugLogsEnabled
|
||||||
|
|
||||||
|
CacheUtil.clear(BuildConfig.NAME)
|
||||||
|
|
||||||
|
val systemTray = SystemTray.get(BuildConfig.NAME) ?: return null
|
||||||
|
val mainMenu = systemTray.menu
|
||||||
|
|
||||||
|
mainMenu.add(
|
||||||
|
MenuItem(
|
||||||
|
"Open Tachidesk"
|
||||||
|
) {
|
||||||
|
openInBrowser()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val icon = ServerConfig::class.java.getResource("/icon/faviconlogo.png")
|
||||||
|
|
||||||
|
// systemTray.setTooltip("Tachidesk")
|
||||||
|
systemTray.setImage(icon)
|
||||||
|
// systemTray.status = "No Mail"
|
||||||
|
|
||||||
|
mainMenu.add(
|
||||||
|
MenuItem("Quit") {
|
||||||
|
shutdownApp(Success)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
systemTray.installShutdownHook()
|
||||||
|
|
||||||
|
return systemTray
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+156
-124
@@ -1,50 +1,4 @@
|
|||||||
package ir.armor.tachidesk.server
|
package suwayomi.tachidesk
|
||||||
|
|
||||||
import io.javalin.Javalin
|
|
||||||
import ir.armor.tachidesk.Main
|
|
||||||
import ir.armor.tachidesk.impl.Category.createCategory
|
|
||||||
import ir.armor.tachidesk.impl.Category.getCategoryList
|
|
||||||
import ir.armor.tachidesk.impl.Category.removeCategory
|
|
||||||
import ir.armor.tachidesk.impl.Category.reorderCategory
|
|
||||||
import ir.armor.tachidesk.impl.Category.updateCategory
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.addMangaToCategory
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.getCategoryMangaList
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.getMangaCategories
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
|
||||||
import ir.armor.tachidesk.impl.Chapter.getChapter
|
|
||||||
import ir.armor.tachidesk.impl.Chapter.getChapterList
|
|
||||||
import ir.armor.tachidesk.impl.Extension.getExtensionIcon
|
|
||||||
import ir.armor.tachidesk.impl.Extension.installExtension
|
|
||||||
import ir.armor.tachidesk.impl.Extension.uninstallExtension
|
|
||||||
import ir.armor.tachidesk.impl.Extension.updateExtension
|
|
||||||
import ir.armor.tachidesk.impl.ExtensionsList.getExtensionList
|
|
||||||
import ir.armor.tachidesk.impl.Library.addMangaToLibrary
|
|
||||||
import ir.armor.tachidesk.impl.Library.getLibraryMangas
|
|
||||||
import ir.armor.tachidesk.impl.Library.removeMangaFromLibrary
|
|
||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
|
||||||
import ir.armor.tachidesk.impl.Manga.getMangaThumbnail
|
|
||||||
import ir.armor.tachidesk.impl.MangaList.getMangaList
|
|
||||||
import ir.armor.tachidesk.impl.Page.getPageImage
|
|
||||||
import ir.armor.tachidesk.impl.Search.sourceFilters
|
|
||||||
import ir.armor.tachidesk.impl.Search.sourceGlobalSearch
|
|
||||||
import ir.armor.tachidesk.impl.Search.sourceSearch
|
|
||||||
import ir.armor.tachidesk.impl.Source.getSource
|
|
||||||
import ir.armor.tachidesk.impl.Source.getSourceList
|
|
||||||
import ir.armor.tachidesk.impl.backup.BackupFlags
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
|
|
||||||
import ir.armor.tachidesk.server.internal.About.getAbout
|
|
||||||
import ir.armor.tachidesk.server.util.openInBrowser
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.future.future
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import java.io.IOException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -53,55 +7,44 @@ import kotlin.concurrent.thread
|
|||||||
* 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/. */
|
||||||
|
|
||||||
object JavalinSetup {
|
import io.javalin.Javalin
|
||||||
private val logger = KotlinLogging.logger {}
|
import suwayomi.server.JavalinSetup.future
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
import suwayomi.server.impl.About
|
||||||
|
import suwayomi.tachidesk.impl.Category
|
||||||
private fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
|
import suwayomi.tachidesk.impl.CategoryManga.addMangaToCategory
|
||||||
return scope.future(block = block)
|
import suwayomi.tachidesk.impl.CategoryManga.getCategoryMangaList
|
||||||
}
|
import suwayomi.tachidesk.impl.CategoryManga.getMangaCategories
|
||||||
|
import suwayomi.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
||||||
fun javalinSetup() {
|
import suwayomi.tachidesk.impl.Chapter.getChapter
|
||||||
var hasWebUiBundled = false
|
import suwayomi.tachidesk.impl.Chapter.getChapterList
|
||||||
|
import suwayomi.tachidesk.impl.Chapter.modifyChapter
|
||||||
val app = Javalin.create { config ->
|
import suwayomi.tachidesk.impl.Library.addMangaToLibrary
|
||||||
try {
|
import suwayomi.tachidesk.impl.Library.getLibraryMangas
|
||||||
// if the bellow line throws an exception then webUI is not bundled
|
import suwayomi.tachidesk.impl.Library.removeMangaFromLibrary
|
||||||
Main::class.java.getResource("/react/index.html")
|
import suwayomi.tachidesk.impl.Manga.getManga
|
||||||
|
import suwayomi.tachidesk.impl.Manga.getMangaThumbnail
|
||||||
// no exception so we can tell javalin to serve webUI
|
import suwayomi.tachidesk.impl.MangaList.getMangaList
|
||||||
hasWebUiBundled = true
|
import suwayomi.tachidesk.impl.Page.getPageImage
|
||||||
config.addStaticFiles("/react")
|
import suwayomi.tachidesk.impl.Search.sourceFilters
|
||||||
config.addSinglePageRoot("/", "/react/index.html")
|
import suwayomi.tachidesk.impl.Search.sourceGlobalSearch
|
||||||
} catch (e: RuntimeException) {
|
import suwayomi.tachidesk.impl.Search.sourceSearch
|
||||||
logger.warn("react build files are missing.")
|
import suwayomi.tachidesk.impl.Source.getSource
|
||||||
hasWebUiBundled = false
|
import suwayomi.tachidesk.impl.Source.getSourceList
|
||||||
}
|
import suwayomi.tachidesk.impl.backup.BackupFlags
|
||||||
config.enableCorsForAllOrigins()
|
import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
||||||
}.start(serverConfig.ip, serverConfig.port)
|
import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
|
||||||
|
import suwayomi.tachidesk.impl.download.DownloadManager
|
||||||
// when JVM is prompted to shutdown, stop javalin gracefully
|
import suwayomi.tachidesk.impl.extension.Extension.getExtensionIcon
|
||||||
Runtime.getRuntime().addShutdownHook(
|
import suwayomi.tachidesk.impl.extension.Extension.installExtension
|
||||||
thread(start = false) {
|
import suwayomi.tachidesk.impl.extension.Extension.uninstallExtension
|
||||||
app.stop()
|
import suwayomi.tachidesk.impl.extension.Extension.updateExtension
|
||||||
}
|
import suwayomi.tachidesk.impl.extension.ExtensionsList.getExtensionList
|
||||||
)
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
|
||||||
openInBrowser()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.exception(NullPointerException::class.java) { e, ctx ->
|
|
||||||
logger.error("NullPointerException while handling the request", e)
|
|
||||||
ctx.status(404)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.exception(IOException::class.java) { e, ctx ->
|
|
||||||
logger.error("IOException while handling the request", e)
|
|
||||||
ctx.status(500)
|
|
||||||
ctx.result(e.message ?: "Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
object TachideskAPI {
|
||||||
|
fun defineEndpoints(app: Javalin) {
|
||||||
|
// list all extensions
|
||||||
app.get("/api/v1/extension/list") { ctx ->
|
app.get("/api/v1/extension/list") { ctx ->
|
||||||
ctx.json(
|
ctx.json(
|
||||||
future {
|
future {
|
||||||
@@ -110,6 +53,7 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// install extension identified with "pkgName"
|
||||||
app.get("/api/v1/extension/install/:pkgName") { ctx ->
|
app.get("/api/v1/extension/install/:pkgName") { ctx ->
|
||||||
val pkgName = ctx.pathParam("pkgName")
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
@@ -120,6 +64,7 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update extension identified with "pkgName"
|
||||||
app.get("/api/v1/extension/update/:pkgName") { ctx ->
|
app.get("/api/v1/extension/update/:pkgName") { ctx ->
|
||||||
val pkgName = ctx.pathParam("pkgName")
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
@@ -130,6 +75,7 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uninstall extension identified with "pkgName"
|
||||||
app.get("/api/v1/extension/uninstall/:pkgName") { ctx ->
|
app.get("/api/v1/extension/uninstall/:pkgName") { ctx ->
|
||||||
val pkgName = ctx.pathParam("pkgName")
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
@@ -138,7 +84,7 @@ object JavalinSetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// icon for extension named `apkName`
|
// icon for extension named `apkName`
|
||||||
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
app.get("/api/v1/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
|
||||||
val apkName = ctx.pathParam("apkName")
|
val apkName = ctx.pathParam("apkName")
|
||||||
|
|
||||||
ctx.result(
|
ctx.result(
|
||||||
@@ -186,9 +132,11 @@ object JavalinSetup {
|
|||||||
// get manga info
|
// get manga info
|
||||||
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
|
||||||
|
|
||||||
ctx.json(
|
ctx.json(
|
||||||
future {
|
future {
|
||||||
getManga(mangaId)
|
getManga(mangaId, onlineFetch)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -206,24 +154,6 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// adds the manga to library
|
|
||||||
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
|
||||||
|
|
||||||
ctx.result(
|
|
||||||
future { addMangaToLibrary(mangaId) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// removes the manga from the library
|
|
||||||
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
|
||||||
|
|
||||||
ctx.result(
|
|
||||||
future { removeMangaFromLibrary(mangaId) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// list manga's categories
|
// list manga's categories
|
||||||
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
@@ -249,7 +179,10 @@ object JavalinSetup {
|
|||||||
// get chapter list when showing a manga
|
// get chapter list when showing a manga
|
||||||
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(future { getChapterList(mangaId) })
|
|
||||||
|
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean()
|
||||||
|
|
||||||
|
ctx.json(future { getChapterList(mangaId, onlineFetch) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// used to display a chapter, get a chapter in order to show it's pages
|
// used to display a chapter, get a chapter in order to show it's pages
|
||||||
@@ -259,6 +192,22 @@ object JavalinSetup {
|
|||||||
ctx.json(future { getChapter(chapterIndex, mangaId) })
|
ctx.json(future { getChapter(chapterIndex, mangaId) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// used to modify a chapter's parameters
|
||||||
|
app.patch("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||||
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
|
val read = ctx.formParam("read")?.toBoolean()
|
||||||
|
val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
|
||||||
|
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
|
||||||
|
val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
|
||||||
|
|
||||||
|
modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get page at index "index"
|
||||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
@@ -273,7 +222,17 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// global search
|
// submit a chapter for download
|
||||||
|
app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel a chapter download
|
||||||
|
app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// global search, Not implemented yet
|
||||||
app.get("/api/v1/search/:searchTerm") { ctx ->
|
app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||||
val searchTerm = ctx.pathParam("searchTerm")
|
val searchTerm = ctx.pathParam("searchTerm")
|
||||||
ctx.json(sourceGlobalSearch(searchTerm))
|
ctx.json(sourceGlobalSearch(searchTerm))
|
||||||
@@ -293,6 +252,24 @@ object JavalinSetup {
|
|||||||
ctx.json(sourceFilters(sourceId))
|
ctx.json(sourceFilters(sourceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adds the manga to library
|
||||||
|
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
|
ctx.result(
|
||||||
|
future { addMangaToLibrary(mangaId) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes the manga from the library
|
||||||
|
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
|
ctx.result(
|
||||||
|
future { removeMangaFromLibrary(mangaId) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// lists mangas that have no category assigned
|
// lists mangas that have no category assigned
|
||||||
app.get("/api/v1/library/") { ctx ->
|
app.get("/api/v1/library/") { ctx ->
|
||||||
ctx.json(getLibraryMangas())
|
ctx.json(getLibraryMangas())
|
||||||
@@ -300,27 +277,27 @@ object JavalinSetup {
|
|||||||
|
|
||||||
// category list
|
// category list
|
||||||
app.get("/api/v1/category/") { ctx ->
|
app.get("/api/v1/category/") { ctx ->
|
||||||
ctx.json(getCategoryList())
|
ctx.json(Category.getCategoryList())
|
||||||
}
|
}
|
||||||
|
|
||||||
// category create
|
// category create
|
||||||
app.post("/api/v1/category/") { ctx ->
|
app.post("/api/v1/category/") { ctx ->
|
||||||
val name = ctx.formParam("name")!!
|
val name = ctx.formParam("name")!!
|
||||||
createCategory(name)
|
Category.createCategory(name)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns some static info of the current app build
|
// returns some static info of the current app build
|
||||||
app.get("/api/v1/about/") { ctx ->
|
app.get("/api/v1/about/") { ctx ->
|
||||||
ctx.json(getAbout())
|
ctx.json(About.getAbout())
|
||||||
}
|
}
|
||||||
|
|
||||||
// category modification
|
// category modification
|
||||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
val name = ctx.formParam("name")
|
val name = ctx.formParam("name")
|
||||||
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
val isDefault = ctx.formParam("default")?.toBoolean()
|
||||||
updateCategory(categoryId, name, isLanding)
|
Category.updateCategory(categoryId, name, isDefault)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,14 +306,14 @@ object JavalinSetup {
|
|||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
val from = ctx.formParam("from")!!.toInt()
|
val from = ctx.formParam("from")!!.toInt()
|
||||||
val to = ctx.formParam("to")!!.toInt()
|
val to = ctx.formParam("to")!!.toInt()
|
||||||
reorderCategory(categoryId, from, to)
|
Category.reorderCategory(categoryId, from, to)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// category delete
|
// category delete
|
||||||
app.delete("/api/v1/category/:categoryId") { ctx ->
|
app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
removeCategory(categoryId)
|
Category.removeCategory(categoryId)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,5 +380,60 @@ object JavalinSetup {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download queue stats
|
||||||
|
app.ws("/api/v1/downloads") { ws ->
|
||||||
|
ws.onConnect { ctx ->
|
||||||
|
DownloadManager.addClient(ctx)
|
||||||
|
DownloadManager.notifyClient(ctx)
|
||||||
|
}
|
||||||
|
ws.onMessage { ctx ->
|
||||||
|
DownloadManager.handleRequest(ctx)
|
||||||
|
}
|
||||||
|
ws.onClose { ctx ->
|
||||||
|
DownloadManager.removeClient(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the downloader
|
||||||
|
app.get("/api/v1/downloads/start") { ctx ->
|
||||||
|
DownloadManager.start()
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the downloader
|
||||||
|
app.get("/api/v1/downloads/stop") { ctx ->
|
||||||
|
DownloadManager.stop()
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear download queue
|
||||||
|
app.get("/api/v1/downloads/clear") { ctx ->
|
||||||
|
DownloadManager.clear()
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue chapter for download
|
||||||
|
app.get("/api/v1/download/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||||
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
|
DownloadManager.enqueue(chapterIndex, mangaId)
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete chapter from download queue
|
||||||
|
app.delete("/api/v1/download/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||||
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
|
DownloadManager.unqueue(chapterIndex, mangaId)
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+9
-9
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,11 +7,6 @@ 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.impl.CategoryManga.removeMangaFromCategory
|
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryTable
|
|
||||||
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,6 +14,11 @@ 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 suwayomi.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
||||||
|
import suwayomi.tachidesk.model.dataclass.CategoryDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryMangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryTable
|
||||||
|
import suwayomi.tachidesk.model.table.toDataClass
|
||||||
|
|
||||||
object Category {
|
object Category {
|
||||||
/**
|
/**
|
||||||
@@ -35,16 +35,16 @@ object Category {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?) {
|
||||||
transaction {
|
transaction {
|
||||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||||
if (name != null) it[CategoryTable.name] = name
|
if (name != null) it[CategoryTable.name] = name
|
||||||
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
if (isDefault != null) it[CategoryTable.isDefault] = isDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the category from position `from` to `to`
|
* Move the category from position `from` to `to`
|
||||||
*/
|
*/
|
||||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
||||||
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,12 +7,6 @@ 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.model.database.table.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.toDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.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
|
||||||
@@ -20,6 +14,12 @@ import org.jetbrains.exposed.sql.insert
|
|||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.model.dataclass.CategoryDataClass
|
||||||
|
import suwayomi.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryMangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryTable
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.toDataClass
|
||||||
|
|
||||||
object CategoryManga {
|
object CategoryManga {
|
||||||
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder.DESC
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.impl.Manga.getManga
|
||||||
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import suwayomi.tachidesk.model.dataclass.ChapterDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.ChapterTable
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.PageTable
|
||||||
|
import suwayomi.tachidesk.model.table.toDataClass
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
object Chapter {
|
||||||
|
/** get chapter list when showing a manga */
|
||||||
|
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean?): List<ChapterDataClass> {
|
||||||
|
return if (onlineFetch == true) {
|
||||||
|
getSourceChapters(mangaId)
|
||||||
|
} else {
|
||||||
|
transaction {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
val chapterCount = chapterList.count()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
||||||
|
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
||||||
|
if (chapterEntry == null) {
|
||||||
|
ChapterTable.insert {
|
||||||
|
it[url] = fetchedChapter.url
|
||||||
|
it[name] = fetchedChapter.name
|
||||||
|
it[date_upload] = fetchedChapter.date_upload
|
||||||
|
it[chapter_number] = fetchedChapter.chapter_number
|
||||||
|
it[scanlator] = fetchedChapter.scanlator
|
||||||
|
|
||||||
|
it[chapterIndex] = index + 1
|
||||||
|
it[manga] = mangaId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
||||||
|
it[name] = fetchedChapter.name
|
||||||
|
it[date_upload] = fetchedChapter.date_upload
|
||||||
|
it[chapter_number] = fetchedChapter.chapter_number
|
||||||
|
it[scanlator] = fetchedChapter.scanlator
|
||||||
|
|
||||||
|
it[chapterIndex] = index + 1
|
||||||
|
it[manga] = mangaId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear any orphaned chapters that are in the db but not in `chapterList`
|
||||||
|
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
||||||
|
if (dbChapterCount > chapterCount) { // we got some clean up due
|
||||||
|
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.toList() }
|
||||||
|
|
||||||
|
dbChapterList.forEach {
|
||||||
|
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
|
||||||
|
chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url]
|
||||||
|
) {
|
||||||
|
transaction {
|
||||||
|
PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] }
|
||||||
|
ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
it.url,
|
||||||
|
it.name,
|
||||||
|
it.date_upload,
|
||||||
|
it.chapter_number,
|
||||||
|
it.scanlator,
|
||||||
|
mangaId,
|
||||||
|
|
||||||
|
dbChapter[ChapterTable.isRead],
|
||||||
|
dbChapter[ChapterTable.isBookmarked],
|
||||||
|
dbChapter[ChapterTable.lastPageRead],
|
||||||
|
dbChapter[ChapterTable.lastReadAt],
|
||||||
|
|
||||||
|
chapterCount - index,
|
||||||
|
dbChapter[ChapterTable.isDownloaded],
|
||||||
|
|
||||||
|
dbChapter[ChapterTable.pageCount],
|
||||||
|
|
||||||
|
chapterList.size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** used to display a chapter, get a chapter in order to show it's pages */
|
||||||
|
suspend fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
||||||
|
val chapterEntry = transaction {
|
||||||
|
ChapterTable.select {
|
||||||
|
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||||
|
}.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (!chapterEntry[ChapterTable.isDownloaded]) {
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
|
val pageList = source.fetchPageList(
|
||||||
|
SChapter.create().apply {
|
||||||
|
url = chapterEntry[ChapterTable.url]
|
||||||
|
name = chapterEntry[ChapterTable.name]
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
|
val chapterId = chapterEntry[ChapterTable.id].value
|
||||||
|
val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
||||||
|
|
||||||
|
// update page list for this chapter
|
||||||
|
transaction {
|
||||||
|
pageList.forEach { page ->
|
||||||
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||||
|
if (pageEntry == null) {
|
||||||
|
PageTable.insert {
|
||||||
|
it[index] = page.index
|
||||||
|
it[url] = page.url
|
||||||
|
it[imageUrl] = page.imageUrl
|
||||||
|
it[chapter] = chapterId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
|
||||||
|
it[url] = page.url
|
||||||
|
it[imageUrl] = page.imageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pageCount = pageList.count()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) {
|
||||||
|
it[ChapterTable.pageCount] = pageCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.lastReadAt],
|
||||||
|
|
||||||
|
chapterEntry[ChapterTable.chapterIndex],
|
||||||
|
chapterEntry[ChapterTable.isDownloaded],
|
||||||
|
pageCount,
|
||||||
|
chapterCount.toInt()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ChapterTable.toDataClass(chapterEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
update[ChapterTable.lastReadAt] = Instant.now().epochSecond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markPrevRead?.let {
|
||||||
|
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex less chapterIndex) }) {
|
||||||
|
it[ChapterTable.isRead] = markPrevRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-8
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,27 +7,39 @@ 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.impl.Manga.getManga
|
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
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.insert
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.impl.Manga.getManga
|
||||||
|
import suwayomi.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryMangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryTable
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.toDataClass
|
||||||
|
|
||||||
object Library {
|
object Library {
|
||||||
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
|
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
|
||||||
// ..implement that shit at some time...
|
// ..implement that shit at some time...
|
||||||
// ..also Consider to rename it to `isDefault`
|
// ..also Consider to rename it to `isDefault`
|
||||||
suspend fun addMangaToLibrary(mangaId: Int) {
|
suspend fun addMangaToLibrary(mangaId: Int) {
|
||||||
val manga = getManga(mangaId)
|
val manga = getManga(mangaId)
|
||||||
if (!manga.inLibrary) {
|
if (!manga.inLibrary) {
|
||||||
transaction {
|
transaction {
|
||||||
|
val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList()
|
||||||
|
|
||||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||||
it[inLibrary] = true
|
it[MangaTable.inLibrary] = true
|
||||||
|
it[MangaTable.defaultCategory] = defaultCategories.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultCategories.forEach { category ->
|
||||||
|
CategoryMangaTable.insert {
|
||||||
|
it[CategoryMangaTable.category] = category[CategoryTable.id].value
|
||||||
|
it[CategoryMangaTable.manga] = mangaId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+40
-28
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -9,22 +9,23 @@ 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.impl.MangaList.proxyThumbnailUrl
|
|
||||||
import ir.armor.tachidesk.impl.Source.getSource
|
|
||||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.await
|
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
|
||||||
import ir.armor.tachidesk.model.database.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.DI
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import suwayomi.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||||
|
import suwayomi.tachidesk.impl.Source.getSource
|
||||||
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import suwayomi.tachidesk.impl.util.network.await
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||||
|
import suwayomi.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.MangaStatus
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
object Manga {
|
object Manga {
|
||||||
@@ -35,17 +36,17 @@ object Manga {
|
|||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
|
||||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
return if (mangaEntry[MangaTable.initialized]) {
|
return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
mangaEntry[MangaTable.sourceReference].toString(),
|
mangaEntry[MangaTable.sourceReference].toString(),
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[MangaTable.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[MangaTable.title],
|
||||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
proxyThumbnailUrl(mangaId),
|
||||||
|
|
||||||
true,
|
true,
|
||||||
|
|
||||||
@@ -55,7 +56,8 @@ object Manga {
|
|||||||
mangaEntry[MangaTable.genre],
|
mangaEntry[MangaTable.genre],
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||||
mangaEntry[MangaTable.inLibrary],
|
mangaEntry[MangaTable.inLibrary],
|
||||||
getSource(mangaEntry[MangaTable.sourceReference])
|
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||||
|
false
|
||||||
)
|
)
|
||||||
} else { // initialize manga
|
} else { // initialize manga
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
@@ -76,13 +78,14 @@ object Manga {
|
|||||||
it[MangaTable.description] = truncate(fetchedManga.description, 4096)
|
it[MangaTable.description] = truncate(fetchedManga.description, 4096)
|
||||||
it[MangaTable.genre] = fetchedManga.genre
|
it[MangaTable.genre] = fetchedManga.genre
|
||||||
it[MangaTable.status] = fetchedManga.status
|
it[MangaTable.status] = fetchedManga.status
|
||||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
|
||||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
clearMangaThumbnail(mangaId)
|
||||||
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
|
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
@@ -90,7 +93,7 @@ object Manga {
|
|||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[MangaTable.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[MangaTable.title],
|
||||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
|
proxyThumbnailUrl(mangaId),
|
||||||
|
|
||||||
true,
|
true,
|
||||||
|
|
||||||
@@ -99,29 +102,38 @@ object Manga {
|
|||||||
fetchedManga.description,
|
fetchedManga.description,
|
||||||
fetchedManga.genre,
|
fetchedManga.genre,
|
||||||
MangaStatus.valueOf(fetchedManga.status).name,
|
MangaStatus.valueOf(fetchedManga.status).name,
|
||||||
false,
|
mangaEntry[MangaTable.inLibrary],
|
||||||
getSource(mangaEntry[MangaTable.sourceReference])
|
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||||
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val saveDir = applicationDirs.mangaThumbnailsRoot
|
||||||
val saveDir = applicationDirs.thumbnailsRoot
|
|
||||||
val fileName = mangaId.toString()
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, fileName) {
|
return getCachedImageResponse(saveDir, fileName) {
|
||||||
|
getManga(mangaId) // make sure is initialized
|
||||||
|
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]!!
|
||||||
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
|
||||||
}
|
|
||||||
|
|
||||||
source.client.newCall(
|
source.client.newCall(
|
||||||
GET(thumbnailUrl, source.headers)
|
GET(thumbnailUrl, source.headers)
|
||||||
).await()
|
).await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearMangaThumbnail(mangaId: Int) {
|
||||||
|
val saveDir = applicationDirs.mangaThumbnailsRoot
|
||||||
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
|
clearCachedImage(saveDir, fileName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+8
-8
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,15 +8,15 @@ 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.impl.util.GetHttpSource.getHttpSource
|
|
||||||
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.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
|
||||||
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import suwayomi.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.model.dataclass.PagedMangaListDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.MangaStatus
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
|
|
||||||
object MangaList {
|
object MangaList {
|
||||||
fun proxyThumbnailUrl(mangaId: Int): String {
|
fun proxyThumbnailUrl(mangaId: Int): String {
|
||||||
@@ -40,7 +40,7 @@ object MangaList {
|
|||||||
val mangasPage = this
|
val mangasPage = this
|
||||||
val mangaList = transaction {
|
val mangaList = transaction {
|
||||||
return@transaction mangasPage.mangas.map { manga ->
|
return@transaction mangasPage.mangas.map { manga ->
|
||||||
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
val mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
||||||
if (mangaEntry == null) { // create manga entry
|
if (mangaEntry == null) { // create manga entry
|
||||||
val mangaId = MangaTable.insertAndGetId {
|
val mangaId = MangaTable.insertAndGetId {
|
||||||
it[url] = manga.url
|
it[url] = manga.url
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user