Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfa59a1691 | |||
| 5023e96301 | |||
| 224c24ee9f | |||
| e3b154cf9e | |||
| d249867c4c | |||
| b56045e984 | |||
| 3777cc646e | |||
| aa5a1083d0 | |||
| 2ae5e0742e | |||
| e5e875c54a | |||
| 1a99ec76e4 | |||
| 1b122d1157 | |||
| 77f2f8cc18 | |||
| f0a99980b6 | |||
| b0d43ffe69 | |||
| 16cb0184a4 | |||
| f211a33ea3 | |||
| 440c815189 | |||
| 25829aacfd | |||
| 700a739f95 | |||
| d9620bec05 | |||
| 4b6c51b1f8 | |||
| bd02edf0b1 | |||
| 5c7123a997 | |||
| c17e3bd04f | |||
| 994ae97256 | |||
| 781428a690 | |||
| c23ac5faa8 | |||
| e8d41f83c2 | |||
| 921a0a3361 | |||
| dda5a2df93 | |||
| 155f9f107d | |||
| 24f68b8f1a | |||
| 0ffbe194fa | |||
| 0b41e2b72b | |||
| ef07b9b4ce | |||
| f3999cf2d9 | |||
| 1729847937 | |||
| 37bff6c76c | |||
| 4ef32d8037 | |||
| d2f6a33f0a | |||
| 31d9903251 | |||
| e97642d92a | |||
| c49fc0ff5f | |||
| deb2ab1ff4 | |||
| 23466cf853 | |||
| 16b34f874d | |||
| 0e0d08ae5a | |||
| 986b4c2c27 | |||
| 0bf9ccfcbd | |||
| 5e8c47928d | |||
| ffae7f911f | |||
| e37fdf6d79 | |||
| b359116745 | |||
| 60073aace3 | |||
| 874b13fa14 | |||
| b146d1024b | |||
| 332e95c021 | |||
| 1f68141df5 | |||
| dd731cd306 | |||
| 38d8d03cae | |||
| ec7d840f37 | |||
| 2813dbb897 | |||
| 77d1402b8a | |||
| 08e8a9d105 | |||
| 71661f70b6 | |||
| ac1e79ba83 | |||
| d082809776 | |||
| a458a696db | |||
| 75786a91b0 |
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
rm -rf preview/*.jar preview/*.zip
|
|
||||||
|
|
||||||
cp master/server/build/Tachidesk-*.jar preview
|
|
||||||
cp master/server/build/Tachidesk-*.zip preview
|
|
||||||
|
|
||||||
cd preview
|
|
||||||
|
|
||||||
new_jar_build=$(ls Tachidesk-*.jar)
|
|
||||||
echo "last jar build file name: $new_jar_build"
|
|
||||||
|
|
||||||
latest=$(echo $new_jar_build | sed -e's/Tachidesk-\|.jar//g')
|
|
||||||
echo "{ \"latest\": \"$latest\" }" > index.json
|
|
||||||
|
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git status
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
|
||||||
git add .
|
|
||||||
git commit -m "Updated to $latest"
|
|
||||||
git push
|
|
||||||
else
|
|
||||||
echo "No changes to commit"
|
|
||||||
fi
|
|
||||||
@@ -61,6 +61,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and copy webUI, Build Jar
|
- name: Build and copy webUI, Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
env:
|
||||||
|
TachideskBuildType: "Preview"
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
@@ -69,11 +71,30 @@ jobs:
|
|||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
# - name: Mock Build and copy webUI, Build Jar
|
||||||
|
# run: |
|
||||||
|
# mkdir -p master/server/build
|
||||||
|
# cd master/server/build
|
||||||
|
# echo "test" > Tachidesk-v0.3.8-r583.jar
|
||||||
|
|
||||||
|
- name: Generate Tag Name
|
||||||
|
id: GenTagName
|
||||||
|
run: |
|
||||||
|
cd master/server/build
|
||||||
|
genTag=$(ls *.jar | sed -e's/Tachidesk-\|.jar//g')
|
||||||
|
echo "$genTag"
|
||||||
|
echo "::set-output name=value::$genTag"
|
||||||
|
|
||||||
- name: make windows packages
|
- name: make windows packages
|
||||||
run: |
|
run: |
|
||||||
cd master/scripts
|
cd master/scripts
|
||||||
./windows32-bundler.sh
|
./windows-bundler.sh win32
|
||||||
./windows64-bundler.sh
|
./windows-bundler.sh win64
|
||||||
|
|
||||||
|
# - name: Mock make windows packages
|
||||||
|
# run: |
|
||||||
|
# cd master/server/build
|
||||||
|
# echo test > Tachidesk-v0.3.8-r580-win32.zip
|
||||||
|
|
||||||
- name: Checkout preview branch
|
- name: Checkout preview branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -83,6 +104,26 @@ jobs:
|
|||||||
path: preview
|
path: preview
|
||||||
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||||
|
|
||||||
- name: Deploy preview
|
- name: Create Tag
|
||||||
run: |
|
run: |
|
||||||
./master/.github/scripts/commit-preview.sh
|
TAG="${{ steps.GenTagName.outputs.value }}"
|
||||||
|
echo "tag: $TAG"
|
||||||
|
cd preview
|
||||||
|
echo "{ \"latest\": \"$TAG\" }" > index.json
|
||||||
|
git add index.json
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git commit -m "Updated to $TAG"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
git tag $TAG
|
||||||
|
git push origin $TAG
|
||||||
|
|
||||||
|
- name: Upload Preview Release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||||
|
artifacts: "master/server/build/*.jar,master/server/build/*.zip"
|
||||||
|
owner: "Suwayomi"
|
||||||
|
repo: "Tachidesk-preview"
|
||||||
|
tag: ${{ steps.GenTagName.outputs.value }}
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and copy webUI, Build Jar
|
- name: Build and copy webUI, Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
env:
|
||||||
|
TachideskBuildType: "Stable"
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
@@ -71,11 +73,11 @@ jobs:
|
|||||||
- name: make windows packages
|
- name: make windows packages
|
||||||
run: |
|
run: |
|
||||||
cd master/scripts
|
cd master/scripts
|
||||||
./windows32-bundler.sh
|
./windows-bundler.sh win32
|
||||||
./windows64-bundler.sh
|
./windows-bundler.sh win64
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: xresloader/upload-to-github-release@master
|
uses: xresloader/upload-to-github-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
+4
-2
@@ -10,5 +10,7 @@ server/src/main/resources/react
|
|||||||
server/tmp/
|
server/tmp/
|
||||||
server/tachiserver-data/
|
server/tachiserver-data/
|
||||||
|
|
||||||
# OpenJDK downlaods
|
# bundle asset downlaods
|
||||||
OpenJDK*
|
OpenJDK*.zip
|
||||||
|
electron-*.zip
|
||||||
|
rcedit-*
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import net.harawata.appdirs.AppDirsFactory
|
|||||||
val ApplicationRootDir: String
|
val ApplicationRootDir: String
|
||||||
get(): String {
|
get(): String {
|
||||||
return System.getProperty(
|
return System.getProperty(
|
||||||
"ir.armor.tachidesk.rootDir",
|
"suwayomi.server.rootDir",
|
||||||
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -17,4 +17,4 @@ fun setLogLevel(level: Level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun debugLogsEnabled(config: Config)
|
fun debugLogsEnabled(config: Config)
|
||||||
= System.getProperty("ir.armor.tachidesk.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
|
= System.getProperty("suwayomi.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
| Build | Stable | Preview | Support Server |
|
| Build | Stable | Preview | Support Server |
|
||||||
|-------|----------|---------|---------|
|
|-------|----------|---------|---------|
|
||||||
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/tree/main/) | [](https://discord.gg/DDZdqZWaHA) |
|
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [](https://discord.gg/DDZdqZWaHA) |
|
||||||
|
|
||||||
# Tachidesk
|
# Tachidesk
|
||||||
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
||||||
@@ -32,14 +32,18 @@ Here is a list of current features:
|
|||||||
### All Operating Systems
|
### All Operating Systems
|
||||||
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.
|
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed(Google is your friend for seeking assitance). Also an internet connection is required as almost everything this app does is downloading stuff.
|
||||||
|
|
||||||
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview branch](https://github.com/Suwayomi/Tachidesk/tree/preview).
|
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
|
||||||
|
|
||||||
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` (or `java -jar Tachidesk-latest.jar` if you have the latest preview) from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` (or `java -jar Tachidesk-latest.jar` if you have the latest preview) from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
Download the latest win32 or win64 (depending on your system, usually you want win64) 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-win64.zip` and run `Tachidesk Launcher.exe` or `Tachidesk Launcher.bat`. The rest works like the previous section.
|
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win64.zip` and run one of the Launcher files depending on what you want(see bellow). The rest works like the previous section.
|
||||||
|
#### Windows Launchers
|
||||||
|
- `Tachidesk Electron Launcher.bat`: Launches Tachidesk inside Electron as a desktop applicaton
|
||||||
|
- `Tachidesk Browser Launcher.bat`: Launches Tachidesk in a browser window
|
||||||
|
- `Tachidesk Debug Launcher.bat`: Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why.
|
||||||
|
|
||||||
### Arch Linux
|
### Arch Linux
|
||||||
You can install Tachidesk from the AUR
|
You can install Tachidesk from the AUR
|
||||||
@@ -48,7 +52,16 @@ yay -S tachidesk
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
Check our Offical Docker release [Tachidesk Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) or use [arbuilder's](https://github.com/arbuilder/Tachidesk-docker) tachidesk docker repo for installation. Source code for our container is available at [docker-tachidesk](https://github.com/Suwayomi/docker-tachidesk). By default the server will be running on http://localhost:4567 open this url in your browser.
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
### Using Tachidesk Remotely
|
### 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.
|
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.
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "ir.armor.tachidesk"
|
group = "suwayomi"
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
jre\bin\java -Dir.armor.tachidesk.debugLogsEnabled=true -jar Tachidesk.jar
|
jre\bin\java -Dsuwayomi.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
jre\bin\javaw "-Dsuwayomi.server.webInterface=electron" "-Dsuwayomi.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
|
||||||
Binary file not shown.
Binary file not shown.
Executable
+83
@@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright (C) Contributors to the Suwayomi project
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
electron_version="v12.0.9"
|
||||||
|
|
||||||
|
if [ $1 = "win32" ]; then
|
||||||
|
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||||
|
arch="win32"
|
||||||
|
electron="electron-$electron_version-win32-ia32.zip"
|
||||||
|
else
|
||||||
|
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||||
|
arch="win64"
|
||||||
|
electron="electron-$electron_version-win32-x64.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
jre_dir="jdk8u292-b10-jre"
|
||||||
|
|
||||||
|
echo "creating windows bundle"
|
||||||
|
|
||||||
|
jar=$(ls ../server/build/Tachidesk-*.jar)
|
||||||
|
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||||
|
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-$arch
|
||||||
|
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
mkdir $release_name
|
||||||
|
|
||||||
|
|
||||||
|
echo "Dealing with jre..."
|
||||||
|
if [ ! -f $jre ]; then
|
||||||
|
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/$jre" -o $jre
|
||||||
|
fi
|
||||||
|
unzip $jre
|
||||||
|
mv $jre_dir $release_name/jre
|
||||||
|
|
||||||
|
echo "Dealing with electron"
|
||||||
|
if [ ! -f $electron ]; then
|
||||||
|
curl -L "https://github.com/electron/electron/releases/download/$electron_version/$electron" -o $electron
|
||||||
|
fi
|
||||||
|
unzip $electron -d $release_name/electron
|
||||||
|
|
||||||
|
# change electron's icon
|
||||||
|
rcedit="rcedit-x86.exe"
|
||||||
|
if [ ! -f $rcedit ]; then
|
||||||
|
curl -L "https://github.com/electron/rcedit/releases/download/v1.1.1/$rcedit" -o $rcedit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check if running under github actions
|
||||||
|
if [ $CI = true ]; then
|
||||||
|
# change electron executable's icon
|
||||||
|
sudo dpkg --add-architecture i386
|
||||||
|
wget -qO - https://dl.winehq.org/wine-builds/winehq.key | sudo apt-key add -
|
||||||
|
sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport
|
||||||
|
sudo apt-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu $(lsb_release -cs) main"
|
||||||
|
sudo apt install --install-recommends winehq-stable
|
||||||
|
fi
|
||||||
|
# this script assumes that wine is installed here on out
|
||||||
|
|
||||||
|
WINEARCH=win32 wine $rcedit $release_name/electron/electron.exe --set-icon ../server/src/main/resources/icon/faviconlogo.ico
|
||||||
|
|
||||||
|
# copy artifacts
|
||||||
|
cp $jar $release_name/Tachidesk.jar
|
||||||
|
#cp "resources/Tachidesk Launcher-$arch.exe" "$release_name/Tachidesk Launcher.exe"
|
||||||
|
cp "resources/Tachidesk Browser Launcher.bat" $release_name
|
||||||
|
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
cp "resources/Tachidesk Electron Launcher.bat" $release_name
|
||||||
|
|
||||||
|
zip_name=$release_name.zip
|
||||||
|
zip -9 -r $zip_name $release_name
|
||||||
|
|
||||||
|
rm -rf $release_name
|
||||||
|
|
||||||
|
# clean up from possible previous runs
|
||||||
|
if [ -f ../server/build/$zip_name ]; then
|
||||||
|
rm ../server/build/$zip_name
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv $zip_name ../server/build/
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Copyright (C) Contributors to the Suwayomi project
|
|
||||||
#
|
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
Write-Output "Downloading jre..."
|
|
||||||
|
|
||||||
$jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
|
||||||
if (!(Test-Path $jre)) {
|
|
||||||
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Output "creating windows bundle"
|
|
||||||
|
|
||||||
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
|
|
||||||
$release_name=$jar.BaseName + "-win32"
|
|
||||||
|
|
||||||
# make release dir
|
|
||||||
New-Item -ItemType Directory $release_name
|
|
||||||
|
|
||||||
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
# move jre
|
|
||||||
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
|
|
||||||
|
|
||||||
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
|
|
||||||
|
|
||||||
Copy-Item "resources/Tachidesk Launcher-win32.exe" $release_name
|
|
||||||
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
|
|
||||||
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
|
|
||||||
|
|
||||||
$zip_name="$release_name.zip"
|
|
||||||
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
Remove-Item -Force -Recurse $release_name
|
|
||||||
|
|
||||||
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Copyright (C) Contributors to the Suwayomi project
|
|
||||||
#
|
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
echo "Downloading jre..."
|
|
||||||
|
|
||||||
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
|
||||||
if [ ! -f $jre ]; then
|
|
||||||
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -o $jre
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "creating windows bundle"
|
|
||||||
|
|
||||||
jar=$(ls ../server/build/Tachidesk-*.jar)
|
|
||||||
jar_name=$(echo $jar | cut -d'/' -f4)
|
|
||||||
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win32
|
|
||||||
|
|
||||||
# make release dir
|
|
||||||
mkdir $release_name
|
|
||||||
|
|
||||||
unzip $jre
|
|
||||||
|
|
||||||
# move jre
|
|
||||||
mv jdk8u292-b10-jre $release_name/jre
|
|
||||||
|
|
||||||
cp $jar $release_name/Tachidesk.jar
|
|
||||||
|
|
||||||
cp "resources/Tachidesk Launcher-win32.exe" "$release_name/Tachidesk Launcher.exe"
|
|
||||||
cp "resources/Tachidesk Launcher.bat" $release_name
|
|
||||||
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
|
||||||
|
|
||||||
zip_name=$release_name.zip
|
|
||||||
zip -9 -r $zip_name $release_name
|
|
||||||
|
|
||||||
rm -rf $release_name
|
|
||||||
|
|
||||||
mv $zip_name ../server/build/
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Copyright (C) Contributors to the Suwayomi project
|
|
||||||
#
|
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
Write-Output "Downloading jre..."
|
|
||||||
|
|
||||||
$jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
|
||||||
if (!(Test-Path $jre)) {
|
|
||||||
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Output "creating windows bundle"
|
|
||||||
|
|
||||||
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
|
|
||||||
$release_name=$jar.BaseName + "-win64"
|
|
||||||
|
|
||||||
# make release dir
|
|
||||||
New-Item -ItemType Directory $release_name
|
|
||||||
|
|
||||||
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
# move jre
|
|
||||||
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
|
|
||||||
|
|
||||||
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
|
|
||||||
|
|
||||||
Copy-Item "resources/Tachidesk Launcher-win64.exe" $release_name
|
|
||||||
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
|
|
||||||
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
|
|
||||||
|
|
||||||
$zip_name="$release_name.zip"
|
|
||||||
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
Remove-Item -Force -Recurse $release_name
|
|
||||||
|
|
||||||
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Copyright (C) Contributors to the Suwayomi project
|
|
||||||
#
|
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
echo "Downloading jre..."
|
|
||||||
|
|
||||||
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
|
||||||
if [ ! -f $jre ]; then
|
|
||||||
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -o $jre
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "creating windows bundle"
|
|
||||||
|
|
||||||
jar=$(ls ../server/build/Tachidesk-*.jar)
|
|
||||||
jar_name=$(echo $jar | cut -d'/' -f4)
|
|
||||||
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win64
|
|
||||||
|
|
||||||
# make release dir
|
|
||||||
mkdir $release_name
|
|
||||||
|
|
||||||
unzip $jre
|
|
||||||
|
|
||||||
# move jre
|
|
||||||
mv jdk8u292-b10-jre $release_name/jre
|
|
||||||
|
|
||||||
cp $jar $release_name/Tachidesk.jar
|
|
||||||
|
|
||||||
cp "resources/Tachidesk Launcher-win64.exe" "$release_name/Tachidesk Launcher.exe"
|
|
||||||
cp "resources/Tachidesk Launcher.bat" $release_name
|
|
||||||
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
|
||||||
|
|
||||||
zip_name=$release_name.zip
|
|
||||||
zip -9 -r $zip_name $release_name
|
|
||||||
|
|
||||||
rm -rf $release_name
|
|
||||||
|
|
||||||
mv $zip_name ../server/build/
|
|
||||||
+21
-10
@@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|||||||
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
||||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
application
|
application
|
||||||
@@ -72,9 +73,15 @@ dependencies {
|
|||||||
testImplementation(kotlin("test-junit5"))
|
testImplementation(kotlin("test-junit5"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val MainClass = "ir.armor.tachidesk.MainKt"
|
val MainClass = "suwayomi.MainKt"
|
||||||
application {
|
application {
|
||||||
mainClass.set(MainClass)
|
mainClass.set(MainClass)
|
||||||
|
|
||||||
|
// for testing electron
|
||||||
|
// applicationDefaultJvmArgs = listOf(
|
||||||
|
// "-Dsuwayomi.tachidesk.webInterface=electron",
|
||||||
|
// "-Dsuwayomi.tachidesk.electronPath=/usr/bin/electron"
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -86,10 +93,11 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should be bumped with each stable release
|
// should be bumped with each stable release
|
||||||
val tachideskVersion = "v0.3.8"
|
val tachideskVersion = "v0.4.2"
|
||||||
|
|
||||||
// counts commit count on master
|
// counts commit count on master
|
||||||
val tachideskRevision = Runtime
|
val tachideskRevision = runCatching {
|
||||||
|
Runtime
|
||||||
.getRuntime()
|
.getRuntime()
|
||||||
.exec("git rev-list HEAD --count")
|
.exec("git rev-list HEAD --count")
|
||||||
.let { process ->
|
.let { process ->
|
||||||
@@ -101,18 +109,21 @@ val tachideskRevision = Runtime
|
|||||||
"r" + output.trim()
|
"r" + output.trim()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}.getOrDefault("r0")
|
||||||
|
|
||||||
buildConfig {
|
buildConfig {
|
||||||
appName = rootProject.name
|
|
||||||
clsName = "BuildConfig"
|
clsName = "BuildConfig"
|
||||||
packageName = "ir.armor.tachidesk.server"
|
packageName = "suwayomi.server"
|
||||||
version = tachideskVersion
|
|
||||||
|
|
||||||
|
|
||||||
buildConfigField("String", "name", rootProject.name) // alias for BuildConfig.NAME
|
buildConfigField("String", "NAME", rootProject.name)
|
||||||
buildConfigField("String", "version", tachideskVersion) // alias for BuildConfig.VERSION
|
buildConfigField("String", "VERSION", tachideskVersion)
|
||||||
buildConfigField("String", "revision", tachideskRevision)
|
buildConfigField("String", "REVISION", tachideskRevision)
|
||||||
buildConfigField("boolean", "debug", project.hasProperty("debugApp").toString())
|
buildConfigField("String", "BUILD_TYPE", if (System.getenv("TachideskBuildType") == "Stable") "Stable" else "Preview")
|
||||||
|
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
|
||||||
|
|
||||||
|
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk")
|
||||||
|
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
|||||||
@@ -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,13 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
// import android.graphics.drawable.Drawable
|
|
||||||
// import eu.kanade.tachiyomi.extension.ExtensionManager
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
// import uy.kohesive.injekt.Injekt
|
|
||||||
// import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
// import android.content.Context
|
import android.content.Context
|
||||||
// import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
open class SourceManager() {
|
open class SourceManager(private val context: Context) {
|
||||||
|
|
||||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.download
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 Download(
|
|
||||||
val chapter: ResultRow,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val downloadQueue = LinkedBlockingQueue<Download>()
|
|
||||||
|
|
||||||
class Downloader {
|
|
||||||
|
|
||||||
fun start() {
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.extension.github
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
@@ -1,65 +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.util.CacheUtil
|
|
||||||
import dorkbox.util.Desktop
|
|
||||||
import ir.armor.tachidesk.server.BuildConfig
|
|
||||||
import ir.armor.tachidesk.server.ServerConfig
|
|
||||||
import ir.armor.tachidesk.server.serverConfig
|
|
||||||
import ir.armor.tachidesk.server.util.ExitCode.Success
|
|
||||||
|
|
||||||
fun openInBrowser() {
|
|
||||||
val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
|
|
||||||
try {
|
|
||||||
Desktop.browseURL("http://$appIP:${serverConfig.port}")
|
|
||||||
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun systemTray(): SystemTray? {
|
|
||||||
try {
|
|
||||||
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
|
||||||
SystemTray.DEBUG = serverConfig.debugLogsEnabled
|
|
||||||
|
|
||||||
CacheUtil.clear(BuildConfig.name)
|
|
||||||
|
|
||||||
val systemTray = SystemTray.get(BuildConfig.name) ?: return null
|
|
||||||
val mainMenu = systemTray.menu
|
|
||||||
|
|
||||||
mainMenu.add(
|
|
||||||
MenuItem(
|
|
||||||
"Open Tachidesk"
|
|
||||||
) {
|
|
||||||
openInBrowser()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val icon = ServerConfig::class.java.getResource("/icon/faviconlogo.png")
|
|
||||||
|
|
||||||
// systemTray.setTooltip("Tachidesk")
|
|
||||||
systemTray.setImage(icon)
|
|
||||||
// systemTray.status = "No Mail"
|
|
||||||
|
|
||||||
mainMenu.add(
|
|
||||||
MenuItem("Quit") {
|
|
||||||
shutdownApp(Success)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
systemTray.installShutdownHook()
|
|
||||||
|
|
||||||
return systemTray
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk
|
package suwayomi
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,8 +7,8 @@ package ir.armor.tachidesk
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.server.JavalinSetup.javalinSetup
|
import suwayomi.server.JavalinSetup.javalinSetup
|
||||||
import ir.armor.tachidesk.server.applicationSetup
|
import suwayomi.server.applicationSetup
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
applicationSetup()
|
applicationSetup()
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
package suwayomi.anime
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import io.javalin.Javalin
|
||||||
|
import suwayomi.anime.impl.Anime.getAnime
|
||||||
|
import suwayomi.anime.impl.Anime.getAnimeThumbnail
|
||||||
|
import suwayomi.anime.impl.AnimeList.getAnimeList
|
||||||
|
import suwayomi.anime.impl.Episode.getEpisode
|
||||||
|
import suwayomi.anime.impl.Episode.getEpisodeList
|
||||||
|
import suwayomi.anime.impl.Episode.modifyEpisode
|
||||||
|
import suwayomi.anime.impl.Source.getAnimeSource
|
||||||
|
import suwayomi.anime.impl.Source.getSourceList
|
||||||
|
import suwayomi.anime.impl.extension.Extension.getExtensionIcon
|
||||||
|
import suwayomi.anime.impl.extension.Extension.installExtension
|
||||||
|
import suwayomi.anime.impl.extension.Extension.uninstallExtension
|
||||||
|
import suwayomi.anime.impl.extension.Extension.updateExtension
|
||||||
|
import suwayomi.anime.impl.extension.ExtensionsList.getExtensionList
|
||||||
|
import suwayomi.server.JavalinSetup.future
|
||||||
|
|
||||||
|
object AnimeAPI {
|
||||||
|
fun defineEndpoints(app: Javalin) {
|
||||||
|
// list all extensions
|
||||||
|
app.get("/api/v1/anime/extension/list") { ctx ->
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getExtensionList()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// install extension identified with "pkgName"
|
||||||
|
app.get("/api/v1/anime/extension/install/:pkgName") { ctx ->
|
||||||
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
installExtension(pkgName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update extension identified with "pkgName"
|
||||||
|
app.get("/api/v1/anime/extension/update/:pkgName") { ctx ->
|
||||||
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
updateExtension(pkgName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// uninstall extension identified with "pkgName"
|
||||||
|
app.get("/api/v1/anime/extension/uninstall/:pkgName") { ctx ->
|
||||||
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
|
uninstallExtension(pkgName)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// icon for extension named `apkName`
|
||||||
|
app.get("/api/v1/anime/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
|
||||||
|
val apkName = ctx.pathParam("apkName")
|
||||||
|
|
||||||
|
ctx.result(
|
||||||
|
future { getExtensionIcon(apkName) }
|
||||||
|
.thenApply {
|
||||||
|
ctx.header("content-type", it.second)
|
||||||
|
it.first
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// list of sources
|
||||||
|
app.get("/api/v1/anime/source/list") { ctx ->
|
||||||
|
ctx.json(getSourceList())
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch source with id `sourceId`
|
||||||
|
app.get("/api/v1/anime/source/:sourceId") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
ctx.json(getAnimeSource(sourceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// popular animes from source with id `sourceId`
|
||||||
|
app.get("/api/v1/anime/source/:sourceId/popular/:pageNum") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getAnimeList(sourceId, pageNum, popular = true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// latest animes from source with id `sourceId`
|
||||||
|
app.get("/api/v1/anime/source/:sourceId/latest/:pageNum") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getAnimeList(sourceId, pageNum, popular = false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get anime info
|
||||||
|
app.get("/api/v1/anime/anime/:animeId/") { ctx ->
|
||||||
|
val animeId = ctx.pathParam("animeId").toInt()
|
||||||
|
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
|
||||||
|
|
||||||
|
ctx.json(
|
||||||
|
future {
|
||||||
|
getAnime(animeId, onlineFetch)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// anime thumbnail
|
||||||
|
app.get("api/v1/anime/anime/:animeId/thumbnail") { ctx ->
|
||||||
|
val animeId = ctx.pathParam("animeId").toInt()
|
||||||
|
|
||||||
|
ctx.result(
|
||||||
|
future { getAnimeThumbnail(animeId) }
|
||||||
|
.thenApply {
|
||||||
|
ctx.header("content-type", it.second)
|
||||||
|
it.first
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// // list manga's categories
|
||||||
|
// app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
// ctx.json(getMangaCategories(mangaId))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // adds the manga to category
|
||||||
|
// app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// addMangaToCategory(mangaId, categoryId)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // removes the manga from the category
|
||||||
|
// app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// removeMangaFromCategory(mangaId, categoryId)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// get episode list when showing a anime
|
||||||
|
app.get("/api/v1/anime/anime/:animeId/episodes") { ctx ->
|
||||||
|
val animeId = ctx.pathParam("animeId").toInt()
|
||||||
|
|
||||||
|
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean()
|
||||||
|
|
||||||
|
ctx.json(future { getEpisodeList(animeId, onlineFetch) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// used to display a episode, get a episode in order to show it's <Quality pending>
|
||||||
|
app.get("/api/v1/anime/anime/:animeId/episode/:episodeIndex") { ctx ->
|
||||||
|
val episodeIndex = ctx.pathParam("episodeIndex").toInt()
|
||||||
|
val animeId = ctx.pathParam("animeId").toInt()
|
||||||
|
ctx.json(future { getEpisode(episodeIndex, animeId) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// used to modify a episode's parameters
|
||||||
|
app.patch("/api/v1/anime/anime/:animeId/episode/:episodeIndex") { ctx ->
|
||||||
|
val episodeIndex = ctx.pathParam("episodeIndex").toInt()
|
||||||
|
val animeId = ctx.pathParam("animeId").toInt()
|
||||||
|
|
||||||
|
val read = ctx.formParam("read")?.toBoolean()
|
||||||
|
val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
|
||||||
|
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
|
||||||
|
val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
|
||||||
|
|
||||||
|
modifyEpisode(animeId, episodeIndex, read, bookmarked, markPrevRead, lastPageRead)
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// // get page at index "index"
|
||||||
|
// app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
// val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
|
// val index = ctx.pathParam("index").toInt()
|
||||||
|
//
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future { getPageImage(mangaId, chapterIndex, index) }
|
||||||
|
// .thenApply {
|
||||||
|
// ctx.header("content-type", it.second)
|
||||||
|
// it.first
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // submit a chapter for download
|
||||||
|
// app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||||
|
// // TODO
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // cancel a chapter download
|
||||||
|
// app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||||
|
// // TODO
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // global search, Not implemented yet
|
||||||
|
// app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||||
|
// val searchTerm = ctx.pathParam("searchTerm")
|
||||||
|
// ctx.json(sourceGlobalSearch(searchTerm))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // single source search
|
||||||
|
// app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
||||||
|
// val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
// val searchTerm = ctx.pathParam("searchTerm")
|
||||||
|
// val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
|
// ctx.json(JavalinSetup.future { sourceSearch(sourceId, searchTerm, pageNum) })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // source filter list
|
||||||
|
// app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||||
|
// val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
// ctx.json(sourceFilters(sourceId))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // adds the manga to library
|
||||||
|
// app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
//
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future { addMangaToLibrary(mangaId) }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // removes the manga from the library
|
||||||
|
// app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
//
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future { removeMangaFromLibrary(mangaId) }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // lists mangas that have no category assigned
|
||||||
|
// app.get("/api/v1/library/") { ctx ->
|
||||||
|
// ctx.json(getLibraryMangas())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // category list
|
||||||
|
// app.get("/api/v1/category/") { ctx ->
|
||||||
|
// ctx.json(Category.getCategoryList())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // category create
|
||||||
|
// app.post("/api/v1/category/") { ctx ->
|
||||||
|
// val name = ctx.formParam("name")!!
|
||||||
|
// Category.createCategory(name)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // returns some static info of the current app build
|
||||||
|
// app.get("/api/v1/about/") { ctx ->
|
||||||
|
// ctx.json(About.getAbout())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // category modification
|
||||||
|
// app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// val name = ctx.formParam("name")
|
||||||
|
// val isDefault = ctx.formParam("default")?.toBoolean()
|
||||||
|
// Category.updateCategory(categoryId, name, isDefault)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // category re-ordering
|
||||||
|
// app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// val from = ctx.formParam("from")!!.toInt()
|
||||||
|
// val to = ctx.formParam("to")!!.toInt()
|
||||||
|
// Category.reorderCategory(categoryId, from, to)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // category delete
|
||||||
|
// app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// Category.removeCategory(categoryId)
|
||||||
|
// ctx.status(200)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // returns the manga list associated with a category
|
||||||
|
// app.get("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
// ctx.json(getCategoryMangaList(categoryId))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // expects a Tachiyomi legacy backup json in the body
|
||||||
|
// app.post("/api/v1/backup/legacy/import") { ctx ->
|
||||||
|
// ctx.result(
|
||||||
|
// future {
|
||||||
|
// restoreLegacyBackup(ctx.bodyAsInputStream())
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json"
|
||||||
|
// app.post("/api/v1/backup/legacy/import/file") { ctx ->
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future {
|
||||||
|
// restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // returns a Tachiyomi legacy backup json created from the current database as a json body
|
||||||
|
// app.get("/api/v1/backup/legacy/export") { ctx ->
|
||||||
|
// ctx.contentType("application/json")
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future {
|
||||||
|
// createLegacyBackup(
|
||||||
|
// BackupFlags(
|
||||||
|
// includeManga = true,
|
||||||
|
// includeCategories = true,
|
||||||
|
// includeChapters = true,
|
||||||
|
// includeTracking = true,
|
||||||
|
// includeHistory = true,
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // returns a Tachiyomi legacy backup json created from the current database as a file
|
||||||
|
// app.get("/api/v1/backup/legacy/export/file") { ctx ->
|
||||||
|
// ctx.contentType("application/json")
|
||||||
|
// val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm")
|
||||||
|
// val currentDate = sdf.format(Date())
|
||||||
|
//
|
||||||
|
// ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
|
||||||
|
// ctx.result(
|
||||||
|
// JavalinSetup.future {
|
||||||
|
// createLegacyBackup(
|
||||||
|
// BackupFlags(
|
||||||
|
// includeManga = true,
|
||||||
|
// includeCategories = true,
|
||||||
|
// includeChapters = true,
|
||||||
|
// includeTracking = true,
|
||||||
|
// includeHistory = true,
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Download queue stats
|
||||||
|
// app.ws("/api/v1/downloads") { ws ->
|
||||||
|
// ws.onConnect { ctx ->
|
||||||
|
// // TODO: send current stat
|
||||||
|
// // TODO: add to downlad subscribers
|
||||||
|
// }
|
||||||
|
// ws.onMessage {
|
||||||
|
// // TODO: send current stat
|
||||||
|
// }
|
||||||
|
// ws.onClose { ctx ->
|
||||||
|
// // TODO: remove from subscribers
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package suwayomi.anime.impl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import suwayomi.anime.impl.AnimeList.proxyThumbnailUrl
|
||||||
|
import suwayomi.anime.impl.Source.getAnimeSource
|
||||||
|
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||||
|
import suwayomi.anime.model.dataclass.AnimeDataClass
|
||||||
|
import suwayomi.anime.model.table.AnimeStatus
|
||||||
|
import suwayomi.anime.model.table.AnimeTable
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import suwayomi.tachidesk.impl.util.network.await
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
object Anime {
|
||||||
|
private fun truncate(text: String?, maxLength: Int): String? {
|
||||||
|
return if (text?.length ?: 0 > maxLength)
|
||||||
|
text?.take(maxLength - 3) + "..."
|
||||||
|
else
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getAnime(animeId: Int, onlineFetch: Boolean = false): AnimeDataClass {
|
||||||
|
var animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
|
||||||
|
|
||||||
|
return if (animeEntry[AnimeTable.initialized] && !onlineFetch) {
|
||||||
|
AnimeDataClass(
|
||||||
|
animeId,
|
||||||
|
animeEntry[AnimeTable.sourceReference].toString(),
|
||||||
|
|
||||||
|
animeEntry[AnimeTable.url],
|
||||||
|
animeEntry[AnimeTable.title],
|
||||||
|
proxyThumbnailUrl(animeId),
|
||||||
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
animeEntry[AnimeTable.artist],
|
||||||
|
animeEntry[AnimeTable.author],
|
||||||
|
animeEntry[AnimeTable.description],
|
||||||
|
animeEntry[AnimeTable.genre],
|
||||||
|
AnimeStatus.valueOf(animeEntry[AnimeTable.status]).name,
|
||||||
|
animeEntry[AnimeTable.inLibrary],
|
||||||
|
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
} else { // initialize anime
|
||||||
|
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
|
||||||
|
val fetchedAnime = source.fetchAnimeDetails(
|
||||||
|
SAnime.create().apply {
|
||||||
|
url = animeEntry[AnimeTable.url]
|
||||||
|
title = animeEntry[AnimeTable.title]
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
AnimeTable.update({ AnimeTable.id eq animeId }) {
|
||||||
|
|
||||||
|
it[AnimeTable.initialized] = true
|
||||||
|
|
||||||
|
it[AnimeTable.artist] = fetchedAnime.artist
|
||||||
|
it[AnimeTable.author] = fetchedAnime.author
|
||||||
|
it[AnimeTable.description] = truncate(fetchedAnime.description, 4096)
|
||||||
|
it[AnimeTable.genre] = fetchedAnime.genre
|
||||||
|
it[AnimeTable.status] = fetchedAnime.status
|
||||||
|
if (fetchedAnime.thumbnail_url != null && fetchedAnime.thumbnail_url.orEmpty().isNotEmpty())
|
||||||
|
it[AnimeTable.thumbnail_url] = fetchedAnime.thumbnail_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAnimeThumbnail(animeId)
|
||||||
|
|
||||||
|
animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
|
||||||
|
|
||||||
|
AnimeDataClass(
|
||||||
|
animeId,
|
||||||
|
animeEntry[AnimeTable.sourceReference].toString(),
|
||||||
|
|
||||||
|
animeEntry[AnimeTable.url],
|
||||||
|
animeEntry[AnimeTable.title],
|
||||||
|
proxyThumbnailUrl(animeId),
|
||||||
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
fetchedAnime.artist,
|
||||||
|
fetchedAnime.author,
|
||||||
|
fetchedAnime.description,
|
||||||
|
fetchedAnime.genre,
|
||||||
|
AnimeStatus.valueOf(fetchedAnime.status).name,
|
||||||
|
animeEntry[AnimeTable.inLibrary],
|
||||||
|
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
suspend fun getAnimeThumbnail(animeId: Int): Pair<InputStream, String> {
|
||||||
|
val saveDir = applicationDirs.animeThumbnailsRoot
|
||||||
|
val fileName = animeId.toString()
|
||||||
|
|
||||||
|
return getCachedImageResponse(saveDir, fileName) {
|
||||||
|
getAnime(animeId) // make sure is initialized
|
||||||
|
|
||||||
|
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
|
||||||
|
|
||||||
|
val sourceId = animeEntry[AnimeTable.sourceReference]
|
||||||
|
val source = getAnimeHttpSource(sourceId)
|
||||||
|
|
||||||
|
val thumbnailUrl = animeEntry[AnimeTable.thumbnail_url]!!
|
||||||
|
|
||||||
|
source.client.newCall(
|
||||||
|
GET(thumbnailUrl, source.headers)
|
||||||
|
).await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearAnimeThumbnail(animeId: Int) {
|
||||||
|
val saveDir = applicationDirs.animeThumbnailsRoot
|
||||||
|
val fileName = animeId.toString()
|
||||||
|
|
||||||
|
clearCachedImage(saveDir, fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package suwayomi.anime.impl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||||
|
import suwayomi.anime.model.dataclass.AnimeDataClass
|
||||||
|
import suwayomi.anime.model.dataclass.PagedAnimeListDataClass
|
||||||
|
import suwayomi.anime.model.table.AnimeStatus
|
||||||
|
import suwayomi.anime.model.table.AnimeTable
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
|
||||||
|
object AnimeList {
|
||||||
|
fun proxyThumbnailUrl(animeId: Int): String {
|
||||||
|
return "/api/v1/anime/anime/$animeId/thumbnail"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getAnimeList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedAnimeListDataClass {
|
||||||
|
val source = getAnimeHttpSource(sourceId)
|
||||||
|
val animesPage = if (popular) {
|
||||||
|
source.fetchPopularAnime(pageNum).awaitSingle()
|
||||||
|
} else {
|
||||||
|
if (source.supportsLatest)
|
||||||
|
source.fetchLatestUpdates(pageNum).awaitSingle()
|
||||||
|
else
|
||||||
|
throw Exception("Source $source doesn't support latest")
|
||||||
|
}
|
||||||
|
return animesPage.processEntries(sourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AnimesPage.processEntries(sourceId: Long): PagedAnimeListDataClass {
|
||||||
|
val animesPage = this
|
||||||
|
val animeList = transaction {
|
||||||
|
return@transaction animesPage.animes.map { anime ->
|
||||||
|
val animeEntry = AnimeTable.select { AnimeTable.url eq anime.url }.firstOrNull()
|
||||||
|
if (animeEntry == null) { // create anime entry
|
||||||
|
val animeId = AnimeTable.insertAndGetId {
|
||||||
|
it[url] = anime.url
|
||||||
|
it[title] = anime.title
|
||||||
|
|
||||||
|
it[artist] = anime.artist
|
||||||
|
it[author] = anime.author
|
||||||
|
it[description] = anime.description
|
||||||
|
it[genre] = anime.genre
|
||||||
|
it[status] = anime.status
|
||||||
|
it[thumbnail_url] = anime.thumbnail_url
|
||||||
|
|
||||||
|
it[sourceReference] = sourceId
|
||||||
|
}.value
|
||||||
|
|
||||||
|
AnimeDataClass(
|
||||||
|
animeId,
|
||||||
|
sourceId.toString(),
|
||||||
|
|
||||||
|
anime.url,
|
||||||
|
anime.title,
|
||||||
|
proxyThumbnailUrl(animeId),
|
||||||
|
|
||||||
|
anime.initialized,
|
||||||
|
|
||||||
|
anime.artist,
|
||||||
|
anime.author,
|
||||||
|
anime.description,
|
||||||
|
anime.genre,
|
||||||
|
AnimeStatus.valueOf(anime.status).name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val animeId = animeEntry[AnimeTable.id].value
|
||||||
|
AnimeDataClass(
|
||||||
|
animeId,
|
||||||
|
sourceId.toString(),
|
||||||
|
|
||||||
|
anime.url,
|
||||||
|
anime.title,
|
||||||
|
proxyThumbnailUrl(animeId),
|
||||||
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
animeEntry[AnimeTable.artist],
|
||||||
|
animeEntry[AnimeTable.author],
|
||||||
|
animeEntry[AnimeTable.description],
|
||||||
|
animeEntry[AnimeTable.genre],
|
||||||
|
AnimeStatus.valueOf(animeEntry[AnimeTable.status]).name,
|
||||||
|
animeEntry[AnimeTable.inLibrary]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PagedAnimeListDataClass(
|
||||||
|
animeList,
|
||||||
|
animesPage.hasNextPage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
package suwayomi.anime.impl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder.DESC
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.anime.impl.Anime.getAnime
|
||||||
|
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||||
|
import suwayomi.anime.model.dataclass.EpisodeDataClass
|
||||||
|
import suwayomi.anime.model.table.AnimeTable
|
||||||
|
import suwayomi.anime.model.table.EpisodeTable
|
||||||
|
import suwayomi.anime.model.table.toDataClass
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
|
||||||
|
object Episode {
|
||||||
|
/** get episode list when showing an anime */
|
||||||
|
suspend fun getEpisodeList(animeId: Int, onlineFetch: Boolean?): List<EpisodeDataClass> {
|
||||||
|
return if (onlineFetch == true) {
|
||||||
|
getSourceEpisodes(animeId)
|
||||||
|
} else {
|
||||||
|
transaction {
|
||||||
|
EpisodeTable.select { EpisodeTable.anime eq animeId }.orderBy(EpisodeTable.episodeIndex to DESC)
|
||||||
|
.map {
|
||||||
|
EpisodeTable.toDataClass(it)
|
||||||
|
}
|
||||||
|
}.ifEmpty {
|
||||||
|
// If it was explicitly set to offline dont grab episodes
|
||||||
|
if (onlineFetch == null) {
|
||||||
|
getSourceEpisodes(animeId)
|
||||||
|
} else emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getSourceEpisodes(animeId: Int): List<EpisodeDataClass> {
|
||||||
|
val animeDetails = getAnime(animeId)
|
||||||
|
val source = getAnimeHttpSource(animeDetails.sourceId.toLong())
|
||||||
|
val episodeList = source.fetchEpisodeList(
|
||||||
|
SAnime.create().apply {
|
||||||
|
title = animeDetails.title
|
||||||
|
url = animeDetails.url
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
|
val episodeCount = episodeList.count()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
episodeList.reversed().forEachIndexed { index, fetchedEpisode ->
|
||||||
|
val episodeEntry = EpisodeTable.select { EpisodeTable.url eq fetchedEpisode.url }.firstOrNull()
|
||||||
|
if (episodeEntry == null) {
|
||||||
|
EpisodeTable.insert {
|
||||||
|
it[url] = fetchedEpisode.url
|
||||||
|
it[name] = fetchedEpisode.name
|
||||||
|
it[date_upload] = fetchedEpisode.date_upload
|
||||||
|
it[episode_number] = fetchedEpisode.episode_number
|
||||||
|
it[scanlator] = fetchedEpisode.scanlator
|
||||||
|
|
||||||
|
it[episodeIndex] = index + 1
|
||||||
|
it[anime] = animeId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EpisodeTable.update({ EpisodeTable.url eq fetchedEpisode.url }) {
|
||||||
|
it[name] = fetchedEpisode.name
|
||||||
|
it[date_upload] = fetchedEpisode.date_upload
|
||||||
|
it[episode_number] = fetchedEpisode.episode_number
|
||||||
|
it[scanlator] = fetchedEpisode.scanlator
|
||||||
|
|
||||||
|
it[episodeIndex] = index + 1
|
||||||
|
it[anime] = animeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear any orphaned episodes that are in the db but not in `episodeList`
|
||||||
|
val dbEpisodeCount = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId }.count() }
|
||||||
|
if (dbEpisodeCount > episodeCount) { // we got some clean up due
|
||||||
|
val dbEpisodeList = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId } }
|
||||||
|
|
||||||
|
dbEpisodeList.forEach {
|
||||||
|
if (it[EpisodeTable.episodeIndex] >= episodeList.size ||
|
||||||
|
episodeList[it[EpisodeTable.episodeIndex] - 1].url != it[EpisodeTable.url]
|
||||||
|
) {
|
||||||
|
transaction {
|
||||||
|
// PageTable.deleteWhere { PageTable.episode eq it[EpisodeTable.id] }
|
||||||
|
EpisodeTable.deleteWhere { EpisodeTable.id eq it[EpisodeTable.id] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dbEpisodeMap = transaction {
|
||||||
|
EpisodeTable.select { EpisodeTable.anime eq animeId }
|
||||||
|
.associateBy({ it[EpisodeTable.url] }, { it })
|
||||||
|
}
|
||||||
|
|
||||||
|
return episodeList.mapIndexed { index, it ->
|
||||||
|
|
||||||
|
val dbEpisode = dbEpisodeMap.getValue(it.url)
|
||||||
|
|
||||||
|
EpisodeDataClass(
|
||||||
|
it.url,
|
||||||
|
it.name,
|
||||||
|
it.date_upload,
|
||||||
|
it.episode_number,
|
||||||
|
it.scanlator,
|
||||||
|
animeId,
|
||||||
|
|
||||||
|
dbEpisode[EpisodeTable.isRead],
|
||||||
|
dbEpisode[EpisodeTable.isBookmarked],
|
||||||
|
dbEpisode[EpisodeTable.lastPageRead],
|
||||||
|
|
||||||
|
episodeCount - index,
|
||||||
|
episodeList.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** used to display a episode, get a episode in order to show it's video */
|
||||||
|
suspend fun getEpisode(episodeIndex: Int, animeId: Int): EpisodeDataClass {
|
||||||
|
val episode = getEpisodeList(animeId, false)
|
||||||
|
.first { it.index == episodeIndex }
|
||||||
|
|
||||||
|
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
|
||||||
|
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
|
||||||
|
val fetchedLinkUrl = source.fetchEpisodeLink(
|
||||||
|
SEpisode.create().also {
|
||||||
|
it.url = episode.url
|
||||||
|
it.name = episode.name
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
|
return EpisodeDataClass(
|
||||||
|
episode.url,
|
||||||
|
episode.name,
|
||||||
|
episode.uploadDate,
|
||||||
|
episode.episodeNumber,
|
||||||
|
episode.scanlator,
|
||||||
|
animeId,
|
||||||
|
episode.read,
|
||||||
|
episode.bookmarked,
|
||||||
|
episode.lastPageRead,
|
||||||
|
episode.index,
|
||||||
|
episode.episodeCount,
|
||||||
|
fetchedLinkUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// /** used to display a episode, get a episode in order to show it's pages */
|
||||||
|
// suspend fun getEpisode(episodeIndex: Int, animeId: Int): EpisodeDataClass {
|
||||||
|
// val episodeEntry = transaction {
|
||||||
|
// EpisodeTable.select {
|
||||||
|
// (EpisodeTable.episodeIndex eq episodeIndex) and (EpisodeTable.anime eq animeId)
|
||||||
|
// }.first()
|
||||||
|
// }
|
||||||
|
// val animeEntry = transaction { MangaTable.select { MangaTable.id eq animeId }.first() }
|
||||||
|
// val source = getAnimeHttpSource(animeEntry[MangaTable.sourceReference])
|
||||||
|
//
|
||||||
|
// val pageList = source.fetchPageList(
|
||||||
|
// SEpisode.create().apply {
|
||||||
|
// url = episodeEntry[EpisodeTable.url]
|
||||||
|
// name = episodeEntry[EpisodeTable.name]
|
||||||
|
// }
|
||||||
|
// ).awaitSingle()
|
||||||
|
//
|
||||||
|
// val episodeId = episodeEntry[EpisodeTable.id].value
|
||||||
|
// val episodeCount = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId }.count() }
|
||||||
|
//
|
||||||
|
// // update page list for this episode
|
||||||
|
// transaction {
|
||||||
|
// pageList.forEach { page ->
|
||||||
|
// val pageEntry = transaction { PageTable.select { (PageTable.episode eq episodeId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||||
|
// if (pageEntry == null) {
|
||||||
|
// PageTable.insert {
|
||||||
|
// it[index] = page.index
|
||||||
|
// it[url] = page.url
|
||||||
|
// it[imageUrl] = page.imageUrl
|
||||||
|
// it[episode] = episodeId
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// PageTable.update({ (PageTable.episode eq episodeId) and (PageTable.index eq page.index) }) {
|
||||||
|
// it[url] = page.url
|
||||||
|
// it[imageUrl] = page.imageUrl
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return EpisodeDataClass(
|
||||||
|
// episodeEntry[EpisodeTable.url],
|
||||||
|
// episodeEntry[EpisodeTable.name],
|
||||||
|
// episodeEntry[EpisodeTable.date_upload],
|
||||||
|
// episodeEntry[EpisodeTable.episode_number],
|
||||||
|
// episodeEntry[EpisodeTable.scanlator],
|
||||||
|
// animeId,
|
||||||
|
// episodeEntry[EpisodeTable.isRead],
|
||||||
|
// episodeEntry[EpisodeTable.isBookmarked],
|
||||||
|
// episodeEntry[EpisodeTable.lastPageRead],
|
||||||
|
//
|
||||||
|
// episodeEntry[EpisodeTable.episodeIndex],
|
||||||
|
// episodeCount.toInt(),
|
||||||
|
// pageList.count()
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun modifyEpisode(animeId: Int, episodeIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
|
||||||
|
transaction {
|
||||||
|
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
|
||||||
|
EpisodeTable.update({ (EpisodeTable.anime eq animeId) and (EpisodeTable.episodeIndex eq episodeIndex) }) { update ->
|
||||||
|
isRead?.also {
|
||||||
|
update[EpisodeTable.isRead] = it
|
||||||
|
}
|
||||||
|
isBookmarked?.also {
|
||||||
|
update[EpisodeTable.isBookmarked] = it
|
||||||
|
}
|
||||||
|
lastPageRead?.also {
|
||||||
|
update[EpisodeTable.lastPageRead] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markPrevRead?.let {
|
||||||
|
EpisodeTable.update({ (EpisodeTable.anime eq animeId) and (EpisodeTable.episodeIndex less episodeIndex) }) {
|
||||||
|
it[EpisodeTable.isRead] = markPrevRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package suwayomi.anime.impl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.anime.impl.extension.Extension.getExtensionIconUrl
|
||||||
|
import suwayomi.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||||
|
import suwayomi.anime.model.dataclass.AnimeSourceDataClass
|
||||||
|
import suwayomi.anime.model.table.AnimeExtensionTable
|
||||||
|
import suwayomi.anime.model.table.AnimeSourceTable
|
||||||
|
|
||||||
|
object Source {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
fun getSourceList(): List<AnimeSourceDataClass> {
|
||||||
|
return transaction {
|
||||||
|
AnimeSourceTable.selectAll().map {
|
||||||
|
AnimeSourceDataClass(
|
||||||
|
it[AnimeSourceTable.id].value.toString(),
|
||||||
|
it[AnimeSourceTable.name],
|
||||||
|
it[AnimeSourceTable.lang],
|
||||||
|
getExtensionIconUrl(AnimeExtensionTable.select { AnimeExtensionTable.id eq it[AnimeSourceTable.extension] }.first()[AnimeExtensionTable.apkName]),
|
||||||
|
getAnimeHttpSource(it[AnimeSourceTable.id].value).supportsLatest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAnimeSource(sourceId: Long): AnimeSourceDataClass {
|
||||||
|
return transaction {
|
||||||
|
val source = AnimeSourceTable.select { AnimeSourceTable.id eq sourceId }.firstOrNull()
|
||||||
|
|
||||||
|
AnimeSourceDataClass(
|
||||||
|
sourceId.toString(),
|
||||||
|
source?.get(AnimeSourceTable.name),
|
||||||
|
source?.get(AnimeSourceTable.lang),
|
||||||
|
source?.let { AnimeExtensionTable.select { AnimeExtensionTable.id eq source[AnimeSourceTable.extension] }.first()[AnimeExtensionTable.iconUrl] },
|
||||||
|
source?.let { getAnimeHttpSource(sourceId).supportsLatest }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package suwayomi.anime.impl.extension
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import okhttp3.Request
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import suwayomi.anime.impl.extension.ExtensionsList.extensionTableAsDataClass
|
||||||
|
import suwayomi.anime.impl.extension.github.ExtensionGithubApi
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.EXTENSION_FEATURE
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MAX
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MIN
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.METADATA_NSFW
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.METADATA_SOURCE_CLASS
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.dex2jar
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.getPackageInfo
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.getSignatureHash
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.loadExtensionSources
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.trustedSignatures
|
||||||
|
import suwayomi.anime.model.table.AnimeExtensionTable
|
||||||
|
import suwayomi.anime.model.table.AnimeSourceTable
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import suwayomi.tachidesk.impl.util.network.await
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
object Extension {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
|
data class InstallableAPK(
|
||||||
|
val apkFilePath: String,
|
||||||
|
val pkgName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun installExtension(pkgName: String): Int {
|
||||||
|
logger.debug("Installing $pkgName")
|
||||||
|
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
|
||||||
|
|
||||||
|
return installAPK {
|
||||||
|
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
|
||||||
|
val apkName = Uri.parse(apkURL).lastPathSegment!!
|
||||||
|
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
||||||
|
// download apk file
|
||||||
|
downloadAPKFile(apkURL, apkSavePath)
|
||||||
|
|
||||||
|
apkSavePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun installAPK(fetcher: suspend () -> String): Int {
|
||||||
|
val apkFilePath = fetcher()
|
||||||
|
val apkName = File(apkFilePath).name
|
||||||
|
|
||||||
|
// check if we don't have the extension already installed
|
||||||
|
// if it's installed and we want to update, it first has to be uninstalled
|
||||||
|
val isInstalled = transaction {
|
||||||
|
AnimeExtensionTable.select { AnimeExtensionTable.apkName eq apkName }.firstOrNull()
|
||||||
|
}?.get(AnimeExtensionTable.isInstalled) ?: false
|
||||||
|
|
||||||
|
if (!isInstalled) {
|
||||||
|
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||||
|
|
||||||
|
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||||
|
val jarFilePath = "$dirPathWithoutType.jar"
|
||||||
|
val dexFilePath = "$dirPathWithoutType.dex"
|
||||||
|
|
||||||
|
val packageInfo = getPackageInfo(apkFilePath)
|
||||||
|
val pkgName = packageInfo.packageName
|
||||||
|
|
||||||
|
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
|
||||||
|
throw Exception("This apk is not a Tachiyomi extension")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate lib version
|
||||||
|
val libVersion = packageInfo.versionName.substringBeforeLast('.').toDouble()
|
||||||
|
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
||||||
|
throw Exception(
|
||||||
|
"Lib version is $libVersion, while only versions " +
|
||||||
|
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val signatureHash = getSignatureHash(packageInfo)
|
||||||
|
|
||||||
|
if (signatureHash == null) {
|
||||||
|
throw Exception("Package $pkgName isn't signed")
|
||||||
|
} else if (signatureHash !in trustedSignatures) {
|
||||||
|
// TODO: allow trusting keys
|
||||||
|
throw Exception("This apk is not a signed with the official tachiyomi signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
|
||||||
|
|
||||||
|
val className = packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
|
||||||
|
|
||||||
|
logger.debug("Main class for extension is $className")
|
||||||
|
|
||||||
|
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
// File(apkFilePath).delete()
|
||||||
|
File(dexFilePath).delete()
|
||||||
|
|
||||||
|
// collect sources from the extension
|
||||||
|
val sources: List<AnimeCatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) {
|
||||||
|
is AnimeSource -> listOf(instance)
|
||||||
|
is AnimeSourceFactory -> instance.createSources()
|
||||||
|
else -> throw RuntimeException("Unknown source class type! ${instance.javaClass}")
|
||||||
|
}.map { it as AnimeCatalogueSource }
|
||||||
|
|
||||||
|
val langs = sources.map { it.lang }.toSet()
|
||||||
|
val extensionLang = when (langs.size) {
|
||||||
|
0 -> ""
|
||||||
|
1 -> langs.first()
|
||||||
|
else -> "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
|
||||||
|
|
||||||
|
// update extension info
|
||||||
|
transaction {
|
||||||
|
if (AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
|
||||||
|
AnimeExtensionTable.insert {
|
||||||
|
it[this.apkName] = apkName
|
||||||
|
it[name] = extensionName
|
||||||
|
it[this.pkgName] = packageInfo.packageName
|
||||||
|
it[versionName] = packageInfo.versionName
|
||||||
|
it[versionCode] = packageInfo.versionCode
|
||||||
|
it[lang] = extensionLang
|
||||||
|
it[this.isNsfw] = isNsfw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq pkgName }) {
|
||||||
|
it[this.isInstalled] = true
|
||||||
|
it[this.classFQName] = className
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionId = AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.first()[AnimeExtensionTable.id].value
|
||||||
|
|
||||||
|
sources.forEach { httpSource ->
|
||||||
|
AnimeSourceTable.insert {
|
||||||
|
it[id] = httpSource.id
|
||||||
|
it[name] = httpSource.name
|
||||||
|
it[lang] = httpSource.lang
|
||||||
|
it[extension] = extensionId
|
||||||
|
}
|
||||||
|
logger.debug("Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 201 // we installed successfully
|
||||||
|
} else {
|
||||||
|
return 302 // extension was already installed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
private suspend fun downloadAPKFile(url: String, savePath: String) {
|
||||||
|
val request = Request.Builder().url(url).build()
|
||||||
|
val response = network.client.newCall(request).await()
|
||||||
|
|
||||||
|
val downloadedFile = File(savePath)
|
||||||
|
downloadedFile.sink().buffer().use { sink ->
|
||||||
|
response.body!!.source().use { source ->
|
||||||
|
sink.writeAll(source)
|
||||||
|
sink.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallExtension(pkgName: String) {
|
||||||
|
logger.debug("Uninstalling $pkgName")
|
||||||
|
|
||||||
|
val extensionRecord = transaction { AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.first() }
|
||||||
|
val fileNameWithoutType = extensionRecord[AnimeExtensionTable.apkName].substringBefore(".apk")
|
||||||
|
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
|
transaction {
|
||||||
|
val extensionId = extensionRecord[AnimeExtensionTable.id].value
|
||||||
|
|
||||||
|
AnimeSourceTable.deleteWhere { AnimeSourceTable.extension eq extensionId }
|
||||||
|
if (extensionRecord[AnimeExtensionTable.isObsolete])
|
||||||
|
AnimeExtensionTable.deleteWhere { AnimeExtensionTable.pkgName eq pkgName }
|
||||||
|
else
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq pkgName }) {
|
||||||
|
it[isInstalled] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File(jarPath).exists()) {
|
||||||
|
File(jarPath).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateExtension(pkgName: String): Int {
|
||||||
|
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
|
||||||
|
uninstallExtension(pkgName)
|
||||||
|
transaction {
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq pkgName }) {
|
||||||
|
it[name] = targetExtension.name
|
||||||
|
it[versionName] = targetExtension.versionName
|
||||||
|
it[versionCode] = targetExtension.versionCode
|
||||||
|
it[lang] = targetExtension.lang
|
||||||
|
it[isNsfw] = targetExtension.isNsfw
|
||||||
|
it[apkName] = targetExtension.apkName
|
||||||
|
it[iconUrl] = targetExtension.iconUrl
|
||||||
|
it[hasUpdate] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return installExtension(pkgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||||
|
val iconUrl = transaction { AnimeExtensionTable.select { AnimeExtensionTable.apkName eq apkName }.first() }[AnimeExtensionTable.iconUrl]
|
||||||
|
|
||||||
|
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||||
|
|
||||||
|
return getCachedImageResponse(saveDir, apkName) {
|
||||||
|
network.client.newCall(
|
||||||
|
GET(iconUrl)
|
||||||
|
).await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtensionIconUrl(apkName: String): String {
|
||||||
|
return "/api/v1/anime/extension/icon/$apkName"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package suwayomi.anime.impl.extension
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.anime.impl.extension.Extension.getExtensionIconUrl
|
||||||
|
import suwayomi.anime.impl.extension.github.ExtensionGithubApi
|
||||||
|
import suwayomi.anime.impl.extension.github.OnlineExtension
|
||||||
|
import suwayomi.anime.model.dataclass.AnimeExtensionDataClass
|
||||||
|
import suwayomi.anime.model.table.AnimeExtensionTable
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
object ExtensionsList {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
var lastUpdateCheck: Long = 0
|
||||||
|
var updateMap = ConcurrentHashMap<String, OnlineExtension>()
|
||||||
|
|
||||||
|
/** 60,000 milliseconds = 60 seconds */
|
||||||
|
private const val ExtensionUpdateDelayTime = 60 * 1000
|
||||||
|
|
||||||
|
suspend fun getExtensionList(): List<AnimeExtensionDataClass> {
|
||||||
|
// update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty
|
||||||
|
if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) {
|
||||||
|
logger.debug("Getting extensions list from the internet")
|
||||||
|
lastUpdateCheck = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val foundExtensions = ExtensionGithubApi.findExtensions()
|
||||||
|
updateExtensionDatabase(foundExtensions)
|
||||||
|
} else {
|
||||||
|
logger.debug("used cached extension list")
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensionTableAsDataClass()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extensionTableAsDataClass() = transaction {
|
||||||
|
AnimeExtensionTable.selectAll().map {
|
||||||
|
AnimeExtensionDataClass(
|
||||||
|
it[AnimeExtensionTable.apkName],
|
||||||
|
getExtensionIconUrl(it[AnimeExtensionTable.apkName]),
|
||||||
|
it[AnimeExtensionTable.name],
|
||||||
|
it[AnimeExtensionTable.pkgName],
|
||||||
|
it[AnimeExtensionTable.versionName],
|
||||||
|
it[AnimeExtensionTable.versionCode],
|
||||||
|
it[AnimeExtensionTable.lang],
|
||||||
|
it[AnimeExtensionTable.isNsfw],
|
||||||
|
it[AnimeExtensionTable.isInstalled],
|
||||||
|
it[AnimeExtensionTable.hasUpdate],
|
||||||
|
it[AnimeExtensionTable.isObsolete],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
|
||||||
|
transaction {
|
||||||
|
foundExtensions.forEach { foundExtension ->
|
||||||
|
val extensionRecord = AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
|
||||||
|
if (extensionRecord != null) {
|
||||||
|
if (extensionRecord[AnimeExtensionTable.isInstalled]) {
|
||||||
|
when {
|
||||||
|
foundExtension.versionCode > extensionRecord[AnimeExtensionTable.versionCode] -> {
|
||||||
|
// there is an update
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
|
||||||
|
it[hasUpdate] = true
|
||||||
|
}
|
||||||
|
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
|
||||||
|
}
|
||||||
|
foundExtension.versionCode < extensionRecord[AnimeExtensionTable.versionCode] -> {
|
||||||
|
// some how the user installed an invalid version
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
|
||||||
|
it[isObsolete] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// extension is not installed so we can overwrite the data without a care
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
|
||||||
|
it[name] = foundExtension.name
|
||||||
|
it[versionName] = foundExtension.versionName
|
||||||
|
it[versionCode] = foundExtension.versionCode
|
||||||
|
it[lang] = foundExtension.lang
|
||||||
|
it[isNsfw] = foundExtension.isNsfw
|
||||||
|
it[apkName] = foundExtension.apkName
|
||||||
|
it[iconUrl] = foundExtension.iconUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// insert new record
|
||||||
|
AnimeExtensionTable.insert {
|
||||||
|
it[name] = foundExtension.name
|
||||||
|
it[pkgName] = foundExtension.pkgName
|
||||||
|
it[versionName] = foundExtension.versionName
|
||||||
|
it[versionCode] = foundExtension.versionCode
|
||||||
|
it[lang] = foundExtension.lang
|
||||||
|
it[isNsfw] = foundExtension.isNsfw
|
||||||
|
it[apkName] = foundExtension.apkName
|
||||||
|
it[iconUrl] = foundExtension.iconUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deal with obsolete extensions
|
||||||
|
AnimeExtensionTable.selectAll().forEach { extensionRecord ->
|
||||||
|
val foundExtension = foundExtensions.find { it.pkgName == extensionRecord[AnimeExtensionTable.pkgName] }
|
||||||
|
if (foundExtension == null) {
|
||||||
|
// not in the repo, so this extensions is obsolete
|
||||||
|
if (extensionRecord[AnimeExtensionTable.isInstalled]) {
|
||||||
|
// is installed so we should mark it as obsolete
|
||||||
|
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq extensionRecord[AnimeExtensionTable.pkgName] }) {
|
||||||
|
it[isObsolete] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// is not installed so we can remove the record without a care
|
||||||
|
AnimeExtensionTable.deleteWhere { AnimeExtensionTable.pkgName eq extensionRecord[AnimeExtensionTable.pkgName] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package suwayomi.anime.impl.extension.github
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.int
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import okhttp3.Request
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MAX
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MIN
|
||||||
|
import suwayomi.anime.model.dataclass.AnimeExtensionDataClass
|
||||||
|
import suwayomi.tachidesk.impl.util.network.UnzippingInterceptor
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
object ExtensionGithubApi {
|
||||||
|
const val BASE_URL = "https://raw.githubusercontent.com"
|
||||||
|
const val REPO_URL_PREFIX = "$BASE_URL/jmir1/tachiyomi-extensions/repo"
|
||||||
|
|
||||||
|
private fun parseResponse(json: JsonArray): List<OnlineExtension> {
|
||||||
|
return json
|
||||||
|
.map { it.asJsonObject }
|
||||||
|
.filter { element ->
|
||||||
|
val versionName = element["version"].string
|
||||||
|
val libVersion = versionName.substringBeforeLast('.').toInt()
|
||||||
|
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
|
||||||
|
}
|
||||||
|
.map { element ->
|
||||||
|
val name = element["name"].string.substringAfter("Tachiyomi: ")
|
||||||
|
val pkgName = element["pkg"].string
|
||||||
|
val apkName = element["apk"].string
|
||||||
|
val versionName = element["version"].string
|
||||||
|
val versionCode = element["code"].int
|
||||||
|
val lang = element["lang"].string
|
||||||
|
val nsfw = element["nsfw"].int == 1
|
||||||
|
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
||||||
|
|
||||||
|
OnlineExtension(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findExtensions(): List<OnlineExtension> {
|
||||||
|
val response = getRepo()
|
||||||
|
return parseResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getApkUrl(extension: AnimeExtensionDataClass): String {
|
||||||
|
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val client by lazy {
|
||||||
|
val network: NetworkHelper by injectLazy()
|
||||||
|
network.client.newBuilder()
|
||||||
|
.addNetworkInterceptor { chain ->
|
||||||
|
val originalResponse = chain.proceed(chain.request())
|
||||||
|
originalResponse.newBuilder()
|
||||||
|
.header("Content-Encoding", "gzip")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.addInterceptor(UnzippingInterceptor())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRepo(): com.google.gson.JsonArray {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$REPO_URL_PREFIX/index.json.gz")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute().use { response -> response.body!!.string() }
|
||||||
|
return JsonParser.parseString(response).asJsonArray
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package suwayomi.anime.impl.extension.github
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
data class OnlineExtension(
|
||||||
|
val name: String,
|
||||||
|
val pkgName: String,
|
||||||
|
val versionName: String,
|
||||||
|
val versionCode: Int,
|
||||||
|
val lang: String,
|
||||||
|
val isNsfw: Boolean,
|
||||||
|
val apkName: String,
|
||||||
|
val iconUrl: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package suwayomi.anime.impl.util
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import suwayomi.anime.impl.util.PackageTools.loadExtensionSources
|
||||||
|
import suwayomi.anime.model.table.AnimeExtensionTable
|
||||||
|
import suwayomi.anime.model.table.AnimeSourceTable
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
object GetAnimeHttpSource {
|
||||||
|
private val sourceCache = ConcurrentHashMap<Long, AnimeHttpSource>()
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
|
fun getAnimeHttpSource(sourceId: Long): AnimeHttpSource {
|
||||||
|
val cachedResult: AnimeHttpSource? = sourceCache[sourceId]
|
||||||
|
if (cachedResult != null) {
|
||||||
|
return cachedResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceRecord = transaction {
|
||||||
|
AnimeSourceTable.select { AnimeSourceTable.id eq sourceId }.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
val extensionId = sourceRecord[AnimeSourceTable.extension]
|
||||||
|
val extensionRecord = transaction {
|
||||||
|
AnimeExtensionTable.select { AnimeExtensionTable.id eq extensionId }.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
val apkName = extensionRecord[AnimeExtensionTable.apkName]
|
||||||
|
val className = extensionRecord[AnimeExtensionTable.classFQName]
|
||||||
|
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||||
|
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||||
|
|
||||||
|
when (val instance = loadExtensionSources(jarPath, className)) {
|
||||||
|
is AnimeSource -> listOf(instance)
|
||||||
|
is AnimeSourceFactory -> instance.createSources()
|
||||||
|
else -> throw Exception("Unknown source class type! ${instance.javaClass}")
|
||||||
|
}.forEach {
|
||||||
|
sourceCache[it.id] = it as AnimeHttpSource
|
||||||
|
}
|
||||||
|
return sourceCache[sourceId]!!
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package suwayomi.anime.impl.util
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.Signature
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.googlecode.d2j.dex.Dex2jar
|
||||||
|
import com.googlecode.d2j.reader.MultiDexFileReader
|
||||||
|
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
|
||||||
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import net.dongliu.apk.parser.ApkFile
|
||||||
|
import net.dongliu.apk.parser.ApkParsers
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
import org.w3c.dom.Node
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
|
||||||
|
import xyz.nulldev.androidcompat.pm.toPackageInfo
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLClassLoader
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
|
||||||
|
object PackageTools {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
|
||||||
|
const val EXTENSION_FEATURE = "tachiyomi.animeextension"
|
||||||
|
const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
|
||||||
|
const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
|
||||||
|
const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
|
||||||
|
const val LIB_VERSION_MIN = 10
|
||||||
|
const val LIB_VERSION_MAX = 10
|
||||||
|
|
||||||
|
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key
|
||||||
|
var trustedSignatures = mutableSetOf<String>() + officialSignature
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert dex to jar, a wrapper for the dex2jar library
|
||||||
|
*/
|
||||||
|
fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
|
||||||
|
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
|
||||||
|
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
|
||||||
|
|
||||||
|
val jarFilePath = File(jarFile).toPath()
|
||||||
|
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
|
||||||
|
val handler = BaksmaliBaseDexExceptionHandler()
|
||||||
|
Dex2jar
|
||||||
|
.from(reader)
|
||||||
|
.withExceptionHandler(handler)
|
||||||
|
.reUseReg(false)
|
||||||
|
.topoLogicalSort()
|
||||||
|
.skipDebug(true)
|
||||||
|
.optimizeSynchronized(false)
|
||||||
|
.printIR(false)
|
||||||
|
.noCode(false)
|
||||||
|
.skipExceptions(false)
|
||||||
|
.to(jarFilePath)
|
||||||
|
if (handler.hasException()) {
|
||||||
|
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
|
||||||
|
logger.error(
|
||||||
|
"""
|
||||||
|
Detail Error Information in File $errorFile
|
||||||
|
Please report this file to one of following link if possible (any one).
|
||||||
|
https://sourceforge.net/p/dex2jar/tickets/
|
||||||
|
https://bitbucket.org/pxb1988/dex2jar/issues
|
||||||
|
https://github.com/pxb1988/dex2jar/issues
|
||||||
|
dex2jar@googlegroups.com
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
handler.dump(errorFile, emptyArray<String>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A modified version of `xyz.nulldev.androidcompat.pm.InstalledPackage.info` */
|
||||||
|
fun getPackageInfo(apkFilePath: String): PackageInfo {
|
||||||
|
val apk = File(apkFilePath)
|
||||||
|
return ApkParsers.getMetaInfo(apk).toPackageInfo(apk).apply {
|
||||||
|
val parsed = ApkFile(apk)
|
||||||
|
val dbFactory = DocumentBuilderFactory.newInstance()
|
||||||
|
val dBuilder = dbFactory.newDocumentBuilder()
|
||||||
|
val doc = parsed.manifestXml.byteInputStream().use {
|
||||||
|
dBuilder.parse(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(parsed.manifestXml)
|
||||||
|
|
||||||
|
applicationInfo.metaData = Bundle().apply {
|
||||||
|
val appTag = doc.getElementsByTagName("application").item(0)
|
||||||
|
|
||||||
|
appTag?.childNodes?.toList()
|
||||||
|
.orEmpty()
|
||||||
|
.asSequence()
|
||||||
|
.filter {
|
||||||
|
it.nodeType == Node.ELEMENT_NODE
|
||||||
|
}.map {
|
||||||
|
it as Element
|
||||||
|
}.filter {
|
||||||
|
it.tagName == "meta-data"
|
||||||
|
}.forEach {
|
||||||
|
putString(
|
||||||
|
it.attributes.getNamedItem("android:name").nodeValue,
|
||||||
|
it.attributes.getNamedItem("android:value").nodeValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signatures = (
|
||||||
|
parsed.apkSingers.flatMap { it.certificateMetas }
|
||||||
|
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
|
||||||
|
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
|
||||||
|
.map { Signature(it.data) }.toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
||||||
|
val signatures = pkgInfo.signatures
|
||||||
|
return if (signatures != null && signatures.isNotEmpty()) {
|
||||||
|
Hash.sha256(signatures.first().toByteArray())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loads the extension main class called $className from the jar located at $jarPath
|
||||||
|
* It may return an instance of HttpSource or SourceFactory depending on the extension.
|
||||||
|
*/
|
||||||
|
fun loadExtensionSources(jarPath: String, className: String): Any {
|
||||||
|
val classLoader = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")))
|
||||||
|
val classToLoad = Class.forName(className, false, classLoader)
|
||||||
|
return classToLoad.getDeclaredConstructor().newInstance()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package suwayomi.anime.model.dataclass
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import suwayomi.anime.model.table.AnimeStatus
|
||||||
|
|
||||||
|
data class AnimeDataClass(
|
||||||
|
val id: Int,
|
||||||
|
val sourceId: String,
|
||||||
|
|
||||||
|
val url: String,
|
||||||
|
val title: String,
|
||||||
|
val thumbnailUrl: String? = null,
|
||||||
|
|
||||||
|
val initialized: Boolean = false,
|
||||||
|
|
||||||
|
val artist: String? = null,
|
||||||
|
val author: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val genre: String? = null,
|
||||||
|
val status: String = AnimeStatus.UNKNOWN.name,
|
||||||
|
val inLibrary: Boolean = false,
|
||||||
|
val source: AnimeSourceDataClass? = null,
|
||||||
|
|
||||||
|
val freshData: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PagedAnimeListDataClass(
|
||||||
|
val mangaList: List<AnimeDataClass>,
|
||||||
|
val hasNextPage: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package suwayomi.anime.model.dataclass
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
data class AnimeExtensionDataClass(
|
||||||
|
val apkName: String,
|
||||||
|
val iconUrl: String,
|
||||||
|
|
||||||
|
val name: String,
|
||||||
|
val pkgName: String,
|
||||||
|
val versionName: String,
|
||||||
|
val versionCode: Int,
|
||||||
|
val lang: String,
|
||||||
|
val isNsfw: Boolean,
|
||||||
|
|
||||||
|
val installed: Boolean,
|
||||||
|
val hasUpdate: Boolean,
|
||||||
|
val obsolete: Boolean,
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package suwayomi.anime.model.dataclass
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
data class AnimeSourceDataClass(
|
||||||
|
val id: String,
|
||||||
|
val name: String?,
|
||||||
|
val lang: String?,
|
||||||
|
val iconUrl: String?,
|
||||||
|
val supportsLatest: Boolean?
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package suwayomi.anime.model.dataclass
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
data class EpisodeDataClass(
|
||||||
|
val url: String,
|
||||||
|
val name: String,
|
||||||
|
val uploadDate: Long,
|
||||||
|
val episodeNumber: Float,
|
||||||
|
val scanlator: String?,
|
||||||
|
val animeId: Int,
|
||||||
|
|
||||||
|
/** chapter is read */
|
||||||
|
val read: Boolean,
|
||||||
|
|
||||||
|
/** chapter is bookmarked */
|
||||||
|
val bookmarked: Boolean,
|
||||||
|
|
||||||
|
/** last read page, zero means not read/no data */
|
||||||
|
val lastPageRead: Int,
|
||||||
|
|
||||||
|
/** this chapter's index, starts with 1 */
|
||||||
|
val index: Int,
|
||||||
|
|
||||||
|
/** total episode count, used to calculate if there's a next and prev episode */
|
||||||
|
val episodeCount: Int? = null,
|
||||||
|
|
||||||
|
/** used to construct pages in the front-end */
|
||||||
|
val linkUrl: String? = null,
|
||||||
|
)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package suwayomi.anime.model.table
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
|
object AnimeExtensionTable : IntIdTable() {
|
||||||
|
val apkName = varchar("apk_name", 1024)
|
||||||
|
|
||||||
|
// default is the local source icon from tachiyomi
|
||||||
|
val iconUrl = varchar("icon_url", 2048)
|
||||||
|
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||||
|
|
||||||
|
val name = varchar("name", 128)
|
||||||
|
val pkgName = varchar("pkg_name", 128)
|
||||||
|
val versionName = varchar("version_name", 16)
|
||||||
|
val versionCode = integer("version_code")
|
||||||
|
val lang = varchar("lang", 10)
|
||||||
|
val isNsfw = bool("is_nsfw")
|
||||||
|
|
||||||
|
val isInstalled = bool("is_installed").default(false)
|
||||||
|
val hasUpdate = bool("has_update").default(false)
|
||||||
|
val isObsolete = bool("is_obsolete").default(false)
|
||||||
|
|
||||||
|
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package suwayomi.anime.model.table
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.id.IdTable
|
||||||
|
|
||||||
|
object AnimeSourceTable : IdTable<Long>() {
|
||||||
|
override val id = long("id").entityId()
|
||||||
|
val name = varchar("name", 128)
|
||||||
|
val lang = varchar("lang", 10)
|
||||||
|
val extension = reference("extension", AnimeExtensionTable)
|
||||||
|
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package suwayomi.anime.model.table
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
import suwayomi.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||||
|
import suwayomi.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.MangaStatus.Companion
|
||||||
|
|
||||||
|
object AnimeTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val title = varchar("title", 512)
|
||||||
|
val initialized = bool("initialized").default(false)
|
||||||
|
|
||||||
|
val artist = varchar("artist", 64).nullable()
|
||||||
|
val author = varchar("author", 64).nullable()
|
||||||
|
val description = varchar("description", 4096).nullable()
|
||||||
|
val genre = varchar("genre", 1024).nullable()
|
||||||
|
|
||||||
|
val status = integer("status").default(SAnime.UNKNOWN)
|
||||||
|
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
|
val inLibrary = bool("in_library").default(false)
|
||||||
|
val defaultCategory = bool("default_category").default(true)
|
||||||
|
|
||||||
|
// source is used by some ancestor of IntIdTable
|
||||||
|
val sourceReference = long("source")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AnimeTable.toDataClass(mangaEntry: ResultRow) =
|
||||||
|
MangaDataClass(
|
||||||
|
mangaEntry[this.id].value,
|
||||||
|
mangaEntry[sourceReference].toString(),
|
||||||
|
|
||||||
|
mangaEntry[url],
|
||||||
|
mangaEntry[title],
|
||||||
|
proxyThumbnailUrl(mangaEntry[this.id].value),
|
||||||
|
|
||||||
|
mangaEntry[initialized],
|
||||||
|
|
||||||
|
mangaEntry[artist],
|
||||||
|
mangaEntry[author],
|
||||||
|
mangaEntry[description],
|
||||||
|
mangaEntry[genre],
|
||||||
|
Companion.valueOf(mangaEntry[status]).name,
|
||||||
|
mangaEntry[inLibrary]
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class AnimeStatus(val status: Int) {
|
||||||
|
UNKNOWN(0),
|
||||||
|
ONGOING(1),
|
||||||
|
COMPLETED(2),
|
||||||
|
LICENSED(3);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun valueOf(value: Int): AnimeStatus = values().find { it.status == value } ?: UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package suwayomi.anime.model.table
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.anime.model.dataclass.EpisodeDataClass
|
||||||
|
|
||||||
|
object EpisodeTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val name = varchar("name", 512)
|
||||||
|
val date_upload = long("date_upload").default(0)
|
||||||
|
val episode_number = float("episode_number").default(-1f)
|
||||||
|
val scanlator = varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
|
val isRead = bool("read").default(false)
|
||||||
|
val isBookmarked = bool("bookmark").default(false)
|
||||||
|
val lastPageRead = integer("last_page_read").default(0)
|
||||||
|
|
||||||
|
// index is reserved by a function
|
||||||
|
val episodeIndex = integer("index")
|
||||||
|
|
||||||
|
val anime = reference("anime", AnimeTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EpisodeTable.toDataClass(episodeEntry: ResultRow) =
|
||||||
|
EpisodeDataClass(
|
||||||
|
episodeEntry[url],
|
||||||
|
episodeEntry[name],
|
||||||
|
episodeEntry[date_upload],
|
||||||
|
episodeEntry[episode_number],
|
||||||
|
episodeEntry[scanlator],
|
||||||
|
episodeEntry[anime].value,
|
||||||
|
episodeEntry[isRead],
|
||||||
|
episodeEntry[isBookmarked],
|
||||||
|
episodeEntry[lastPageRead],
|
||||||
|
episodeEntry[episodeIndex],
|
||||||
|
transaction { EpisodeTable.select { anime eq episodeEntry[anime] }.count().toInt() }
|
||||||
|
)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package suwayomi.server
|
||||||
|
|
||||||
|
import io.javalin.Javalin
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.future.future
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import suwayomi.anime.AnimeAPI
|
||||||
|
import suwayomi.server.util.Browser
|
||||||
|
import suwayomi.tachidesk.TachideskAPI
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
object JavalinSetup {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
|
||||||
|
return scope.future(block = block)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun javalinSetup() {
|
||||||
|
var hasWebUiBundled = false
|
||||||
|
|
||||||
|
val app = Javalin.create { config ->
|
||||||
|
try {
|
||||||
|
// if the bellow line throws an exception then webUI is not bundled
|
||||||
|
this::class.java.getResource("/react/index.html")
|
||||||
|
|
||||||
|
// no exception so we can tell javalin to serve webUI
|
||||||
|
hasWebUiBundled = true
|
||||||
|
config.addStaticFiles("/react")
|
||||||
|
config.addSinglePageRoot("/", "/react/index.html")
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
logger.warn("react build files are missing.")
|
||||||
|
hasWebUiBundled = false
|
||||||
|
}
|
||||||
|
config.enableCorsForAllOrigins()
|
||||||
|
}.events { event ->
|
||||||
|
event.serverStarted {
|
||||||
|
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
||||||
|
Browser.openInBrowser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start(serverConfig.ip, serverConfig.port)
|
||||||
|
|
||||||
|
// when JVM is prompted to shutdown, stop javalin gracefully
|
||||||
|
Runtime.getRuntime().addShutdownHook(
|
||||||
|
thread(start = false) {
|
||||||
|
app.stop()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app.exception(NullPointerException::class.java) { e, ctx ->
|
||||||
|
logger.error("NullPointerException while handling the request", e)
|
||||||
|
ctx.status(404)
|
||||||
|
}
|
||||||
|
app.exception(NoSuchElementException::class.java) { e, ctx ->
|
||||||
|
logger.error("NoSuchElementException while handling the request", e)
|
||||||
|
ctx.status(404)
|
||||||
|
}
|
||||||
|
app.exception(IOException::class.java) { e, ctx ->
|
||||||
|
logger.error("IOException while handling the request", e)
|
||||||
|
ctx.status(500)
|
||||||
|
ctx.result(e.message ?: "Internal Server Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
TachideskAPI.defineEndpoints(app)
|
||||||
|
AnimeAPI.defineEndpoints(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.server
|
package suwayomi.server
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+9
-7
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.server
|
package suwayomi.server
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,14 +8,14 @@ package ir.armor.tachidesk.server
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import ir.armor.tachidesk.model.database.databaseUp
|
|
||||||
import ir.armor.tachidesk.server.util.AppMutex.handleAppMutex
|
|
||||||
import ir.armor.tachidesk.server.util.systemTray
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.bind
|
import org.kodein.di.bind
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.singleton
|
import org.kodein.di.singleton
|
||||||
|
import suwayomi.server.database.databaseUp
|
||||||
|
import suwayomi.server.util.AppMutex.handleAppMutex
|
||||||
|
import suwayomi.server.util.SystemTray.systemTray
|
||||||
import xyz.nulldev.androidcompat.AndroidCompat
|
import xyz.nulldev.androidcompat.AndroidCompat
|
||||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||||
import xyz.nulldev.ts.config.ApplicationRootDir
|
import xyz.nulldev.ts.config.ApplicationRootDir
|
||||||
@@ -29,7 +29,8 @@ class ApplicationDirs(
|
|||||||
val dataRoot: String = ApplicationRootDir
|
val dataRoot: String = ApplicationRootDir
|
||||||
) {
|
) {
|
||||||
val extensionsRoot = "$dataRoot/extensions"
|
val extensionsRoot = "$dataRoot/extensions"
|
||||||
val thumbnailsRoot = "$dataRoot/thumbnails"
|
val mangaThumbnailsRoot = "$dataRoot/manga-thumbnails"
|
||||||
|
val animeThumbnailsRoot = "$dataRoot/anime-thumbnails"
|
||||||
val mangaRoot = "$dataRoot/manga"
|
val mangaRoot = "$dataRoot/manga"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ val systemTrayInstance by lazy { systemTray() }
|
|||||||
val androidCompat by lazy { AndroidCompat() }
|
val androidCompat by lazy { AndroidCompat() }
|
||||||
|
|
||||||
fun applicationSetup() {
|
fun applicationSetup() {
|
||||||
logger.info("Running Tachidesk ${BuildConfig.version} revision ${BuildConfig.revision}")
|
logger.info("Running Tachidesk ${BuildConfig.VERSION} revision ${BuildConfig.REVISION}")
|
||||||
|
|
||||||
// Application dirs
|
// Application dirs
|
||||||
val applicationDirs = ApplicationDirs()
|
val applicationDirs = ApplicationDirs()
|
||||||
@@ -55,7 +56,8 @@ fun applicationSetup() {
|
|||||||
applicationDirs.dataRoot,
|
applicationDirs.dataRoot,
|
||||||
applicationDirs.extensionsRoot,
|
applicationDirs.extensionsRoot,
|
||||||
applicationDirs.extensionsRoot + "/icon",
|
applicationDirs.extensionsRoot + "/icon",
|
||||||
applicationDirs.thumbnailsRoot
|
applicationDirs.mangaThumbnailsRoot,
|
||||||
|
applicationDirs.animeThumbnailsRoot,
|
||||||
).forEach {
|
).forEach {
|
||||||
File(it).mkdirs()
|
File(it).mkdirs()
|
||||||
}
|
}
|
||||||
+5
-5
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package suwayomi.server.database
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,13 +7,13 @@ package ir.armor.tachidesk.model.database
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.model.database.migration.lib.loadMigrationsFrom
|
|
||||||
import ir.armor.tachidesk.model.database.migration.lib.runMigrations
|
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import suwayomi.server.database.migration.lib.loadMigrationsFrom
|
||||||
|
import suwayomi.server.database.migration.lib.runMigrations
|
||||||
|
|
||||||
object DBManager {
|
object DBManager {
|
||||||
val db by lazy {
|
val db by lazy {
|
||||||
@@ -27,6 +27,6 @@ fun databaseUp() {
|
|||||||
val db = DBManager.db
|
val db = DBManager.db
|
||||||
db.useNestedTransactions = true
|
db.useNestedTransactions = true
|
||||||
|
|
||||||
val migrations = loadMigrationsFrom("ir.armor.tachidesk.model.database.migration")
|
val migrations = loadMigrationsFrom("suwayomi.server.database.migration")
|
||||||
runMigrations(migrations)
|
runMigrations(migrations)
|
||||||
}
|
}
|
||||||
+6
-6
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,11 +8,11 @@ package ir.armor.tachidesk.model.database.migration
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.model.database.migration.lib.Migration
|
|
||||||
import org.jetbrains.exposed.dao.id.IdTable
|
import org.jetbrains.exposed.dao.id.IdTable
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
@Suppress("ClassName", "unused")
|
@Suppress("ClassName", "unused")
|
||||||
class M0001_Initial : Migration() {
|
class M0001_Initial : Migration() {
|
||||||
@@ -104,10 +104,10 @@ class M0001_Initial : Migration() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CategoryMangaTable : IntIdTable() {
|
private class CategoryMangaTable(categoryTable: CategoryTable, mangaTable: MangaTable) : IntIdTable() {
|
||||||
init {
|
init {
|
||||||
reference("category", ir.armor.tachidesk.model.database.table.CategoryTable)
|
reference("category", categoryTable)
|
||||||
reference("manga", ir.armor.tachidesk.model.database.table.MangaTable)
|
reference("manga", mangaTable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ class M0001_Initial : Migration() {
|
|||||||
val chapterTable = ChapterTable(mangaTable)
|
val chapterTable = ChapterTable(mangaTable)
|
||||||
val pageTable = PageTable(chapterTable)
|
val pageTable = PageTable(chapterTable)
|
||||||
val categoryTable = CategoryTable()
|
val categoryTable = CategoryTable()
|
||||||
val categoryMangaTable = CategoryMangaTable()
|
val categoryMangaTable = CategoryMangaTable(categoryTable, mangaTable)
|
||||||
SchemaUtils.create(
|
SchemaUtils.create(
|
||||||
extensionTable,
|
extensionTable,
|
||||||
sourceTable,
|
sourceTable,
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,9 +7,9 @@ package ir.armor.tachidesk.model.database.migration
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.model.database.migration.lib.Migration
|
|
||||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
import org.jetbrains.exposed.sql.vendors.currentDialect
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
@Suppress("ClassName", "unused")
|
@Suppress("ClassName", "unused")
|
||||||
class M0002_ChapterTableIndexRename : Migration() {
|
class M0002_ChapterTableIndexRename : Migration() {
|
||||||
+6
-6
@@ -1,16 +1,16 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
import ir.armor.tachidesk.model.database.migration.lib.Migration
|
|
||||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
|
||||||
import org.jetbrains.exposed.sql.vendors.currentDialect
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
@Suppress("ClassName", "unused")
|
@Suppress("ClassName", "unused")
|
||||||
class M0003_DefaultCategory : Migration() {
|
class M0003_DefaultCategory : Migration() {
|
||||||
/** this migration renamed CategoryTable.IS_LANDING to ChapterTable.IS_DEFAULT */
|
/** this migration renamed CategoryTable.IS_LANDING to ChapterTable.IS_DEFAULT */
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.id.IdTable
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
class M0004_AnimeTablesBatch1 : Migration() {
|
||||||
|
private object AnimeExtensionTable : IntIdTable() {
|
||||||
|
val apkName = varchar("apk_name", 1024)
|
||||||
|
|
||||||
|
// default is the local source icon from tachiyomi
|
||||||
|
val iconUrl = varchar("icon_url", 2048)
|
||||||
|
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||||
|
|
||||||
|
val name = varchar("name", 128)
|
||||||
|
val pkgName = varchar("pkg_name", 128)
|
||||||
|
val versionName = varchar("version_name", 16)
|
||||||
|
val versionCode = integer("version_code")
|
||||||
|
val lang = varchar("lang", 10)
|
||||||
|
val isNsfw = bool("is_nsfw")
|
||||||
|
|
||||||
|
val isInstalled = bool("is_installed").default(false)
|
||||||
|
val hasUpdate = bool("has_update").default(false)
|
||||||
|
val isObsolete = bool("is_obsolete").default(false)
|
||||||
|
|
||||||
|
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
|
||||||
|
}
|
||||||
|
|
||||||
|
private object AnimeSourceTable : IdTable<Long>() {
|
||||||
|
override val id = long("id").entityId()
|
||||||
|
val name = varchar("name", 128)
|
||||||
|
val lang = varchar("lang", 10)
|
||||||
|
val extension = reference("extension", suwayomi.anime.model.table.AnimeExtensionTable)
|
||||||
|
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(
|
||||||
|
AnimeExtensionTable,
|
||||||
|
AnimeSourceTable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
class M0005_AnimeTablesBatch2 : Migration() {
|
||||||
|
private object AnimeTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val title = varchar("title", 512)
|
||||||
|
val initialized = bool("initialized").default(false)
|
||||||
|
|
||||||
|
val artist = varchar("artist", 64).nullable()
|
||||||
|
val author = varchar("author", 64).nullable()
|
||||||
|
val description = varchar("description", 4096).nullable()
|
||||||
|
val genre = varchar("genre", 1024).nullable()
|
||||||
|
|
||||||
|
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
|
||||||
|
val status = integer("status").default(SAnime.UNKNOWN)
|
||||||
|
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
|
val inLibrary = bool("in_library").default(false)
|
||||||
|
val defaultCategory = bool("default_category").default(true)
|
||||||
|
|
||||||
|
// source is used by some ancestor of IntIdTable
|
||||||
|
val sourceReference = long("source")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(
|
||||||
|
AnimeTable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.anime.model.table.AnimeTable
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
class M0006_AnimeTablesBatch3 : Migration() {
|
||||||
|
private object EpisodeTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val name = varchar("name", 512)
|
||||||
|
val date_upload = long("date_upload").default(0)
|
||||||
|
val episode_number = float("episode_number").default(-1f)
|
||||||
|
val scanlator = varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
|
val isRead = bool("read").default(false)
|
||||||
|
val isBookmarked = bool("bookmark").default(false)
|
||||||
|
val lastPageRead = integer("last_page_read").default(0)
|
||||||
|
|
||||||
|
// index is reserved by a function
|
||||||
|
val animeIndex = integer("index")
|
||||||
|
|
||||||
|
val anime = reference("anime", AnimeTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(
|
||||||
|
EpisodeTable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0007_ChapterIsDownloaded : Migration() {
|
||||||
|
/** this migration added IS_DOWNLOADED to CHAPTER */
|
||||||
|
override fun run() {
|
||||||
|
with(TransactionManager.current()) {
|
||||||
|
exec("ALTER TABLE CHAPTER ADD COLUMN IS_DOWNLOADED BOOLEAN DEFAULT FALSE")
|
||||||
|
commit()
|
||||||
|
currentDialect.resetCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0008_ChapterPageCount : Migration() {
|
||||||
|
/** this migration added PAGE_COUNT to CHAPTER */
|
||||||
|
override fun run() {
|
||||||
|
with(TransactionManager.current()) {
|
||||||
|
exec("ALTER TABLE CHAPTER ADD COLUMN PAGE_COUNT INT DEFAULT -1")
|
||||||
|
commit()
|
||||||
|
currentDialect.resetCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package suwayomi.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
import suwayomi.server.database.migration.lib.Migration
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0009_ChapterLastReadAt : Migration() {
|
||||||
|
/** this migration added PAGE_COUNT to CHAPTER */
|
||||||
|
override fun run() {
|
||||||
|
with(TransactionManager.current()) {
|
||||||
|
// BIGINT == Long
|
||||||
|
exec("ALTER TABLE CHAPTER ADD COLUMN LAST_READ_AT BIGINT DEFAULT 0")
|
||||||
|
commit()
|
||||||
|
currentDialect.resetCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration.lib
|
package suwayomi.server.database.migration.lib
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration.lib
|
package suwayomi.server.database.migration.lib
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration.lib
|
package suwayomi.server.database.migration.lib
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -10,7 +10,6 @@ package ir.armor.tachidesk.model.database.migration.lib
|
|||||||
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
|
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
|
||||||
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
||||||
|
|
||||||
import ir.armor.tachidesk.server.ServerConfig
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
@@ -18,6 +17,7 @@ import org.jetbrains.exposed.sql.SchemaUtils.create
|
|||||||
import org.jetbrains.exposed.sql.exists
|
import org.jetbrains.exposed.sql.exists
|
||||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.server.ServerConfig
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
@@ -63,7 +63,7 @@ fun runMigrations(migrations: List<Migration>, database: Database = TransactionM
|
|||||||
|
|
||||||
@OptIn(ExperimentalPathApi::class)
|
@OptIn(ExperimentalPathApi::class)
|
||||||
private fun getTopLevelClasses(packageName: String): List<Class<*>> {
|
private fun getTopLevelClasses(packageName: String): List<Class<*>> {
|
||||||
ServerConfig::class.java.getResource("/" + "ir.armor.tachidesk.model.database.migration".replace('.', '/'))
|
ServerConfig::class.java.getResource("/" + "suwayomi.server.database.migration".replace('.', '/'))
|
||||||
val path = "/" + packageName.replace('.', '/')
|
val path = "/" + packageName.replace('.', '/')
|
||||||
val uri = ServerConfig::class.java.getResource(path).toURI()
|
val uri = ServerConfig::class.java.getResource(path).toURI()
|
||||||
|
|
||||||
+14
-4
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.server.impl_internal
|
package suwayomi.server.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,18 +7,28 @@ package ir.armor.tachidesk.server.impl_internal
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.server.BuildConfig
|
import suwayomi.server.BuildConfig
|
||||||
|
|
||||||
data class AboutDataClass(
|
data class AboutDataClass(
|
||||||
|
val name: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
val revision: String,
|
val revision: String,
|
||||||
|
val buildType: String,
|
||||||
|
val buildTime: Long,
|
||||||
|
val github: String,
|
||||||
|
val discord: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
object About {
|
object About {
|
||||||
fun getAbout(): AboutDataClass {
|
fun getAbout(): AboutDataClass {
|
||||||
return AboutDataClass(
|
return AboutDataClass(
|
||||||
BuildConfig.version,
|
BuildConfig.NAME,
|
||||||
BuildConfig.revision,
|
BuildConfig.VERSION,
|
||||||
|
BuildConfig.REVISION,
|
||||||
|
BuildConfig.BUILD_TYPE,
|
||||||
|
BuildConfig.BUILD_TIME,
|
||||||
|
BuildConfig.GITHUB,
|
||||||
|
BuildConfig.DISCORD,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+8
-1
@@ -1,4 +1,11 @@
|
|||||||
package ir.armor.tachidesk.server.util
|
package suwayomi.server.util
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
+11
-8
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.server.util
|
package suwayomi.server.util
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,14 +8,17 @@ package ir.armor.tachidesk.server.util
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import io.javalin.plugin.json.JavalinJackson
|
import io.javalin.plugin.json.JavalinJackson
|
||||||
import ir.armor.tachidesk.server.impl_internal.AboutDataClass
|
|
||||||
import ir.armor.tachidesk.server.serverConfig
|
|
||||||
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.Clear
|
|
||||||
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.OtherApplicationRunning
|
|
||||||
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.TachideskInstanceRunning
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request.Builder
|
import okhttp3.Request.Builder
|
||||||
|
import suwayomi.server.impl.AboutDataClass
|
||||||
|
import suwayomi.server.serverConfig
|
||||||
|
import suwayomi.server.util.AppMutex.AppMutexStat.Clear
|
||||||
|
import suwayomi.server.util.AppMutex.AppMutexStat.OtherApplicationRunning
|
||||||
|
import suwayomi.server.util.AppMutex.AppMutexStat.TachideskInstanceRunning
|
||||||
|
import suwayomi.server.util.Browser.openInBrowser
|
||||||
|
import suwayomi.server.util.ExitCode.MutexCheckFailedAnotherAppRunning
|
||||||
|
import suwayomi.server.util.ExitCode.MutexCheckFailedTachideskRunning
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@@ -66,11 +69,11 @@ object AppMutex {
|
|||||||
|
|
||||||
logger.info("Aborting startup.")
|
logger.info("Aborting startup.")
|
||||||
|
|
||||||
shutdownApp(ExitCode.MutexCheckFailedTachideskRunning)
|
shutdownApp(MutexCheckFailedTachideskRunning)
|
||||||
}
|
}
|
||||||
OtherApplicationRunning -> {
|
OtherApplicationRunning -> {
|
||||||
logger.error("A non Tachidesk application is running on $appIP:${serverConfig.port}, aborting startup.")
|
logger.error("A non Tachidesk application is running on $appIP:${serverConfig.port}, aborting startup.")
|
||||||
shutdownApp(ExitCode.MutexCheckFailedAnotherAppRunning)
|
shutdownApp(MutexCheckFailedAnotherAppRunning)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package suwayomi.server.util
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import dorkbox.util.Desktop
|
||||||
|
import suwayomi.server.serverConfig
|
||||||
|
|
||||||
|
object Browser {
|
||||||
|
private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
|
||||||
|
private val appBaseUrl = "http://$appIP:${serverConfig.port}"
|
||||||
|
|
||||||
|
private val electronInstances = mutableListOf<Any>()
|
||||||
|
|
||||||
|
fun openInBrowser() {
|
||||||
|
|
||||||
|
val openInElectron = System.getProperty("suwayomi.server.webInterface")?.equals("electron")
|
||||||
|
|
||||||
|
if (openInElectron == true) {
|
||||||
|
try {
|
||||||
|
val electronPath = System.getProperty("suwayomi.server.electronPath")!!
|
||||||
|
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
|
||||||
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Desktop.browseURL(appBaseUrl)
|
||||||
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package suwayomi.server.util
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import dorkbox.systemTray.MenuItem
|
||||||
|
import dorkbox.systemTray.SystemTray
|
||||||
|
import dorkbox.util.CacheUtil
|
||||||
|
import suwayomi.server.BuildConfig
|
||||||
|
import suwayomi.server.ServerConfig
|
||||||
|
import suwayomi.server.serverConfig
|
||||||
|
import suwayomi.server.util.Browser.openInBrowser
|
||||||
|
import suwayomi.server.util.ExitCode.Success
|
||||||
|
|
||||||
|
object SystemTray {
|
||||||
|
fun systemTray(): SystemTray? {
|
||||||
|
try {
|
||||||
|
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
||||||
|
SystemTray.DEBUG = serverConfig.debugLogsEnabled
|
||||||
|
|
||||||
|
CacheUtil.clear(BuildConfig.NAME)
|
||||||
|
|
||||||
|
val systemTray = SystemTray.get(BuildConfig.NAME) ?: return null
|
||||||
|
val mainMenu = systemTray.menu
|
||||||
|
|
||||||
|
mainMenu.add(
|
||||||
|
MenuItem(
|
||||||
|
"Open Tachidesk"
|
||||||
|
) {
|
||||||
|
openInBrowser()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val icon = ServerConfig::class.java.getResource("/icon/faviconlogo.png")
|
||||||
|
|
||||||
|
// systemTray.setTooltip("Tachidesk")
|
||||||
|
systemTray.setImage(icon)
|
||||||
|
// systemTray.status = "No Mail"
|
||||||
|
|
||||||
|
mainMenu.add(
|
||||||
|
MenuItem("Quit") {
|
||||||
|
shutdownApp(Success)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
systemTray.installShutdownHook()
|
||||||
|
|
||||||
|
return systemTray
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
-112
@@ -1,110 +1,49 @@
|
|||||||
package ir.armor.tachidesk.server
|
package suwayomi.tachidesk
|
||||||
|
|
||||||
import io.javalin.Javalin
|
|
||||||
import ir.armor.tachidesk.impl.Category.createCategory
|
|
||||||
import ir.armor.tachidesk.impl.Category.getCategoryList
|
|
||||||
import ir.armor.tachidesk.impl.Category.removeCategory
|
|
||||||
import ir.armor.tachidesk.impl.Category.reorderCategory
|
|
||||||
import ir.armor.tachidesk.impl.Category.updateCategory
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.addMangaToCategory
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.getCategoryMangaList
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.getMangaCategories
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
|
||||||
import ir.armor.tachidesk.impl.Chapter.getChapter
|
|
||||||
import ir.armor.tachidesk.impl.Chapter.getChapterList
|
|
||||||
import ir.armor.tachidesk.impl.Chapter.modifyChapter
|
|
||||||
import ir.armor.tachidesk.impl.Library
|
|
||||||
import ir.armor.tachidesk.impl.Library.getLibraryMangas
|
|
||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
|
||||||
import ir.armor.tachidesk.impl.Manga.getMangaThumbnail
|
|
||||||
import ir.armor.tachidesk.impl.MangaList.getMangaList
|
|
||||||
import ir.armor.tachidesk.impl.Page.getPageImage
|
|
||||||
import ir.armor.tachidesk.impl.Search.sourceFilters
|
|
||||||
import ir.armor.tachidesk.impl.Search.sourceGlobalSearch
|
|
||||||
import ir.armor.tachidesk.impl.Search.sourceSearch
|
|
||||||
import ir.armor.tachidesk.impl.Source.getSource
|
|
||||||
import ir.armor.tachidesk.impl.Source.getSourceList
|
|
||||||
import ir.armor.tachidesk.impl.backup.BackupFlags
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
|
|
||||||
import ir.armor.tachidesk.impl.extension.Extension.getExtensionIcon
|
|
||||||
import ir.armor.tachidesk.impl.extension.Extension.installExtension
|
|
||||||
import ir.armor.tachidesk.impl.extension.Extension.uninstallExtension
|
|
||||||
import ir.armor.tachidesk.impl.extension.Extension.updateExtension
|
|
||||||
import ir.armor.tachidesk.impl.extension.ExtensionsList.getExtensionList
|
|
||||||
import ir.armor.tachidesk.server.impl_internal.About.getAbout
|
|
||||||
import ir.armor.tachidesk.server.util.openInBrowser
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.future.future
|
|
||||||
import mu.KotlinLogging
|
|
||||||
import java.io.IOException
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
object JavalinSetup {
|
import io.javalin.Javalin
|
||||||
private val logger = KotlinLogging.logger {}
|
import suwayomi.server.JavalinSetup.future
|
||||||
|
import suwayomi.server.impl.About
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
import suwayomi.tachidesk.impl.Category
|
||||||
|
import suwayomi.tachidesk.impl.CategoryManga.addMangaToCategory
|
||||||
private fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
|
import suwayomi.tachidesk.impl.CategoryManga.getCategoryMangaList
|
||||||
return scope.future(block = block)
|
import suwayomi.tachidesk.impl.CategoryManga.getMangaCategories
|
||||||
}
|
import suwayomi.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
||||||
|
import suwayomi.tachidesk.impl.Chapter.getChapter
|
||||||
fun javalinSetup() {
|
import suwayomi.tachidesk.impl.Chapter.getChapterList
|
||||||
var hasWebUiBundled = false
|
import suwayomi.tachidesk.impl.Chapter.modifyChapter
|
||||||
|
import suwayomi.tachidesk.impl.Library.addMangaToLibrary
|
||||||
val app = Javalin.create { config ->
|
import suwayomi.tachidesk.impl.Library.getLibraryMangas
|
||||||
try {
|
import suwayomi.tachidesk.impl.Library.removeMangaFromLibrary
|
||||||
// if the bellow line throws an exception then webUI is not bundled
|
import suwayomi.tachidesk.impl.Manga.getManga
|
||||||
this::class.java.getResource("/react/index.html")
|
import suwayomi.tachidesk.impl.Manga.getMangaThumbnail
|
||||||
|
import suwayomi.tachidesk.impl.MangaList.getMangaList
|
||||||
// no exception so we can tell javalin to serve webUI
|
import suwayomi.tachidesk.impl.Page.getPageImage
|
||||||
hasWebUiBundled = true
|
import suwayomi.tachidesk.impl.Search.sourceFilters
|
||||||
config.addStaticFiles("/react")
|
import suwayomi.tachidesk.impl.Search.sourceGlobalSearch
|
||||||
config.addSinglePageRoot("/", "/react/index.html")
|
import suwayomi.tachidesk.impl.Search.sourceSearch
|
||||||
} catch (e: RuntimeException) {
|
import suwayomi.tachidesk.impl.Source.getSource
|
||||||
logger.warn("react build files are missing.")
|
import suwayomi.tachidesk.impl.Source.getSourceList
|
||||||
hasWebUiBundled = false
|
import suwayomi.tachidesk.impl.backup.BackupFlags
|
||||||
}
|
import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
||||||
config.enableCorsForAllOrigins()
|
import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
|
||||||
}.start(serverConfig.ip, serverConfig.port)
|
import suwayomi.tachidesk.impl.download.DownloadManager
|
||||||
|
import suwayomi.tachidesk.impl.extension.Extension.getExtensionIcon
|
||||||
// when JVM is prompted to shutdown, stop javalin gracefully
|
import suwayomi.tachidesk.impl.extension.Extension.installExtension
|
||||||
Runtime.getRuntime().addShutdownHook(
|
import suwayomi.tachidesk.impl.extension.Extension.uninstallExtension
|
||||||
thread(start = false) {
|
import suwayomi.tachidesk.impl.extension.Extension.updateExtension
|
||||||
app.stop()
|
import suwayomi.tachidesk.impl.extension.ExtensionsList.getExtensionList
|
||||||
}
|
import java.text.SimpleDateFormat
|
||||||
)
|
import java.util.Date
|
||||||
|
|
||||||
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
|
||||||
openInBrowser()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.exception(NullPointerException::class.java) { e, ctx ->
|
|
||||||
logger.error("NullPointerException while handling the request", e)
|
|
||||||
ctx.status(404)
|
|
||||||
}
|
|
||||||
app.exception(NoSuchElementException::class.java) { e, ctx ->
|
|
||||||
logger.error("NoSuchElementException while handling the request", e)
|
|
||||||
ctx.status(404)
|
|
||||||
}
|
|
||||||
app.exception(IOException::class.java) { e, ctx ->
|
|
||||||
logger.error("IOException while handling the request", e)
|
|
||||||
ctx.status(500)
|
|
||||||
ctx.result(e.message ?: "Internal Server Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
object TachideskAPI {
|
||||||
|
fun defineEndpoints(app: Javalin) {
|
||||||
// list all extensions
|
// list all extensions
|
||||||
app.get("/api/v1/extension/list") { ctx ->
|
app.get("/api/v1/extension/list") { ctx ->
|
||||||
ctx.json(
|
ctx.json(
|
||||||
@@ -318,7 +257,7 @@ object JavalinSetup {
|
|||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
ctx.result(
|
ctx.result(
|
||||||
future { Library.addMangaToLibrary(mangaId) }
|
future { addMangaToLibrary(mangaId) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +266,7 @@ object JavalinSetup {
|
|||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
ctx.result(
|
ctx.result(
|
||||||
future { Library.removeMangaFromLibrary(mangaId) }
|
future { removeMangaFromLibrary(mangaId) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,19 +277,19 @@ object JavalinSetup {
|
|||||||
|
|
||||||
// category list
|
// category list
|
||||||
app.get("/api/v1/category/") { ctx ->
|
app.get("/api/v1/category/") { ctx ->
|
||||||
ctx.json(getCategoryList())
|
ctx.json(Category.getCategoryList())
|
||||||
}
|
}
|
||||||
|
|
||||||
// category create
|
// category create
|
||||||
app.post("/api/v1/category/") { ctx ->
|
app.post("/api/v1/category/") { ctx ->
|
||||||
val name = ctx.formParam("name")!!
|
val name = ctx.formParam("name")!!
|
||||||
createCategory(name)
|
Category.createCategory(name)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns some static info of the current app build
|
// returns some static info of the current app build
|
||||||
app.get("/api/v1/about/") { ctx ->
|
app.get("/api/v1/about/") { ctx ->
|
||||||
ctx.json(getAbout())
|
ctx.json(About.getAbout())
|
||||||
}
|
}
|
||||||
|
|
||||||
// category modification
|
// category modification
|
||||||
@@ -358,7 +297,7 @@ object JavalinSetup {
|
|||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
val name = ctx.formParam("name")
|
val name = ctx.formParam("name")
|
||||||
val isDefault = ctx.formParam("default")?.toBoolean()
|
val isDefault = ctx.formParam("default")?.toBoolean()
|
||||||
updateCategory(categoryId, name, isDefault)
|
Category.updateCategory(categoryId, name, isDefault)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,14 +306,14 @@ object JavalinSetup {
|
|||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
val from = ctx.formParam("from")!!.toInt()
|
val from = ctx.formParam("from")!!.toInt()
|
||||||
val to = ctx.formParam("to")!!.toInt()
|
val to = ctx.formParam("to")!!.toInt()
|
||||||
reorderCategory(categoryId, from, to)
|
Category.reorderCategory(categoryId, from, to)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// category delete
|
// category delete
|
||||||
app.delete("/api/v1/category/:categoryId") { ctx ->
|
app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
removeCategory(categoryId)
|
Category.removeCategory(categoryId)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,15 +384,56 @@ object JavalinSetup {
|
|||||||
// Download queue stats
|
// Download queue stats
|
||||||
app.ws("/api/v1/downloads") { ws ->
|
app.ws("/api/v1/downloads") { ws ->
|
||||||
ws.onConnect { ctx ->
|
ws.onConnect { ctx ->
|
||||||
// TODO: send current stat
|
DownloadManager.addClient(ctx)
|
||||||
// TODO: add to downlad subscribers
|
DownloadManager.notifyClient(ctx)
|
||||||
}
|
}
|
||||||
ws.onMessage {
|
ws.onMessage { ctx ->
|
||||||
// TODO: send current stat
|
DownloadManager.handleRequest(ctx)
|
||||||
}
|
}
|
||||||
ws.onClose { ctx ->
|
ws.onClose { ctx ->
|
||||||
// TODO: remove from subscribers
|
DownloadManager.removeClient(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the downloader
|
||||||
|
app.get("/api/v1/downloads/start") { ctx ->
|
||||||
|
DownloadManager.start()
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the downloader
|
||||||
|
app.get("/api/v1/downloads/stop") { ctx ->
|
||||||
|
DownloadManager.stop()
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear download queue
|
||||||
|
app.get("/api/v1/downloads/clear") { ctx ->
|
||||||
|
DownloadManager.clear()
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue chapter for download
|
||||||
|
app.get("/api/v1/download/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||||
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
|
DownloadManager.enqueue(chapterIndex, mangaId)
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete chapter from download queue
|
||||||
|
app.delete("/api/v1/download/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||||
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
|
DownloadManager.unqueue(chapterIndex, mangaId)
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-6
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,11 +7,6 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.toDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
@@ -19,6 +14,11 @@ import org.jetbrains.exposed.sql.select
|
|||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
||||||
|
import suwayomi.tachidesk.model.dataclass.CategoryDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryMangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryTable
|
||||||
|
import suwayomi.tachidesk.model.table.toDataClass
|
||||||
|
|
||||||
object Category {
|
object Category {
|
||||||
/**
|
/**
|
||||||
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,12 +7,6 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.toDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
@@ -20,6 +14,12 @@ import org.jetbrains.exposed.sql.insert
|
|||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.model.dataclass.CategoryDataClass
|
||||||
|
import suwayomi.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryMangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryTable
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.toDataClass
|
||||||
|
|
||||||
object CategoryManga {
|
object CategoryManga {
|
||||||
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||||
+73
-52
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -9,14 +9,6 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
|
||||||
import ir.armor.tachidesk.model.database.table.ChapterTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.PageTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.toDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
|
|
||||||
import org.jetbrains.exposed.sql.SortOrder.DESC
|
import org.jetbrains.exposed.sql.SortOrder.DESC
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
@@ -24,6 +16,15 @@ import org.jetbrains.exposed.sql.insert
|
|||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.impl.Manga.getManga
|
||||||
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import suwayomi.tachidesk.model.dataclass.ChapterDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.ChapterTable
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.PageTable
|
||||||
|
import suwayomi.tachidesk.model.table.toDataClass
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
object Chapter {
|
object Chapter {
|
||||||
/** get chapter list when showing a manga */
|
/** get chapter list when showing a manga */
|
||||||
@@ -88,7 +89,7 @@ object Chapter {
|
|||||||
// clear any orphaned chapters that are in the db but not in `chapterList`
|
// clear any orphaned chapters that are in the db but not in `chapterList`
|
||||||
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
||||||
if (dbChapterCount > chapterCount) { // we got some clean up due
|
if (dbChapterCount > chapterCount) { // we got some clean up due
|
||||||
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } }
|
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.toList() }
|
||||||
|
|
||||||
dbChapterList.forEach {
|
dbChapterList.forEach {
|
||||||
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
|
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
|
||||||
@@ -122,9 +123,14 @@ object Chapter {
|
|||||||
dbChapter[ChapterTable.isRead],
|
dbChapter[ChapterTable.isRead],
|
||||||
dbChapter[ChapterTable.isBookmarked],
|
dbChapter[ChapterTable.isBookmarked],
|
||||||
dbChapter[ChapterTable.lastPageRead],
|
dbChapter[ChapterTable.lastPageRead],
|
||||||
|
dbChapter[ChapterTable.lastReadAt],
|
||||||
|
|
||||||
chapterCount - index,
|
chapterCount - index,
|
||||||
chapterList.size
|
dbChapter[ChapterTable.isDownloaded],
|
||||||
|
|
||||||
|
dbChapter[ChapterTable.pageCount],
|
||||||
|
|
||||||
|
chapterList.size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,54 +142,68 @@ object Chapter {
|
|||||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||||
}.first()
|
}.first()
|
||||||
}
|
}
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
|
||||||
|
|
||||||
val pageList = source.fetchPageList(
|
return if (!chapterEntry[ChapterTable.isDownloaded]) {
|
||||||
SChapter.create().apply {
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
url = chapterEntry[ChapterTable.url]
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
name = chapterEntry[ChapterTable.name]
|
|
||||||
}
|
|
||||||
).awaitSingle()
|
|
||||||
|
|
||||||
val chapterId = chapterEntry[ChapterTable.id].value
|
val pageList = source.fetchPageList(
|
||||||
val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
SChapter.create().apply {
|
||||||
|
url = chapterEntry[ChapterTable.url]
|
||||||
|
name = chapterEntry[ChapterTable.name]
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
// update page list for this chapter
|
val chapterId = chapterEntry[ChapterTable.id].value
|
||||||
transaction {
|
val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
||||||
pageList.forEach { page ->
|
|
||||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
// update page list for this chapter
|
||||||
if (pageEntry == null) {
|
transaction {
|
||||||
PageTable.insert {
|
pageList.forEach { page ->
|
||||||
it[index] = page.index
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||||
it[url] = page.url
|
if (pageEntry == null) {
|
||||||
it[imageUrl] = page.imageUrl
|
PageTable.insert {
|
||||||
it[chapter] = chapterId
|
it[index] = page.index
|
||||||
}
|
it[url] = page.url
|
||||||
} else {
|
it[imageUrl] = page.imageUrl
|
||||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
|
it[chapter] = chapterId
|
||||||
it[url] = page.url
|
}
|
||||||
it[imageUrl] = page.imageUrl
|
} else {
|
||||||
|
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
|
||||||
|
it[url] = page.url
|
||||||
|
it[imageUrl] = page.imageUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val pageCount = pageList.count()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) {
|
||||||
|
it[ChapterTable.pageCount] = pageCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ChapterDataClass(
|
||||||
|
chapterEntry[ChapterTable.url],
|
||||||
|
chapterEntry[ChapterTable.name],
|
||||||
|
chapterEntry[ChapterTable.date_upload],
|
||||||
|
chapterEntry[ChapterTable.chapter_number],
|
||||||
|
chapterEntry[ChapterTable.scanlator],
|
||||||
|
mangaId,
|
||||||
|
chapterEntry[ChapterTable.isRead],
|
||||||
|
chapterEntry[ChapterTable.isBookmarked],
|
||||||
|
chapterEntry[ChapterTable.lastPageRead],
|
||||||
|
chapterEntry[ChapterTable.lastReadAt],
|
||||||
|
|
||||||
|
chapterEntry[ChapterTable.chapterIndex],
|
||||||
|
chapterEntry[ChapterTable.isDownloaded],
|
||||||
|
pageCount,
|
||||||
|
chapterCount.toInt()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ChapterTable.toDataClass(chapterEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChapterDataClass(
|
|
||||||
chapterEntry[ChapterTable.url],
|
|
||||||
chapterEntry[ChapterTable.name],
|
|
||||||
chapterEntry[ChapterTable.date_upload],
|
|
||||||
chapterEntry[ChapterTable.chapter_number],
|
|
||||||
chapterEntry[ChapterTable.scanlator],
|
|
||||||
mangaId,
|
|
||||||
chapterEntry[ChapterTable.isRead],
|
|
||||||
chapterEntry[ChapterTable.isBookmarked],
|
|
||||||
chapterEntry[ChapterTable.lastPageRead],
|
|
||||||
|
|
||||||
chapterEntry[ChapterTable.chapterIndex],
|
|
||||||
chapterCount.toInt(),
|
|
||||||
pageList.count()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
|
fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
|
||||||
@@ -198,6 +218,7 @@ object Chapter {
|
|||||||
}
|
}
|
||||||
lastPageRead?.also {
|
lastPageRead?.also {
|
||||||
update[ChapterTable.lastPageRead] = it
|
update[ChapterTable.lastPageRead] = it
|
||||||
|
update[ChapterTable.lastReadAt] = Instant.now().epochSecond
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,18 +7,18 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.toDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.impl.Manga.getManga
|
||||||
|
import suwayomi.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryMangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.CategoryTable
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.toDataClass
|
||||||
|
|
||||||
object Library {
|
object Library {
|
||||||
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
|
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
|
||||||
+15
-15
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -9,23 +9,23 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
|
||||||
import ir.armor.tachidesk.impl.Source.getSource
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.await
|
|
||||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
|
||||||
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage
|
|
||||||
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaStatus
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import suwayomi.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||||
|
import suwayomi.tachidesk.impl.Source.getSource
|
||||||
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import suwayomi.tachidesk.impl.util.network.await
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||||
|
import suwayomi.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.MangaStatus
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
object Manga {
|
object Manga {
|
||||||
@@ -102,7 +102,7 @@ object Manga {
|
|||||||
fetchedManga.description,
|
fetchedManga.description,
|
||||||
fetchedManga.genre,
|
fetchedManga.genre,
|
||||||
MangaStatus.valueOf(fetchedManga.status).name,
|
MangaStatus.valueOf(fetchedManga.status).name,
|
||||||
false,
|
mangaEntry[MangaTable.inLibrary],
|
||||||
getSource(mangaEntry[MangaTable.sourceReference]),
|
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
@@ -111,7 +111,7 @@ object Manga {
|
|||||||
|
|
||||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||||
val saveDir = applicationDirs.thumbnailsRoot
|
val saveDir = applicationDirs.mangaThumbnailsRoot
|
||||||
val fileName = mangaId.toString()
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, fileName) {
|
return getCachedImageResponse(saveDir, fileName) {
|
||||||
@@ -131,7 +131,7 @@ object Manga {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun clearMangaThumbnail(mangaId: Int) {
|
private fun clearMangaThumbnail(mangaId: Int) {
|
||||||
val saveDir = applicationDirs.thumbnailsRoot
|
val saveDir = applicationDirs.mangaThumbnailsRoot
|
||||||
val fileName = mangaId.toString()
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
clearCachedImage(saveDir, fileName)
|
clearCachedImage(saveDir, fileName)
|
||||||
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,15 +8,15 @@ package ir.armor.tachidesk.impl
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaStatus
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
|
||||||
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
|
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import suwayomi.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
import suwayomi.tachidesk.model.dataclass.PagedMangaListDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.MangaStatus
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
|
|
||||||
object MangaList {
|
object MangaList {
|
||||||
fun proxyThumbnailUrl(mangaId: Int): String {
|
fun proxyThumbnailUrl(mangaId: Int): String {
|
||||||
+9
-9
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -9,14 +9,6 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
|
||||||
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
|
||||||
import ir.armor.tachidesk.impl.util.storage.SafePath
|
|
||||||
import ir.armor.tachidesk.model.database.table.ChapterTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.PageTable
|
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
@@ -24,6 +16,14 @@ import org.jetbrains.exposed.sql.update
|
|||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
import suwayomi.server.ApplicationDirs
|
||||||
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||||
|
import suwayomi.tachidesk.impl.util.storage.SafePath
|
||||||
|
import suwayomi.tachidesk.model.table.ChapterTable
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
|
import suwayomi.tachidesk.model.table.PageTable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
+5
-5
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,10 +7,10 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.impl.MangaList.processEntries
|
import suwayomi.tachidesk.impl.MangaList.processEntries
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
|
import suwayomi.tachidesk.model.dataclass.PagedMangaListDataClass
|
||||||
|
|
||||||
object Search {
|
object Search {
|
||||||
// TODO
|
// TODO
|
||||||
+6
-6
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package suwayomi.tachidesk.impl
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,15 +7,15 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.impl.extension.Extension.getExtensionIconUrl
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.SourceTable
|
|
||||||
import ir.armor.tachidesk.model.dataclass.SourceDataClass
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.tachidesk.impl.extension.Extension.getExtensionIconUrl
|
||||||
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import suwayomi.tachidesk.model.dataclass.SourceDataClass
|
||||||
|
import suwayomi.tachidesk.model.table.ExtensionTable
|
||||||
|
import suwayomi.tachidesk.model.table.SourceTable
|
||||||
|
|
||||||
object Source {
|
object Source {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl.backup
|
package suwayomi.tachidesk.impl.backup
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+11
-11
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy
|
package suwayomi.tachidesk.impl.backup.legacy
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -11,16 +11,16 @@ import com.github.salomonbrys.kotson.registerTypeAdapter
|
|||||||
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
|
import suwayomi.tachidesk.impl.backup.legacy.models.DHistory
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.CategoryTypeAdapter
|
import suwayomi.tachidesk.impl.backup.legacy.serializer.CategoryTypeAdapter
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.ChapterTypeAdapter
|
import suwayomi.tachidesk.impl.backup.legacy.serializer.ChapterTypeAdapter
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.HistoryTypeAdapter
|
import suwayomi.tachidesk.impl.backup.legacy.serializer.HistoryTypeAdapter
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.MangaTypeAdapter
|
import suwayomi.tachidesk.impl.backup.legacy.serializer.MangaTypeAdapter
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.serializer.TrackTypeAdapter
|
import suwayomi.tachidesk.impl.backup.legacy.serializer.TrackTypeAdapter
|
||||||
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
import suwayomi.tachidesk.impl.backup.models.CategoryImpl
|
||||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
import suwayomi.tachidesk.impl.backup.models.ChapterImpl
|
||||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
import suwayomi.tachidesk.impl.backup.models.MangaImpl
|
||||||
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
import suwayomi.tachidesk.impl.backup.models.TrackImpl
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
open class LegacyBackupBase {
|
open class LegacyBackupBase {
|
||||||
+13
-13
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy
|
package suwayomi.tachidesk.impl.backup.legacy
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -12,20 +12,20 @@ import com.google.gson.JsonArray
|
|||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import ir.armor.tachidesk.impl.Category.getCategoryList
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.getMangaCategories
|
|
||||||
import ir.armor.tachidesk.impl.backup.BackupFlags
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup.CURRENT_VERSION
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.Manga
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.model.database.table.ChapterTable
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.tachidesk.impl.Category.getCategoryList
|
||||||
|
import suwayomi.tachidesk.impl.CategoryManga.getMangaCategories
|
||||||
|
import suwayomi.tachidesk.impl.backup.BackupFlags
|
||||||
|
import suwayomi.tachidesk.impl.backup.legacy.models.Backup
|
||||||
|
import suwayomi.tachidesk.impl.backup.legacy.models.Backup.CURRENT_VERSION
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.CategoryImpl
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.ChapterImpl
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.Manga
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.MangaImpl
|
||||||
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import suwayomi.tachidesk.model.table.ChapterTable
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
|
|
||||||
object LegacyBackupExport : LegacyBackupBase() {
|
object LegacyBackupExport : LegacyBackupBase() {
|
||||||
|
|
||||||
+17
-17
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy
|
package suwayomi.tachidesk.impl.backup.legacy
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
@@ -7,28 +7,28 @@ import com.google.gson.JsonObject
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.impl.Category.createCategory
|
|
||||||
import ir.armor.tachidesk.impl.Category.getCategoryList
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupValidator.ValidationResult
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupValidator.validate
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.Chapter
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.Manga
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.Track
|
|
||||||
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
|
||||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.impl.Category.createCategory
|
||||||
|
import suwayomi.tachidesk.impl.Category.getCategoryList
|
||||||
|
import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupValidator.ValidationResult
|
||||||
|
import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupValidator.validate
|
||||||
|
import suwayomi.tachidesk.impl.backup.legacy.models.Backup
|
||||||
|
import suwayomi.tachidesk.impl.backup.legacy.models.DHistory
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.CategoryImpl
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.Chapter
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.ChapterImpl
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.Manga
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.MangaImpl
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.Track
|
||||||
|
import suwayomi.tachidesk.impl.backup.models.TrackImpl
|
||||||
|
import suwayomi.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
|
import suwayomi.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import suwayomi.tachidesk.model.table.MangaTable
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy
|
package suwayomi.tachidesk.impl.backup.legacy
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,10 +8,10 @@ package ir.armor.tachidesk.impl.backup.legacy
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
|
||||||
import ir.armor.tachidesk.model.database.table.SourceTable
|
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import suwayomi.tachidesk.impl.backup.legacy.models.Backup
|
||||||
|
import suwayomi.tachidesk.model.table.SourceTable
|
||||||
|
|
||||||
object LegacyBackupValidator {
|
object LegacyBackupValidator {
|
||||||
data class ValidationResult(val missingSources: List<String>, val missingTrackers: List<String>)
|
data class ValidationResult(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy.models
|
package suwayomi.tachidesk.impl.backup.legacy.models
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy.models
|
package suwayomi.tachidesk.impl.backup.legacy.models
|
||||||
|
|
||||||
data class DHistory(val url: String, val lastRead: Long)
|
data class DHistory(val url: String, val lastRead: Long)
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
package suwayomi.tachidesk.impl.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
||||||
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
import suwayomi.tachidesk.impl.backup.models.CategoryImpl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy.serializer
|
package suwayomi.tachidesk.impl.backup.legacy.serializer
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
||||||
import com.google.gson.stream.JsonToken
|
import com.google.gson.stream.JsonToken
|
||||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
import suwayomi.tachidesk.impl.backup.models.ChapterImpl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user