Compare commits
358 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a325440f24 | |||
| 14072bb5a0 | |||
| 7fc33ba8db | |||
| 47e51b6615 | |||
| 857562eaff | |||
| bace854b50 | |||
| e4a404472d | |||
| 7bfa215b4c | |||
| ab7af4b80b | |||
| 2c7ebd8ece | |||
| c96da79058 | |||
| 8f09ebacf5 | |||
| e21f3b9c75 | |||
| 37eeef06e2 | |||
| b7fe56687c | |||
| 60565729ca | |||
| 36f4e1c340 | |||
| abc2a5214b | |||
| a29010e0d7 | |||
| db99ab66ae | |||
| 84cc73c149 | |||
| 10a29cab33 | |||
| 849e2f103a | |||
| 6c22fe193a | |||
| e69dbbf418 | |||
| 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 | |||
| 63a078cf7d | |||
| 5304917e53 | |||
| 831b74d2ec | |||
| 1bad9dcd69 | |||
| dd43716851 | |||
| f2e55e95a2 | |||
| 14658a0c4d | |||
| 4195e7056b | |||
| 1d29e8b248 | |||
| b718c718df | |||
| a3601cf1b5 | |||
| 0236a9639b | |||
| 5f4c7454ee | |||
| 773120c96a | |||
| 4b273c6bf9 | |||
| b626aa66ba | |||
| 1dd029559e | |||
| 59cbe5d5bc | |||
| 40d1173653 | |||
| bf6a0aba5d | |||
| 34d8feacdd | |||
| 1ea821584c | |||
| 3d2fee19bb | |||
| 449d12779a | |||
| 6fb6a251e7 | |||
| 4d6220f894 | |||
| fe747bfc52 | |||
| 0c2d038870 | |||
| 4e3f73af75 | |||
| 63e5e1b45f | |||
| 2e1558bd96 | |||
| 0671dee8b2 | |||
| 8f91b8089a | |||
| 009b45f676 | |||
| 8f7d5eb311 | |||
| f3de835ef3 | |||
| fd6662f428 | |||
| fde137b3ed | |||
| a1349aa0e3 | |||
| c9ef5f9b9d | |||
| 8fbf564177 | |||
| ae0b1a818c | |||
| c04cc780b7 | |||
| 71ad1bb6e3 | |||
| c1be77ee9b | |||
| d1fa857ffb | |||
| 93fd81b38b | |||
| 2f116b40b2 | |||
| b884d34bdf | |||
| 309803368b | |||
| 19fc5be8f3 | |||
| c28fac14c0 | |||
| 66e38de29f | |||
| 282cb1d3be | |||
| b741ded595 | |||
| 6b290695fc | |||
| 4e43c554c0 | |||
| 090a72b35f | |||
| 3fcc269df3 | |||
| 9958e0eb34 | |||
| c5269002a2 | |||
| 455a35f8ae | |||
| 0c79f207c3 | |||
| cd16d32a35 | |||
| 1989c1eb48 | |||
| f56856529f | |||
| 52e27a3e39 | |||
| 177c971b52 | |||
| 7a52e19235 | |||
| 5171e509a5 | |||
| 975a3b1828 | |||
| c11887fada | |||
| e043cb5690 | |||
| b2d5354798 | |||
| a211a4143b | |||
| c0df7d314b | |||
| c8a8ce07e2 | |||
| e0e474dfce | |||
| 7591748811 | |||
| 884308690f | |||
| 15bd5b4b7a | |||
| abc3a16ee3 | |||
| bb09ccf3c0 | |||
| ad2ea8095b | |||
| 760d1116a1 | |||
| 47fcf7eb97 | |||
| b0e90c2f63 | |||
| f502884fdd | |||
| 5ed79523d2 | |||
| da5dd70969 | |||
| 68e69085df | |||
| 640ce8f5d7 | |||
| c960cc1ee5 | |||
| 2b2601aa4a | |||
| 99a10ec7db | |||
| 035105adf0 | |||
| f983f0e359 | |||
| 769472b24c | |||
| 8c80ad7575 | |||
| 63db2e6695 | |||
| d6d5e97fbd | |||
| 1ae0a8326e | |||
| 57693fef7b | |||
| 5656016700 | |||
| 90ae180b3e | |||
| 2a3c78d43e | |||
| 11000af718 | |||
| b808121f1d | |||
| addadefeb1 | |||
| 838cd20e57 | |||
| 5b9219522d | |||
| caeb4d273d | |||
| 77cf87c989 | |||
| 50c2dbed5d | |||
| 71a9396952 | |||
| bc3ad75328 | |||
| 077bbc3c38 | |||
| b1b1abad1d |
+3
-2
@@ -9,7 +9,7 @@
|
||||
# Gradle wrapper
|
||||
*.jar binary
|
||||
|
||||
# Images
|
||||
# Binary files types
|
||||
*.webp binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
@@ -24,4 +24,5 @@
|
||||
*.woff binary
|
||||
*.pyc binary
|
||||
*.swp binary
|
||||
*.pdf binary
|
||||
*.pdf binary
|
||||
*.exe binary
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
git lfs install
|
||||
#git lfs track "*.zip"
|
||||
|
||||
cp ../master/repo/* .
|
||||
new_jar_build=$(ls *.jar| tail -1)
|
||||
echo "last jar build file name: $new_jar_build"
|
||||
|
||||
new_win32_build=$(ls *.zip| tail -1)
|
||||
echo "last win32 build file name: $new_win32_build"
|
||||
|
||||
cp -f $new_jar_build Tachidesk-latest.jar
|
||||
cp -f $new_win32_build Tachidesk-latest-win32.zip
|
||||
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git status
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git add .
|
||||
git commit -m "Update repo"
|
||||
git push
|
||||
else
|
||||
echo "No changes to commit"
|
||||
fi
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get last commit message
|
||||
#last_commit_log=$(git log -1 --pretty=format:"%s")
|
||||
#echo "last commit log: $last_commit_log"
|
||||
#
|
||||
#filter_count=$(echo "$last_commit_log" | grep -e '\[RELEASE CI\]' -e '\[CI RELEASE\]' | wc -c)
|
||||
#echo "count is: $filter_count"
|
||||
|
||||
mkdir -p repo/
|
||||
cp server/build/Tachidesk-*.jar repo/
|
||||
cp server/build/Tachidesk-*.zip repo/
|
||||
|
||||
ls repo
|
||||
pwd
|
||||
|
||||
#if [ "$filter_count" -gt 0 ]; then
|
||||
# cp server/build/Tachidesk-*.jar repo/
|
||||
# cp server/build/Tachidesk-*.zip repo/
|
||||
#fi
|
||||
@@ -1,9 +1,6 @@
|
||||
name: CI
|
||||
name: CI Pull Request
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -19,21 +16,21 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build FatJar
|
||||
name: Build pull request
|
||||
needs: check_wrapper
|
||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Cancel previous runs
|
||||
uses: styfle/cancel-workflow-action@0.5.0
|
||||
uses: styfle/cancel-workflow-action@0.9.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Checkout master branch
|
||||
- name: Checkout pull request
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
path: master
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -51,21 +48,21 @@ jobs:
|
||||
- name: Download android.jar
|
||||
run: |
|
||||
cd master
|
||||
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/react/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||
**/webUI/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
|
||||
|
||||
- name: Build Jar and launch4j
|
||||
- name: Build and copy webUI, Build Jar
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :server:windowsPackage --stacktrace
|
||||
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
@@ -0,0 +1,133 @@
|
||||
name: CI build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
check_wrapper:
|
||||
name: Validate Gradle Wrapper
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build artifacts and deploy preview
|
||||
needs: check_wrapper
|
||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Cancel previous runs
|
||||
uses: styfle/cancel-workflow-action@0.9.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Checkout master branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
path: master
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
cd master
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Download android.jar
|
||||
run: |
|
||||
cd master
|
||||
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/webUI/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
|
||||
|
||||
- name: Build and copy webUI, Build Jar
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
env:
|
||||
ProductBuildType: "Preview"
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
# - name: 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
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: 'Suwayomi/Tachidesk-preview'
|
||||
ref: main
|
||||
path: preview
|
||||
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||
|
||||
- name: Create Tag
|
||||
run: |
|
||||
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 }}
|
||||
|
||||
- name: Run Docker build workflow
|
||||
run: |
|
||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "preview"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Publish
|
||||
name: CI Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -18,20 +18,20 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build FatJar
|
||||
name: Build artifacts and release
|
||||
needs: check_wrapper
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Cancel previous runs
|
||||
uses: styfle/cancel-workflow-action@0.5.0
|
||||
uses: styfle/cancel-workflow-action@0.9.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Checkout master branch
|
||||
- name: Checkout ${{ github.ref }}
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
ref: ${{ github.ref }}
|
||||
path: master
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -49,67 +49,44 @@ jobs:
|
||||
- name: Download android.jar
|
||||
run: |
|
||||
cd master
|
||||
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/react/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
**/webUI/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
|
||||
|
||||
- name: Build Jar and launch4j
|
||||
- name: Build and copy webUI, Build Jar
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
env:
|
||||
ProductBuildType: "Stable"
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :server:windowsPackage --stacktrace
|
||||
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
|
||||
- name: Create repo artifacts
|
||||
- name: make windows packages
|
||||
run: |
|
||||
cd master
|
||||
./.github/scripts/create-repo.sh
|
||||
cd master/scripts
|
||||
./windows-bundler.sh win32
|
||||
./windows-bundler.sh win64
|
||||
|
||||
- name: Upload Release
|
||||
uses: xresloader/upload-to-github-release@master
|
||||
uses: xresloader/upload-to-github-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
file: "master/repo/*"
|
||||
file: "master/server/build/*.jar;master/server/build/*.zip"
|
||||
tags: true
|
||||
draft: 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 }}
|
||||
- name: Run Docker build workflow
|
||||
run: |
|
||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
|
||||
|
||||
|
||||
+9
-1
@@ -1,8 +1,16 @@
|
||||
# Ignore Gradle project-specific cache directory
|
||||
.gradle
|
||||
.idea
|
||||
gradle.properties
|
||||
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
server/src/main/resources/react
|
||||
server/src/main/resources/webUI
|
||||
server/tmp/
|
||||
server/tachiserver-data/
|
||||
|
||||
# bundle asset downlaods
|
||||
OpenJDK*.zip
|
||||
electron-*.zip
|
||||
rcedit-*
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
import net.harawata.appdirs.AppDirsFactory
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
val ApplicationRootDir: String
|
||||
get(): String {
|
||||
return System.getProperty(
|
||||
"suwayomi.tachidesk.server.rootDir",
|
||||
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||
)
|
||||
}
|
||||
@@ -7,19 +7,17 @@ package xyz.nulldev.ts.config
|
||||
* 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 com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import mu.KotlinLogging
|
||||
import net.harawata.appdirs.AppDirsFactory
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Manages app config.
|
||||
*/
|
||||
open class ConfigManager {
|
||||
private val dataRoot by lazy { AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!! }
|
||||
|
||||
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
||||
val config by lazy { loadConfigs() }
|
||||
|
||||
@@ -27,8 +25,6 @@ open class ConfigManager {
|
||||
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
|
||||
get() = generatedModules
|
||||
|
||||
open val appConfigFile: String = "$dataRoot/server.conf"
|
||||
|
||||
val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
@@ -46,21 +42,34 @@ open class ConfigManager {
|
||||
*/
|
||||
fun loadConfigs(): Config {
|
||||
//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 baseConfig =
|
||||
ConfigFactory.parseMap(
|
||||
mapOf(
|
||||
"ts.server.rootDir" to ApplicationRootDir
|
||||
)
|
||||
)
|
||||
|
||||
//Load user config
|
||||
val userConfig =
|
||||
File(appConfigFile).let{
|
||||
File(ApplicationRootDir, "server.conf").let {
|
||||
ConfigFactory.parseFile(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val config = ConfigFactory.empty()
|
||||
.withFallback(baseConfig)
|
||||
.withFallback(userConfig)
|
||||
.withFallback(compatConfig)
|
||||
.withFallback(serverConfig)
|
||||
.resolve()
|
||||
|
||||
// set log level early
|
||||
if (debugLogsEnabled(config)) {
|
||||
setLogLevel(Level.DEBUG)
|
||||
}
|
||||
|
||||
logger.debug {
|
||||
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
|
||||
}
|
||||
@@ -69,7 +78,7 @@ open class ConfigManager {
|
||||
}
|
||||
|
||||
fun registerModule(module: ConfigModule) {
|
||||
generatedModules.put(module.javaClass, module)
|
||||
generatedModules[module.javaClass] = module
|
||||
}
|
||||
|
||||
fun registerModules(vararg modules: ConfigModule) {
|
||||
|
||||
@@ -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.tachidesk.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
plugins {
|
||||
application
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
@@ -30,23 +30,12 @@ dependencies {
|
||||
// Javassist
|
||||
compileOnly( "org.javassist:javassist:3.27.0-GA")
|
||||
|
||||
// Coroutines
|
||||
val kotlinx_coroutines_version = "1.4.2"
|
||||
compileOnly( "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version")
|
||||
compileOnly( "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinx_coroutines_version")
|
||||
|
||||
// XML
|
||||
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
||||
|
||||
// Config API
|
||||
implementation(project(":AndroidCompat:Config"))
|
||||
|
||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||
compileOnly("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
||||
|
||||
// APK parser
|
||||
compileOnly("net.dongliu:apk-parser:2.6.10")
|
||||
|
||||
// APK sig verifier
|
||||
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13")
|
||||
|
||||
@@ -58,6 +47,17 @@ dependencies {
|
||||
implementation("org.mozilla:rhino-runtime:1.7.13")
|
||||
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
|
||||
implementation("org.mozilla:rhino-engine:1.7.13")
|
||||
|
||||
// Kotlin wrapper around Java Preferences, makes certain things easier
|
||||
val multiplatformSettingsVersion = "0.7.7"
|
||||
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
|
||||
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
||||
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
|
||||
|
||||
@@ -15,7 +15,7 @@ Write-Output "Getting required Android.jar..."
|
||||
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
|
||||
|
||||
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT").content
|
||||
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" -UseBasicParsing).content
|
||||
|
||||
$android_jar = (Get-Location).Path + "\tmp\android.jar"
|
||||
|
||||
@@ -87,7 +87,6 @@ function Dedupe($path)
|
||||
}
|
||||
|
||||
Dedupe "AndroidCompat/src/main/java"
|
||||
Dedupe "server/src/main/java"
|
||||
Dedupe "server/src/main/kotlin"
|
||||
|
||||
Write-Output "Copying Android.jar to library folder..."
|
||||
|
||||
@@ -8,8 +8,19 @@
|
||||
|
||||
# This is a bash script to create android.jar stubs
|
||||
|
||||
for dep in "curl" "base64" "zip"
|
||||
do
|
||||
which $dep >/dev/null 2>&1 || { echo >&2 "Error: This script needs $dep installed."; abort=yes; }
|
||||
done
|
||||
|
||||
if [ $abort = yes ]; then
|
||||
echo "Some of the dependencies didn't exist. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# foolproof against running from AndroidCompat dir instead of running from project root
|
||||
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
|
||||
if [ "$(basename "$(pwd)")" = "AndroidCompat" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
@@ -48,7 +59,7 @@ zip --delete android.jar javax/*
|
||||
echo "Removing 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/Service.class
|
||||
zip --delete android.jar android/net/Uri.class
|
||||
@@ -57,12 +68,12 @@ zip --delete android.jar android/os/Environment.class
|
||||
zip --delete android.jar android/text/format/Formatter.class
|
||||
zip --delete android.jar android/text/Html.class
|
||||
|
||||
# Dedup overriden Android classes
|
||||
# Dedup overridden Android classes
|
||||
ABS_JAR="$(realpath android.jar)"
|
||||
function dedup() {
|
||||
pushd "$1"
|
||||
CLASSES="$(find * -type f)"
|
||||
echo "$CLASSES" | while read class
|
||||
CLASSES="$(find ./* -type f)"
|
||||
echo "$CLASSES" | while read -r class
|
||||
do
|
||||
NAME="${class%.*}"
|
||||
echo "Processing class: $NAME"
|
||||
@@ -71,13 +82,10 @@ function dedup() {
|
||||
popd
|
||||
}
|
||||
|
||||
pushd ..
|
||||
popd
|
||||
dedup AndroidCompat/src/main/java
|
||||
dedup server/src/main/java
|
||||
dedup server/src/main/kotlin
|
||||
popd
|
||||
|
||||
popd
|
||||
echo "Copying Android.jar to library folder..."
|
||||
mv tmp/android.jar AndroidCompat/lib
|
||||
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
package com.squareup.duktape;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/. */
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.squareup.duktape;
|
||||
|
||||
import kotlin.NotImplementedError;
|
||||
|
||||
@@ -64,18 +73,18 @@ public final class Duktape implements Closeable, AutoCloseable {
|
||||
throw new NotImplementedError("Not implemented!");
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Attaches to a global JavaScript object called {@code name} that implements {@code type}.
|
||||
// * {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
|
||||
// * {@code type} must be an interface that does not extend any other interfaces, and cannot define
|
||||
// * any overloaded methods.
|
||||
// * <p>Methods of the interface may return {@code void} or any of the following supported argument
|
||||
// * types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
|
||||
// * {@link Double}, {@link String}.
|
||||
// */
|
||||
// public synchronized <T> T get(final String name, final Class<T> type) {
|
||||
// throw new NotImplementedError("Not implemented!");
|
||||
// }
|
||||
/**
|
||||
* Attaches to a global JavaScript object called {@code name} that implements {@code type}.
|
||||
* {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
|
||||
* {@code type} must be an interface that does not extend any other interfaces, and cannot define
|
||||
* any overloaded methods.
|
||||
* <p>Methods of the interface may return {@code void} or any of the following supported argument
|
||||
* types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
|
||||
* {@link Double}, {@link String}.
|
||||
*/
|
||||
public synchronized <T> T get(final String name, final Class<T> type) {
|
||||
throw new NotImplementedError("Not implemented!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the native resources associated with this object. You <strong>must</strong> call this
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
package com.squareup.duktape;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
// part of tachiyomi-extensions which was originally licensed under Apache License Version 2.0
|
||||
|
||||
/* part of tachiyomi-extensions which is licensed under Apache License Version 2.0 */
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
+9
-10
@@ -38,7 +38,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import xyz.nulldev.androidcompat.info.ApplicationInfoImpl;
|
||||
import xyz.nulldev.androidcompat.io.AndroidFiles;
|
||||
import xyz.nulldev.androidcompat.io.sharedprefs.JsonSharedPreferences;
|
||||
import xyz.nulldev.androidcompat.io.sharedprefs.JavaSharedPreferences;
|
||||
import xyz.nulldev.androidcompat.service.ServiceSupport;
|
||||
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper;
|
||||
|
||||
@@ -165,23 +165,22 @@ public class CustomContext extends Context implements DIAware {
|
||||
/** Fake shared prefs! **/
|
||||
private Map<String, SharedPreferences> prefs = new HashMap<>(); //Cache
|
||||
|
||||
private File sharedPrefsFileFromString(String s) {
|
||||
return new File(androidFiles.getPrefsDir(), s + ".json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized SharedPreferences getSharedPreferences(String s, int i) {
|
||||
SharedPreferences preferences = prefs.get(s);
|
||||
//Create new shared preferences if one does not exist
|
||||
if(preferences == null) {
|
||||
preferences = getSharedPreferences(sharedPrefsFileFromString(s), i);
|
||||
preferences = new JavaSharedPreferences(s);
|
||||
prefs.put(s, preferences);
|
||||
}
|
||||
return preferences;
|
||||
}
|
||||
|
||||
public SharedPreferences getSharedPreferences(File file, int mode) {
|
||||
return new JsonSharedPreferences(file);
|
||||
@Override
|
||||
public SharedPreferences getSharedPreferences(@NotNull File file, int mode) {
|
||||
String path = file.getAbsolutePath().replace('\\', '/');
|
||||
int firstSlash = path.indexOf("/");
|
||||
return new JavaSharedPreferences(path.substring(firstSlash));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -191,8 +190,8 @@ public class CustomContext extends Context implements DIAware {
|
||||
|
||||
@Override
|
||||
public boolean deleteSharedPreferences(String name) {
|
||||
prefs.remove(name);
|
||||
return sharedPrefsFileFromString(name).delete();
|
||||
JavaSharedPreferences item = (JavaSharedPreferences) prefs.remove(name);
|
||||
return item.deleteAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,8 +26,6 @@ class AndroidFiles(val configManager: ConfigManager = GlobalConfigManager) {
|
||||
val downloadCacheDir: File get() = registerFile(filesConfig.downloadCacheDir)
|
||||
val databasesDir: File get() = registerFile(filesConfig.databasesDir)
|
||||
|
||||
val prefsDir: File get() = registerFile(filesConfig.prefsDir)
|
||||
|
||||
val packagesDir: File get() = registerFile(filesConfig.packageDir)
|
||||
|
||||
fun registerFile(file: String): File {
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
package xyz.nulldev.androidcompat.io.sharedprefs
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.russhwolf.settings.ExperimentalSettingsApi
|
||||
import com.russhwolf.settings.ExperimentalSettingsImplementation
|
||||
import com.russhwolf.settings.JvmPreferencesSettings
|
||||
import com.russhwolf.settings.serialization.decodeValue
|
||||
import com.russhwolf.settings.serialization.encodeValue
|
||||
import com.russhwolf.settings.set
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.builtins.SetSerializer
|
||||
import kotlinx.serialization.builtins.nullable
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import java.util.prefs.PreferenceChangeListener
|
||||
import java.util.prefs.Preferences
|
||||
|
||||
@OptIn(ExperimentalSettingsImplementation::class, ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
|
||||
class JavaSharedPreferences(key: String) : SharedPreferences {
|
||||
private val javaPreferences = Preferences.userRoot().node("suwayomi/tachidesk/$key")
|
||||
private val preferences = JvmPreferencesSettings(javaPreferences)
|
||||
private val listeners = mutableMapOf<SharedPreferences.OnSharedPreferenceChangeListener, PreferenceChangeListener>()
|
||||
|
||||
// TODO: 2021-05-29 Need to find a way to get this working with all pref types
|
||||
override fun getAll(): MutableMap<String, *> {
|
||||
return preferences.keys.associateWith { preferences.getStringOrNull(it) }.toMutableMap()
|
||||
}
|
||||
|
||||
override fun getString(key: String, defValue: String?): String? {
|
||||
return if (defValue != null) {
|
||||
preferences.getString(key, defValue)
|
||||
} else {
|
||||
preferences.getStringOrNull(key)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
|
||||
try {
|
||||
return if (defValues != null) {
|
||||
preferences.decodeValue(SetSerializer(String.serializer()).nullable, key, defValues)
|
||||
} else {
|
||||
preferences.decodeValue(SetSerializer(String.serializer()).nullable, key, null)
|
||||
}?.toMutableSet()
|
||||
} catch (e: SerializationException) {
|
||||
throw ClassCastException("$key was not a StringSet")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInt(key: String, defValue: Int): Int {
|
||||
return preferences.getInt(key, defValue)
|
||||
}
|
||||
|
||||
override fun getLong(key: String, defValue: Long): Long {
|
||||
return preferences.getLong(key, defValue)
|
||||
}
|
||||
|
||||
override fun getFloat(key: String, defValue: Float): Float {
|
||||
return preferences.getFloat(key, defValue)
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String, defValue: Boolean): Boolean {
|
||||
return preferences.getBoolean(key, defValue)
|
||||
}
|
||||
|
||||
override fun contains(key: String): Boolean {
|
||||
return key in preferences.keys
|
||||
}
|
||||
|
||||
override fun edit(): SharedPreferences.Editor {
|
||||
return Editor(preferences)
|
||||
}
|
||||
|
||||
class Editor(private val preferences: JvmPreferencesSettings) : SharedPreferences.Editor {
|
||||
val itemsToAdd = mutableMapOf<String, Any>()
|
||||
|
||||
override fun putString(key: String, value: String?): SharedPreferences.Editor {
|
||||
if (value != null) {
|
||||
itemsToAdd[key] = value
|
||||
} else {
|
||||
remove(key)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putStringSet(
|
||||
key: String,
|
||||
values: MutableSet<String>?
|
||||
): SharedPreferences.Editor {
|
||||
if (values != null) {
|
||||
itemsToAdd[key] = values
|
||||
} else {
|
||||
remove(key)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putInt(key: String, value: Int): SharedPreferences.Editor {
|
||||
itemsToAdd[key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putLong(key: String, value: Long): SharedPreferences.Editor {
|
||||
itemsToAdd[key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putFloat(key: String, value: Float): SharedPreferences.Editor {
|
||||
itemsToAdd[key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
|
||||
itemsToAdd[key] = value
|
||||
return this
|
||||
}
|
||||
|
||||
override fun remove(key: String): SharedPreferences.Editor {
|
||||
itemsToAdd.remove(key)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun clear(): SharedPreferences.Editor {
|
||||
itemsToAdd.clear()
|
||||
return this
|
||||
}
|
||||
|
||||
override fun commit(): Boolean {
|
||||
addToPreferences()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun apply() {
|
||||
addToPreferences()
|
||||
}
|
||||
|
||||
private fun addToPreferences() {
|
||||
itemsToAdd.forEach { (key, value) ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (value) {
|
||||
is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), key, value as Set<String>)
|
||||
else -> {
|
||||
preferences[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
val javaListener = PreferenceChangeListener {
|
||||
listener.onSharedPreferenceChanged(this, it.key)
|
||||
}
|
||||
listeners[listener] = javaListener
|
||||
javaPreferences.addPreferenceChangeListener(javaListener)
|
||||
}
|
||||
|
||||
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
val registeredListener = listeners.remove(listener)
|
||||
if (registeredListener != null) {
|
||||
javaPreferences.removePreferenceChangeListener(registeredListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAll(): Boolean {
|
||||
javaPreferences.removeNode()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ data class InstalledPackage(val root: File) {
|
||||
val icon = File(root, "icon.png")
|
||||
|
||||
val info: PackageInfo
|
||||
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(root, apk).also {
|
||||
get() = ApkParsers.getMetaInfo(apk).toPackageInfo(apk).also {
|
||||
val parsed = ApkFile(apk)
|
||||
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||
val dBuilder = dbFactory.newDocumentBuilder()
|
||||
@@ -82,12 +82,14 @@ data class InstalledPackage(val root: File) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun NodeList.toList(): List<Node> {
|
||||
val out = mutableListOf<Node>()
|
||||
companion object {
|
||||
fun NodeList.toList(): List<Node> {
|
||||
val out = mutableListOf<Node>()
|
||||
|
||||
for(i in 0 until length)
|
||||
out += item(i)
|
||||
for (i in 0 until length)
|
||||
out += item(i)
|
||||
|
||||
return out
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import android.content.pm.PackageInfo
|
||||
import net.dongliu.apk.parser.bean.ApkMeta
|
||||
import java.io.File
|
||||
|
||||
fun ApkMeta.toPackageInfo(root: File, apk: File): PackageInfo {
|
||||
fun ApkMeta.toPackageInfo(apk: File): PackageInfo {
|
||||
return PackageInfo().also {
|
||||
it.packageName = packageName
|
||||
it.versionCode = versionCode.toInt()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Code Of Conduct
|
||||
- Don't be a dick.
|
||||
|
||||
# expanding the code of conduct!
|
||||
The contents of this document is up for debate and improvement! Discussions on discord.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Contributing
|
||||
## Where should I start?
|
||||
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
|
||||
|
||||
**Note to potential contributors:** Notify the developers on Suwayomi discord (#programming channel) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
|
||||
|
||||
## How does Tachidesk work?
|
||||
This project has two components:
|
||||
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
||||
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation.
|
||||
|
||||
## Why a web app?
|
||||
This structure is chosen to
|
||||
- Achieve the maximum multi-platform-ness
|
||||
- Gives the ability to acces Tachidesk from a remote web browser e.g. your phone, tablet or smart TV
|
||||
- Eaise development of alternative user intefaces for Tachidesk
|
||||
|
||||
## User Interfaces for Tachidesk server
|
||||
Currently, there are three known interfaces for Tachidesk:
|
||||
1. [webUI](https://github.com/Suwayomi/Tachidesk/tree/master/webUI/react): The react SPA that Tachidesk is traditionally shipped with.
|
||||
2. [TachideskJUI](https://github.com/Suwayomi/TachideskJUI): A Jetbrains Compose Native app, re-uses components made for the upcoming Tachiyomi 1.x
|
||||
3. [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stages of development.
|
||||
|
||||
## Building from source
|
||||
### Prerequisites
|
||||
You need these software packages installed in order to build the project
|
||||
### Server
|
||||
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
||||
- Android stubs jar
|
||||
- Manual download: Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||
- Automated download: Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
|
||||
### webUI
|
||||
- Nodejs LTS or latest
|
||||
- Yarn
|
||||
- Git
|
||||
### building the full-blown jar
|
||||
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||
### building without `webUI` bundled(server only)
|
||||
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||
### building the Windows package
|
||||
First Build the jar, then cd into the `scripts` directory and run `./windows<bits>-bundler.sh` (or `./windows<bits>-bundler.ps1` if you are on windows), the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win64.zip`.
|
||||
## Running in development mode
|
||||
First satisfy [the prerequisites](#prerequisites)
|
||||
### server
|
||||
run `./gradlew :server:run --stacktrace` to run the server
|
||||
### webUI
|
||||
How to do it is described in `webUI/react/README.md` but for short,
|
||||
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
||||
then `yarn start` to start the development server, if a new browser window doesn't get opened automatically,
|
||||
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
||||
and supports HMR and all the other goodies you'll need.
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
|
||||

|
||||
| Build | Stable | Preview | Support Server |
|
||||
|-------|----------|---------|---------|
|
||||
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [](https://discord.gg/DDZdqZWaHA) |
|
||||
|
||||
# Tachidesk
|
||||
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
||||
|
||||
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||
|
||||
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
|
||||
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
|
||||
|
||||
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
|
||||
|
||||
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
||||
|
||||
**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?
|
||||
Here is a list of current features:
|
||||
|
||||
@@ -14,24 +23,27 @@ Here is a list of current features:
|
||||
- A library to save your mangas and categories to put them into.
|
||||
- Searching and browsing installed sources.
|
||||
- A decent chapter reader.
|
||||
- Ability to download Mangas for offline read(This partially works)
|
||||
- Ability to download Mangas for offline read
|
||||
- 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.
|
||||
|
||||
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.
|
||||
**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.
|
||||
|
||||
## Downloading and Running the app
|
||||
### All Operating Systems
|
||||
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
|
||||
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed(Google is your friend for seeking assitance). Also an internet connection is required as almost everything this app does is downloading stuff.
|
||||
|
||||
Download the latest jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
|
||||
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
|
||||
|
||||
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
||||
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` (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
|
||||
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
|
||||
You can install Tachidesk from the AUR
|
||||
@@ -40,53 +52,25 @@ yay -S tachidesk
|
||||
```
|
||||
|
||||
### 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
|
||||
If the app breaks try deleting the directory below and re-running the app (**This will delete all your data!**) and if the problem persists open an issue.
|
||||
Install from the command line:
|
||||
```
|
||||
$ 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`
|
||||
|
||||
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
||||
|
||||
## Support and help
|
||||
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support and help.
|
||||
|
||||
## How does it work?
|
||||
This project has two components:
|
||||
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
||||
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
||||
|
||||
## Building from source
|
||||
### Prerequisite: Get Android stubs jar
|
||||
#### Manual download
|
||||
Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
|
||||
Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
|
||||
### Prerequisite: Software dependencies
|
||||
You need this software packages installed in order to build this project:
|
||||
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
||||
- Nodejs LTS or latest
|
||||
- Yarn
|
||||
### building the full-blown jar
|
||||
Run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||
### building without `webUI` bundled
|
||||
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar -x :webUI:copyBuild`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||
### building the Windows package
|
||||
Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
||||
## Running for development purposes
|
||||
### `server` module
|
||||
Follow [Get Android stubs jar](#prerequisite-get-android-stubs-jar) then run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
|
||||
### `webUI` module
|
||||
How to do it is described in `webUI/react/README.md` but for short,
|
||||
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
||||
then `yarn start` to start the client if a new browser window doesn't start automatically,
|
||||
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
||||
and supports HMR and all the other goodies you'll need.
|
||||
## Contributing and Technical info
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
## 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.
|
||||
|
||||
+34
-31
@@ -1,33 +1,31 @@
|
||||
import org.jetbrains.kotlin.config.KotlinCompilerVersion
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("org.jetbrains.kotlin.jvm") version "1.4.21" apply false // Also in buildSrc Config.kt
|
||||
id("java")
|
||||
kotlin("jvm") version "1.4.32"
|
||||
kotlin("plugin.serialization") version "1.4.32" apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = "xyz.nulldev.ts"
|
||||
group = "suwayomi"
|
||||
|
||||
version = "1.0"
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven("https://maven.google.com/")
|
||||
maven("https://jitpack.io")
|
||||
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
||||
maven("https://dl.bintray.com/inorichi/maven")
|
||||
maven("https://dl.google.com/dl/android/maven2/")
|
||||
}
|
||||
}
|
||||
|
||||
val javaProjects = listOf(
|
||||
val projects = listOf(
|
||||
project(":AndroidCompat"),
|
||||
project(":AndroidCompat:Config"),
|
||||
project(":server")
|
||||
)
|
||||
|
||||
configure(javaProjects) {
|
||||
apply(plugin = "java")
|
||||
configure(projects) {
|
||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||
|
||||
java {
|
||||
@@ -35,51 +33,56 @@ configure(javaProjects) {
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Kotlin
|
||||
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
||||
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
||||
testImplementation(kotlin("test", version = "1.4.21"))
|
||||
}
|
||||
}
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
implementation(kotlin("reflect"))
|
||||
testImplementation(kotlin("test"))
|
||||
|
||||
// coroutines
|
||||
val coroutinesVersion = "1.4.3"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
||||
|
||||
val kotlinSerializationVersion = "1.1.0"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
|
||||
configure(listOf(
|
||||
project(":AndroidCompat"),
|
||||
project(":server"),
|
||||
project(":AndroidCompat:Config")
|
||||
|
||||
)) {
|
||||
dependencies {
|
||||
// Dependency Injection
|
||||
implementation("org.kodein.di:kodein-di-conf-jvm:7.1.0")
|
||||
implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0")
|
||||
|
||||
// Logging
|
||||
implementation("org.slf4j:slf4j-api:1.7.30")
|
||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
||||
implementation("io.github.microutils:kotlin-logging:2.0.3")
|
||||
implementation("io.github.microutils:kotlin-logging:2.0.6")
|
||||
|
||||
// RxJava
|
||||
// ReactiveX
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("io.reactivex:rxkotlin:1.0.0")
|
||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
|
||||
// JSoup
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
|
||||
// Kotlin
|
||||
implementation(kotlin("reflect", version = "1.4.21"))
|
||||
|
||||
// dependency of :AndroidCompat:Config
|
||||
implementation("com.typesafe:config:1.4.0")
|
||||
implementation("com.typesafe:config:1.4.1")
|
||||
implementation("io.github.config4k:config4k:0.4.2")
|
||||
|
||||
|
||||
// to get application content root
|
||||
implementation("net.harawata:appdirs:1.2.0")
|
||||
implementation("net.harawata:appdirs:1.2.1")
|
||||
|
||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
||||
|
||||
// APK parser
|
||||
implementation("net.dongliu:apk-parser:2.6.10")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
start "" jre/bin/javaw -jar Tachidesk.jar
|
||||
@@ -0,0 +1 @@
|
||||
jre\bin\java -Dsuwayomi.tachidesk.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||
@@ -0,0 +1 @@
|
||||
jre\bin\javaw "-Dsuwayomi.tachidesk.server.webInterface=electron" "-Dsuwayomi.tachidesk.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/*.jar | tail -n1)
|
||||
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||
release_name=$(echo $jar_name | sed 's/.jar//')-$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/
|
||||
+122
-153
@@ -1,100 +1,87 @@
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||
import java.io.BufferedReader
|
||||
import java.time.Instant
|
||||
|
||||
plugins {
|
||||
// id("org.jetbrains.kotlin.jvm") version "1.4.21"
|
||||
application
|
||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
||||
id("org.jmailen.kotlinter") version "3.3.0"
|
||||
id("edu.sc.seis.launch4j") version "2.4.9"
|
||||
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||
id("org.jmailen.kotlinter") version "3.4.3"
|
||||
id("de.fuerstenau.buildconfig") version "1.1.8"
|
||||
}
|
||||
|
||||
val TachideskVersion = "v0.2.7"
|
||||
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url = uri("https://repo1.maven.org/maven2/")
|
||||
}
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
// okhttp
|
||||
val okhttpVersion = "4.9.1" // version is locked by Tachiyomi extensions
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
implementation("com.squareup.okio:okio:2.10.0")
|
||||
|
||||
// Javalin api
|
||||
implementation("io.javalin:javalin:3.13.6")
|
||||
// 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
|
||||
val exposedVersion = "0.31.1"
|
||||
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
|
||||
// current database driver
|
||||
implementation("com.h2database:h2:1.4.200")
|
||||
|
||||
// tray icon
|
||||
implementation("com.dorkbox:SystemTray:4.1")
|
||||
implementation("com.dorkbox:Utilities:1.9")
|
||||
|
||||
|
||||
// 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")
|
||||
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
|
||||
val okhttp_version = "4.10.0-RC1"
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttp_version")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttp_version")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version")
|
||||
implementation("com.squareup.okio:okio:2.9.0")
|
||||
|
||||
|
||||
// retrofit
|
||||
val retrofit_version = "2.9.0"
|
||||
implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
|
||||
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:$retrofit_version")
|
||||
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofit_version")
|
||||
|
||||
|
||||
// reactivex
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
// implementation("io.reactivex:rxandroid:1.2.1")
|
||||
// implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
// implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0")
|
||||
implementation("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")
|
||||
|
||||
|
||||
val coroutinesVersion = "1.3.9"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
|
||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
||||
|
||||
|
||||
// api
|
||||
implementation("io.javalin:javalin:3.12.0")
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
||||
|
||||
// Exposed ORM
|
||||
val exposed_version = "0.28.1"
|
||||
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
|
||||
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
||||
implementation("com.h2database:h2:1.4.199")
|
||||
|
||||
// tray icon
|
||||
implementation("com.dorkbox:SystemTray:3.17")
|
||||
|
||||
|
||||
// AndroidCompat
|
||||
implementation(project(":AndroidCompat"))
|
||||
implementation(project(":AndroidCompat:Config"))
|
||||
|
||||
// uncomment to test extensions directly
|
||||
// implementation(fileTree("lib/"))
|
||||
|
||||
// Testing
|
||||
testImplementation(kotlin("test-junit5"))
|
||||
}
|
||||
|
||||
val name = "ir.armor.tachidesk.Main"
|
||||
val MainClass = "suwayomi.tachidesk.MainKt"
|
||||
application {
|
||||
mainClass.set(name)
|
||||
mainClass.set(MainClass)
|
||||
|
||||
// Required by ShadowJar.
|
||||
mainClassName = name
|
||||
// for testing electron
|
||||
// applicationDefaultJvmArgs = listOf(
|
||||
// "-Dsuwayomi.tachidesk.webInterface=electron",
|
||||
// "-Dsuwayomi.tachidesk.electronPath=/usr/bin/electron"
|
||||
// )
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -105,9 +92,14 @@ sourceSets {
|
||||
}
|
||||
}
|
||||
|
||||
val TachideskRevision = Runtime
|
||||
// should be bumped with each stable release
|
||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.3"
|
||||
|
||||
// counts commit count on master
|
||||
val tachideskRevision = runCatching {
|
||||
System.getenv("ProductRevision") ?: Runtime
|
||||
.getRuntime()
|
||||
.exec("git rev-list master --count")
|
||||
.exec("git rev-list HEAD --count")
|
||||
.let { process ->
|
||||
process.waitFor()
|
||||
val output = process.inputStream.use {
|
||||
@@ -115,97 +107,74 @@ val TachideskRevision = Runtime
|
||||
}
|
||||
process.destroy()
|
||||
"r" + output.trim()
|
||||
|
||||
}
|
||||
}.getOrDefault("r0")
|
||||
|
||||
buildConfig {
|
||||
clsName = "BuildConfig"
|
||||
packageName = "suwayomi.server"
|
||||
|
||||
|
||||
buildConfigField("String", "NAME", rootProject.name)
|
||||
buildConfigField("String", "VERSION", tachideskVersion)
|
||||
buildConfigField("String", "REVISION", tachideskRevision)
|
||||
buildConfigField("String", "BUILD_TYPE", if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview")
|
||||
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
|
||||
|
||||
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk")
|
||||
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA")
|
||||
}
|
||||
|
||||
tasks {
|
||||
jar {
|
||||
shadowJar {
|
||||
manifest {
|
||||
attributes(
|
||||
mapOf(
|
||||
"Main-Class" to "com.example.MainKt", //will make your jar (produced by jar task) runnable
|
||||
"ImplementationTitle" to project.name,
|
||||
"Implementation-Version" to project.version)
|
||||
"Main-Class" to MainClass,
|
||||
"Implementation-Title" to rootProject.name,
|
||||
"Implementation-Vendor" to "The Suwayomi Project",
|
||||
"Specification-Version" to tachideskVersion,
|
||||
"Implementation-Version" to tachideskRevision
|
||||
)
|
||||
)
|
||||
}
|
||||
archiveBaseName.set(rootProject.name)
|
||||
archiveVersion.set(tachideskVersion)
|
||||
archiveClassifier.set(tachideskRevision)
|
||||
}
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
||||
)
|
||||
}
|
||||
}
|
||||
shadowJar {
|
||||
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
||||
archiveBaseName.set("Tachidesk")
|
||||
archiveVersion.set(TachideskVersion)
|
||||
archiveClassifier.set(TachideskRevision)
|
||||
|
||||
test {
|
||||
useJUnit()
|
||||
}
|
||||
|
||||
withType<ShadowJar> {
|
||||
destinationDirectory.set(File("$rootDir/server/build"))
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
}
|
||||
|
||||
named("run") {
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
}
|
||||
|
||||
named<Copy>("processResources") {
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
mustRunAfter(":webUI:copyBuild")
|
||||
}
|
||||
|
||||
withType<LintTask> {
|
||||
source(files("src/kotlin"))
|
||||
}
|
||||
|
||||
withType<FormatTask> {
|
||||
source(files("src/kotlin"))
|
||||
}
|
||||
}
|
||||
|
||||
launch4j { //used for windows
|
||||
mainClassName = name
|
||||
bundledJrePath = "jre"
|
||||
bundledJre64Bit = true
|
||||
jreMinVersion = "8"
|
||||
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
||||
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
||||
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
|
||||
}
|
||||
|
||||
tasks.register<Zip>("windowsPackage") {
|
||||
from(fileTree("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32"))
|
||||
destinationDirectory.set(File("$buildDir"))
|
||||
archiveFileName.set("Tachidesk-$TachideskVersion-$TachideskRevision-win32.zip")
|
||||
dependsOn("windowsPackageWorkaround2")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("windowsPackageWorkaround2") {
|
||||
delete(
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jre",
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/lib",
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/server.exe",
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
||||
)
|
||||
dependsOn("windowsPackageWorkaround")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("windowsPackageWorkaround") {
|
||||
from("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
||||
dependsOn("deleteUnwantedJreDir")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("deleteUnwantedJreDir") {
|
||||
delete(
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jdk8u282-b08-jre"
|
||||
)
|
||||
dependsOn("addJreToDistributable")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("addJreToDistributable") {
|
||||
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
|
||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
||||
eachFile {
|
||||
path = path.replace(".*-jre".toRegex(),"jre")
|
||||
}
|
||||
dependsOn("downloadJre")
|
||||
dependsOn("createExe")
|
||||
}
|
||||
|
||||
tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
|
||||
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
||||
dest(buildDir)
|
||||
overwrite(false)
|
||||
onlyIfModified(true)
|
||||
}
|
||||
|
||||
tasks.withType<ShadowJar> {
|
||||
destinationDir = File("$rootDir/server/build")
|
||||
dependsOn("lintKotlin")
|
||||
}
|
||||
|
||||
tasks.named("processResources") {
|
||||
dependsOn(":webUI:copyBuild")
|
||||
}
|
||||
|
||||
tasks.named("run") {
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,90 +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 android.content.Context
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||
// import kotlinx.coroutines.Dispatchers
|
||||
// import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
|
||||
internal class ExtensionGithubApi {
|
||||
|
||||
// private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
suspend fun findExtensions(): List<Extension.Available> {
|
||||
val service: ExtensionGithubService = ExtensionGithubService.create()
|
||||
|
||||
val response = service.getRepo()
|
||||
return parseResponse(response)
|
||||
}
|
||||
|
||||
// suspend fun checkForUpdates(): List<Extension.Installed> {
|
||||
// val extensions = fin dExtensions()
|
||||
//
|
||||
// // preferences.lastExtCheck().set(Date().time)
|
||||
//
|
||||
// val installedExtensions = ExtensionLoader.loadExtensions(context)
|
||||
// .filterIsInstance<LoadResult.Success>()
|
||||
// .map { it.extension }
|
||||
//
|
||||
// val extensionsWithUpdate = mutableListOf<Extension.Installed>()
|
||||
// for (installedExt in installedExtensions) {
|
||||
// val pkgName = installedExt.pkgName
|
||||
// val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
|
||||
//
|
||||
// val hasUpdate = availableExt.versionCode > installedExt.versionCode
|
||||
// if (hasUpdate) {
|
||||
// extensionsWithUpdate.add(installedExt)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return extensionsWithUpdate
|
||||
// }
|
||||
|
||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: ExtensionDataClass): String {
|
||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://raw.githubusercontent.com/"
|
||||
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo"
|
||||
}
|
||||
}
|
||||
@@ -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.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.http.GET
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
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
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import rx.Observable
|
||||
|
||||
// import com.github.junrar.Archive
|
||||
// import com.google.gson.JsonParser
|
||||
// import eu.kanade.tachiyomi.R
|
||||
// import eu.kanade.tachiyomi.source.model.Filter
|
||||
// import eu.kanade.tachiyomi.source.model.FilterList
|
||||
// import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
// import eu.kanade.tachiyomi.source.model.Page
|
||||
// import eu.kanade.tachiyomi.source.model.SChapter
|
||||
// import eu.kanade.tachiyomi.source.model.SManga
|
||||
// import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
// import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
// import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
// import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
// import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
// import rx.Observable
|
||||
// import timber.log.Timber
|
||||
// import java.io.File
|
||||
// import java.io.FileInputStream
|
||||
// import java.io.InputStream
|
||||
// import java.util.Locale
|
||||
// import java.util.concurrent.TimeUnit
|
||||
// import java.util.zip.ZipFile
|
||||
|
||||
class LocalSource(private val context: Context) : CatalogueSource {
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
// const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
||||
//
|
||||
// private const val COVER_NAME = "cover.jpg"
|
||||
// private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||
//
|
||||
// private val POPULAR_FILTERS = FilterList(OrderBy())
|
||||
// private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
||||
// private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
//
|
||||
// fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||
// val dir = getBaseDirectories(context).firstOrNull()
|
||||
// if (dir == null) {
|
||||
// input.close()
|
||||
// return null
|
||||
// }
|
||||
// val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
||||
//
|
||||
// // It might not exist if using the external SD card
|
||||
// cover.parentFile?.mkdirs()
|
||||
// input.use {
|
||||
// cover.outputStream().use {
|
||||
// input.copyTo(it)
|
||||
// }
|
||||
// }
|
||||
// return cover
|
||||
// }
|
||||
//
|
||||
// private fun getBaseDirectories(context: Context): List<File> {
|
||||
// val c = context.getString(R.string.app_name) + File.separator + "local"
|
||||
// return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
||||
// }
|
||||
}
|
||||
|
||||
override val id = ID
|
||||
override val name = "Local source"
|
||||
override val lang = ""
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
//
|
||||
// override fun toString() = context.getString(R.string.local_source)
|
||||
//
|
||||
// override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||
//
|
||||
// override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
// val baseDirs = getBaseDirectories(context)
|
||||
//
|
||||
// val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
// var mangaDirs = baseDirs
|
||||
// .asSequence()
|
||||
// .mapNotNull { it.listFiles()?.toList() }
|
||||
// .flatten()
|
||||
// .filter { it.isDirectory }
|
||||
// .filterNot { it.name.startsWith('.') }
|
||||
// .filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||
// .distinctBy { it.name }
|
||||
//
|
||||
// val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
||||
// when (state?.index) {
|
||||
// 0 -> {
|
||||
// mangaDirs = if (state.ascending) {
|
||||
// mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
|
||||
// } else {
|
||||
// mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
|
||||
// }
|
||||
// }
|
||||
// 1 -> {
|
||||
// mangaDirs = if (state.ascending) {
|
||||
// mangaDirs.sortedBy(File::lastModified)
|
||||
// } else {
|
||||
// mangaDirs.sortedByDescending(File::lastModified)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val mangas = mangaDirs.map { mangaDir ->
|
||||
// SManga.create().apply {
|
||||
// title = mangaDir.name
|
||||
// url = mangaDir.name
|
||||
//
|
||||
// // Try to find the cover
|
||||
// for (dir in baseDirs) {
|
||||
// val cover = File("${dir.absolutePath}/$url", COVER_NAME)
|
||||
// if (cover.exists()) {
|
||||
// thumbnail_url = cover.absolutePath
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val chapters = fetchChapterList(this).toBlocking().first()
|
||||
// if (chapters.isNotEmpty()) {
|
||||
// val chapter = chapters.last()
|
||||
// val format = getFormat(chapter)
|
||||
// if (format is Format.Epub) {
|
||||
// EpubFile(format.file).use { epub ->
|
||||
// epub.fillMangaMetadata(this)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Copy the cover from the first chapter found.
|
||||
// if (thumbnail_url == null) {
|
||||
// try {
|
||||
// val dest = updateCover(chapter, this)
|
||||
// thumbnail_url = dest?.absolutePath
|
||||
// } catch (e: Exception) {
|
||||
// Timber.e(e)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return Observable.just(MangasPage(mangas.toList(), false))
|
||||
// }
|
||||
//
|
||||
// override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||
//
|
||||
// override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
// getBaseDirectories(context)
|
||||
// .asSequence()
|
||||
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||
// .flatten()
|
||||
// .firstOrNull { it.extension == "json" }
|
||||
// ?.apply {
|
||||
// val reader = this.inputStream().bufferedReader()
|
||||
// val json = JsonParser.parseReader(reader).asJsonObject
|
||||
//
|
||||
// manga.title = json["title"]?.asString ?: manga.title
|
||||
// manga.author = json["author"]?.asString ?: manga.author
|
||||
// manga.artist = json["artist"]?.asString ?: manga.artist
|
||||
// manga.description = json["description"]?.asString ?: manga.description
|
||||
// manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString }
|
||||
// ?: manga.genre
|
||||
// manga.status = json["status"]?.asInt ?: manga.status
|
||||
// }
|
||||
//
|
||||
// return Observable.just(manga)
|
||||
// }
|
||||
//
|
||||
// override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
// val chapters = getBaseDirectories(context)
|
||||
// .asSequence()
|
||||
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||
// .flatten()
|
||||
// .filter { it.isDirectory || isSupportedFile(it.extension) }
|
||||
// .map { chapterFile ->
|
||||
// SChapter.create().apply {
|
||||
// url = "${manga.url}/${chapterFile.name}"
|
||||
// name = if (chapterFile.isDirectory) {
|
||||
// chapterFile.name
|
||||
// } else {
|
||||
// chapterFile.nameWithoutExtension
|
||||
// }
|
||||
// date_upload = chapterFile.lastModified()
|
||||
//
|
||||
// val format = getFormat(this)
|
||||
// if (format is Format.Epub) {
|
||||
// EpubFile(format.file).use { epub ->
|
||||
// epub.fillChapterMetadata(this)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val chapNameCut = stripMangaTitle(name, manga.title)
|
||||
// if (chapNameCut.isNotEmpty()) name = chapNameCut
|
||||
// ChapterRecognition.parseChapterNumber(this, manga)
|
||||
// }
|
||||
// }
|
||||
// .sortedWith(
|
||||
// Comparator { c1, c2 ->
|
||||
// val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||
// if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||
// }
|
||||
// )
|
||||
// .toList()
|
||||
//
|
||||
// return Observable.just(chapters)
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace
|
||||
// * characters.
|
||||
// */
|
||||
// private fun stripMangaTitle(chapterName: String, mangaTitle: String): String {
|
||||
// var chapterNameIndex = 0
|
||||
// var mangaTitleIndex = 0
|
||||
// while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
||||
// val chapterChar = chapterName[chapterNameIndex]
|
||||
// val mangaChar = mangaTitle[mangaTitleIndex]
|
||||
// if (!chapterChar.equals(mangaChar, true)) {
|
||||
// val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
||||
// val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
||||
//
|
||||
// if (!invalidChapterChar && !invalidMangaChar) {
|
||||
// return chapterName
|
||||
// }
|
||||
//
|
||||
// if (invalidChapterChar) {
|
||||
// chapterNameIndex++
|
||||
// }
|
||||
//
|
||||
// if (invalidMangaChar) {
|
||||
// mangaTitleIndex++
|
||||
// }
|
||||
// } else {
|
||||
// chapterNameIndex++
|
||||
// mangaTitleIndex++
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
|
||||
// }
|
||||
//
|
||||
// override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
// return Observable.error(Exception("Unused"))
|
||||
// }
|
||||
//
|
||||
// private fun isSupportedFile(extension: String): Boolean {
|
||||
// return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
|
||||
// }
|
||||
//
|
||||
// fun getFormat(chapter: SChapter): Format {
|
||||
// val baseDirs = getBaseDirectories(context)
|
||||
//
|
||||
// for (dir in baseDirs) {
|
||||
// val chapFile = File(dir, chapter.url)
|
||||
// if (!chapFile.exists()) continue
|
||||
//
|
||||
// return getFormat(chapFile)
|
||||
// }
|
||||
// throw Exception("Chapter not found")
|
||||
// }
|
||||
//
|
||||
// private fun getFormat(file: File): Format {
|
||||
// val extension = file.extension
|
||||
// return if (file.isDirectory) {
|
||||
// Format.Directory(file)
|
||||
// } else if (extension.equals("zip", true) || extension.equals("cbz", true)) {
|
||||
// Format.Zip(file)
|
||||
// } else if (extension.equals("rar", true) || extension.equals("cbr", true)) {
|
||||
// Format.Rar(file)
|
||||
// } else if (extension.equals("epub", true)) {
|
||||
// Format.Epub(file)
|
||||
// } else {
|
||||
// throw Exception("Invalid chapter format")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
||||
// return when (val format = getFormat(chapter)) {
|
||||
// is Format.Directory -> {
|
||||
// val entry = format.file.listFiles()
|
||||
// ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
// ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
//
|
||||
// entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||
// }
|
||||
// is Format.Zip -> {
|
||||
// ZipFile(format.file).use { zip ->
|
||||
// val entry = zip.entries().toList()
|
||||
// .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
// .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
//
|
||||
// entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
||||
// }
|
||||
// }
|
||||
// is Format.Rar -> {
|
||||
// Archive(format.file).use { archive ->
|
||||
// val entry = archive.fileHeaders
|
||||
// .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
// .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
//
|
||||
// entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
||||
// }
|
||||
// }
|
||||
// is Format.Epub -> {
|
||||
// EpubFile(format.file).use { epub ->
|
||||
// val entry = epub.getImagesFromPages()
|
||||
// .firstOrNull()
|
||||
// ?.let { epub.getEntry(it) }
|
||||
//
|
||||
// entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Selection(0, true))
|
||||
//
|
||||
// override fun getFilterList() = FilterList(OrderBy())
|
||||
//
|
||||
// sealed class Format {
|
||||
// data class Directory(val file: File) : Format()
|
||||
// data class Zip(val file: File) : Format()
|
||||
// data class Rar(val file: File) : Format()
|
||||
// data class Epub(val file: File) : Format()
|
||||
// }
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
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.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
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...
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
// import android.content.Context
|
||||
// import eu.kanade.tachiyomi.R
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import rx.Observable
|
||||
|
||||
open class SourceManager() {
|
||||
open class SourceManager(private val context: Context) {
|
||||
|
||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package eu.kanade.tachiyomi.util.lang
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
object Hash {
|
||||
|
||||
private val chars = charArrayOf(
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'a', 'b', 'c', 'd', 'e', 'f'
|
||||
)
|
||||
|
||||
private val MD5 get() = MessageDigest.getInstance("MD5")
|
||||
|
||||
private val SHA256 get() = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
fun sha256(bytes: ByteArray): String {
|
||||
return encodeHex(SHA256.digest(bytes))
|
||||
}
|
||||
|
||||
fun sha256(string: String): String {
|
||||
return sha256(string.toByteArray())
|
||||
}
|
||||
|
||||
fun md5(bytes: ByteArray): String {
|
||||
return encodeHex(MD5.digest(bytes))
|
||||
}
|
||||
|
||||
fun md5(string: String): String {
|
||||
return md5(string.toByteArray())
|
||||
}
|
||||
|
||||
private fun encodeHex(data: ByteArray): String {
|
||||
val l = data.size
|
||||
val out = CharArray(l shl 1)
|
||||
var i = 0
|
||||
var j = 0
|
||||
while (i < l) {
|
||||
out[j++] = chars[(240 and data[i].toInt()).ushr(4)]
|
||||
out[j++] = chars[15 and data[i].toInt()]
|
||||
i++
|
||||
}
|
||||
return String(out)
|
||||
}
|
||||
}
|
||||
@@ -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.applicationSetup
|
||||
import ir.armor.tachidesk.server.javalinSetup
|
||||
|
||||
class Main {
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
applicationSetup()
|
||||
javalinSetup()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package ir.armor.tachidesk.database
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.PageTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import ir.armor.tachidesk.server.applicationDirs
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
object DBMangaer {
|
||||
val db by lazy {
|
||||
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
||||
}
|
||||
}
|
||||
|
||||
fun makeDataBaseTables() {
|
||||
// must mention db object so the lazy block executes
|
||||
val db = DBMangaer.db
|
||||
db.useNestedTransactions = true
|
||||
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
ExtensionTable,
|
||||
SourceTable,
|
||||
MangaTable,
|
||||
ChapterTable,
|
||||
PageTable,
|
||||
CategoryTable,
|
||||
CategoryMangaTable,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
data class ChapterDataClass(
|
||||
val id: Int,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val date_upload: Long,
|
||||
val chapter_number: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Int,
|
||||
val chapterIndex: Int,
|
||||
val chapterCount: Int,
|
||||
val pageCount: Int? = null,
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
|
||||
class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<ExtensionEntity>(ExtensionTable)
|
||||
|
||||
var name by ExtensionTable.name
|
||||
var pkgName by ExtensionTable.pkgName
|
||||
var versionName by ExtensionTable.versionName
|
||||
var versionCode by ExtensionTable.versionCode
|
||||
var lang by ExtensionTable.lang
|
||||
var isNsfw by ExtensionTable.isNsfw
|
||||
var apkName by ExtensionTable.apkName
|
||||
var iconUrl by ExtensionTable.iconUrl
|
||||
var installed by ExtensionTable.installed
|
||||
var classFQName by ExtensionTable.classFQName
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
|
||||
class MangaEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<MangaEntity>(MangaTable)
|
||||
|
||||
var url by MangaTable.url
|
||||
var title by MangaTable.title
|
||||
var initialized by MangaTable.initialized
|
||||
|
||||
var artist by MangaTable.artist
|
||||
var author by MangaTable.author
|
||||
var description by MangaTable.description
|
||||
var genre by MangaTable.genre
|
||||
var status by MangaTable.status
|
||||
var thumbnail_url by MangaTable.thumbnail_url
|
||||
|
||||
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import org.jetbrains.exposed.dao.EntityClass
|
||||
import org.jetbrains.exposed.dao.LongEntity
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
|
||||
class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
|
||||
companion object : EntityClass<Long, SourceEntity>(SourceTable, null)
|
||||
|
||||
var sourceId by SourceTable.id
|
||||
var name by SourceTable.name
|
||||
var lang by SourceTable.lang
|
||||
var extension by ExtensionEntity referencedOn SourceTable.extension
|
||||
var partOfFactorySource by SourceTable.partOfFactorySource
|
||||
var positionInFactorySource by SourceTable.positionInFactorySource
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
object ChapterTable : IntIdTable() {
|
||||
val url = varchar("url", 2048)
|
||||
val name = varchar("name", 512)
|
||||
val date_upload = long("date_upload").default(0)
|
||||
val chapter_number = float("chapter_number").default(-1f)
|
||||
val scanlator = varchar("scanlator", 128).nullable()
|
||||
|
||||
val chapterIndex = integer("number_in_list")
|
||||
|
||||
val manga = reference("manga", MangaTable)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.database.table.toDataClass
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
fun createCategory(name: String) {
|
||||
transaction {
|
||||
val count = CategoryTable.selectAll().count()
|
||||
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
||||
CategoryTable.insert {
|
||||
it[CategoryTable.name] = name
|
||||
it[CategoryTable.order] = count.toInt() + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
||||
transaction {
|
||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||
if (name != null) it[CategoryTable.name] = name
|
||||
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
||||
transaction {
|
||||
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||
categories.add(to - 1, categories.removeAt(from - 1))
|
||||
categories.forEachIndexed { index, cat ->
|
||||
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
|
||||
it[CategoryTable.order] = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeCategory(categoryId: Int) {
|
||||
transaction {
|
||||
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
|
||||
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
|
||||
}
|
||||
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
||||
}
|
||||
}
|
||||
|
||||
fun getCategoryList(): List<CategoryDataClass> {
|
||||
return transaction {
|
||||
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||
CategoryTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.toDataClass
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||
transaction {
|
||||
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
|
||||
CategoryMangaTable.insert {
|
||||
it[CategoryMangaTable.category] = categoryId
|
||||
it[CategoryMangaTable.manga] = mangaId
|
||||
}
|
||||
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
it[MangaTable.defaultCategory] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
||||
transaction {
|
||||
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
||||
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
it[MangaTable.defaultCategory] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
||||
return transaction {
|
||||
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
||||
MangaTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
|
||||
return transaction {
|
||||
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||
CategoryTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +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.database.dataclass.ChapterDataClass
|
||||
import ir.armor.tachidesk.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.PageTable
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
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
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
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.insertAndGetId {
|
||||
it[url] = fetchedChapter.url
|
||||
it[name] = fetchedChapter.name
|
||||
it[date_upload] = fetchedChapter.date_upload
|
||||
it[chapter_number] = fetchedChapter.chapter_number
|
||||
it[scanlator] = fetchedChapter.scanlator
|
||||
|
||||
it[chapterIndex] = index + 1
|
||||
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
|
||||
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
|
||||
if (dbChapterCount > chapterCount) { // we got some clean up due
|
||||
// TODO
|
||||
}
|
||||
|
||||
return@transaction chapterList.mapIndexed { index, it ->
|
||||
ChapterDataClass(
|
||||
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
|
||||
it.url,
|
||||
it.name,
|
||||
it.date_upload,
|
||||
it.chapter_number,
|
||||
it.scanlator,
|
||||
mangaId,
|
||||
chapterCount - index,
|
||||
chapterCount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
||||
return transaction {
|
||||
val chapterEntry = ChapterTable.select {
|
||||
ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId)
|
||||
}.firstOrNull()!!
|
||||
val mangaEntry = 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]
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
val chapterId = chapterEntry[ChapterTable.id].value
|
||||
val chapterCount = transaction { ChapterTable.selectAll().count() }
|
||||
|
||||
val chapter = ChapterDataClass(
|
||||
chapterId,
|
||||
chapterEntry[ChapterTable.url],
|
||||
chapterEntry[ChapterTable.name],
|
||||
chapterEntry[ChapterTable.date_upload],
|
||||
chapterEntry[ChapterTable.chapter_number],
|
||||
chapterEntry[ChapterTable.scanlator],
|
||||
mangaId,
|
||||
chapterEntry[ChapterTable.chapterIndex],
|
||||
chapterCount.toInt(),
|
||||
|
||||
pageList.count()
|
||||
)
|
||||
|
||||
pageList.forEach { page ->
|
||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||
if (pageEntry == null) {
|
||||
transaction {
|
||||
PageTable.insert {
|
||||
it[index] = page.index
|
||||
it[url] = page.url
|
||||
it[imageUrl] = page.imageUrl
|
||||
it[this.chapter] = chapterId
|
||||
}
|
||||
}
|
||||
} else {
|
||||
transaction {
|
||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
|
||||
it[url] = page.url
|
||||
it[imageUrl] = page.imageUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return@transaction chapter
|
||||
}
|
||||
}
|
||||
@@ -1,207 +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 com.googlecode.d2j.dex.Dex2jar
|
||||
import com.googlecode.d2j.reader.MultiDexFileReader
|
||||
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import ir.armor.tachidesk.impl.util.APKExtractor
|
||||
import ir.armor.tachidesk.server.applicationDirs
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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 uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
|
||||
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
|
||||
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
|
||||
|
||||
val jarFilePath = File(jarFile).toPath()
|
||||
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
|
||||
val handler = BaksmaliBaseDexExceptionHandler()
|
||||
Dex2jar
|
||||
.from(reader)
|
||||
.withExceptionHandler(handler)
|
||||
.reUseReg(false)
|
||||
.topoLogicalSort()
|
||||
.skipDebug(true)
|
||||
.optimizeSynchronized(false)
|
||||
.printIR(false)
|
||||
.noCode(false)
|
||||
.skipExceptions(false)
|
||||
.to(jarFilePath)
|
||||
if (handler.hasException()) {
|
||||
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
|
||||
logger.error(
|
||||
"Detail Error Information in File $errorFile\n" +
|
||||
"Please report this file to one of following link if possible (any one).\n" +
|
||||
" https://sourceforge.net/p/dex2jar/tickets/\n" +
|
||||
" https://bitbucket.org/pxb1988/dex2jar/issues\n" +
|
||||
" https://github.com/pxb1988/dex2jar/issues\n" +
|
||||
" dex2jar@googlegroups.com"
|
||||
)
|
||||
handler.dump(errorFile, emptyArray<String>())
|
||||
}
|
||||
}
|
||||
|
||||
fun installAPK(apkName: String): Int {
|
||||
logger.debug("Installing $apkName")
|
||||
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
|
||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||
|
||||
// check if we don't have the dex file already downloaded
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
if (!File(jarPath).exists()) {
|
||||
runBlocking {
|
||||
val api = ExtensionGithubApi()
|
||||
val apkToDownload = api.getApkUrl(extensionRecord)
|
||||
|
||||
val apkFilePath = "$dirPathWithoutType.apk"
|
||||
val jarFilePath = "$dirPathWithoutType.jar"
|
||||
val dexFilePath = "$dirPathWithoutType.dex"
|
||||
|
||||
// download apk file
|
||||
downloadAPKFile(apkToDownload, apkFilePath)
|
||||
|
||||
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
|
||||
logger.debug(className)
|
||||
// dex -> jar
|
||||
dex2jar(dexFilePath, jarFilePath, fileNameWithoutType)
|
||||
|
||||
// clean up
|
||||
File(apkFilePath).delete()
|
||||
File(dexFilePath).delete()
|
||||
|
||||
// update sources of the extension
|
||||
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarFilePath")), this::class.java.classLoader)
|
||||
val classToLoad = Class.forName(className, true, child)
|
||||
val instance = classToLoad.newInstance()
|
||||
|
||||
val extensionId = transaction {
|
||||
return@transaction ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
||||
}
|
||||
|
||||
if (instance is HttpSource) { // single source
|
||||
val httpSource = instance as HttpSource
|
||||
transaction {
|
||||
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
|
||||
SourceTable.insert {
|
||||
it[this.id] = httpSource.id
|
||||
it[name] = httpSource.name
|
||||
it[this.lang] = httpSource.lang
|
||||
it[extension] = extensionId
|
||||
}
|
||||
}
|
||||
logger.debug("Installed source ${httpSource.name} with id ${httpSource.id}")
|
||||
}
|
||||
} else { // multi source
|
||||
val sourceFactory = instance as SourceFactory
|
||||
transaction {
|
||||
sourceFactory.createSources().forEachIndexed { index, source ->
|
||||
val httpSource = source as HttpSource
|
||||
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
|
||||
SourceTable.insert {
|
||||
it[this.id] = httpSource.id
|
||||
it[name] = httpSource.name
|
||||
it[this.lang] = httpSource.lang
|
||||
it[extension] = extensionId
|
||||
it[partOfFactorySource] = true
|
||||
it[positionInFactorySource] = index
|
||||
}
|
||||
}
|
||||
logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update extension info
|
||||
transaction {
|
||||
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
|
||||
it[installed] = true
|
||||
it[classFQName] = className
|
||||
}
|
||||
}
|
||||
}
|
||||
return 201 // we downloaded successfully
|
||||
} else {
|
||||
return 302
|
||||
}
|
||||
}
|
||||
|
||||
val networkHelper: NetworkHelper by injectLazy()
|
||||
|
||||
private fun downloadAPKFile(url: String, apkPath: String) {
|
||||
val request = Request.Builder().url(url).build()
|
||||
val response = networkHelper.client.newCall(request).execute()
|
||||
|
||||
val downloadedFile = File(apkPath)
|
||||
val sink = downloadedFile.sink().buffer()
|
||||
sink.writeAll(response.body!!.source())
|
||||
sink.close()
|
||||
}
|
||||
|
||||
fun removeExtension(apkName: String) {
|
||||
logger.debug("Uninstalling $apkName")
|
||||
|
||||
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
|
||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
transaction {
|
||||
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
||||
|
||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
|
||||
it[ExtensionTable.installed] = false
|
||||
}
|
||||
}
|
||||
|
||||
if (File(jarPath).exists()) {
|
||||
File(jarPath).delete()
|
||||
}
|
||||
}
|
||||
|
||||
val network: NetworkHelper by injectLazy()
|
||||
|
||||
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
||||
|
||||
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||
|
||||
return getCachedImageResponse(saveDir, apkName) {
|
||||
network.client.newCall(
|
||||
GET(iconUrl)
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun getExtensionIconUrl(apkName: String): String {
|
||||
return "/api/v1/extension/icon/$apkName"
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mu.KotlinLogging
|
||||
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
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private object Data {
|
||||
var lastExtensionCheck: Long = 0
|
||||
}
|
||||
|
||||
private fun extensionDatabaseIsEmtpy(): Boolean {
|
||||
return transaction {
|
||||
return@transaction ExtensionTable.selectAll().count() == 0L
|
||||
}
|
||||
}
|
||||
|
||||
fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
||||
// update if 60 seconds has passed or requested offline and database is empty
|
||||
if (Data.lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) {
|
||||
logger.debug("Getting extensions list from the internet")
|
||||
Data.lastExtensionCheck = System.currentTimeMillis()
|
||||
var foundExtensions: List<Extension.Available>
|
||||
runBlocking {
|
||||
val api = ExtensionGithubApi()
|
||||
foundExtensions = api.findExtensions()
|
||||
transaction {
|
||||
foundExtensions.forEach { foundExtension ->
|
||||
val extensionRecord = ExtensionTable.select { ExtensionTable.name eq foundExtension.name }.firstOrNull()
|
||||
if (extensionRecord != null) {
|
||||
// update the record
|
||||
ExtensionTable.update({ ExtensionTable.name eq foundExtension.name }) {
|
||||
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
|
||||
}
|
||||
} else {
|
||||
// insert new record
|
||||
ExtensionTable.insert {
|
||||
it[name] = foundExtension.name
|
||||
it[pkgName] = foundExtension.pkgName
|
||||
it[versionName] = foundExtension.versionName
|
||||
it[versionCode] = foundExtension.versionCode
|
||||
it[lang] = foundExtension.lang
|
||||
it[isNsfw] = foundExtension.isNsfw
|
||||
it[apkName] = foundExtension.apkName
|
||||
it[iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug("used cached extension list")
|
||||
}
|
||||
|
||||
return transaction {
|
||||
return@transaction ExtensionTable.selectAll().map {
|
||||
ExtensionDataClass(
|
||||
it[ExtensionTable.name],
|
||||
it[ExtensionTable.pkgName],
|
||||
it[ExtensionTable.versionName],
|
||||
it[ExtensionTable.versionCode],
|
||||
it[ExtensionTable.lang],
|
||||
it[ExtensionTable.isNsfw],
|
||||
it[ExtensionTable.apkName],
|
||||
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
||||
it[ExtensionTable.installed],
|
||||
it[ExtensionTable.classFQName]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.toDataClass
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
fun addMangaToLibrary(mangaId: Int) {
|
||||
val manga = getManga(mangaId)
|
||||
if (!manga.inLibrary) {
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||
it[inLibrary] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMangaFromLibrary(mangaId: Int) {
|
||||
val manga = getManga(mangaId)
|
||||
if (manga.inLibrary) {
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||
it[inLibrary] = false
|
||||
it[defaultCategory] = true
|
||||
}
|
||||
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryMangas(): List<MangaDataClass> {
|
||||
return transaction {
|
||||
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
||||
MangaTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.table.MangaStatus
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.server.applicationDirs
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import java.io.InputStream
|
||||
|
||||
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
|
||||
return if (mangaEntry[MangaTable.initialized]) {
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].toString(),
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
||||
|
||||
true,
|
||||
|
||||
mangaEntry[MangaTable.artist],
|
||||
mangaEntry[MangaTable.author],
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre],
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
mangaEntry[MangaTable.inLibrary],
|
||||
getSource(mangaEntry[MangaTable.sourceReference])
|
||||
)
|
||||
} else { // initialize manga
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
val fetchedManga = source.fetchMangaDetails(
|
||||
SManga.create().apply {
|
||||
url = mangaEntry[MangaTable.url]
|
||||
title = mangaEntry[MangaTable.title]
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
|
||||
it[MangaTable.initialized] = true
|
||||
|
||||
it[MangaTable.artist] = fetchedManga.artist
|
||||
it[MangaTable.author] = fetchedManga.author
|
||||
it[MangaTable.description] = fetchedManga.description
|
||||
it[MangaTable.genre] = fetchedManga.genre
|
||||
it[MangaTable.status] = fetchedManga.status
|
||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||
}
|
||||
}
|
||||
|
||||
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
|
||||
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].toString(),
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
|
||||
|
||||
true,
|
||||
|
||||
fetchedManga.artist,
|
||||
fetchedManga.author,
|
||||
fetchedManga.description,
|
||||
fetchedManga.genre,
|
||||
MangaStatus.valueOf(fetchedManga.status).name,
|
||||
false,
|
||||
getSource(mangaEntry[MangaTable.sourceReference])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
val saveDir = applicationDirs.thumbnailsRoot
|
||||
val fileName = mangaId.toString()
|
||||
|
||||
return getCachedImageResponse(saveDir, fileName) {
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||
val source = getHttpSource(sourceId)
|
||||
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
||||
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
||||
}
|
||||
|
||||
source.client.newCall(
|
||||
GET(thumbnailUrl, source.headers)
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.URLEncoder
|
||||
|
||||
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,98 +0,0 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
|
||||
import ir.armor.tachidesk.database.table.MangaStatus
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
fun proxyThumbnailUrl(mangaId: Int): String {
|
||||
return "/api/v1/manga/$mangaId/thumbnail"
|
||||
}
|
||||
|
||||
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
||||
val source = getHttpSource(sourceId.toLong())
|
||||
val mangasPage = if (popular) {
|
||||
source.fetchPopularManga(pageNum).toBlocking().first()
|
||||
} else {
|
||||
if (source.supportsLatest)
|
||||
source.fetchLatestUpdates(pageNum).toBlocking().first()
|
||||
else
|
||||
throw Exception("Source $source doesn't support latest")
|
||||
}
|
||||
return mangasPage.processEntries(sourceId)
|
||||
}
|
||||
|
||||
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
||||
val mangasPage = this
|
||||
val mangaList = transaction {
|
||||
return@transaction mangasPage.mangas.map { manga ->
|
||||
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
||||
if (mangaEntry == null) { // create manga entry
|
||||
val mangaId = MangaTable.insertAndGetId {
|
||||
it[url] = manga.url
|
||||
it[title] = manga.title
|
||||
|
||||
it[artist] = manga.artist
|
||||
it[author] = manga.author
|
||||
it[description] = manga.description
|
||||
it[genre] = manga.genre
|
||||
it[status] = manga.status
|
||||
it[thumbnail_url] = manga.thumbnail_url
|
||||
|
||||
it[sourceReference] = sourceId
|
||||
}.value
|
||||
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
sourceId.toString(),
|
||||
|
||||
manga.url,
|
||||
manga.title,
|
||||
proxyThumbnailUrl(mangaId),
|
||||
|
||||
manga.initialized,
|
||||
|
||||
manga.artist,
|
||||
manga.author,
|
||||
manga.description,
|
||||
manga.genre,
|
||||
MangaStatus.valueOf(manga.status).name
|
||||
)
|
||||
} else {
|
||||
val mangaId = mangaEntry[MangaTable.id].value
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
sourceId.toString(),
|
||||
|
||||
manga.url,
|
||||
manga.title,
|
||||
proxyThumbnailUrl(mangaId),
|
||||
|
||||
true,
|
||||
|
||||
mangaEntry[MangaTable.artist],
|
||||
mangaEntry[MangaTable.author],
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre],
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
mangaEntry[MangaTable.inLibrary]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return PagedMangaListDataClass(
|
||||
mangaList,
|
||||
mangasPage.hasNextPage
|
||||
)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import ir.armor.tachidesk.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.PageTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import ir.armor.tachidesk.server.applicationDirs
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||
if (page.imageUrl == null) {
|
||||
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!!
|
||||
}
|
||||
return page.imageUrl!!
|
||||
}
|
||||
|
||||
fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
val chapterEntry = transaction {
|
||||
ChapterTable.select {
|
||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||
}.firstOrNull()!!
|
||||
}
|
||||
val chapterId = chapterEntry[ChapterTable.id].value
|
||||
|
||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
|
||||
|
||||
val tachiPage = Page(
|
||||
pageEntry[PageTable.index],
|
||||
pageEntry[PageTable.url],
|
||||
pageEntry[PageTable.imageUrl]
|
||||
)
|
||||
|
||||
if (pageEntry[PageTable.imageUrl] == null) {
|
||||
transaction {
|
||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
|
||||
it[imageUrl] = getTrueImageUrl(tachiPage, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val saveDir = getChapterDir(mangaId, chapterId)
|
||||
File(saveDir).mkdirs()
|
||||
val fileName = index.toString()
|
||||
|
||||
return getCachedImageResponse(saveDir, fileName) {
|
||||
source.fetchImage(tachiPage).toBlocking().first()
|
||||
}
|
||||
}
|
||||
|
||||
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||
val source = getHttpSource(sourceId)
|
||||
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||
|
||||
val chapterDir = when {
|
||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||
else -> chapterEntry[ChapterTable.name]
|
||||
}
|
||||
|
||||
val mangaTitle = mangaEntry[MangaTable.title]
|
||||
val sourceName = source.toString()
|
||||
|
||||
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
||||
// make sure dirs exist
|
||||
File(mangaDir).mkdirs()
|
||||
return mangaDir
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import ir.armor.tachidesk.database.dataclass.SourceDataClass
|
||||
import ir.armor.tachidesk.database.entity.ExtensionEntity
|
||||
import ir.armor.tachidesk.database.entity.SourceEntity
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import ir.armor.tachidesk.server.applicationDirs
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import java.lang.NullPointerException
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
|
||||
private val extensionCache = mutableListOf<Pair<String, Any>>()
|
||||
|
||||
fun getHttpSource(sourceId: Long): HttpSource {
|
||||
val sourceRecord = transaction {
|
||||
SourceEntity.findById(sourceId)
|
||||
} ?: throw NullPointerException("Source with id $sourceId is not installed")
|
||||
|
||||
val cachedResult: Pair<Long, HttpSource>? = sourceCache.firstOrNull { it.first == sourceId }
|
||||
if (cachedResult != null) {
|
||||
logger.debug("used cached HttpSource: ${cachedResult.second.name}")
|
||||
return cachedResult.second
|
||||
}
|
||||
|
||||
val result: HttpSource = transaction {
|
||||
val extensionId = sourceRecord.extension.id.value
|
||||
val extensionRecord = ExtensionEntity.findById(extensionId)!!
|
||||
val apkName = extensionRecord.apkName
|
||||
val className = extensionRecord.classFQName
|
||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||
|
||||
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
|
||||
var usedCached = false
|
||||
val instance =
|
||||
if (cachedExtensionPair != null) {
|
||||
usedCached = true
|
||||
logger.debug("Used cached Extension")
|
||||
cachedExtensionPair.second
|
||||
} else {
|
||||
logger.debug("No Extension cache")
|
||||
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
|
||||
val classToLoad = Class.forName(className, true, child)
|
||||
classToLoad.newInstance()
|
||||
}
|
||||
if (sourceRecord.partOfFactorySource) {
|
||||
return@transaction if (usedCached) {
|
||||
(instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!]
|
||||
} else {
|
||||
val list = (instance as SourceFactory).createSources()
|
||||
extensionCache.add(Pair(jarPath, list))
|
||||
list[sourceRecord.positionInFactorySource!!] as HttpSource
|
||||
}
|
||||
} else {
|
||||
if (!usedCached)
|
||||
extensionCache.add(Pair(jarPath, instance))
|
||||
return@transaction instance as HttpSource
|
||||
}
|
||||
}
|
||||
sourceCache.add(Pair(sourceId, result))
|
||||
return result
|
||||
}
|
||||
|
||||
fun getSourceList(): List<SourceDataClass> {
|
||||
return transaction {
|
||||
return@transaction SourceTable.selectAll().map {
|
||||
SourceDataClass(
|
||||
it[SourceTable.id].value.toString(),
|
||||
it[SourceTable.name],
|
||||
it[SourceTable.lang],
|
||||
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
|
||||
getHttpSource(it[SourceTable.id].value).supportsLatest
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSource(sourceId: Long): SourceDataClass {
|
||||
return transaction {
|
||||
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
|
||||
|
||||
return@transaction SourceDataClass(
|
||||
sourceId.toString(),
|
||||
source?.get(SourceTable.name),
|
||||
source?.get(SourceTable.lang),
|
||||
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
|
||||
source?.let { getHttpSource(sourceId).supportsLatest }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,510 +0,0 @@
|
||||
package ir.armor.tachidesk.impl.util
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import org.xml.sax.InputSource
|
||||
import java.io.IOException
|
||||
import java.io.StringReader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.util.zip.ZipFile
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
object APKExtractor {
|
||||
// decompressXML -- Parse the 'compressed' binary form of Android XML docs
|
||||
// such as for AndroidManifest.xml in .apk files
|
||||
var endDocTag = 0x00100101
|
||||
var startTag = 0x00100102
|
||||
var endTag = 0x00100103
|
||||
fun prt(str: String?) {
|
||||
// System.err.print(str);
|
||||
}
|
||||
|
||||
fun decompressXML(xml: ByteArray): String {
|
||||
val finalXML = StringBuilder()
|
||||
|
||||
// Compressed XML file/bytes starts with 24x bytes of data,
|
||||
// 9 32 bit words in little endian order (LSB first):
|
||||
// 0th word is 03 00 08 00
|
||||
// 3rd word SEEMS TO BE: Offset at then of StringTable
|
||||
// 4th word is: Number of strings in string table
|
||||
// WARNING: Sometime I indiscriminently display or refer to word in
|
||||
// little endian storage format, or in integer format (ie MSB first).
|
||||
val numbStrings = LEW(xml, 4 * 4)
|
||||
|
||||
// StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
|
||||
// of the length/string data in the StringTable.
|
||||
val sitOff = 0x24 // Offset of start of StringIndexTable
|
||||
|
||||
// StringTable, each string is represented with a 16 bit little endian
|
||||
// character count, followed by that number of 16 bit (LE) (Unicode)
|
||||
// chars.
|
||||
val stOff = sitOff + numbStrings * 4 // StringTable follows
|
||||
// StrIndexTable
|
||||
|
||||
// XMLTags, The XML tag tree starts after some unknown content after the
|
||||
// StringTable. There is some unknown data after the StringTable, scan
|
||||
// forward from this point to the flag for the start of an XML start
|
||||
// tag.
|
||||
var xmlTagOff = LEW(xml, 3 * 4) // Start from the offset in the 3rd
|
||||
// word.
|
||||
// Scan forward until we find the bytes: 0x02011000(x00100102 in normal
|
||||
// int)
|
||||
var ii = xmlTagOff
|
||||
while (ii < xml.size - 4) {
|
||||
if (LEW(xml, ii) == startTag) {
|
||||
xmlTagOff = ii
|
||||
break
|
||||
}
|
||||
ii += 4
|
||||
}
|
||||
|
||||
// XML tags and attributes:
|
||||
// Every XML start and end tag consists of 6 32 bit words:
|
||||
// 0th word: 02011000 for startTag and 03011000 for endTag
|
||||
// 1st word: a flag?, like 38000000
|
||||
// 2nd word: Line of where this tag appeared in the original source file
|
||||
// 3rd word: FFFFFFFF ??
|
||||
// 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
|
||||
// 5th word: StringIndex of Element Name
|
||||
// (Note: 01011000 in 0th word means end of XML document, endDocTag)
|
||||
|
||||
// Start tags (not end tags) contain 3 more words:
|
||||
// 6th word: 14001400 meaning??
|
||||
// 7th word: Number of Attributes that follow this tag(follow word 8th)
|
||||
// 8th word: 00000000 meaning??
|
||||
|
||||
// Attributes consist of 5 words:
|
||||
// 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
|
||||
// 1st word: StringIndex of Attribute Name
|
||||
// 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId
|
||||
// used
|
||||
// 3rd word: Flags?
|
||||
// 4th word: str ind of attr value again, or ResourceId of value
|
||||
|
||||
// TMP, dump string table to tr for debugging
|
||||
// tr.addSelect("strings", null);
|
||||
// for (int ii=0; ii<numbStrings; ii++) {
|
||||
// // Length of string starts at StringTable plus offset in StrIndTable
|
||||
// String str = compXmlString(xml, sitOff, stOff, ii);
|
||||
// tr.add(String.valueOf(ii), str);
|
||||
// }
|
||||
// tr.parent();
|
||||
|
||||
// Step through the XML tree element tags and attributes
|
||||
var off = xmlTagOff
|
||||
var indent = 0
|
||||
var startTagLineNo = -2
|
||||
while (off < xml.size) {
|
||||
val tag0 = LEW(xml, off)
|
||||
// int tag1 = LEW(xml, off+1*4);
|
||||
val lineNo = LEW(xml, off + 2 * 4)
|
||||
// int tag3 = LEW(xml, off+3*4);
|
||||
val nameNsSi = LEW(xml, off + 4 * 4)
|
||||
val nameSi = LEW(xml, off + 5 * 4)
|
||||
if (tag0 == startTag) { // XML START TAG
|
||||
val tag6 = LEW(xml, off + 6 * 4) // Expected to be 14001400
|
||||
val numbAttrs = LEW(xml, off + 7 * 4) // Number of Attributes
|
||||
// to follow
|
||||
// int tag8 = LEW(xml, off+8*4); // Expected to be 00000000
|
||||
off += 9 * 4 // Skip over 6+3 words of startTag data
|
||||
val name = compXmlString(xml, sitOff, stOff, nameSi)
|
||||
// tr.addSelect(name, null);
|
||||
startTagLineNo = lineNo
|
||||
|
||||
// Look for the Attributes
|
||||
val sb = StringBuffer()
|
||||
for (ii in 0 until numbAttrs) {
|
||||
val attrNameNsSi = LEW(xml, off) // AttrName Namespace Str
|
||||
// Ind, or FFFFFFFF
|
||||
val attrNameSi = LEW(xml, off + 1 * 4) // AttrName String
|
||||
// Index
|
||||
val attrValueSi = LEW(xml, off + 2 * 4) // AttrValue Str
|
||||
// Ind, or
|
||||
// FFFFFFFF
|
||||
val attrFlags = LEW(xml, off + 3 * 4)
|
||||
val attrResId = LEW(xml, off + 4 * 4) // AttrValue
|
||||
// ResourceId or dup
|
||||
// AttrValue StrInd
|
||||
off += 5 * 4 // Skip over the 5 words of an attribute
|
||||
val attrName = compXmlString(
|
||||
xml, sitOff, stOff,
|
||||
attrNameSi
|
||||
)
|
||||
val attrValue = if (attrValueSi != -1) compXmlString(xml, sitOff, stOff, attrValueSi)
|
||||
else "resourceID 0x ${Integer.toHexString(attrResId)}"
|
||||
sb.append(" $attrName=\"$attrValue\"")
|
||||
// tr.add(attrName, attrValue);
|
||||
}
|
||||
finalXML.append("<$name$sb>")
|
||||
prtIndent(indent, "<$name$sb>")
|
||||
indent++
|
||||
} else if (tag0 == endTag) { // XML END TAG
|
||||
indent--
|
||||
off += 6 * 4 // Skip over 6 words of endTag data
|
||||
val name = compXmlString(xml, sitOff, stOff, nameSi)
|
||||
finalXML.append("</$name>")
|
||||
prtIndent(
|
||||
indent,
|
||||
"</" + name + "> (line " + startTagLineNo +
|
||||
"-" + lineNo + ")"
|
||||
)
|
||||
// tr.parent(); // Step back up the NobTree
|
||||
} else if (tag0 == endDocTag) { // END OF XML DOC TAG
|
||||
break
|
||||
} else {
|
||||
prt(
|
||||
" Unrecognized tag code '" + Integer.toHexString(tag0) +
|
||||
"' at offset " + off
|
||||
)
|
||||
break
|
||||
}
|
||||
} // end of while loop scanning tags and attributes of XML tree
|
||||
// prt(" end at offset " + off);
|
||||
return finalXML.toString()
|
||||
} // end of decompressXML
|
||||
|
||||
fun compXmlString(xml: ByteArray, sitOff: Int, stOff: Int, strInd: Int): String? {
|
||||
if (strInd < 0) return null
|
||||
val strOff = stOff + LEW(xml, sitOff + strInd * 4)
|
||||
return compXmlStringAt(xml, strOff)
|
||||
}
|
||||
|
||||
var spaces = " "
|
||||
fun prtIndent(indent: Int, str: String) {
|
||||
prt(spaces.substring(0, Math.min(indent * 2, spaces.length)) + str)
|
||||
}
|
||||
|
||||
// compXmlStringAt -- Return the string stored in StringTable format at
|
||||
// offset strOff. This offset points to the 16 bit string length, which
|
||||
// is followed by that number of 16 bit (Unicode) chars.
|
||||
fun compXmlStringAt(arr: ByteArray, strOff: Int): String {
|
||||
val strLen: Int = arr[strOff + 1].toInt() shl 8 and 0xff00 or arr[strOff].toInt() and 0xff
|
||||
val chars = ByteArray(strLen)
|
||||
for (ii in 0 until strLen) {
|
||||
chars[ii] = arr[strOff + 2 + ii * 2]
|
||||
}
|
||||
return String(chars) // Hack, just use 8 byte chars
|
||||
} // end of compXmlStringAt
|
||||
|
||||
// LEW -- Return value of a Little Endian 32 bit word from the byte array
|
||||
// at offset off.
|
||||
fun LEW(arr: ByteArray, off: Int): Int {
|
||||
|
||||
return (arr[off + 3].toInt() shl 24) and -0x1000000 or
|
||||
(arr[off + 2].toInt() shl 16 and 0xff0000) or
|
||||
(arr[off + 1].toInt() shl 8 and 0xff00) or
|
||||
(arr[off].toInt() and 0xFF)
|
||||
} // end of LEW
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun loadXMLFromString(xml: String?): Document {
|
||||
val docBuilderFactory = DocumentBuilderFactory.newInstance()
|
||||
val docBuilder = docBuilderFactory.newDocumentBuilder()
|
||||
return docBuilder.parse(InputSource(StringReader(xml)))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun extract_dex_and_read_className(filePath: String?, dexPath: String?): String {
|
||||
var zip: ZipFile? = null
|
||||
zip = ZipFile(filePath)
|
||||
val androidManifest = zip.getEntry("AndroidManifest.xml")
|
||||
val classesDex = zip.getEntry("classes.dex")
|
||||
|
||||
// write dex file
|
||||
val dexStream = zip.getInputStream(classesDex)
|
||||
Files.newOutputStream(Paths.get(dexPath)).use { os ->
|
||||
val buffer = ByteArray(1024)
|
||||
var len: Int
|
||||
while (dexStream.read(buffer).also { len = it } > 0) {
|
||||
os.write(buffer, 0, len)
|
||||
}
|
||||
}
|
||||
|
||||
// read xml file
|
||||
val `is` = zip.getInputStream(androidManifest)
|
||||
val buf = ByteArray(1024000) // 100 kb
|
||||
`is`.read(buf)
|
||||
`is`.close()
|
||||
zip.close()
|
||||
val xml = decompressXML(buf)
|
||||
try {
|
||||
val xmlDoc = loadXMLFromString(xml)
|
||||
val pkg = xmlDoc.documentElement.getAttribute("package")
|
||||
val nodes = xmlDoc.getElementsByTagName("meta-data")
|
||||
for (i in 0 until nodes.length) {
|
||||
val attributes = nodes.item(i).attributes
|
||||
println(attributes.getNamedItem("name").nodeValue)
|
||||
if (attributes.getNamedItem("name").nodeValue == "tachiyomi.extension.class") return pkg + attributes.getNamedItem("value").nodeValue
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// original Java code
|
||||
|
||||
// package ir.armor.tachidesk.impl.util;
|
||||
//
|
||||
// /*
|
||||
// * Copyright (C) Contributors to the Suwayomi project
|
||||
// *
|
||||
// * This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// * License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
//
|
||||
// import org.w3c.dom.Document;
|
||||
// import org.w3c.dom.NamedNodeMap;
|
||||
// import org.w3c.dom.NodeList;
|
||||
// import org.xml.sax.InputSource;
|
||||
//
|
||||
// import javax.xml.parsers.DocumentBuilder;
|
||||
// import javax.xml.parsers.DocumentBuilderFactory;
|
||||
// import java.io.*;
|
||||
// import java.nio.file.Files;
|
||||
// import java.nio.file.Paths;
|
||||
// import java.util.zip.ZipEntry;
|
||||
// import java.util.zip.ZipFile;
|
||||
//
|
||||
// public class APKExtractor {
|
||||
// // decompressXML -- Parse the 'compressed' binary form of Android XML docs
|
||||
// // such as for AndroidManifest.xml in .apk files
|
||||
// public static int endDocTag = 0x00100101;
|
||||
// public static int startTag = 0x00100102;
|
||||
// public static int endTag = 0x00100103;
|
||||
//
|
||||
// static void prt(String str) {
|
||||
// //System.err.print(str);
|
||||
// }
|
||||
//
|
||||
// public static String decompressXML(byte[] xml) {
|
||||
//
|
||||
// StringBuilder finalXML = new StringBuilder();
|
||||
//
|
||||
// // Compressed XML file/bytes starts with 24x bytes of data,
|
||||
// // 9 32 bit words in little endian order (LSB first):
|
||||
// // 0th word is 03 00 08 00
|
||||
// // 3rd word SEEMS TO BE: Offset at then of StringTable
|
||||
// // 4th word is: Number of strings in string table
|
||||
// // WARNING: Sometime I indiscriminently display or refer to word in
|
||||
// // little endian storage format, or in integer format (ie MSB first).
|
||||
// int numbStrings = LEW(xml, 4 * 4);
|
||||
//
|
||||
// // StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
|
||||
// // of the length/string data in the StringTable.
|
||||
// int sitOff = 0x24; // Offset of start of StringIndexTable
|
||||
//
|
||||
// // StringTable, each string is represented with a 16 bit little endian
|
||||
// // character count, followed by that number of 16 bit (LE) (Unicode)
|
||||
// // chars.
|
||||
// int stOff = sitOff + numbStrings * 4; // StringTable follows
|
||||
// // StrIndexTable
|
||||
//
|
||||
// // XMLTags, The XML tag tree starts after some unknown content after the
|
||||
// // StringTable. There is some unknown data after the StringTable, scan
|
||||
// // forward from this point to the flag for the start of an XML start
|
||||
// // tag.
|
||||
// int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd
|
||||
// // word.
|
||||
// // Scan forward until we find the bytes: 0x02011000(x00100102 in normal
|
||||
// // int)
|
||||
// for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) {
|
||||
// if (LEW(xml, ii) == startTag) {
|
||||
// xmlTagOff = ii;
|
||||
// break;
|
||||
// }
|
||||
// } // end of hack, scanning for start of first start tag
|
||||
//
|
||||
// // XML tags and attributes:
|
||||
// // Every XML start and end tag consists of 6 32 bit words:
|
||||
// // 0th word: 02011000 for startTag and 03011000 for endTag
|
||||
// // 1st word: a flag?, like 38000000
|
||||
// // 2nd word: Line of where this tag appeared in the original source file
|
||||
// // 3rd word: FFFFFFFF ??
|
||||
// // 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
|
||||
// // 5th word: StringIndex of Element Name
|
||||
// // (Note: 01011000 in 0th word means end of XML document, endDocTag)
|
||||
//
|
||||
// // Start tags (not end tags) contain 3 more words:
|
||||
// // 6th word: 14001400 meaning??
|
||||
// // 7th word: Number of Attributes that follow this tag(follow word 8th)
|
||||
// // 8th word: 00000000 meaning??
|
||||
//
|
||||
// // Attributes consist of 5 words:
|
||||
// // 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
|
||||
// // 1st word: StringIndex of Attribute Name
|
||||
// // 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId
|
||||
// // used
|
||||
// // 3rd word: Flags?
|
||||
// // 4th word: str ind of attr value again, or ResourceId of value
|
||||
//
|
||||
// // TMP, dump string table to tr for debugging
|
||||
// // tr.addSelect("strings", null);
|
||||
// // for (int ii=0; ii<numbStrings; ii++) {
|
||||
// // // Length of string starts at StringTable plus offset in StrIndTable
|
||||
// // String str = compXmlString(xml, sitOff, stOff, ii);
|
||||
// // tr.add(String.valueOf(ii), str);
|
||||
// // }
|
||||
// // tr.parent();
|
||||
//
|
||||
// // Step through the XML tree element tags and attributes
|
||||
// int off = xmlTagOff;
|
||||
// int indent = 0;
|
||||
// int startTagLineNo = -2;
|
||||
// while (off < xml.length) {
|
||||
// int tag0 = LEW(xml, off);
|
||||
// // int tag1 = LEW(xml, off+1*4);
|
||||
// int lineNo = LEW(xml, off + 2 * 4);
|
||||
// // int tag3 = LEW(xml, off+3*4);
|
||||
// int nameNsSi = LEW(xml, off + 4 * 4);
|
||||
// int nameSi = LEW(xml, off + 5 * 4);
|
||||
//
|
||||
// if (tag0 == startTag) { // XML START TAG
|
||||
// int tag6 = LEW(xml, off + 6 * 4); // Expected to be 14001400
|
||||
// int numbAttrs = LEW(xml, off + 7 * 4); // Number of Attributes
|
||||
// // to follow
|
||||
// // int tag8 = LEW(xml, off+8*4); // Expected to be 00000000
|
||||
// off += 9 * 4; // Skip over 6+3 words of startTag data
|
||||
// String name = compXmlString(xml, sitOff, stOff, nameSi);
|
||||
// // tr.addSelect(name, null);
|
||||
// startTagLineNo = lineNo;
|
||||
//
|
||||
// // Look for the Attributes
|
||||
// StringBuffer sb = new StringBuffer();
|
||||
// for (int ii = 0; ii < numbAttrs; ii++) {
|
||||
// int attrNameNsSi = LEW(xml, off); // AttrName Namespace Str
|
||||
// // Ind, or FFFFFFFF
|
||||
// int attrNameSi = LEW(xml, off + 1 * 4); // AttrName String
|
||||
// // Index
|
||||
// int attrValueSi = LEW(xml, off + 2 * 4); // AttrValue Str
|
||||
// // Ind, or
|
||||
// // FFFFFFFF
|
||||
// int attrFlags = LEW(xml, off + 3 * 4);
|
||||
// int attrResId = LEW(xml, off + 4 * 4); // AttrValue
|
||||
// // ResourceId or dup
|
||||
// // AttrValue StrInd
|
||||
// off += 5 * 4; // Skip over the 5 words of an attribute
|
||||
//
|
||||
// String attrName = compXmlString(xml, sitOff, stOff,
|
||||
// attrNameSi);
|
||||
// String attrValue = attrValueSi != -1 ? compXmlString(xml,
|
||||
// sitOff, stOff, attrValueSi) : "resourceID 0x"
|
||||
// + Integer.toHexString(attrResId);
|
||||
// sb.append(" " + attrName + "=\"" + attrValue + "\"");
|
||||
// // tr.add(attrName, attrValue);
|
||||
// }
|
||||
// finalXML.append("<" + name + sb + ">");
|
||||
// prtIndent(indent, "<" + name + sb + ">");
|
||||
// indent++;
|
||||
//
|
||||
// } else if (tag0 == endTag) { // XML END TAG
|
||||
// indent--;
|
||||
// off += 6 * 4; // Skip over 6 words of endTag data
|
||||
// String name = compXmlString(xml, sitOff, stOff, nameSi);
|
||||
// finalXML.append("</" + name + ">");
|
||||
// prtIndent(indent, "</" + name + "> (line " + startTagLineNo
|
||||
// + "-" + lineNo + ")");
|
||||
// // tr.parent(); // Step back up the NobTree
|
||||
//
|
||||
// } else if (tag0 == endDocTag) { // END OF XML DOC TAG
|
||||
// break;
|
||||
//
|
||||
// } else {
|
||||
// prt(" Unrecognized tag code '" + Integer.toHexString(tag0)
|
||||
// + "' at offset " + off);
|
||||
// break;
|
||||
// }
|
||||
// } // end of while loop scanning tags and attributes of XML tree
|
||||
// //prt(" end at offset " + off);
|
||||
// return finalXML.toString();
|
||||
// } // end of decompressXML
|
||||
//
|
||||
// public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) {
|
||||
// if (strInd < 0)
|
||||
// return null;
|
||||
// int strOff = stOff + LEW(xml, sitOff + strInd * 4);
|
||||
// return compXmlStringAt(xml, strOff);
|
||||
// }
|
||||
//
|
||||
// public static String spaces = " ";
|
||||
//
|
||||
// public static void prtIndent(int indent, String str) {
|
||||
// prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str);
|
||||
// }
|
||||
//
|
||||
// // compXmlStringAt -- Return the string stored in StringTable format at
|
||||
// // offset strOff. This offset points to the 16 bit string length, which
|
||||
// // is followed by that number of 16 bit (Unicode) chars.
|
||||
// public static String compXmlStringAt(byte[] arr, int strOff) {
|
||||
// int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff;
|
||||
// byte[] chars = new byte[strLen];
|
||||
// for (int ii = 0; ii < strLen; ii++) {
|
||||
// chars[ii] = arr[strOff + 2 + ii * 2];
|
||||
// }
|
||||
// return new String(chars); // Hack, just use 8 byte chars
|
||||
// } // end of compXmlStringAt
|
||||
//
|
||||
// // LEW -- Return value of a Little Endian 32 bit word from the byte array
|
||||
// // at offset off.
|
||||
// public static int LEW(byte[] arr, int off) {
|
||||
// return ((arr[off + 3] << 24) & 0xff000000) |
|
||||
// ((arr[off + 2] << 16) & 0xff0000) |
|
||||
// ((arr[off + 1] << 8) & 0xff00) |
|
||||
// (arr[off] & 0xFF);
|
||||
// } // end of LEW
|
||||
//
|
||||
// public static Document loadXMLFromString(String xml) throws Exception {
|
||||
// DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||
// DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
|
||||
// return docBuilder.parse(new InputSource(new StringReader(xml)));
|
||||
// }
|
||||
//
|
||||
// public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException {
|
||||
// ZipFile zip = null;
|
||||
//
|
||||
// zip = new ZipFile(filePath);
|
||||
// ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml");
|
||||
// ZipEntry classesDex = zip.getEntry("classes.dex");
|
||||
//
|
||||
// // write dex file
|
||||
// InputStream dexStream = zip.getInputStream(classesDex);
|
||||
// try (OutputStream os = Files.newOutputStream(Paths.get(dexPath))) {
|
||||
// byte[] buffer = new byte[1024];
|
||||
// int len;
|
||||
// while ((len = dexStream.read(buffer)) > 0) {
|
||||
// os.write(buffer, 0, len);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // read xml file
|
||||
// InputStream is = zip.getInputStream(androidManifest);
|
||||
// byte[] buf = new byte[1024000]; // 100 kb
|
||||
// is.read(buf);
|
||||
// is.close();
|
||||
// zip.close();
|
||||
//
|
||||
// String xml = APKExtractor.decompressXML(buf);
|
||||
// try {
|
||||
// Document xmlDoc = loadXMLFromString(xml);
|
||||
// String pkg = xmlDoc.getDocumentElement().getAttribute("package");
|
||||
// NodeList nodes = xmlDoc.getElementsByTagName("meta-data");
|
||||
// for (int i = 0; i < nodes.getLength(); i++) {
|
||||
// NamedNodeMap attributes = nodes.item(i).getAttributes();
|
||||
// System.out.println(attributes.getNamedItem("name").getNodeValue());
|
||||
// if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class"))
|
||||
// return pkg + attributes.getNamedItem("value").getNodeValue();
|
||||
// }
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// return "";
|
||||
// }
|
||||
// }
|
||||
@@ -1,85 +0,0 @@
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import okhttp3.Response
|
||||
import okio.BufferedSource
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
// fun writeStream(fileStream: InputStream, path: String) {
|
||||
// Files.newOutputStream(Paths.get(path)).use { os ->
|
||||
// val buffer = ByteArray(128 * 1024)
|
||||
// var len: Int
|
||||
// while (fileStream.read(buffer).also { len = it } > 0) {
|
||||
// os.write(buffer, 0, len)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fun pathToInputStream(path: String): InputStream {
|
||||
return BufferedInputStream(FileInputStream(path))
|
||||
}
|
||||
|
||||
fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
||||
File(directoryPath).listFiles().forEach { file ->
|
||||
if (file.name.startsWith(fileName))
|
||||
return "$directoryPath/${file.name}"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given source to an output stream and closes both resources.
|
||||
*
|
||||
* @param stream the stream where the source is copied.
|
||||
*/
|
||||
private fun BufferedSource.saveTo(stream: OutputStream) {
|
||||
use { input ->
|
||||
stream.sink().buffer().use {
|
||||
it.writeAll(input)
|
||||
it.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
|
||||
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
||||
val filePath = "$saveDir/$fileName"
|
||||
if (cachedFile != null) {
|
||||
val fileType = cachedFile.substringAfter(filePath)
|
||||
return Pair(
|
||||
pathToInputStream(cachedFile),
|
||||
"image/$fileType"
|
||||
)
|
||||
}
|
||||
|
||||
val response = fetcher()
|
||||
|
||||
if (response.code == 200) {
|
||||
val contentType = response.headers["content-type"]!!
|
||||
val fullPath = filePath + "." + contentType.substringAfter("image/")
|
||||
|
||||
Files.newOutputStream(Paths.get(fullPath)).use { os ->
|
||||
response.body!!.source().saveTo(os)
|
||||
}
|
||||
return Pair(
|
||||
pathToInputStream(fullPath),
|
||||
contentType
|
||||
)
|
||||
} else {
|
||||
throw Exception("request error! ${response.code}")
|
||||
}
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
package ir.armor.tachidesk.server
|
||||
|
||||
import io.javalin.Javalin
|
||||
import ir.armor.tachidesk.Main
|
||||
import ir.armor.tachidesk.impl.addMangaToCategory
|
||||
import ir.armor.tachidesk.impl.addMangaToLibrary
|
||||
import ir.armor.tachidesk.impl.createCategory
|
||||
import ir.armor.tachidesk.impl.getCategoryList
|
||||
import ir.armor.tachidesk.impl.getCategoryMangaList
|
||||
import ir.armor.tachidesk.impl.getChapter
|
||||
import ir.armor.tachidesk.impl.getChapterList
|
||||
import ir.armor.tachidesk.impl.getExtensionIcon
|
||||
import ir.armor.tachidesk.impl.getExtensionList
|
||||
import ir.armor.tachidesk.impl.getLibraryMangas
|
||||
import ir.armor.tachidesk.impl.getManga
|
||||
import ir.armor.tachidesk.impl.getMangaCategories
|
||||
import ir.armor.tachidesk.impl.getMangaList
|
||||
import ir.armor.tachidesk.impl.getPageImage
|
||||
import ir.armor.tachidesk.impl.getSource
|
||||
import ir.armor.tachidesk.impl.getSourceList
|
||||
import ir.armor.tachidesk.impl.getThumbnail
|
||||
import ir.armor.tachidesk.impl.installAPK
|
||||
import ir.armor.tachidesk.impl.removeCategory
|
||||
import ir.armor.tachidesk.impl.removeExtension
|
||||
import ir.armor.tachidesk.impl.removeMangaFromCategory
|
||||
import ir.armor.tachidesk.impl.removeMangaFromLibrary
|
||||
import ir.armor.tachidesk.impl.reorderCategory
|
||||
import ir.armor.tachidesk.impl.sourceFilters
|
||||
import ir.armor.tachidesk.impl.sourceGlobalSearch
|
||||
import ir.armor.tachidesk.impl.sourceSearch
|
||||
import ir.armor.tachidesk.impl.updateCategory
|
||||
import ir.armor.tachidesk.server.util.openInBrowser
|
||||
import mu.KotlinLogging
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun javalinSetup() {
|
||||
var hasWebUiBundled = false
|
||||
|
||||
val app = Javalin.create { config ->
|
||||
try {
|
||||
Main::class.java.getResource("/react/index.html")
|
||||
hasWebUiBundled = true
|
||||
config.addStaticFiles("/react")
|
||||
config.addSinglePageRoot("/", "/react/index.html")
|
||||
} catch (e: RuntimeException) {
|
||||
logger.warn("react build files are missing.")
|
||||
hasWebUiBundled = false
|
||||
}
|
||||
config.enableCorsForAllOrigins()
|
||||
}.start(serverConfig.ip, serverConfig.port)
|
||||
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
||||
openInBrowser()
|
||||
}
|
||||
|
||||
app.exception(NullPointerException::class.java) { e, ctx ->
|
||||
logger.error("NullPointerException while handling the request", e)
|
||||
ctx.status(404)
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/list") { ctx ->
|
||||
ctx.json(getExtensionList())
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
|
||||
ctx.status(
|
||||
installAPK(apkName)
|
||||
)
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
|
||||
removeExtension(apkName)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// icon for extension named `apkName`
|
||||
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
val result = getExtensionIcon(apkName)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
// list of sources
|
||||
app.get("/api/v1/source/list") { ctx ->
|
||||
ctx.json(getSourceList())
|
||||
}
|
||||
|
||||
// fetch source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(getSource(sourceId))
|
||||
}
|
||||
|
||||
// popular mangas from source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(getMangaList(sourceId, pageNum, popular = true))
|
||||
}
|
||||
|
||||
// latest mangas from source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(getMangaList(sourceId, pageNum, popular = false))
|
||||
}
|
||||
|
||||
// get manga info
|
||||
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getManga(mangaId))
|
||||
}
|
||||
|
||||
// manga thumbnail
|
||||
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val result = getThumbnail(mangaId)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
// adds the manga to library
|
||||
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
addMangaToLibrary(mangaId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// removes the manga from the library
|
||||
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
removeMangaFromLibrary(mangaId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// list manga's categories
|
||||
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getMangaCategories(mangaId))
|
||||
}
|
||||
|
||||
// adds the manga to category
|
||||
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
addMangaToCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// removes the manga from the category
|
||||
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
removeMangaFromCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getChapterList(mangaId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getChapter(chapterIndex, mangaId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val index = ctx.pathParam("index").toInt()
|
||||
val result = getPageImage(mangaId, chapterIndex, index)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
// global search
|
||||
app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
ctx.json(sourceGlobalSearch(searchTerm))
|
||||
}
|
||||
|
||||
// single source search
|
||||
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
|
||||
}
|
||||
|
||||
// source filter list
|
||||
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(sourceFilters(sourceId))
|
||||
}
|
||||
|
||||
// lists mangas that have no category assigned
|
||||
app.get("/api/v1/library/") { ctx ->
|
||||
ctx.json(getLibraryMangas())
|
||||
}
|
||||
|
||||
// category list
|
||||
app.get("/api/v1/category/") { ctx ->
|
||||
ctx.json(getCategoryList())
|
||||
}
|
||||
|
||||
// category create
|
||||
app.post("/api/v1/category/") { ctx ->
|
||||
val name = ctx.formParam("name")!!
|
||||
createCategory(name)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category modification
|
||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
val name = ctx.formParam("name")
|
||||
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
||||
updateCategory(categoryId, name, isLanding)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category re-ordering
|
||||
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
val from = ctx.formParam("from")!!.toInt()
|
||||
val to = ctx.formParam("to")!!.toInt()
|
||||
reorderCategory(categoryId, from, to)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category delete
|
||||
app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
removeCategory(categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// returns the manga list associated with a category
|
||||
app.get("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
ctx.json(getCategoryMangaList(categoryId))
|
||||
}
|
||||
}
|
||||
@@ -1,71 +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.serverConfig
|
||||
import java.awt.event.ActionListener
|
||||
import java.io.IOException
|
||||
|
||||
fun openInBrowser() {
|
||||
try {
|
||||
Desktop.browseURL("http://127.0.0.1:4567")
|
||||
} catch (e1: IOException) {
|
||||
e1.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()
|
||||
|
||||
val systemTray = SystemTray.get() ?: return null
|
||||
val mainMenu = systemTray.menu
|
||||
|
||||
mainMenu.add(
|
||||
MenuItem(
|
||||
"Open Tachidesk",
|
||||
ActionListener {
|
||||
try {
|
||||
Desktop.browseURL("http://127.0.0.1:4567")
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
|
||||
|
||||
// systemTray.setTooltip("Tachidesk")
|
||||
systemTray.setImage(icon)
|
||||
// systemTray.status = "No Mail"
|
||||
|
||||
systemTray.getMenu().add(
|
||||
MenuItem("Quit") {
|
||||
systemTray.shutdown()
|
||||
System.exit(0)
|
||||
}
|
||||
)
|
||||
|
||||
return systemTray
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package suwayomi.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 suwayomi.tachidesk.server.JavalinSetup.javalinSetup
|
||||
import suwayomi.tachidesk.server.applicationSetup
|
||||
|
||||
fun main() {
|
||||
applicationSetup()
|
||||
javalinSetup()
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
package suwayomi.tachidesk.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.tachidesk.anime.impl.Anime.getAnime
|
||||
import suwayomi.tachidesk.anime.impl.Anime.getAnimeThumbnail
|
||||
import suwayomi.tachidesk.anime.impl.AnimeList.getAnimeList
|
||||
import suwayomi.tachidesk.anime.impl.Episode.getEpisode
|
||||
import suwayomi.tachidesk.anime.impl.Episode.getEpisodeList
|
||||
import suwayomi.tachidesk.anime.impl.Episode.modifyEpisode
|
||||
import suwayomi.tachidesk.anime.impl.Source.getAnimeSource
|
||||
import suwayomi.tachidesk.anime.impl.Source.getSourceList
|
||||
import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIcon
|
||||
import suwayomi.tachidesk.anime.impl.extension.Extension.installExtension
|
||||
import suwayomi.tachidesk.anime.impl.extension.Extension.uninstallExtension
|
||||
import suwayomi.tachidesk.anime.impl.extension.Extension.updateExtension
|
||||
import suwayomi.tachidesk.anime.impl.extension.ExtensionsList.getExtensionList
|
||||
import suwayomi.tachidesk.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.tachidesk.anime.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 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.tachidesk.anime.impl.AnimeList.proxyThumbnailUrl
|
||||
import suwayomi.tachidesk.anime.impl.Source.getAnimeSource
|
||||
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||
import suwayomi.tachidesk.anime.model.dataclass.AnimeDataClass
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeStatus
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeTable
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.clearCachedImage
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
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.tachidesk.anime.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 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.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||
import suwayomi.tachidesk.anime.model.dataclass.AnimeDataClass
|
||||
import suwayomi.tachidesk.anime.model.dataclass.PagedAnimeListDataClass
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeStatus
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeTable
|
||||
import suwayomi.tachidesk.manga.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.tachidesk.anime.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 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.tachidesk.anime.impl.Anime.getAnime
|
||||
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||
import suwayomi.tachidesk.anime.model.dataclass.EpisodeDataClass
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeTable
|
||||
import suwayomi.tachidesk.anime.model.table.EpisodeTable
|
||||
import suwayomi.tachidesk.anime.model.table.toDataClass
|
||||
import suwayomi.tachidesk.manga.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.tachidesk.anime.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIconUrl
|
||||
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||
import suwayomi.tachidesk.anime.model.dataclass.AnimeSourceDataClass
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeExtensionTable
|
||||
import suwayomi.tachidesk.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.tachidesk.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.tachidesk.anime.impl.extension.ExtensionsList.extensionTableAsDataClass
|
||||
import suwayomi.tachidesk.anime.impl.extension.github.ExtensionGithubApi
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.EXTENSION_FEATURE
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.LIB_VERSION_MAX
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.LIB_VERSION_MIN
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.METADATA_NSFW
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.METADATA_SOURCE_CLASS
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.dex2jar
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.getPackageInfo
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.getSignatureHash
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.loadExtensionSources
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.trustedSignatures
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeExtensionTable
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeSourceTable
|
||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
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.tachidesk.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.tachidesk.anime.impl.extension.Extension.getExtensionIconUrl
|
||||
import suwayomi.tachidesk.anime.impl.extension.github.ExtensionGithubApi
|
||||
import suwayomi.tachidesk.anime.impl.extension.github.OnlineExtension
|
||||
import suwayomi.tachidesk.anime.model.dataclass.AnimeExtensionDataClass
|
||||
import suwayomi.tachidesk.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] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package suwayomi.tachidesk.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.tachidesk.anime.impl.util.PackageTools.LIB_VERSION_MAX
|
||||
import suwayomi.tachidesk.anime.impl.util.PackageTools.LIB_VERSION_MIN
|
||||
import suwayomi.tachidesk.anime.model.dataclass.AnimeExtensionDataClass
|
||||
import suwayomi.tachidesk.manga.impl.util.network.UnzippingInterceptor
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
object ExtensionGithubApi {
|
||||
private const val BASE_URL = "https://raw.githubusercontent.com"
|
||||
private 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(): 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
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package suwayomi.tachidesk.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.tachidesk.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.tachidesk.anime.impl.util.PackageTools.loadExtensionSources
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeExtensionTable
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeSourceTable
|
||||
import suwayomi.tachidesk.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.tachidesk.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.tachidesk.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.tachidesk.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.tachidesk.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
|
||||
)
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package suwayomi.tachidesk.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,
|
||||
)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package suwayomi.tachidesk.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.tachidesk.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.tachidesk.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.tachidesk.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.tachidesk.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.manga.impl.MangaList.proxyThumbnailUrl
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||
import suwayomi.tachidesk.manga.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.tachidesk.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.tachidesk.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() }
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user