Compare commits

..

95 Commits

Author SHA1 Message Date
Aria Moradi a325440f24 bump to v0.4.3
CI Publish / Validate Gradle Wrapper (push) Successful in 10s
CI Publish / Build artifacts and release (push) Failing after 22s
2021-06-17 00:28:19 +04:30
Aria Moradi 14072bb5a0 fix manga extensions not loading 2021-06-17 00:26:34 +04:30
Aria Moradi 7fc33ba8db fix cache 2021-06-06 03:19:03 +04:30
Aria Moradi 47e51b6615 fix naming 2021-06-06 03:13:09 +04:30
Aria Moradi 857562eaff add build flexiblity for Equinox 2021-06-06 02:48:26 +04:30
Aria Moradi bace854b50 rm dummy 2021-06-06 02:05:47 +04:30
Aria Moradi e4a404472d Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-06-06 02:05:26 +04:30
Aria Moradi 7bfa215b4c change old paths 2021-06-06 01:52:05 +04:30
Aria Moradi ab7af4b80b fix typo 2021-06-06 01:35:39 +04:30
Aria Moradi 2c7ebd8ece prepare for integration with Equinox 2021-06-06 01:34:49 +04:30
Syer10 c96da79058 Fix MacOS crashing on launch (#132) 2021-06-05 22:36:36 +04:30
Aria Moradi 8f09ebacf5 dummy file to trigger gh actions 2021-06-04 23:10:40 +04:30
Aria Moradi e21f3b9c75 closes #130 2021-06-04 21:51:48 +04:30
Aria Moradi 37eeef06e2 correct spelling 2021-06-04 16:34:19 +04:30
Aria Moradi b7fe56687c Bump styfle/cancel-workflow-action from 0.5.0 to 0.9.0 2021-06-04 13:35:20 +04:30
Aria Moradi 60565729ca lint by linter 2021-06-04 13:35:07 +04:30
Aria Moradi 36f4e1c340 move all packages to 'suwayomi.tachidesk' 2021-06-04 13:08:20 +04:30
Aria Moradi abc2a5214b Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-06-04 10:43:39 +04:30
Aria Moradi a29010e0d7 fix chached image returning file type with extra . 2021-06-04 10:43:03 +04:30
arbuilder db99ab66ae Update build_push.yml (#124)
* Update build_push.yml

docker workflow for preview build

* Update build_push.yml

remove cd master

* Update build_push.yml

Change access token
2021-06-01 13:27:57 +04:30
arbuilder 84cc73c149 Update publish.yml (#123)
* Update publish.yml

add docker build workflow

* Update publish.yml

Remove cd master

* Update publish.yml

Change access token
2021-06-01 13:25:38 +04:30
Aria Moradi 10a29cab33 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-30 04:56:59 +04:30
Aria Moradi 849e2f103a [SKIP CI] download chapters for real now 2021-05-30 04:24:21 +04:30
Syer10 6c22fe193a Add meta info for clients to store custom data in (#113)
* Add meta info for clients to store custom data in

* PR comments

* Really update migration
2021-05-30 04:18:08 +04:30
Syer10 e69dbbf418 Working shared preferences (#112)
* Working shared preferences

* Remove unneeded prefs dir

* Todo
2021-05-30 04:05:56 +04:30
Aria Moradi dfa59a1691 bump version to v0.4.2
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-05-30 04:04:11 +04:30
Aria Moradi 5023e96301 Implemented Dowloads front-end 2021-05-30 04:01:49 +04:30
Aria Moradi 224c24ee9f a little reminder 2021-05-30 02:21:43 +04:30
Aria Moradi e3b154cf9e Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-29 23:59:29 +04:30
Aria Moradi d249867c4c finishing touches of download backend, done @jipfr's requests 2021-05-29 23:57:22 +04:30
Aria Moradi b56045e984 downloader backend done 2021-05-29 23:05:51 +04:30
Manchewable 3777cc646e Improve continuous horizontal reader (#110)
* differentiate ContinuesHorizontalLTR and ContinuesHorizontalRTL

* fix displaying pages in horizontal viewer

* add scroll handler for horizontal mode

* update curPage when images pass through center of the screen

* add click events to navigate pages

* remove console.log

* fix click mapping for ContinuesHorizontalRTL

* remove disable eslint inline comment

* fix ContinuesHorizontalRTL not updating curPage on scroll

* add ability to click to drag

* add margin in between images
2021-05-29 19:41:59 +04:30
Manchewable aa5a1083d0 fit images to height (#108) 2021-05-28 23:27:31 +04:30
Manchewable 2ae5e0742e reference to img elements directly (#106) 2021-05-28 23:25:04 +04:30
Aria Moradi e5e875c54a closes #100 2021-05-28 20:21:05 +04:30
Aria Moradi 1a99ec76e4 spinner image, closes #77 2021-05-28 19:37:26 +04:30
Manchewable 1b122d1157 Add a Double Page Viewer (#105)
* add double page reader

* implement singleRTL

* add on image load handler

* add retry display time interval

* remove comments

* add double page wrapper

* fix image getting out of bounds

* remove comments

* remove unused styles

* return imageStyle as type CSSProperties

* rename DoublePagedReader to DoublePagedPager
2021-05-28 17:06:55 +04:30
Aria Moradi 77f2f8cc18 add copyright notice to files that miss it 2021-05-28 16:23:26 +04:30
Aria Moradi f0a99980b6 fixed issue with clearing up orphan chapters 2021-05-28 03:46:32 +04:30
Aria Moradi b0d43ffe69 anime filter everywhere
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-05-28 03:02:14 +04:30
Aria Moradi 16cb0184a4 fix catalog source imports 2021-05-28 02:53:36 +04:30
Aria Moradi f211a33ea3 bump to v0.4.1 2021-05-28 02:49:01 +04:30
Aria Moradi 440c815189 missed from previous commit 2021-05-28 02:46:19 +04:30
Aria Moradi 25829aacfd new anime library 2021-05-28 02:43:30 +04:30
Aria Moradi 700a739f95 probably fixes http leaks (by @Syer10) 2021-05-27 22:45:44 +04:30
Aria Moradi d9620bec05 fix getManga returning false for inLibrary 2021-05-27 22:30:29 +04:30
Aria Moradi 4b6c51b1f8 bump to v0.4.0
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-05-27 19:32:16 +04:30
Aria Moradi bd02edf0b1 barebones anime player 2021-05-27 18:37:45 +04:30
Aria Moradi 5c7123a997 Manga page Finished 2021-05-27 17:13:22 +04:30
Aria Moradi c17e3bd04f can work with anime extensions successfully 2021-05-27 05:13:01 +04:30
Aria Moradi 994ae97256 no dependenct on tachidesk 2021-05-27 03:33:56 +04:30
Aria Moradi 781428a690 add initial anime stuff 2021-05-27 03:25:55 +04:30
Aria Moradi c23ac5faa8 fix compile issue 2021-05-27 02:23:17 +04:30
Aria Moradi e8d41f83c2 move databse to server package, move tables to a better place 2021-05-27 02:21:53 +04:30
Aria Moradi 921a0a3361 Merge branch 'master' into anime 2021-05-27 02:16:07 +04:30
Aria Moradi dda5a2df93 reconsider package strings 2021-05-27 02:13:17 +04:30
Aria Moradi 155f9f107d more of package moving 2021-05-27 02:07:32 +04:30
Aria Moradi 24f68b8f1a move packages 2021-05-27 01:57:40 +04:30
Aria Moradi 0ffbe194fa move packages 2021-05-27 01:57:15 +04:30
Aria Moradi 0b41e2b72b Adapted Tachiyomi-mi extensions-lib implementation 2021-05-27 00:04:33 +04:30
arbuilder ef07b9b4ce [SKIP CI] Update Docker info (#99)
* Update README.md

update docker info

* [SKIP CI] update docker info

* [SKIP CI] Update Readme

change to small case
2021-05-26 11:00:13 +04:30
Aria Moradi f3999cf2d9 [SKIP CI] update docker info 2021-05-26 02:48:13 +04:30
Syer10 1729847937 Allow building without git access (#98)
Just something SY needs...
2021-05-26 02:39:25 +04:30
Aria Moradi 37bff6c76c About Screen 2021-05-25 21:06:27 +04:30
Aria Moradi 4ef32d8037 build type 2021-05-25 19:23:47 +04:30
Aria Moradi d2f6a33f0a fix listener not being removed 2021-05-25 16:19:08 +04:30
Aria Moradi 31d9903251 got rid of all instances of diabling no-unused-vars 2021-05-25 13:24:56 +04:30
Aria Moradi e97642d92a prevPage handle
* go back to previous chapter on page 0 when prevPage is triggered
2021-05-25 13:14:07 +04:30
Aria Moradi c49fc0ff5f only show supported pagers 2021-05-25 13:12:42 +04:30
Aria Moradi deb2ab1ff4 closes #96 2021-05-25 13:12:17 +04:30
Manchewable 23466cf853 Added some key mappings to navigate pages (#95)
* Added some key mappings to navigate pages

* use keyboard event codes

* unused files removed

* use a reference to current page

* fix some bugs with Virtuoso

* add keymapping for space to navigate to next page

* commit my changes

* fix functions not regenerating

* fix partial scroll back to start of page issue

Co-authored-by: Aria Moradi <aria.moradi007@gmail.com>
2021-05-24 23:46:05 +04:30
Aria Moradi 16b34f874d fix some bugs with Virtuoso 2021-05-24 21:26:55 +04:30
Aria Moradi 0e0d08ae5a bump to v0.3.9
CI Publish / Validate Gradle Wrapper (push) Successful in 14s
CI Publish / Build artifacts and release (push) Failing after 19s
2021-05-24 18:32:01 +04:30
Aria Moradi 986b4c2c27 unused files removed 2021-05-24 18:31:39 +04:30
Aria Moradi 0bf9ccfcbd [SKIP CI] fix more typo 2021-05-24 18:22:48 +04:30
Aria Moradi 5e8c47928d [SKIP CI] fix typo 2021-05-24 18:22:16 +04:30
Aria Moradi ffae7f911f [SKIP CI] update windows instructions 2021-05-24 18:21:23 +04:30
Aria Moradi e37fdf6d79 [SKIP CI] update preview link 2021-05-24 17:31:07 +04:30
Aria Moradi b359116745 clean up tests 2021-05-24 17:09:05 +04:30
Aria Moradi 60073aace3 test new publish 2021-05-24 17:04:47 +04:30
Aria Moradi 874b13fa14 test new publish 2021-05-24 17:02:30 +04:30
Aria Moradi b146d1024b test new publish 2021-05-24 16:56:03 +04:30
Aria Moradi 332e95c021 test new publish 2021-05-24 16:52:30 +04:30
Aria Moradi 1f68141df5 test new publish 2021-05-24 16:39:17 +04:30
Aria Moradi dd731cd306 test new publish 2021-05-24 16:34:12 +04:30
Aria Moradi 38d8d03cae test new publish 2021-05-24 16:30:32 +04:30
Aria Moradi ec7d840f37 test new publish 2021-05-24 16:29:03 +04:30
Aria Moradi 2813dbb897 test new publish 2021-05-24 16:27:46 +04:30
Aria Moradi 77d1402b8a test new publish 2021-05-24 16:25:42 +04:30
Aria Moradi 08e8a9d105 Electron launcher 2021-05-24 15:37:25 +04:30
Aria Moradi 71661f70b6 flexible z names 2021-05-24 01:49:42 +04:30
Aria Moradi ac1e79ba83 electron! 2021-05-24 01:39:12 +04:30
Aria Moradi d082809776 prepare for electron 2021-05-24 00:42:25 +04:30
Aria Moradi a458a696db webview starts! 2021-05-23 23:04:02 +04:30
Aria Moradi 75786a91b0 add webview 2021-05-23 22:10:04 +04:30
234 changed files with 7354 additions and 1354 deletions
-25
View File
@@ -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
+4 -4
View File
@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Cancel previous runs - name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0 uses: styfle/cancel-workflow-action@0.9.0
with: with:
access_token: ${{ github.token }} access_token: ${{ github.token }}
@@ -48,14 +48,14 @@ jobs:
- name: Download android.jar - name: Download android.jar
run: | run: |
cd master cd master
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: | path: |
**/react/node_modules **/webUI/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }} key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
- 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
+53 -8
View File
@@ -25,7 +25,7 @@ jobs:
steps: steps:
- name: Cancel previous runs - name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0 uses: styfle/cancel-workflow-action@0.9.0
with: with:
access_token: ${{ github.token }} access_token: ${{ github.token }}
@@ -50,17 +50,19 @@ jobs:
- name: Download android.jar - name: Download android.jar
run: | run: |
cd master cd master
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: | path: |
**/react/node_modules **/webUI/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }} key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
- 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:
ProductBuildType: "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,30 @@ 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 }}
- name: Run Docker build workflow
run: |
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "preview"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
+14 -7
View File
@@ -24,7 +24,7 @@ jobs:
steps: steps:
- name: Cancel previous runs - name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0 uses: styfle/cancel-workflow-action@0.9.0
with: with:
access_token: ${{ github.token }} access_token: ${{ github.token }}
@@ -49,17 +49,19 @@ jobs:
- name: Download android.jar - name: Download android.jar
run: | run: |
cd master cd master
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: | path: |
**/react/node_modules **/webUI/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }} key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
- 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:
ProductBuildType: "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:
@@ -83,3 +85,8 @@ jobs:
tags: true tags: true
draft: true draft: true
verbose: true verbose: true
- name: Run Docker build workflow
run: |
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${{ secrets.DEPLOY_PREVIEW_TOKEN }}" -d '{"ref":"main", "inputs":{"tachidesk_release_type": "stable"}}' https://api.github.com/repos/suwayomi/docker-tachidesk/actions/workflows/build_container_images.yml/dispatches
+5 -3
View File
@@ -6,9 +6,11 @@ gradle.properties
# Ignore Gradle build output directory # Ignore Gradle build output directory
build build
server/src/main/resources/react server/src/main/resources/webUI
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.tachidesk.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.tachidesk.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
+12
View File
@@ -1,6 +1,7 @@
plugins { plugins {
application application
kotlin("plugin.serialization")
} }
@@ -46,6 +47,17 @@ dependencies {
implementation("org.mozilla:rhino-runtime:1.7.13") implementation("org.mozilla:rhino-runtime:1.7.13")
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn // 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
implementation("org.mozilla:rhino-engine:1.7.13") implementation("org.mozilla:rhino-engine:1.7.13")
// Kotlin wrapper around Java Preferences, makes certain things easier
val multiplatformSettingsVersion = "0.7.7"
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
}
tasks {
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
}
} }
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar') //def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
@@ -38,7 +38,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import xyz.nulldev.androidcompat.info.ApplicationInfoImpl; import xyz.nulldev.androidcompat.info.ApplicationInfoImpl;
import xyz.nulldev.androidcompat.io.AndroidFiles; import xyz.nulldev.androidcompat.io.AndroidFiles;
import xyz.nulldev.androidcompat.io.sharedprefs.JsonSharedPreferences; import xyz.nulldev.androidcompat.io.sharedprefs.JavaSharedPreferences;
import xyz.nulldev.androidcompat.service.ServiceSupport; import xyz.nulldev.androidcompat.service.ServiceSupport;
import xyz.nulldev.androidcompat.util.KodeinGlobalHelper; import xyz.nulldev.androidcompat.util.KodeinGlobalHelper;
@@ -165,23 +165,22 @@ public class CustomContext extends Context implements DIAware {
/** Fake shared prefs! **/ /** Fake shared prefs! **/
private Map<String, SharedPreferences> prefs = new HashMap<>(); //Cache private Map<String, SharedPreferences> prefs = new HashMap<>(); //Cache
private File sharedPrefsFileFromString(String s) {
return new File(androidFiles.getPrefsDir(), s + ".json");
}
@Override @Override
public synchronized SharedPreferences getSharedPreferences(String s, int i) { public synchronized SharedPreferences getSharedPreferences(String s, int i) {
SharedPreferences preferences = prefs.get(s); SharedPreferences preferences = prefs.get(s);
//Create new shared preferences if one does not exist //Create new shared preferences if one does not exist
if(preferences == null) { if(preferences == null) {
preferences = getSharedPreferences(sharedPrefsFileFromString(s), i); preferences = new JavaSharedPreferences(s);
prefs.put(s, preferences); prefs.put(s, preferences);
} }
return preferences; return preferences;
} }
public SharedPreferences getSharedPreferences(File file, int mode) { @Override
return new JsonSharedPreferences(file); public SharedPreferences getSharedPreferences(@NotNull File file, int mode) {
String path = file.getAbsolutePath().replace('\\', '/');
int firstSlash = path.indexOf("/");
return new JavaSharedPreferences(path.substring(firstSlash));
} }
@Override @Override
@@ -191,8 +190,8 @@ public class CustomContext extends Context implements DIAware {
@Override @Override
public boolean deleteSharedPreferences(String name) { public boolean deleteSharedPreferences(String name) {
prefs.remove(name); JavaSharedPreferences item = (JavaSharedPreferences) prefs.remove(name);
return sharedPrefsFileFromString(name).delete(); return item.deleteAll();
} }
@Override @Override
@@ -26,8 +26,6 @@ class AndroidFiles(val configManager: ConfigManager = GlobalConfigManager) {
val downloadCacheDir: File get() = registerFile(filesConfig.downloadCacheDir) val downloadCacheDir: File get() = registerFile(filesConfig.downloadCacheDir)
val databasesDir: File get() = registerFile(filesConfig.databasesDir) val databasesDir: File get() = registerFile(filesConfig.databasesDir)
val prefsDir: File get() = registerFile(filesConfig.prefsDir)
val packagesDir: File get() = registerFile(filesConfig.packageDir) val packagesDir: File get() = registerFile(filesConfig.packageDir)
fun registerFile(file: String): File { fun registerFile(file: String): File {
@@ -0,0 +1,168 @@
package xyz.nulldev.androidcompat.io.sharedprefs
import android.content.SharedPreferences
import com.russhwolf.settings.ExperimentalSettingsApi
import com.russhwolf.settings.ExperimentalSettingsImplementation
import com.russhwolf.settings.JvmPreferencesSettings
import com.russhwolf.settings.serialization.decodeValue
import com.russhwolf.settings.serialization.encodeValue
import com.russhwolf.settings.set
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.SetSerializer
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import java.util.prefs.PreferenceChangeListener
import java.util.prefs.Preferences
@OptIn(ExperimentalSettingsImplementation::class, ExperimentalSerializationApi::class, ExperimentalSettingsApi::class)
class JavaSharedPreferences(key: String) : SharedPreferences {
private val javaPreferences = Preferences.userRoot().node("suwayomi/tachidesk/$key")
private val preferences = JvmPreferencesSettings(javaPreferences)
private val listeners = mutableMapOf<SharedPreferences.OnSharedPreferenceChangeListener, PreferenceChangeListener>()
// TODO: 2021-05-29 Need to find a way to get this working with all pref types
override fun getAll(): MutableMap<String, *> {
return preferences.keys.associateWith { preferences.getStringOrNull(it) }.toMutableMap()
}
override fun getString(key: String, defValue: String?): String? {
return if (defValue != null) {
preferences.getString(key, defValue)
} else {
preferences.getStringOrNull(key)
}
}
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
try {
return if (defValues != null) {
preferences.decodeValue(SetSerializer(String.serializer()).nullable, key, defValues)
} else {
preferences.decodeValue(SetSerializer(String.serializer()).nullable, key, null)
}?.toMutableSet()
} catch (e: SerializationException) {
throw ClassCastException("$key was not a StringSet")
}
}
override fun getInt(key: String, defValue: Int): Int {
return preferences.getInt(key, defValue)
}
override fun getLong(key: String, defValue: Long): Long {
return preferences.getLong(key, defValue)
}
override fun getFloat(key: String, defValue: Float): Float {
return preferences.getFloat(key, defValue)
}
override fun getBoolean(key: String, defValue: Boolean): Boolean {
return preferences.getBoolean(key, defValue)
}
override fun contains(key: String): Boolean {
return key in preferences.keys
}
override fun edit(): SharedPreferences.Editor {
return Editor(preferences)
}
class Editor(private val preferences: JvmPreferencesSettings) : SharedPreferences.Editor {
val itemsToAdd = mutableMapOf<String, Any>()
override fun putString(key: String, value: String?): SharedPreferences.Editor {
if (value != null) {
itemsToAdd[key] = value
} else {
remove(key)
}
return this
}
override fun putStringSet(
key: String,
values: MutableSet<String>?
): SharedPreferences.Editor {
if (values != null) {
itemsToAdd[key] = values
} else {
remove(key)
}
return this
}
override fun putInt(key: String, value: Int): SharedPreferences.Editor {
itemsToAdd[key] = value
return this
}
override fun putLong(key: String, value: Long): SharedPreferences.Editor {
itemsToAdd[key] = value
return this
}
override fun putFloat(key: String, value: Float): SharedPreferences.Editor {
itemsToAdd[key] = value
return this
}
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
itemsToAdd[key] = value
return this
}
override fun remove(key: String): SharedPreferences.Editor {
itemsToAdd.remove(key)
return this
}
override fun clear(): SharedPreferences.Editor {
itemsToAdd.clear()
return this
}
override fun commit(): Boolean {
addToPreferences()
return true
}
override fun apply() {
addToPreferences()
}
private fun addToPreferences() {
itemsToAdd.forEach { (key, value) ->
@Suppress("UNCHECKED_CAST")
when (value) {
is Set<*> -> preferences.encodeValue(SetSerializer(String.serializer()), key, value as Set<String>)
else -> {
preferences[key] = value
}
}
}
}
}
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
val javaListener = PreferenceChangeListener {
listener.onSharedPreferenceChanged(this, it.key)
}
listeners[listener] = javaListener
javaPreferences.addPreferenceChangeListener(javaListener)
}
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
val registeredListener = listeners.remove(listener)
if (registeredListener != null) {
javaPreferences.removePreferenceChangeListener(registeredListener)
}
}
fun deleteAll(): Boolean {
javaPreferences.removeNode()
return true
}
}
+19 -6
View File
@@ -1,7 +1,7 @@
| Build | Stable | Preview | Support Server | | Build | Stable | Preview | Support Server |
|-------|----------|---------|---------| |-------|----------|---------|---------|
| ![CI](https://github.com/Suwayomi/Tachidesk/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Tachidesk.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Tachidesk/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Tachidesk-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk-preview/tree/main/) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) | | ![CI](https://github.com/Suwayomi/Tachidesk/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Tachidesk.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Tachidesk/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Tachidesk-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](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"/>
@@ -23,7 +23,7 @@ Here is a list of current features:
- A library to save your mangas and categories to put them into. - A library to save your mangas and categories to put them into.
- Searching and browsing installed sources. - Searching and browsing installed sources.
- A decent chapter reader. - A decent chapter reader.
- Ability to download Mangas for offline read(This partially works) - Ability to download Mangas for offline read
- Backup and restore support powered by Tachiyomi Legacy Backups - Backup and restore support powered by Tachiyomi Legacy Backups
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting) if it happens. **Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting) if it happens.
@@ -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.
+6 -1
View File
@@ -2,10 +2,11 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") version "1.4.32" kotlin("jvm") version "1.4.32"
kotlin("plugin.serialization") version "1.4.32" apply false
} }
allprojects { allprojects {
group = "ir.armor.tachidesk" group = "suwayomi"
version = "1.0" version = "1.0"
@@ -50,6 +51,10 @@ configure(projects) {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
val kotlinSerializationVersion = "1.1.0"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
// Dependency Injection // Dependency Injection
implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0") implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0")
@@ -1 +1 @@
jre\bin\java -Dir.armor.tachidesk.debugLogsEnabled=true -jar Tachidesk.jar jre\bin\java -Dsuwayomi.tachidesk.server.debugLogsEnabled=true -jar Tachidesk.jar
@@ -0,0 +1 @@
jre\bin\javaw "-Dsuwayomi.tachidesk.server.webInterface=electron" "-Dsuwayomi.tachidesk.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
Binary file not shown.
Binary file not shown.
+83
View File
@@ -0,0 +1,83 @@
#!/bin/bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
electron_version="v12.0.9"
if [ $1 = "win32" ]; then
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
arch="win32"
electron="electron-$electron_version-win32-ia32.zip"
else
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
arch="win64"
electron="electron-$electron_version-win32-x64.zip"
fi
jre_dir="jdk8u292-b10-jre"
echo "creating windows bundle"
jar=$(ls ../server/build/*.jar | tail -n1)
jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | sed 's/.jar//')-$arch
# make release dir
mkdir $release_name
echo "Dealing with jre..."
if [ ! -f $jre ]; then
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/$jre" -o $jre
fi
unzip $jre
mv $jre_dir $release_name/jre
echo "Dealing with electron"
if [ ! -f $electron ]; then
curl -L "https://github.com/electron/electron/releases/download/$electron_version/$electron" -o $electron
fi
unzip $electron -d $release_name/electron
# change electron's icon
rcedit="rcedit-x86.exe"
if [ ! -f $rcedit ]; then
curl -L "https://github.com/electron/rcedit/releases/download/v1.1.1/$rcedit" -o $rcedit
fi
# check if running under github actions
if [ $CI = true ]; then
# change electron executable's icon
sudo dpkg --add-architecture i386
wget -qO - https://dl.winehq.org/wine-builds/winehq.key | sudo apt-key add -
sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport
sudo apt-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu $(lsb_release -cs) main"
sudo apt install --install-recommends winehq-stable
fi
# this script assumes that wine is installed here on out
WINEARCH=win32 wine $rcedit $release_name/electron/electron.exe --set-icon ../server/src/main/resources/icon/faviconlogo.ico
# copy artifacts
cp $jar $release_name/Tachidesk.jar
#cp "resources/Tachidesk Launcher-$arch.exe" "$release_name/Tachidesk Launcher.exe"
cp "resources/Tachidesk Browser Launcher.bat" $release_name
cp "resources/Tachidesk Debug Launcher.bat" $release_name
cp "resources/Tachidesk Electron Launcher.bat" $release_name
zip_name=$release_name.zip
zip -9 -r $zip_name $release_name
rm -rf $release_name
# clean up from possible previous runs
if [ -f ../server/build/$zip_name ]; then
rm ../server/build/$zip_name
fi
mv $zip_name ../server/build/
-38
View File
@@ -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
-41
View File
@@ -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/
-38
View File
@@ -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
-41
View File
@@ -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 -11
View File
@@ -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.tachidesk.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 = System.getenv("ProductVersion") ?: "v0.4.3"
// counts commit count on master // counts commit count on master
val tachideskRevision = Runtime val tachideskRevision = runCatching {
System.getenv("ProductRevision") ?: Runtime
.getRuntime() .getRuntime()
.exec("git rev-list HEAD --count") .exec("git rev-list HEAD --count")
.let { process -> .let { process ->
@@ -99,20 +107,22 @@ val tachideskRevision = Runtime
} }
process.destroy() process.destroy()
"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("ProductBuildType") == "Stable") "Stable" else "Preview")
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk")
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA")
} }
tasks { 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"
}
}
@@ -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) }
}
@@ -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,3 +0,0 @@
package ir.armor.tachidesk.impl.backup.legacy.models
data class DHistory(val url: String, val lastRead: Long)
@@ -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,43 +0,0 @@
package ir.armor.tachidesk.model.database.table
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
object ChapterTable : IntIdTable() {
val url = varchar("url", 2048)
val name = varchar("name", 512)
val date_upload = long("date_upload").default(0)
val chapter_number = float("chapter_number").default(-1f)
val scanlator = varchar("scanlator", 128).nullable()
val isRead = bool("read").default(false)
val isBookmarked = bool("bookmark").default(false)
val lastPageRead = integer("last_page_read").default(0)
// index is reserved by a function
val chapterIndex = integer("index")
val manga = reference("manga", MangaTable)
}
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
ChapterDataClass(
chapterEntry[this.url],
chapterEntry[this.name],
chapterEntry[this.date_upload],
chapterEntry[this.chapter_number],
chapterEntry[this.scanlator],
chapterEntry[this.manga].value,
chapterEntry[this.isRead],
chapterEntry[this.isBookmarked],
chapterEntry[this.lastPageRead],
chapterEntry[this.chapterIndex],
)
@@ -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
}
}
@@ -1,4 +1,4 @@
package ir.armor.tachidesk package suwayomi.tachidesk
/* /*
* 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.tachidesk.server.JavalinSetup.javalinSetup
import ir.armor.tachidesk.server.applicationSetup import suwayomi.tachidesk.server.applicationSetup
fun main() { fun main() {
applicationSetup() applicationSetup()
@@ -0,0 +1,379 @@
package suwayomi.tachidesk.anime
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.Javalin
import suwayomi.tachidesk.anime.impl.Anime.getAnime
import suwayomi.tachidesk.anime.impl.Anime.getAnimeThumbnail
import suwayomi.tachidesk.anime.impl.AnimeList.getAnimeList
import suwayomi.tachidesk.anime.impl.Episode.getEpisode
import suwayomi.tachidesk.anime.impl.Episode.getEpisodeList
import suwayomi.tachidesk.anime.impl.Episode.modifyEpisode
import suwayomi.tachidesk.anime.impl.Source.getAnimeSource
import suwayomi.tachidesk.anime.impl.Source.getSourceList
import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIcon
import suwayomi.tachidesk.anime.impl.extension.Extension.installExtension
import suwayomi.tachidesk.anime.impl.extension.Extension.uninstallExtension
import suwayomi.tachidesk.anime.impl.extension.Extension.updateExtension
import suwayomi.tachidesk.anime.impl.extension.ExtensionsList.getExtensionList
import suwayomi.tachidesk.server.JavalinSetup.future
object AnimeAPI {
fun defineEndpoints(app: Javalin) {
// list all extensions
app.get("/api/v1/anime/extension/list") { ctx ->
ctx.json(
future {
getExtensionList()
}
)
}
// install extension identified with "pkgName"
app.get("/api/v1/anime/extension/install/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName")
ctx.json(
future {
installExtension(pkgName)
}
)
}
// update extension identified with "pkgName"
app.get("/api/v1/anime/extension/update/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName")
ctx.json(
future {
updateExtension(pkgName)
}
)
}
// uninstall extension identified with "pkgName"
app.get("/api/v1/anime/extension/uninstall/:pkgName") { ctx ->
val pkgName = ctx.pathParam("pkgName")
uninstallExtension(pkgName)
ctx.status(200)
}
// icon for extension named `apkName`
app.get("/api/v1/anime/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
val apkName = ctx.pathParam("apkName")
ctx.result(
future { getExtensionIcon(apkName) }
.thenApply {
ctx.header("content-type", it.second)
it.first
}
)
}
// list of sources
app.get("/api/v1/anime/source/list") { ctx ->
ctx.json(getSourceList())
}
// fetch source with id `sourceId`
app.get("/api/v1/anime/source/:sourceId") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(getAnimeSource(sourceId))
}
// popular animes from source with id `sourceId`
app.get("/api/v1/anime/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(
future {
getAnimeList(sourceId, pageNum, popular = true)
}
)
}
// latest animes from source with id `sourceId`
app.get("/api/v1/anime/source/:sourceId/latest/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(
future {
getAnimeList(sourceId, pageNum, popular = false)
}
)
}
// get anime info
app.get("/api/v1/anime/anime/:animeId/") { ctx ->
val animeId = ctx.pathParam("animeId").toInt()
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
ctx.json(
future {
getAnime(animeId, onlineFetch)
}
)
}
// anime thumbnail
app.get("api/v1/anime/anime/:animeId/thumbnail") { ctx ->
val animeId = ctx.pathParam("animeId").toInt()
ctx.result(
future { getAnimeThumbnail(animeId) }
.thenApply {
ctx.header("content-type", it.second)
it.first
}
)
}
//
// // list manga's categories
// app.get("api/v1/manga/:mangaId/category/") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
// ctx.json(getMangaCategories(mangaId))
// }
//
// // adds the manga to category
// app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
// val categoryId = ctx.pathParam("categoryId").toInt()
// addMangaToCategory(mangaId, categoryId)
// ctx.status(200)
// }
//
// // removes the manga from the category
// app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
// val categoryId = ctx.pathParam("categoryId").toInt()
// removeMangaFromCategory(mangaId, categoryId)
// ctx.status(200)
// }
//
// get episode list when showing a anime
app.get("/api/v1/anime/anime/:animeId/episodes") { ctx ->
val animeId = ctx.pathParam("animeId").toInt()
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean()
ctx.json(future { getEpisodeList(animeId, onlineFetch) })
}
// used to display a episode, get a episode in order to show it's <Quality pending>
app.get("/api/v1/anime/anime/:animeId/episode/:episodeIndex") { ctx ->
val episodeIndex = ctx.pathParam("episodeIndex").toInt()
val animeId = ctx.pathParam("animeId").toInt()
ctx.json(future { getEpisode(episodeIndex, animeId) })
}
// used to modify a episode's parameters
app.patch("/api/v1/anime/anime/:animeId/episode/:episodeIndex") { ctx ->
val episodeIndex = ctx.pathParam("episodeIndex").toInt()
val animeId = ctx.pathParam("animeId").toInt()
val read = ctx.formParam("read")?.toBoolean()
val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
modifyEpisode(animeId, episodeIndex, read, bookmarked, markPrevRead, lastPageRead)
ctx.status(200)
}
//
// // get page at index "index"
// app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
// val chapterIndex = ctx.pathParam("chapterIndex").toInt()
// val index = ctx.pathParam("index").toInt()
//
// ctx.result(
// JavalinSetup.future { getPageImage(mangaId, chapterIndex, index) }
// .thenApply {
// ctx.header("content-type", it.second)
// it.first
// }
// )
// }
//
// // submit a chapter for download
// app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
// // TODO
// }
//
// // cancel a chapter download
// app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
// // TODO
// }
//
// // global search, Not implemented yet
// app.get("/api/v1/search/:searchTerm") { ctx ->
// val searchTerm = ctx.pathParam("searchTerm")
// ctx.json(sourceGlobalSearch(searchTerm))
// }
//
// // single source search
// app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
// val sourceId = ctx.pathParam("sourceId").toLong()
// val searchTerm = ctx.pathParam("searchTerm")
// val pageNum = ctx.pathParam("pageNum").toInt()
// ctx.json(JavalinSetup.future { sourceSearch(sourceId, searchTerm, pageNum) })
// }
//
// // source filter list
// app.get("/api/v1/source/:sourceId/filters/") { ctx ->
// val sourceId = ctx.pathParam("sourceId").toLong()
// ctx.json(sourceFilters(sourceId))
// }
//
// // adds the manga to library
// app.get("api/v1/manga/:mangaId/library") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
//
// ctx.result(
// JavalinSetup.future { addMangaToLibrary(mangaId) }
// )
// }
//
// // removes the manga from the library
// app.delete("api/v1/manga/:mangaId/library") { ctx ->
// val mangaId = ctx.pathParam("mangaId").toInt()
//
// ctx.result(
// JavalinSetup.future { removeMangaFromLibrary(mangaId) }
// )
// }
//
// // lists mangas that have no category assigned
// app.get("/api/v1/library/") { ctx ->
// ctx.json(getLibraryMangas())
// }
//
// // category list
// app.get("/api/v1/category/") { ctx ->
// ctx.json(Category.getCategoryList())
// }
//
// // category create
// app.post("/api/v1/category/") { ctx ->
// val name = ctx.formParam("name")!!
// Category.createCategory(name)
// ctx.status(200)
// }
//
// // returns some static info of the current app build
// app.get("/api/v1/about/") { ctx ->
// ctx.json(About.getAbout())
// }
//
// // category modification
// app.patch("/api/v1/category/:categoryId") { ctx ->
// val categoryId = ctx.pathParam("categoryId").toInt()
// val name = ctx.formParam("name")
// val isDefault = ctx.formParam("default")?.toBoolean()
// Category.updateCategory(categoryId, name, isDefault)
// ctx.status(200)
// }
//
// // category re-ordering
// app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
// val categoryId = ctx.pathParam("categoryId").toInt()
// val from = ctx.formParam("from")!!.toInt()
// val to = ctx.formParam("to")!!.toInt()
// Category.reorderCategory(categoryId, from, to)
// ctx.status(200)
// }
//
// // category delete
// app.delete("/api/v1/category/:categoryId") { ctx ->
// val categoryId = ctx.pathParam("categoryId").toInt()
// Category.removeCategory(categoryId)
// ctx.status(200)
// }
//
// // returns the manga list associated with a category
// app.get("/api/v1/category/:categoryId") { ctx ->
// val categoryId = ctx.pathParam("categoryId").toInt()
// ctx.json(getCategoryMangaList(categoryId))
// }
//
// // expects a Tachiyomi legacy backup json in the body
// app.post("/api/v1/backup/legacy/import") { ctx ->
// ctx.result(
// future {
// restoreLegacyBackup(ctx.bodyAsInputStream())
// }
// )
// }
//
// // expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json"
// app.post("/api/v1/backup/legacy/import/file") { ctx ->
// ctx.result(
// JavalinSetup.future {
// restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
// }
// )
// }
//
// // returns a Tachiyomi legacy backup json created from the current database as a json body
// app.get("/api/v1/backup/legacy/export") { ctx ->
// ctx.contentType("application/json")
// ctx.result(
// JavalinSetup.future {
// createLegacyBackup(
// BackupFlags(
// includeManga = true,
// includeCategories = true,
// includeChapters = true,
// includeTracking = true,
// includeHistory = true,
// )
// )
// }
// )
// }
//
// // returns a Tachiyomi legacy backup json created from the current database as a file
// app.get("/api/v1/backup/legacy/export/file") { ctx ->
// ctx.contentType("application/json")
// val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm")
// val currentDate = sdf.format(Date())
//
// ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
// ctx.result(
// JavalinSetup.future {
// createLegacyBackup(
// BackupFlags(
// includeManga = true,
// includeCategories = true,
// includeChapters = true,
// includeTracking = true,
// includeHistory = true,
// )
// )
// }
// )
// }
//
// // Download queue stats
// app.ws("/api/v1/downloads") { ws ->
// ws.onConnect { ctx ->
// // TODO: send current stat
// // TODO: add to downlad subscribers
// }
// ws.onMessage {
// // TODO: send current stat
// }
// ws.onClose { ctx ->
// // TODO: remove from subscribers
// }
// }
}
}
@@ -0,0 +1,139 @@
package suwayomi.tachidesk.anime.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.network.GET
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.anime.impl.AnimeList.proxyThumbnailUrl
import suwayomi.tachidesk.anime.impl.Source.getAnimeSource
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.tachidesk.anime.model.dataclass.AnimeDataClass
import suwayomi.tachidesk.anime.model.table.AnimeStatus
import suwayomi.tachidesk.anime.model.table.AnimeTable
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.clearCachedImage
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.getCachedImageResponse
import suwayomi.tachidesk.server.ApplicationDirs
import java.io.InputStream
object Anime {
private fun truncate(text: String?, maxLength: Int): String? {
return if (text?.length ?: 0 > maxLength)
text?.take(maxLength - 3) + "..."
else
text
}
suspend fun getAnime(animeId: Int, onlineFetch: Boolean = false): AnimeDataClass {
var animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
return if (animeEntry[AnimeTable.initialized] && !onlineFetch) {
AnimeDataClass(
animeId,
animeEntry[AnimeTable.sourceReference].toString(),
animeEntry[AnimeTable.url],
animeEntry[AnimeTable.title],
proxyThumbnailUrl(animeId),
true,
animeEntry[AnimeTable.artist],
animeEntry[AnimeTable.author],
animeEntry[AnimeTable.description],
animeEntry[AnimeTable.genre],
AnimeStatus.valueOf(animeEntry[AnimeTable.status]).name,
animeEntry[AnimeTable.inLibrary],
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
false
)
} else { // initialize anime
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
val fetchedAnime = source.fetchAnimeDetails(
SAnime.create().apply {
url = animeEntry[AnimeTable.url]
title = animeEntry[AnimeTable.title]
}
).awaitSingle()
transaction {
AnimeTable.update({ AnimeTable.id eq animeId }) {
it[AnimeTable.initialized] = true
it[AnimeTable.artist] = fetchedAnime.artist
it[AnimeTable.author] = fetchedAnime.author
it[AnimeTable.description] = truncate(fetchedAnime.description, 4096)
it[AnimeTable.genre] = fetchedAnime.genre
it[AnimeTable.status] = fetchedAnime.status
if (fetchedAnime.thumbnail_url != null && fetchedAnime.thumbnail_url.orEmpty().isNotEmpty())
it[AnimeTable.thumbnail_url] = fetchedAnime.thumbnail_url
}
}
clearAnimeThumbnail(animeId)
animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
AnimeDataClass(
animeId,
animeEntry[AnimeTable.sourceReference].toString(),
animeEntry[AnimeTable.url],
animeEntry[AnimeTable.title],
proxyThumbnailUrl(animeId),
true,
fetchedAnime.artist,
fetchedAnime.author,
fetchedAnime.description,
fetchedAnime.genre,
AnimeStatus.valueOf(fetchedAnime.status).name,
animeEntry[AnimeTable.inLibrary],
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
true
)
}
}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
suspend fun getAnimeThumbnail(animeId: Int): Pair<InputStream, String> {
val saveDir = applicationDirs.animeThumbnailsRoot
val fileName = animeId.toString()
return getCachedImageResponse(saveDir, fileName) {
getAnime(animeId) // make sure is initialized
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
val sourceId = animeEntry[AnimeTable.sourceReference]
val source = getAnimeHttpSource(sourceId)
val thumbnailUrl = animeEntry[AnimeTable.thumbnail_url]!!
source.client.newCall(
GET(thumbnailUrl, source.headers)
).await()
}
}
private fun clearAnimeThumbnail(animeId: Int) {
val saveDir = applicationDirs.animeThumbnailsRoot
val fileName = animeId.toString()
clearCachedImage(saveDir, fileName)
}
}
@@ -0,0 +1,102 @@
package suwayomi.tachidesk.anime.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.tachidesk.anime.model.dataclass.AnimeDataClass
import suwayomi.tachidesk.anime.model.dataclass.PagedAnimeListDataClass
import suwayomi.tachidesk.anime.model.table.AnimeStatus
import suwayomi.tachidesk.anime.model.table.AnimeTable
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
object AnimeList {
fun proxyThumbnailUrl(animeId: Int): String {
return "/api/v1/anime/anime/$animeId/thumbnail"
}
suspend fun getAnimeList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedAnimeListDataClass {
val source = getAnimeHttpSource(sourceId)
val animesPage = if (popular) {
source.fetchPopularAnime(pageNum).awaitSingle()
} else {
if (source.supportsLatest)
source.fetchLatestUpdates(pageNum).awaitSingle()
else
throw Exception("Source $source doesn't support latest")
}
return animesPage.processEntries(sourceId)
}
fun AnimesPage.processEntries(sourceId: Long): PagedAnimeListDataClass {
val animesPage = this
val animeList = transaction {
return@transaction animesPage.animes.map { anime ->
val animeEntry = AnimeTable.select { AnimeTable.url eq anime.url }.firstOrNull()
if (animeEntry == null) { // create anime entry
val animeId = AnimeTable.insertAndGetId {
it[url] = anime.url
it[title] = anime.title
it[artist] = anime.artist
it[author] = anime.author
it[description] = anime.description
it[genre] = anime.genre
it[status] = anime.status
it[thumbnail_url] = anime.thumbnail_url
it[sourceReference] = sourceId
}.value
AnimeDataClass(
animeId,
sourceId.toString(),
anime.url,
anime.title,
proxyThumbnailUrl(animeId),
anime.initialized,
anime.artist,
anime.author,
anime.description,
anime.genre,
AnimeStatus.valueOf(anime.status).name
)
} else {
val animeId = animeEntry[AnimeTable.id].value
AnimeDataClass(
animeId,
sourceId.toString(),
anime.url,
anime.title,
proxyThumbnailUrl(animeId),
true,
animeEntry[AnimeTable.artist],
animeEntry[AnimeTable.author],
animeEntry[AnimeTable.description],
animeEntry[AnimeTable.genre],
AnimeStatus.valueOf(animeEntry[AnimeTable.status]).name,
animeEntry[AnimeTable.inLibrary]
)
}
}
}
return PagedAnimeListDataClass(
animeList,
animesPage.hasNextPage
)
}
}
@@ -0,0 +1,241 @@
package suwayomi.tachidesk.anime.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import org.jetbrains.exposed.sql.SortOrder.DESC
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.anime.impl.Anime.getAnime
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.tachidesk.anime.model.dataclass.EpisodeDataClass
import suwayomi.tachidesk.anime.model.table.AnimeTable
import suwayomi.tachidesk.anime.model.table.EpisodeTable
import suwayomi.tachidesk.anime.model.table.toDataClass
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
object Episode {
/** get episode list when showing an anime */
suspend fun getEpisodeList(animeId: Int, onlineFetch: Boolean?): List<EpisodeDataClass> {
return if (onlineFetch == true) {
getSourceEpisodes(animeId)
} else {
transaction {
EpisodeTable.select { EpisodeTable.anime eq animeId }.orderBy(EpisodeTable.episodeIndex to DESC)
.map {
EpisodeTable.toDataClass(it)
}
}.ifEmpty {
// If it was explicitly set to offline dont grab episodes
if (onlineFetch == null) {
getSourceEpisodes(animeId)
} else emptyList()
}
}
}
private suspend fun getSourceEpisodes(animeId: Int): List<EpisodeDataClass> {
val animeDetails = getAnime(animeId)
val source = getAnimeHttpSource(animeDetails.sourceId.toLong())
val episodeList = source.fetchEpisodeList(
SAnime.create().apply {
title = animeDetails.title
url = animeDetails.url
}
).awaitSingle()
val episodeCount = episodeList.count()
transaction {
episodeList.reversed().forEachIndexed { index, fetchedEpisode ->
val episodeEntry = EpisodeTable.select { EpisodeTable.url eq fetchedEpisode.url }.firstOrNull()
if (episodeEntry == null) {
EpisodeTable.insert {
it[url] = fetchedEpisode.url
it[name] = fetchedEpisode.name
it[date_upload] = fetchedEpisode.date_upload
it[episode_number] = fetchedEpisode.episode_number
it[scanlator] = fetchedEpisode.scanlator
it[episodeIndex] = index + 1
it[anime] = animeId
}
} else {
EpisodeTable.update({ EpisodeTable.url eq fetchedEpisode.url }) {
it[name] = fetchedEpisode.name
it[date_upload] = fetchedEpisode.date_upload
it[episode_number] = fetchedEpisode.episode_number
it[scanlator] = fetchedEpisode.scanlator
it[episodeIndex] = index + 1
it[anime] = animeId
}
}
}
}
// clear any orphaned episodes that are in the db but not in `episodeList`
val dbEpisodeCount = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId }.count() }
if (dbEpisodeCount > episodeCount) { // we got some clean up due
val dbEpisodeList = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId } }
dbEpisodeList.forEach {
if (it[EpisodeTable.episodeIndex] >= episodeList.size ||
episodeList[it[EpisodeTable.episodeIndex] - 1].url != it[EpisodeTable.url]
) {
transaction {
// PageTable.deleteWhere { PageTable.episode eq it[EpisodeTable.id] }
EpisodeTable.deleteWhere { EpisodeTable.id eq it[EpisodeTable.id] }
}
}
}
}
val dbEpisodeMap = transaction {
EpisodeTable.select { EpisodeTable.anime eq animeId }
.associateBy({ it[EpisodeTable.url] }, { it })
}
return episodeList.mapIndexed { index, it ->
val dbEpisode = dbEpisodeMap.getValue(it.url)
EpisodeDataClass(
it.url,
it.name,
it.date_upload,
it.episode_number,
it.scanlator,
animeId,
dbEpisode[EpisodeTable.isRead],
dbEpisode[EpisodeTable.isBookmarked],
dbEpisode[EpisodeTable.lastPageRead],
episodeCount - index,
episodeList.size
)
}
}
/** used to display a episode, get a episode in order to show it's video */
suspend fun getEpisode(episodeIndex: Int, animeId: Int): EpisodeDataClass {
val episode = getEpisodeList(animeId, false)
.first { it.index == episodeIndex }
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
val fetchedLinkUrl = source.fetchEpisodeLink(
SEpisode.create().also {
it.url = episode.url
it.name = episode.name
}
).awaitSingle()
return EpisodeDataClass(
episode.url,
episode.name,
episode.uploadDate,
episode.episodeNumber,
episode.scanlator,
animeId,
episode.read,
episode.bookmarked,
episode.lastPageRead,
episode.index,
episode.episodeCount,
fetchedLinkUrl
)
}
// /** used to display a episode, get a episode in order to show it's pages */
// suspend fun getEpisode(episodeIndex: Int, animeId: Int): EpisodeDataClass {
// val episodeEntry = transaction {
// EpisodeTable.select {
// (EpisodeTable.episodeIndex eq episodeIndex) and (EpisodeTable.anime eq animeId)
// }.first()
// }
// val animeEntry = transaction { MangaTable.select { MangaTable.id eq animeId }.first() }
// val source = getAnimeHttpSource(animeEntry[MangaTable.sourceReference])
//
// val pageList = source.fetchPageList(
// SEpisode.create().apply {
// url = episodeEntry[EpisodeTable.url]
// name = episodeEntry[EpisodeTable.name]
// }
// ).awaitSingle()
//
// val episodeId = episodeEntry[EpisodeTable.id].value
// val episodeCount = transaction { EpisodeTable.select { EpisodeTable.anime eq animeId }.count() }
//
// // update page list for this episode
// transaction {
// pageList.forEach { page ->
// val pageEntry = transaction { PageTable.select { (PageTable.episode eq episodeId) and (PageTable.index eq page.index) }.firstOrNull() }
// if (pageEntry == null) {
// PageTable.insert {
// it[index] = page.index
// it[url] = page.url
// it[imageUrl] = page.imageUrl
// it[episode] = episodeId
// }
// } else {
// PageTable.update({ (PageTable.episode eq episodeId) and (PageTable.index eq page.index) }) {
// it[url] = page.url
// it[imageUrl] = page.imageUrl
// }
// }
// }
// }
//
// return EpisodeDataClass(
// episodeEntry[EpisodeTable.url],
// episodeEntry[EpisodeTable.name],
// episodeEntry[EpisodeTable.date_upload],
// episodeEntry[EpisodeTable.episode_number],
// episodeEntry[EpisodeTable.scanlator],
// animeId,
// episodeEntry[EpisodeTable.isRead],
// episodeEntry[EpisodeTable.isBookmarked],
// episodeEntry[EpisodeTable.lastPageRead],
//
// episodeEntry[EpisodeTable.episodeIndex],
// episodeCount.toInt(),
// pageList.count()
// )
// }
fun modifyEpisode(animeId: Int, episodeIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
transaction {
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
EpisodeTable.update({ (EpisodeTable.anime eq animeId) and (EpisodeTable.episodeIndex eq episodeIndex) }) { update ->
isRead?.also {
update[EpisodeTable.isRead] = it
}
isBookmarked?.also {
update[EpisodeTable.isBookmarked] = it
}
lastPageRead?.also {
update[EpisodeTable.lastPageRead] = it
}
}
}
markPrevRead?.let {
EpisodeTable.update({ (EpisodeTable.anime eq animeId) and (EpisodeTable.episodeIndex less episodeIndex) }) {
it[EpisodeTable.isRead] = markPrevRead
}
}
}
}
}
@@ -0,0 +1,50 @@
package suwayomi.tachidesk.anime.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import mu.KotlinLogging
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIconUrl
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.tachidesk.anime.model.dataclass.AnimeSourceDataClass
import suwayomi.tachidesk.anime.model.table.AnimeExtensionTable
import suwayomi.tachidesk.anime.model.table.AnimeSourceTable
object Source {
private val logger = KotlinLogging.logger {}
fun getSourceList(): List<AnimeSourceDataClass> {
return transaction {
AnimeSourceTable.selectAll().map {
AnimeSourceDataClass(
it[AnimeSourceTable.id].value.toString(),
it[AnimeSourceTable.name],
it[AnimeSourceTable.lang],
getExtensionIconUrl(AnimeExtensionTable.select { AnimeExtensionTable.id eq it[AnimeSourceTable.extension] }.first()[AnimeExtensionTable.apkName]),
getAnimeHttpSource(it[AnimeSourceTable.id].value).supportsLatest
)
}
}
}
fun getAnimeSource(sourceId: Long): AnimeSourceDataClass {
return transaction {
val source = AnimeSourceTable.select { AnimeSourceTable.id eq sourceId }.firstOrNull()
AnimeSourceDataClass(
sourceId.toString(),
source?.get(AnimeSourceTable.name),
source?.get(AnimeSourceTable.lang),
source?.let { AnimeExtensionTable.select { AnimeExtensionTable.id eq source[AnimeSourceTable.extension] }.first()[AnimeExtensionTable.iconUrl] },
source?.let { getAnimeHttpSource(sourceId).supportsLatest }
)
}
}
}
@@ -0,0 +1,251 @@
package suwayomi.tachidesk.anime.impl.extension
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.net.Uri
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import mu.KotlinLogging
import okhttp3.Request
import okio.buffer
import okio.sink
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.anime.impl.extension.ExtensionsList.extensionTableAsDataClass
import suwayomi.tachidesk.anime.impl.extension.github.ExtensionGithubApi
import suwayomi.tachidesk.anime.impl.util.PackageTools.EXTENSION_FEATURE
import suwayomi.tachidesk.anime.impl.util.PackageTools.LIB_VERSION_MAX
import suwayomi.tachidesk.anime.impl.util.PackageTools.LIB_VERSION_MIN
import suwayomi.tachidesk.anime.impl.util.PackageTools.METADATA_NSFW
import suwayomi.tachidesk.anime.impl.util.PackageTools.METADATA_SOURCE_CLASS
import suwayomi.tachidesk.anime.impl.util.PackageTools.dex2jar
import suwayomi.tachidesk.anime.impl.util.PackageTools.getPackageInfo
import suwayomi.tachidesk.anime.impl.util.PackageTools.getSignatureHash
import suwayomi.tachidesk.anime.impl.util.PackageTools.loadExtensionSources
import suwayomi.tachidesk.anime.impl.util.PackageTools.trustedSignatures
import suwayomi.tachidesk.anime.model.table.AnimeExtensionTable
import suwayomi.tachidesk.anime.model.table.AnimeSourceTable
import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.getCachedImageResponse
import suwayomi.tachidesk.server.ApplicationDirs
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
object Extension {
private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
data class InstallableAPK(
val apkFilePath: String,
val pkgName: String
)
suspend fun installExtension(pkgName: String): Int {
logger.debug("Installing $pkgName")
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
return installAPK {
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
val apkName = Uri.parse(apkURL).lastPathSegment!!
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
// download apk file
downloadAPKFile(apkURL, apkSavePath)
apkSavePath
}
}
suspend fun installAPK(fetcher: suspend () -> String): Int {
val apkFilePath = fetcher()
val apkName = File(apkFilePath).name
// check if we don't have the extension already installed
// if it's installed and we want to update, it first has to be uninstalled
val isInstalled = transaction {
AnimeExtensionTable.select { AnimeExtensionTable.apkName eq apkName }.firstOrNull()
}?.get(AnimeExtensionTable.isInstalled) ?: false
if (!isInstalled) {
val fileNameWithoutType = apkName.substringBefore(".apk")
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
val jarFilePath = "$dirPathWithoutType.jar"
val dexFilePath = "$dirPathWithoutType.dex"
val packageInfo = getPackageInfo(apkFilePath)
val pkgName = packageInfo.packageName
if (!packageInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }) {
throw Exception("This apk is not a Tachiyomi extension")
}
// Validate lib version
val libVersion = packageInfo.versionName.substringBeforeLast('.').toDouble()
if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
throw Exception(
"Lib version is $libVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
)
}
val signatureHash = getSignatureHash(packageInfo)
if (signatureHash == null) {
throw Exception("Package $pkgName isn't signed")
} else if (signatureHash !in trustedSignatures) {
// TODO: allow trusting keys
throw Exception("This apk is not a signed with the official tachiyomi signature")
}
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1"
val className = packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
logger.debug("Main class for extension is $className")
dex2jar(apkFilePath, jarFilePath, fileNameWithoutType)
// clean up
// File(apkFilePath).delete()
File(dexFilePath).delete()
// collect sources from the extension
val sources: List<AnimeCatalogueSource> = when (val instance = loadExtensionSources(jarFilePath, className)) {
is AnimeSource -> listOf(instance)
is AnimeSourceFactory -> instance.createSources()
else -> throw RuntimeException("Unknown source class type! ${instance.javaClass}")
}.map { it as AnimeCatalogueSource }
val langs = sources.map { it.lang }.toSet()
val extensionLang = when (langs.size) {
0 -> ""
1 -> langs.first()
else -> "all"
}
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
// update extension info
transaction {
if (AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
AnimeExtensionTable.insert {
it[this.apkName] = apkName
it[name] = extensionName
it[this.pkgName] = packageInfo.packageName
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode
it[lang] = extensionLang
it[this.isNsfw] = isNsfw
}
}
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq pkgName }) {
it[this.isInstalled] = true
it[this.classFQName] = className
}
val extensionId = AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.first()[AnimeExtensionTable.id].value
sources.forEach { httpSource ->
AnimeSourceTable.insert {
it[id] = httpSource.id
it[name] = httpSource.name
it[lang] = httpSource.lang
it[extension] = extensionId
}
logger.debug("Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}")
}
}
return 201 // we installed successfully
} else {
return 302 // extension was already installed
}
}
private val network: NetworkHelper by injectLazy()
private suspend fun downloadAPKFile(url: String, savePath: String) {
val request = Request.Builder().url(url).build()
val response = network.client.newCall(request).await()
val downloadedFile = File(savePath)
downloadedFile.sink().buffer().use { sink ->
response.body!!.source().use { source ->
sink.writeAll(source)
sink.flush()
}
}
}
fun uninstallExtension(pkgName: String) {
logger.debug("Uninstalling $pkgName")
val extensionRecord = transaction { AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq pkgName }.first() }
val fileNameWithoutType = extensionRecord[AnimeExtensionTable.apkName].substringBefore(".apk")
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
transaction {
val extensionId = extensionRecord[AnimeExtensionTable.id].value
AnimeSourceTable.deleteWhere { AnimeSourceTable.extension eq extensionId }
if (extensionRecord[AnimeExtensionTable.isObsolete])
AnimeExtensionTable.deleteWhere { AnimeExtensionTable.pkgName eq pkgName }
else
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq pkgName }) {
it[isInstalled] = false
}
}
if (File(jarPath).exists()) {
File(jarPath).delete()
}
}
suspend fun updateExtension(pkgName: String): Int {
val targetExtension = ExtensionsList.updateMap.remove(pkgName)!!
uninstallExtension(pkgName)
transaction {
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq pkgName }) {
it[name] = targetExtension.name
it[versionName] = targetExtension.versionName
it[versionCode] = targetExtension.versionCode
it[lang] = targetExtension.lang
it[isNsfw] = targetExtension.isNsfw
it[apkName] = targetExtension.apkName
it[iconUrl] = targetExtension.iconUrl
it[hasUpdate] = false
}
}
return installExtension(pkgName)
}
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = transaction { AnimeExtensionTable.select { AnimeExtensionTable.apkName eq apkName }.first() }[AnimeExtensionTable.iconUrl]
val saveDir = "${applicationDirs.extensionsRoot}/icon"
return getCachedImageResponse(saveDir, apkName) {
network.client.newCall(
GET(iconUrl)
).await()
}
}
fun getExtensionIconUrl(apkName: String): String {
return "/api/v1/anime/extension/icon/$apkName"
}
}
@@ -0,0 +1,132 @@
package suwayomi.tachidesk.anime.impl.extension
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import mu.KotlinLogging
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIconUrl
import suwayomi.tachidesk.anime.impl.extension.github.ExtensionGithubApi
import suwayomi.tachidesk.anime.impl.extension.github.OnlineExtension
import suwayomi.tachidesk.anime.model.dataclass.AnimeExtensionDataClass
import suwayomi.tachidesk.anime.model.table.AnimeExtensionTable
import java.util.concurrent.ConcurrentHashMap
object ExtensionsList {
private val logger = KotlinLogging.logger {}
var lastUpdateCheck: Long = 0
var updateMap = ConcurrentHashMap<String, OnlineExtension>()
/** 60,000 milliseconds = 60 seconds */
private const val ExtensionUpdateDelayTime = 60 * 1000
suspend fun getExtensionList(): List<AnimeExtensionDataClass> {
// update if {ExtensionUpdateDelayTime} seconds has passed or requested offline and database is empty
if (lastUpdateCheck + ExtensionUpdateDelayTime < System.currentTimeMillis()) {
logger.debug("Getting extensions list from the internet")
lastUpdateCheck = System.currentTimeMillis()
val foundExtensions = ExtensionGithubApi.findExtensions()
updateExtensionDatabase(foundExtensions)
} else {
logger.debug("used cached extension list")
}
return extensionTableAsDataClass()
}
fun extensionTableAsDataClass() = transaction {
AnimeExtensionTable.selectAll().map {
AnimeExtensionDataClass(
it[AnimeExtensionTable.apkName],
getExtensionIconUrl(it[AnimeExtensionTable.apkName]),
it[AnimeExtensionTable.name],
it[AnimeExtensionTable.pkgName],
it[AnimeExtensionTable.versionName],
it[AnimeExtensionTable.versionCode],
it[AnimeExtensionTable.lang],
it[AnimeExtensionTable.isNsfw],
it[AnimeExtensionTable.isInstalled],
it[AnimeExtensionTable.hasUpdate],
it[AnimeExtensionTable.isObsolete],
)
}
}
private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
transaction {
foundExtensions.forEach { foundExtension ->
val extensionRecord = AnimeExtensionTable.select { AnimeExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
if (extensionRecord != null) {
if (extensionRecord[AnimeExtensionTable.isInstalled]) {
when {
foundExtension.versionCode > extensionRecord[AnimeExtensionTable.versionCode] -> {
// there is an update
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
it[hasUpdate] = true
}
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
}
foundExtension.versionCode < extensionRecord[AnimeExtensionTable.versionCode] -> {
// some how the user installed an invalid version
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
it[isObsolete] = true
}
}
}
} else {
// extension is not installed so we can overwrite the data without a care
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq foundExtension.pkgName }) {
it[name] = foundExtension.name
it[versionName] = foundExtension.versionName
it[versionCode] = foundExtension.versionCode
it[lang] = foundExtension.lang
it[isNsfw] = foundExtension.isNsfw
it[apkName] = foundExtension.apkName
it[iconUrl] = foundExtension.iconUrl
}
}
} else {
// insert new record
AnimeExtensionTable.insert {
it[name] = foundExtension.name
it[pkgName] = foundExtension.pkgName
it[versionName] = foundExtension.versionName
it[versionCode] = foundExtension.versionCode
it[lang] = foundExtension.lang
it[isNsfw] = foundExtension.isNsfw
it[apkName] = foundExtension.apkName
it[iconUrl] = foundExtension.iconUrl
}
}
}
// deal with obsolete extensions
AnimeExtensionTable.selectAll().forEach { extensionRecord ->
val foundExtension = foundExtensions.find { it.pkgName == extensionRecord[AnimeExtensionTable.pkgName] }
if (foundExtension == null) {
// not in the repo, so this extensions is obsolete
if (extensionRecord[AnimeExtensionTable.isInstalled]) {
// is installed so we should mark it as obsolete
AnimeExtensionTable.update({ AnimeExtensionTable.pkgName eq extensionRecord[AnimeExtensionTable.pkgName] }) {
it[isObsolete] = true
}
} else {
// is not installed so we can remove the record without a care
AnimeExtensionTable.deleteWhere { AnimeExtensionTable.pkgName eq extensionRecord[AnimeExtensionTable.pkgName] }
}
}
}
}
}
}
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.extension.github package suwayomi.tachidesk.anime.impl.extension.github
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -12,32 +12,24 @@ import com.github.salomonbrys.kotson.string
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Interceptor.Chain
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import suwayomi.tachidesk.anime.impl.util.PackageTools.LIB_VERSION_MAX
import okhttp3.internal.http.RealResponseBody import suwayomi.tachidesk.anime.impl.util.PackageTools.LIB_VERSION_MIN
import okio.GzipSource import suwayomi.tachidesk.anime.model.dataclass.AnimeExtensionDataClass
import okio.buffer import suwayomi.tachidesk.manga.impl.util.network.UnzippingInterceptor
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException
object ExtensionGithubApi { object ExtensionGithubApi {
const val BASE_URL = "https://raw.githubusercontent.com" private const val BASE_URL = "https://raw.githubusercontent.com"
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo" private const val REPO_URL_PREFIX = "$BASE_URL/jmir1/tachiyomi-extensions/repo"
private const val LIB_VERSION_MIN = "1.2"
private const val LIB_VERSION_MAX = "1.2"
private fun parseResponse(json: JsonArray): List<OnlineExtension> { private fun parseResponse(json: JsonArray): List<OnlineExtension> {
return json return json
.map { it.asJsonObject } .map { it.asJsonObject }
.filter { element -> .filter { element ->
val versionName = element["version"].string val versionName = element["version"].string
val libVersion = versionName.substringBeforeLast('.') val libVersion = versionName.substringBeforeLast('.').toInt()
libVersion == LIB_VERSION_MAX libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
} }
.map { element -> .map { element ->
val name = element["name"].string.substringAfter("Tachiyomi: ") val name = element["name"].string.substringAfter("Tachiyomi: ")
@@ -58,7 +50,7 @@ object ExtensionGithubApi {
return parseResponse(response) return parseResponse(response)
} }
fun getApkUrl(extension: ExtensionDataClass): String { fun getApkUrl(extension: AnimeExtensionDataClass): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}" return "$REPO_URL_PREFIX/apk/${extension.apkName}"
} }
@@ -76,7 +68,7 @@ object ExtensionGithubApi {
.build() .build()
} }
private fun getRepo(): com.google.gson.JsonArray { private fun getRepo(): JsonArray {
val request = Request.Builder() val request = Request.Builder()
.url("$REPO_URL_PREFIX/index.json.gz") .url("$REPO_URL_PREFIX/index.json.gz")
.build() .build()
@@ -85,35 +77,3 @@ object ExtensionGithubApi {
return JsonParser.parseString(response).asJsonArray return JsonParser.parseString(response).asJsonArray
} }
} }
// ref: https://stackoverflow.com/questions/51901333/okhttp-3-how-to-decompress-gzip-deflate-response-manually-using-java-android
private class UnzippingInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Chain): Response {
val response: Response = chain.proceed(chain.request())
return unzip(response)
}
@Throws(IOException::class)
private fun unzip(response: Response): Response {
if (response.body == null) {
return response
}
// check if we have gzip response
val contentEncoding: String? = response.headers["Content-Encoding"]
// this is used to decompress gzipped responses
return if (contentEncoding != null && contentEncoding == "gzip") {
val body = response.body!!
val contentLength: Long = body.contentLength()
val responseBody = GzipSource(body.source())
val strippedHeaders: Headers = response.headers.newBuilder().build()
response.newBuilder().headers(strippedHeaders)
.body(RealResponseBody(body.contentType().toString(), contentLength, responseBody.buffer()))
.build()
} else {
response
}
}
}
@@ -0,0 +1,19 @@
package suwayomi.tachidesk.anime.impl.extension.github
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class OnlineExtension(
val name: String,
val pkgName: String,
val versionName: String,
val versionCode: Int,
val lang: String,
val isNsfw: Boolean,
val apkName: String,
val iconUrl: String
)
@@ -0,0 +1,57 @@
package suwayomi.tachidesk.anime.impl.util
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.anime.impl.util.PackageTools.loadExtensionSources
import suwayomi.tachidesk.anime.model.table.AnimeExtensionTable
import suwayomi.tachidesk.anime.model.table.AnimeSourceTable
import suwayomi.tachidesk.server.ApplicationDirs
import java.util.concurrent.ConcurrentHashMap
object GetAnimeHttpSource {
private val sourceCache = ConcurrentHashMap<Long, AnimeHttpSource>()
private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getAnimeHttpSource(sourceId: Long): AnimeHttpSource {
val cachedResult: AnimeHttpSource? = sourceCache[sourceId]
if (cachedResult != null) {
return cachedResult
}
val sourceRecord = transaction {
AnimeSourceTable.select { AnimeSourceTable.id eq sourceId }.first()
}
val extensionId = sourceRecord[AnimeSourceTable.extension]
val extensionRecord = transaction {
AnimeExtensionTable.select { AnimeExtensionTable.id eq extensionId }.first()
}
val apkName = extensionRecord[AnimeExtensionTable.apkName]
val className = extensionRecord[AnimeExtensionTable.classFQName]
val jarName = apkName.substringBefore(".apk") + ".jar"
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
when (val instance = loadExtensionSources(jarPath, className)) {
is AnimeSource -> listOf(instance)
is AnimeSourceFactory -> instance.createSources()
else -> throw Exception("Unknown source class type! ${instance.javaClass}")
}.forEach {
sourceCache[it.id] = it as AnimeHttpSource
}
return sourceCache[sourceId]!!
}
}
@@ -0,0 +1,145 @@
package suwayomi.tachidesk.anime.impl.util
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.content.pm.PackageInfo
import android.content.pm.Signature
import android.os.Bundle
import com.googlecode.d2j.dex.Dex2jar
import com.googlecode.d2j.reader.MultiDexFileReader
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
import eu.kanade.tachiyomi.util.lang.Hash
import mu.KotlinLogging
import net.dongliu.apk.parser.ApkFile
import net.dongliu.apk.parser.ApkParsers
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import org.w3c.dom.Element
import org.w3c.dom.Node
import suwayomi.tachidesk.server.ApplicationDirs
import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList
import xyz.nulldev.androidcompat.pm.toPackageInfo
import java.io.File
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
import javax.xml.parsers.DocumentBuilderFactory
object PackageTools {
private val logger = KotlinLogging.logger {}
private val applicationDirs by DI.global.instance<ApplicationDirs>()
const val EXTENSION_FEATURE = "tachiyomi.animeextension"
const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
const val LIB_VERSION_MIN = 10
const val LIB_VERSION_MAX = 10
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key
var trustedSignatures = mutableSetOf<String>() + officialSignature
/**
* Convert dex to jar, a wrapper for the dex2jar library
*/
fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
val jarFilePath = File(jarFile).toPath()
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
val handler = BaksmaliBaseDexExceptionHandler()
Dex2jar
.from(reader)
.withExceptionHandler(handler)
.reUseReg(false)
.topoLogicalSort()
.skipDebug(true)
.optimizeSynchronized(false)
.printIR(false)
.noCode(false)
.skipExceptions(false)
.to(jarFilePath)
if (handler.hasException()) {
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
logger.error(
"""
Detail Error Information in File $errorFile
Please report this file to one of following link if possible (any one).
https://sourceforge.net/p/dex2jar/tickets/
https://bitbucket.org/pxb1988/dex2jar/issues
https://github.com/pxb1988/dex2jar/issues
dex2jar@googlegroups.com
""".trimIndent()
)
handler.dump(errorFile, emptyArray<String>())
}
}
/** A modified version of `xyz.nulldev.androidcompat.pm.InstalledPackage.info` */
fun getPackageInfo(apkFilePath: String): PackageInfo {
val apk = File(apkFilePath)
return ApkParsers.getMetaInfo(apk).toPackageInfo(apk).apply {
val parsed = ApkFile(apk)
val dbFactory = DocumentBuilderFactory.newInstance()
val dBuilder = dbFactory.newDocumentBuilder()
val doc = parsed.manifestXml.byteInputStream().use {
dBuilder.parse(it)
}
logger.debug(parsed.manifestXml)
applicationInfo.metaData = Bundle().apply {
val appTag = doc.getElementsByTagName("application").item(0)
appTag?.childNodes?.toList()
.orEmpty()
.asSequence()
.filter {
it.nodeType == Node.ELEMENT_NODE
}.map {
it as Element
}.filter {
it.tagName == "meta-data"
}.forEach {
putString(
it.attributes.getNamedItem("android:name").nodeValue,
it.attributes.getNamedItem("android:value").nodeValue
)
}
}
signatures = (
parsed.apkSingers.flatMap { it.certificateMetas }
/*+ parsed.apkV2Singers.flatMap { it.certificateMetas }*/
) // Blocked by: https://github.com/hsiafan/apk-parser/issues/72
.map { Signature(it.data) }.toTypedArray()
}
}
fun getSignatureHash(pkgInfo: PackageInfo): String? {
val signatures = pkgInfo.signatures
return if (signatures != null && signatures.isNotEmpty()) {
Hash.sha256(signatures.first().toByteArray())
} else {
null
}
}
/**
* loads the extension main class called $className from the jar located at $jarPath
* It may return an instance of HttpSource or SourceFactory depending on the extension.
*/
fun loadExtensionSources(jarPath: String, className: String): Any {
val classLoader = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")))
val classToLoad = Class.forName(className, false, classLoader)
return classToLoad.getDeclaredConstructor().newInstance()
}
}
@@ -0,0 +1,36 @@
package suwayomi.tachidesk.anime.model.dataclass
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import suwayomi.tachidesk.anime.model.table.AnimeStatus
data class AnimeDataClass(
val id: Int,
val sourceId: String,
val url: String,
val title: String,
val thumbnailUrl: String? = null,
val initialized: Boolean = false,
val artist: String? = null,
val author: String? = null,
val description: String? = null,
val genre: String? = null,
val status: String = AnimeStatus.UNKNOWN.name,
val inLibrary: Boolean = false,
val source: AnimeSourceDataClass? = null,
val freshData: Boolean = false
)
data class PagedAnimeListDataClass(
val mangaList: List<AnimeDataClass>,
val hasNextPage: Boolean
)
@@ -0,0 +1,24 @@
package suwayomi.tachidesk.anime.model.dataclass
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class AnimeExtensionDataClass(
val apkName: String,
val iconUrl: String,
val name: String,
val pkgName: String,
val versionName: String,
val versionCode: Int,
val lang: String,
val isNsfw: Boolean,
val installed: Boolean,
val hasUpdate: Boolean,
val obsolete: Boolean,
)
@@ -0,0 +1,16 @@
package suwayomi.tachidesk.anime.model.dataclass
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class AnimeSourceDataClass(
val id: String,
val name: String?,
val lang: String?,
val iconUrl: String?,
val supportsLatest: Boolean?
)
@@ -0,0 +1,35 @@
package suwayomi.tachidesk.anime.model.dataclass
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class EpisodeDataClass(
val url: String,
val name: String,
val uploadDate: Long,
val episodeNumber: Float,
val scanlator: String?,
val animeId: Int,
/** chapter is read */
val read: Boolean,
/** chapter is bookmarked */
val bookmarked: Boolean,
/** last read page, zero means not read/no data */
val lastPageRead: Int,
/** this chapter's index, starts with 1 */
val index: Int,
/** total episode count, used to calculate if there's a next and prev episode */
val episodeCount: Int? = null,
/** used to construct pages in the front-end */
val linkUrl: String? = null,
)
@@ -0,0 +1,31 @@
package suwayomi.tachidesk.anime.model.table
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
object AnimeExtensionTable : IntIdTable() {
val apkName = varchar("apk_name", 1024)
// default is the local source icon from tachiyomi
val iconUrl = varchar("icon_url", 2048)
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128)
val versionName = varchar("version_name", 16)
val versionCode = integer("version_code")
val lang = varchar("lang", 10)
val isNsfw = bool("is_nsfw")
val isInstalled = bool("is_installed").default(false)
val hasUpdate = bool("has_update").default(false)
val isObsolete = bool("is_obsolete").default(false)
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
}
@@ -0,0 +1,18 @@
package suwayomi.tachidesk.anime.model.table
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IdTable
object AnimeSourceTable : IdTable<Long>() {
override val id = long("id").entityId()
val name = varchar("name", 128)
val lang = varchar("lang", 10)
val extension = reference("extension", AnimeExtensionTable)
val partOfFactorySource = bool("part_of_factory_source").default(false)
}
@@ -0,0 +1,65 @@
package suwayomi.tachidesk.anime.model.table
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.animesource.model.SAnime
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.table.MangaStatus.Companion
object AnimeTable : IntIdTable() {
val url = varchar("url", 2048)
val title = varchar("title", 512)
val initialized = bool("initialized").default(false)
val artist = varchar("artist", 64).nullable()
val author = varchar("author", 64).nullable()
val description = varchar("description", 4096).nullable()
val genre = varchar("genre", 1024).nullable()
val status = integer("status").default(SAnime.UNKNOWN)
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
val inLibrary = bool("in_library").default(false)
val defaultCategory = bool("default_category").default(true)
// source is used by some ancestor of IntIdTable
val sourceReference = long("source")
}
fun AnimeTable.toDataClass(mangaEntry: ResultRow) =
MangaDataClass(
mangaEntry[this.id].value,
mangaEntry[sourceReference].toString(),
mangaEntry[url],
mangaEntry[title],
proxyThumbnailUrl(mangaEntry[this.id].value),
mangaEntry[initialized],
mangaEntry[artist],
mangaEntry[author],
mangaEntry[description],
mangaEntry[genre],
Companion.valueOf(mangaEntry[status]).name,
mangaEntry[inLibrary]
)
enum class AnimeStatus(val status: Int) {
UNKNOWN(0),
ONGOING(1),
COMPLETED(2),
LICENSED(3);
companion object {
fun valueOf(value: Int): AnimeStatus = values().find { it.status == value } ?: UNKNOWN
}
}
@@ -0,0 +1,46 @@
package suwayomi.tachidesk.anime.model.table
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.anime.model.dataclass.EpisodeDataClass
object EpisodeTable : IntIdTable() {
val url = varchar("url", 2048)
val name = varchar("name", 512)
val date_upload = long("date_upload").default(0)
val episode_number = float("episode_number").default(-1f)
val scanlator = varchar("scanlator", 128).nullable()
val isRead = bool("read").default(false)
val isBookmarked = bool("bookmark").default(false)
val lastPageRead = integer("last_page_read").default(0)
// index is reserved by a function
val episodeIndex = integer("index")
val anime = reference("anime", AnimeTable)
}
fun EpisodeTable.toDataClass(episodeEntry: ResultRow) =
EpisodeDataClass(
episodeEntry[url],
episodeEntry[name],
episodeEntry[date_upload],
episodeEntry[episode_number],
episodeEntry[scanlator],
episodeEntry[anime].value,
episodeEntry[isRead],
episodeEntry[isBookmarked],
episodeEntry[lastPageRead],
episodeEntry[episodeIndex],
transaction { EpisodeTable.select { anime eq episodeEntry[anime] }.count().toInt() }
)
@@ -1,110 +1,51 @@
package ir.armor.tachidesk.server package suwayomi.tachidesk.manga
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.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.CategoryManga.addMangaToCategory
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) import suwayomi.tachidesk.manga.impl.CategoryManga.getCategoryMangaList
import suwayomi.tachidesk.manga.impl.CategoryManga.getMangaCategories
private fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> { import suwayomi.tachidesk.manga.impl.CategoryManga.removeMangaFromCategory
return scope.future(block = block) import suwayomi.tachidesk.manga.impl.Chapter.getChapter
} import suwayomi.tachidesk.manga.impl.Chapter.getChapterList
import suwayomi.tachidesk.manga.impl.Chapter.modifyChapter
fun javalinSetup() { import suwayomi.tachidesk.manga.impl.Chapter.modifyChapterMeta
var hasWebUiBundled = false import suwayomi.tachidesk.manga.impl.Library.addMangaToLibrary
import suwayomi.tachidesk.manga.impl.Library.getLibraryMangas
val app = Javalin.create { config -> import suwayomi.tachidesk.manga.impl.Library.removeMangaFromLibrary
try { import suwayomi.tachidesk.manga.impl.Manga.getManga
// if the bellow line throws an exception then webUI is not bundled import suwayomi.tachidesk.manga.impl.Manga.getMangaThumbnail
this::class.java.getResource("/react/index.html") import suwayomi.tachidesk.manga.impl.Manga.modifyMangaMeta
import suwayomi.tachidesk.manga.impl.MangaList.getMangaList
// no exception so we can tell javalin to serve webUI import suwayomi.tachidesk.manga.impl.Page.getPageImage
hasWebUiBundled = true import suwayomi.tachidesk.manga.impl.Search.sourceFilters
config.addStaticFiles("/react") import suwayomi.tachidesk.manga.impl.Search.sourceGlobalSearch
config.addSinglePageRoot("/", "/react/index.html") import suwayomi.tachidesk.manga.impl.Search.sourceSearch
} catch (e: RuntimeException) { import suwayomi.tachidesk.manga.impl.Source.getSource
logger.warn("react build files are missing.") import suwayomi.tachidesk.manga.impl.Source.getSourceList
hasWebUiBundled = false import suwayomi.tachidesk.manga.impl.backup.BackupFlags
} import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
config.enableCorsForAllOrigins() import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
}.start(serverConfig.ip, serverConfig.port) import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIcon
// when JVM is prompted to shutdown, stop javalin gracefully import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
Runtime.getRuntime().addShutdownHook( import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
thread(start = false) { import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
app.stop() import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
} import suwayomi.tachidesk.server.JavalinSetup.future
) import suwayomi.tachidesk.server.impl.About
import java.text.SimpleDateFormat
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) { import java.util.Date
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(
@@ -246,6 +187,18 @@ object JavalinSetup {
ctx.json(future { getChapterList(mangaId, onlineFetch) }) ctx.json(future { getChapterList(mangaId, onlineFetch) })
} }
// used to modify a manga's meta paramaters
app.patch("/api/v1/manga/:mangaId/meta") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val key = ctx.formParam("key")!!
val value = ctx.formParam("value")!!
modifyMangaMeta(mangaId, key, value)
ctx.status(200)
}
// used to display a chapter, get a chapter in order to show it's pages // used to display a chapter, get a chapter in order to show it's pages
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx -> app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").toInt() val chapterIndex = ctx.pathParam("chapterIndex").toInt()
@@ -268,6 +221,19 @@ object JavalinSetup {
ctx.status(200) ctx.status(200)
} }
// used to modify a chapter's meta paramaters
app.patch("/api/v1/manga/:mangaId/chapter/:chapterIndex/meta") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
val key = ctx.formParam("key")!!
val value = ctx.formParam("value")!!
modifyChapterMeta(mangaId, chapterIndex, key, value)
ctx.status(200)
}
// get page at index "index" // get page at index "index"
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx -> app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
@@ -318,7 +284,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 +293,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 +304,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 +324,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 +333,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 +411,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)
}
} }
} }
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package suwayomi.tachidesk.manga.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.manga.impl.CategoryManga.removeMangaFromCategory
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.toDataClass
object Category { object Category {
/** /**
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package suwayomi.tachidesk.manga.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.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.toDataClass
object CategoryManga { object CategoryManga {
fun addMangaToCategory(mangaId: Int, categoryId: Int) { fun addMangaToCategory(mangaId: Int, categoryId: Int) {
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package suwayomi.tachidesk.manga.impl
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -9,14 +9,7 @@ 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 org.jetbrains.exposed.dao.id.EntityID
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 +17,16 @@ 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.manga.impl.Manga.getManga
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.PageTable
import suwayomi.tachidesk.manga.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 +91,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 +125,15 @@ 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,
meta = getChapterMetaMap(dbChapter[ChapterTable.id])
) )
} }
} }
@@ -136,54 +145,69 @@ 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(),
getChapterMetaMap(chapterEntry[ChapterTable.id])
)
} 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 +222,7 @@ object Chapter {
} }
lastPageRead?.also { lastPageRead?.also {
update[ChapterTable.lastPageRead] = it update[ChapterTable.lastPageRead] = it
update[ChapterTable.lastReadAt] = Instant.now().epochSecond
} }
} }
} }
@@ -209,4 +234,30 @@ object Chapter {
} }
} }
} }
fun getChapterMetaMap(chapter: EntityID<Int>): Map<String, String> {
return transaction {
ChapterMetaTable.select { ChapterMetaTable.ref eq chapter }
.associate { it[ChapterMetaTable.key] to it[ChapterMetaTable.value] }
}
}
fun modifyChapterMeta(mangaId: Int, chapterIndex: Int, key: String, value: String) {
transaction {
val chapter = ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }
.first()[ChapterTable.id]
val meta = transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapter) and (ChapterMetaTable.key eq key) } }.firstOrNull()
if (meta == null) {
ChapterMetaTable.insert {
it[ChapterMetaTable.key] = key
it[ChapterMetaTable.value] = value
it[ChapterMetaTable.ref] = chapter
}
} else {
ChapterMetaTable.update {
it[ChapterMetaTable.value] = value
}
}
}
}
} }
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package suwayomi.tachidesk.manga.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.manga.impl.Manga.getManga
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.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,
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package suwayomi.tachidesk.manga.impl
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -9,23 +9,27 @@ 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 org.jetbrains.exposed.dao.id.EntityID
import ir.armor.tachidesk.impl.Source.getSource import org.jetbrains.exposed.sql.and
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import org.jetbrains.exposed.sql.insert
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.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
import suwayomi.tachidesk.manga.impl.Source.getSource
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.network.await
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.clearCachedImage
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.getCachedImageResponse
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.ApplicationDirs
import java.io.InputStream import java.io.InputStream
object Manga { object Manga {
@@ -57,6 +61,7 @@ object Manga {
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary], mangaEntry[MangaTable.inLibrary],
getSource(mangaEntry[MangaTable.sourceReference]), getSource(mangaEntry[MangaTable.sourceReference]),
getMangaMetaMap(mangaEntry[MangaTable.id]),
false false
) )
} else { // initialize manga } else { // initialize manga
@@ -102,16 +107,43 @@ 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]),
getMangaMetaMap(mangaEntry[MangaTable.id]),
true true
) )
} }
} }
fun getMangaMetaMap(manga: EntityID<Int>): Map<String, String> {
return transaction {
MangaMetaTable.select { MangaMetaTable.ref eq manga }
.associate { it[MangaMetaTable.key] to it[MangaMetaTable.value] }
}
}
fun modifyMangaMeta(mangaId: Int, key: String, value: String) {
transaction {
val manga = MangaMetaTable.select { (MangaTable.id eq mangaId) }
.first()[MangaTable.id]
val meta = transaction { MangaMetaTable.select { (MangaMetaTable.ref eq manga) and (MangaMetaTable.key eq key) } }.firstOrNull()
if (meta == null) {
MangaMetaTable.insert {
it[MangaMetaTable.key] = key
it[MangaMetaTable.value] = value
it[MangaMetaTable.ref] = manga
}
} else {
MangaMetaTable.update {
it[MangaMetaTable.value] = value
}
}
}
}
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 +163,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)
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package suwayomi.tachidesk.manga.impl
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -8,15 +8,16 @@ 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.manga.impl.Manga.getMangaMetaMap
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable
object MangaList { object MangaList {
fun proxyThumbnailUrl(mangaId: Int): String { fun proxyThumbnailUrl(mangaId: Int): String {
@@ -89,7 +90,8 @@ object MangaList {
mangaEntry[MangaTable.description], mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre], mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary] mangaEntry[MangaTable.inLibrary],
meta = getMangaMetaMap(mangaEntry[MangaTable.id])
) )
} }
} }
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package suwayomi.tachidesk.manga.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.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.impl.util.storage.CachedImageResponse.getCachedImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.SafePath
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.PageTable
import suwayomi.tachidesk.server.ApplicationDirs
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package suwayomi.tachidesk.manga.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.manga.impl.MangaList.processEntries
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
object Search { object Search {
// TODO // TODO
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl package suwayomi.tachidesk.manga.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.manga.impl.extension.Extension.getExtensionIconUrl
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable
object Source { object Source {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup package suwayomi.tachidesk.manga.impl.backup
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.legacy package suwayomi.tachidesk.manga.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.manga.impl.backup.legacy.models.DHistory
import ir.armor.tachidesk.impl.backup.legacy.serializer.CategoryTypeAdapter import suwayomi.tachidesk.manga.impl.backup.legacy.serializer.CategoryTypeAdapter
import ir.armor.tachidesk.impl.backup.legacy.serializer.ChapterTypeAdapter import suwayomi.tachidesk.manga.impl.backup.legacy.serializer.ChapterTypeAdapter
import ir.armor.tachidesk.impl.backup.legacy.serializer.HistoryTypeAdapter import suwayomi.tachidesk.manga.impl.backup.legacy.serializer.HistoryTypeAdapter
import ir.armor.tachidesk.impl.backup.legacy.serializer.MangaTypeAdapter import suwayomi.tachidesk.manga.impl.backup.legacy.serializer.MangaTypeAdapter
import ir.armor.tachidesk.impl.backup.legacy.serializer.TrackTypeAdapter import suwayomi.tachidesk.manga.impl.backup.legacy.serializer.TrackTypeAdapter
import ir.armor.tachidesk.impl.backup.models.CategoryImpl import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl
import ir.armor.tachidesk.impl.backup.models.ChapterImpl import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
import ir.armor.tachidesk.impl.backup.models.MangaImpl import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl
import ir.armor.tachidesk.impl.backup.models.TrackImpl import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
import java.util.Date import java.util.Date
open class LegacyBackupBase { open class LegacyBackupBase {
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.legacy package suwayomi.tachidesk.manga.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.manga.impl.Category.getCategoryList
import suwayomi.tachidesk.manga.impl.CategoryManga.getMangaCategories
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.legacy.models.Backup
import suwayomi.tachidesk.manga.impl.backup.legacy.models.Backup.CURRENT_VERSION
import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl
import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
import suwayomi.tachidesk.manga.impl.backup.models.Manga
import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
object LegacyBackupExport : LegacyBackupBase() { object LegacyBackupExport : LegacyBackupBase() {
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.legacy package suwayomi.tachidesk.manga.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.manga.impl.Category.createCategory
import suwayomi.tachidesk.manga.impl.Category.getCategoryList
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupValidator.ValidationResult
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupValidator.validate
import suwayomi.tachidesk.manga.impl.backup.legacy.models.Backup
import suwayomi.tachidesk.manga.impl.backup.legacy.models.DHistory
import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl
import suwayomi.tachidesk.manga.impl.backup.models.Chapter
import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl
import suwayomi.tachidesk.manga.impl.backup.models.Manga
import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl
import suwayomi.tachidesk.manga.impl.backup.models.Track
import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
import suwayomi.tachidesk.manga.impl.util.GetHttpSource.getHttpSource
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.model.table.MangaTable
import java.io.InputStream import java.io.InputStream
import java.util.Date import java.util.Date
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.legacy package suwayomi.tachidesk.manga.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.manga.impl.backup.legacy.models.Backup
import suwayomi.tachidesk.manga.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,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.legacy.models package suwayomi.tachidesk.manga.impl.backup.legacy.models
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@@ -0,0 +1,3 @@
package suwayomi.tachidesk.manga.impl.backup.legacy.models
data class DHistory(val url: String, val lastRead: Long)
@@ -1,8 +1,8 @@
package ir.armor.tachidesk.impl.backup.legacy.serializer package suwayomi.tachidesk.manga.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.manga.impl.backup.models.CategoryImpl
/** /**
* JSON Serializer used to write / read [CategoryImpl] to / from json * JSON Serializer used to write / read [CategoryImpl] to / from json
@@ -1,9 +1,9 @@
package ir.armor.tachidesk.impl.backup.legacy.serializer package suwayomi.tachidesk.manga.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.manga.impl.backup.models.ChapterImpl
/** /**
* JSON Serializer used to write / read [ChapterImpl] to / from json * JSON Serializer used to write / read [ChapterImpl] to / from json
@@ -1,8 +1,8 @@
package ir.armor.tachidesk.impl.backup.legacy.serializer package suwayomi.tachidesk.manga.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.legacy.models.DHistory import suwayomi.tachidesk.manga.impl.backup.legacy.models.DHistory
/** /**
* JSON Serializer used to write / read [DHistory] to / from json * JSON Serializer used to write / read [DHistory] to / from json
@@ -1,8 +1,8 @@
package ir.armor.tachidesk.impl.backup.legacy.serializer package suwayomi.tachidesk.manga.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.MangaImpl import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl
/** /**
* JSON Serializer used to write / read [MangaImpl] to / from json * JSON Serializer used to write / read [MangaImpl] to / from json
@@ -1,9 +1,9 @@
package ir.armor.tachidesk.impl.backup.legacy.serializer package suwayomi.tachidesk.manga.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.TrackImpl import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl
/** /**
* JSON Serializer used to write / read [TrackImpl] to / from json * JSON Serializer used to write / read [TrackImpl] to / from json
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
import java.io.Serializable import java.io.Serializable
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
class CategoryImpl : Category { class CategoryImpl : Category {
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import java.io.Serializable import java.io.Serializable
@@ -1,7 +1,7 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
import ir.armor.tachidesk.model.database.table.ChapterTable
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import suwayomi.tachidesk.manga.model.table.ChapterTable
class ChapterImpl : Chapter { class ChapterImpl : Chapter {
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
import java.io.Serializable import java.io.Serializable
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
/** /**
* Object containing the history statistics of a chapter * Object containing the history statistics of a chapter
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
class LibraryManga : MangaImpl() { class LibraryManga : MangaImpl() {
@@ -1,6 +1,7 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
// import tachiyomi.source.model.MangaInfo // import tachiyomi.source.model.MangaInfo
interface Manga : SManga { interface Manga : SManga {
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
class MangaCategory { class MangaCategory {
@@ -1,3 +1,3 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
class MangaChapter(val manga: Manga, val chapter: Chapter) class MangaChapter(val manga: Manga, val chapter: Chapter)
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
/** /**
* Object containing manga, chapter and history * Object containing manga, chapter and history
@@ -1,7 +1,7 @@
package ir.armor.tachidesk.impl.backup.models package suwayomi.tachidesk.manga.impl.backup.models
import ir.armor.tachidesk.model.database.table.MangaTable
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import suwayomi.tachidesk.manga.model.table.MangaTable
open class MangaImpl : Manga { open class MangaImpl : Manga {

Some files were not shown because too many files have changed in this diff Show More