Compare commits
212 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc2072e81f | |||
| f36bc3f643 | |||
| f7901ad843 | |||
| 3771030ed6 | |||
| 57197e58b5 | |||
| ac601399ac | |||
| 6a0e221153 | |||
| 6a949fc851 | |||
| f1a077dc2f | |||
| f20962b02b | |||
| 77e057f244 | |||
| 2c5114c770 | |||
| a30895a199 | |||
| 6d46d4b3da | |||
| 006efbbb77 | |||
| 52334087ad | |||
| ea8fb2c70a | |||
| 531d148718 | |||
| d83ddea323 | |||
| b5dea34090 | |||
| 7d8e3202b5 | |||
| d956e0af4b | |||
| 9ad70990b5 | |||
| 82d711f077 | |||
| 5fc28ef711 | |||
| 1f3dc682e2 | |||
| cce7768246 | |||
| 01172b0664 | |||
| 1ca11fdd34 | |||
| 52a064ae45 | |||
| 6c62ddf927 | |||
| 47e04c08d0 | |||
| d1601874b4 | |||
| 6bc19af041 | |||
| 4e72a3886f | |||
| 8e6b219eea | |||
| bd638251e4 | |||
| 0173d5e4b3 | |||
| 4f364e134b | |||
| 94d2519717 | |||
| 6a11d2e357 | |||
| 0a9e0bc9e4 | |||
| 5914e367d1 | |||
| aeaed888d4 | |||
| a0f054b005 | |||
| 4498e9d444 | |||
| 43e0763fef | |||
| a519c8a482 | |||
| 19bc595a2a | |||
| db07825c58 | |||
| b199e3bf0e | |||
| 1f9ea0891e | |||
| 0a7aa48f1e | |||
| 4b65b7da6c | |||
| 183a7dac4b | |||
| 6726f008c1 | |||
| 89cf0c140f | |||
| 504025ce80 | |||
| fee9e914f1 | |||
| 76efa71c68 | |||
| 26e61959ae | |||
| 9a8956ef9d | |||
| 10d3ffc2f6 | |||
| 090399f61d | |||
| ae7d975a92 | |||
| 55ec6bcafe | |||
| f0566d15af | |||
| a730b692bc | |||
| 826d767423 | |||
| 6d227c7fcd | |||
| 7d9d97840e | |||
| 110ded45a0 | |||
| 7872b593c7 | |||
| 90be30bddb | |||
| a298c61dab | |||
| eb416c45bd | |||
| b05b817aeb | |||
| 666602283a | |||
| ac040a4bae | |||
| b4982c8f22 | |||
| ce3ad92095 | |||
| 8e1ac8698c | |||
| b60a39c7cb | |||
| 3b3e8ba4c8 | |||
| e387f6d3be | |||
| 799d469cb6 | |||
| a54a596fa7 | |||
| 92d73d0285 | |||
| acb752e4e8 | |||
| 9e377abba6 | |||
| 04552c0923 | |||
| 2e1fb85b73 | |||
| 50db32d9b4 | |||
| 28743d953e | |||
| b109d26aa7 | |||
| 7fd21bd06d | |||
| 12e0ffb466 | |||
| 9259341df8 | |||
| 8e8aca7e7b | |||
| 7f0bcd987b | |||
| ef21de95cb | |||
| ca3246de02 | |||
| f0940b7926 | |||
| 0066e0b901 | |||
| 9771f566b0 | |||
| 38ad4c6dec | |||
| 37cf80a188 | |||
| c86ee53f66 | |||
| c2cea7e797 | |||
| a8ef6cdd4f | |||
| 53d157fee8 | |||
| c2e07b13f6 | |||
| 2e8cc48311 | |||
| f6f811eb77 | |||
| ac5528fb15 | |||
| 940d2b7862 | |||
| 835fe3dad3 | |||
| dfaecc08c5 | |||
| 87f5e9b847 | |||
| 3d3939e808 | |||
| 90822e3858 | |||
| 14eec47e9c | |||
| 15ed3fcc69 | |||
| fd8fa9f3ef | |||
| b81075f4a7 | |||
| f11a52e8e1 | |||
| 9c007483d4 | |||
| ff4e818e4c | |||
| 45a50ca0c1 | |||
| 65d9021c37 | |||
| 66481a0391 | |||
| a14a82bc9a | |||
| 756c57a16e | |||
| 8b19e34dc5 | |||
| 50083019ee | |||
| 155272e638 | |||
| 08443ceb3d | |||
| c215696f04 | |||
| 5ca42bf9b6 | |||
| 3272b9dec5 | |||
| 2ebd5da4aa | |||
| 34f024ace2 | |||
| b31f2d50f6 | |||
| da44d3b2b4 | |||
| 99ec2aca6a | |||
| 6c278604ec | |||
| 1e094a467a | |||
| 978ccfeeba | |||
| e93d66d8a1 | |||
| c29a749833 | |||
| b08d5d1261 | |||
| 9b129789e9 | |||
| a76a6d2798 | |||
| 086a760378 | |||
| f78c8d4fd8 | |||
| 7b91489997 | |||
| 36a8980c95 | |||
| 7c65640cb7 | |||
| d70e68495a | |||
| 2586202772 | |||
| b5f771368a | |||
| 0c28320ce3 | |||
| c8b4fbc36b | |||
| e9b07849fe | |||
| 409260af6f | |||
| d3d53d1a4e | |||
| e2db191f70 | |||
| d61816734d | |||
| f4dad8058f | |||
| 70bdb375c3 | |||
| e724ab0a29 | |||
| 7d0ee2ac11 | |||
| 59b7e852e2 | |||
| b2eb1a391d | |||
| 9b3aee98d3 | |||
| 0476f4144c | |||
| ed77f45fae | |||
| 0cd529e746 | |||
| 5969048318 | |||
| d1a7f8baa0 | |||
| 18dc936002 | |||
| b4b7b5d572 | |||
| 291c2e692d | |||
| 8a9a4f21b1 | |||
| cd31332b39 | |||
| cc8d2162a0 | |||
| e6313cdc67 | |||
| 5af64892e7 | |||
| a5578a7ac7 | |||
| fcdda6406e | |||
| 9bdd9f8aa6 | |||
| f3856f051b | |||
| d3a6662c60 | |||
| 5474eddf84 | |||
| b666cd47d4 | |||
| 8a986383fe | |||
| 9fa17f617e | |||
| e46e165704 | |||
| a6af423fb4 | |||
| 3397e694c0 | |||
| 77ff82505e | |||
| c3f2838270 | |||
| aed7f205b6 | |||
| 95b3587f7a | |||
| a4baeb995a | |||
| 032ab54206 | |||
| 1f9c1eb1c0 | |||
| a213e568ba | |||
| 7aeaeb4b86 | |||
| 81aef4b8fa | |||
| 31f0b6a16c | |||
| 44e3a682fd |
@@ -74,15 +74,18 @@ jobs:
|
||||
id: GenTagName
|
||||
run: |
|
||||
cd master/server/build
|
||||
genTag=$(ls *.jar | sed -e's/Tachidesk-\|.jar//g')
|
||||
genTag=$(ls *.jar | sed -e's/Tachidesk-Server-\|.jar//g')
|
||||
echo "$genTag"
|
||||
echo "::set-output name=value::$genTag"
|
||||
|
||||
- name: make windows packages
|
||||
- name: make bundle packages
|
||||
run: |
|
||||
cd master/scripts
|
||||
./windows-bundler.sh win32
|
||||
./windows-bundler.sh win64
|
||||
./unix-bundler.sh linux-x64
|
||||
./unix-bundler.sh macOS-x64
|
||||
./unix-bundler.sh macOS-arm64
|
||||
|
||||
# - name: Mock make windows packages
|
||||
# run: |
|
||||
@@ -116,11 +119,7 @@ jobs:
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||
artifacts: "master/server/build/*.jar,master/server/build/*.zip"
|
||||
artifacts: "master/server/build/*.jar,master/server/build/*.zip,master/server/build/*.tar.gz"
|
||||
owner: "Suwayomi"
|
||||
repo: "Tachidesk-Server-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
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Docker Build Stable
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build_publish_docker_container:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: run docker build and publish script
|
||||
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
|
||||
|
||||
@@ -51,13 +51,6 @@ jobs:
|
||||
cd master
|
||||
curl https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/webUI/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/webUI/yarn.lock') }}
|
||||
|
||||
- name: Build and copy webUI, Build Jar
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
env:
|
||||
@@ -70,23 +63,21 @@ jobs:
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
- name: make windows packages
|
||||
- name: make bundle packages
|
||||
run: |
|
||||
cd master/scripts
|
||||
./windows-bundler.sh win32
|
||||
./windows-bundler.sh win64
|
||||
./unix-bundler.sh linux-x64
|
||||
./unix-bundler.sh macOS-x64
|
||||
./unix-bundler.sh macOS-arm64
|
||||
|
||||
- name: Upload Release
|
||||
uses: xresloader/upload-to-github-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
file: "master/server/build/*.jar;master/server/build/*.zip"
|
||||
file: "master/server/build/*.jar;master/server/build/*.zip;master/server/build/*.tar.gz"
|
||||
tags: true
|
||||
draft: 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
|
||||
|
||||
|
||||
+3
-2
@@ -11,6 +11,7 @@ server/tmp/
|
||||
server/tachiserver-data/
|
||||
|
||||
# bundle asset downlaods
|
||||
OpenJDK*.zip
|
||||
electron-*.zip
|
||||
OpenJDK*.*
|
||||
zulu*jre*
|
||||
electron-*.*
|
||||
rcedit-*
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
dependencies {
|
||||
// Config API, moved to the global build.gradle
|
||||
// implementation("com.typesafe:config:1.4.0")
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
import net.harawata.appdirs.AppDirsFactory
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import net.harawata.appdirs.AppDirsFactory
|
||||
|
||||
const val CONFIG_PREFIX = "suwayomi.tachidesk.config"
|
||||
|
||||
val ApplicationRootDir: String
|
||||
get(): String {
|
||||
return System.getProperty(
|
||||
"suwayomi.tachidesk.server.rootDir",
|
||||
"$CONFIG_PREFIX.server.rootDir",
|
||||
AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||
)
|
||||
}
|
||||
@@ -35,6 +35,7 @@ open class ConfigManager {
|
||||
/**
|
||||
* Get a config module (Java API)
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
|
||||
|
||||
/**
|
||||
@@ -47,7 +48,7 @@ open class ConfigManager {
|
||||
val baseConfig =
|
||||
ConfigFactory.parseMap(
|
||||
mapOf(
|
||||
"ts.server.rootDir" to ApplicationRootDir
|
||||
"androidcompat.rootDir" to "$ApplicationRootDir/android-compat" // override AndroidCompat's rootDir
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,44 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import io.github.config4k.getValue
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* Abstract config module.
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
abstract class ConfigModule(config: Config)
|
||||
|
||||
/**
|
||||
* Abstract jvm-commandline-argument-overridable config module.
|
||||
*/
|
||||
abstract class SystemPropertyOverridableConfigModule(config: Config, moduleName: String): ConfigModule(config) {
|
||||
val overridableConfig = SystemPropertyOverrideDelegate(config, moduleName)
|
||||
}
|
||||
|
||||
/** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */
|
||||
class SystemPropertyOverrideDelegate(val config: Config, val moduleName: String) {
|
||||
inline operator fun <R, reified T> getValue(thisRef: R, property: KProperty<*>): T {
|
||||
val configValue: T = config.getValue(thisRef, property)
|
||||
|
||||
val combined = System.getProperty(
|
||||
"$CONFIG_PREFIX.$moduleName.${property.name}",
|
||||
configValue.toString()
|
||||
)
|
||||
|
||||
return when(T::class.simpleName) {
|
||||
"Int" -> combined.toInt()
|
||||
"Boolean" -> combined.toBoolean()
|
||||
// add more types as needed
|
||||
else -> combined // covers String
|
||||
} as T
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,4 @@ fun setLogLevel(level: Level) {
|
||||
}
|
||||
|
||||
fun debugLogsEnabled(config: Config)
|
||||
= System.getProperty("suwayomi.tachidesk.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
|
||||
= System.getProperty("suwayomi.tachidesk.config.server.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
|
||||
-1
@@ -1 +0,0 @@
|
||||
xyz.nulldev.ts.api.v2.java.impl.ServerAPIImpl
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
File diff suppressed because one or more lines are too long
@@ -1,22 +0,0 @@
|
||||
[
|
||||
{
|
||||
"label": "Sync",
|
||||
"icon": "import_export",
|
||||
"type": "nested",
|
||||
"prefs": []
|
||||
},
|
||||
{
|
||||
"label": "Server",
|
||||
"icon": "dns",
|
||||
"type": "nested",
|
||||
"prefs": [
|
||||
{
|
||||
"label": "Password authentication",
|
||||
"type": "text-password",
|
||||
"default": "",
|
||||
"key": "pref_ts_server_password",
|
||||
"hint": "Enter a password"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,77 +1,28 @@
|
||||
|
||||
plugins {
|
||||
application
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
|
||||
maven {
|
||||
url = uri("https://maven.google.com")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Android stub library
|
||||
implementation(fileTree("lib/"))
|
||||
|
||||
|
||||
// Android JAR libs
|
||||
// compileOnly( fileTree(dir: new File(rootProject.rootDir, "libs/other"), include: "*.jar")
|
||||
|
||||
// JSON
|
||||
compileOnly("com.google.code.gson:gson:2.8.6")
|
||||
|
||||
// Javassist
|
||||
compileOnly("org.javassist:javassist:3.27.0-GA")
|
||||
|
||||
// XML
|
||||
compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
||||
compileOnly("xmlpull:xmlpull:1.1.3.4a")
|
||||
|
||||
// Config API
|
||||
implementation(project(":AndroidCompat:Config"))
|
||||
|
||||
// APK sig verifier
|
||||
compileOnly("com.android.tools.build:apksig:4.2.0-alpha13")
|
||||
compileOnly("com.android.tools.build:apksig:7.1.0-alpha12")
|
||||
|
||||
// AndroidX annotations
|
||||
compileOnly("androidx.annotation:annotation:1.2.0-alpha01")
|
||||
compileOnly("androidx.annotation:annotation:1.2.0")
|
||||
|
||||
// substitute for duktape-android
|
||||
// 'org.mozilla:rhino' includes some code that we don't need so use 'org.mozilla:rhino-runtime' instead
|
||||
implementation("org.mozilla:rhino-runtime:1.7.13")
|
||||
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
|
||||
implementation("org.mozilla:rhino-engine:1.7.13")
|
||||
implementation("org.mozilla:rhino-runtime:1.7.13") // slimmer version of 'org.mozilla:rhino'
|
||||
implementation("org.mozilla:rhino-engine:1.7.13") // provides the same interface as 'javax.script' a.k.a Nashorn
|
||||
|
||||
// Kotlin wrapper around Java Preferences, makes certain things easier
|
||||
val multiplatformSettingsVersion = "0.7.7"
|
||||
val multiplatformSettingsVersion = "0.8"
|
||||
implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion")
|
||||
implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion")
|
||||
|
||||
// Android version of SimpleDateFormat
|
||||
implementation("com.ibm.icu:icu4j:69.1")
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
||||
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
|
||||
//
|
||||
//// Copy JVM core patches
|
||||
//task copyJVMPatches(type: Copy) {
|
||||
// from fatJarTask.outputs.files
|
||||
// into 'src/main/resources/patches'
|
||||
//}
|
||||
//
|
||||
//compileOnly(Java.dependsOn gradle.includedBuild('dex2jar').task(':dex-translator:assemble')
|
||||
//compileOnly(Java.dependsOn copyJVMPatches
|
||||
//copyJVMPatches.dependsOn fatJarTask
|
||||
//
|
||||
|
||||
@@ -13,7 +13,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
public class TwoStatePreference extends Preference {
|
||||
// Note: remove @JsonIgnore and implement methods if any extension ever uses these methods or the variables behind them
|
||||
|
||||
public TwoStatePreference(Context context) { super(context); }
|
||||
public TwoStatePreference(Context context) {
|
||||
super(context);
|
||||
setDefaultValue(false);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isChecked() { throw new RuntimeException("Stub!"); }
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
package com.f2prateek;
|
||||
//TODO Consider if we can change this package into an Android dependency
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 Prateek Srivastava
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
This file may have been modified after being copied from it's original source.
|
||||
*/
|
||||
package com.f2prateek.rx.preferences;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
final class BooleanAdapter implements Preference.Adapter<Boolean> {
|
||||
static final BooleanAdapter INSTANCE = new BooleanAdapter();
|
||||
|
||||
@Override public Boolean get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
||||
return preferences.getBoolean(key, false);
|
||||
}
|
||||
|
||||
@Override public void set(@NonNull String key, @NonNull Boolean value,
|
||||
@NonNull SharedPreferences.Editor editor) {
|
||||
editor.putBoolean(key, value);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 Prateek Srivastava
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
This file may have been modified after being copied from it's original source.
|
||||
*/
|
||||
package com.f2prateek.rx.preferences;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
final class EnumAdapter<T extends Enum<T>> implements Preference.Adapter<T> {
|
||||
private final Class<T> enumClass;
|
||||
|
||||
EnumAdapter(Class<T> enumClass) {
|
||||
this.enumClass = enumClass;
|
||||
}
|
||||
|
||||
@Override public T get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
||||
String value = preferences.getString(key, null);
|
||||
assert value != null; // Not called unless key is present.
|
||||
return Enum.valueOf(enumClass, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(@NonNull String key, @NonNull T value, @NonNull SharedPreferences.Editor editor) {
|
||||
editor.putString(key, value.name());
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 Prateek Srivastava
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
This file may have been modified after being copied from it's original source.
|
||||
*/
|
||||
package com.f2prateek.rx.preferences;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
final class FloatAdapter implements Preference.Adapter<Float> {
|
||||
static final FloatAdapter INSTANCE = new FloatAdapter();
|
||||
|
||||
@Override public Float get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
||||
return preferences.getFloat(key, 0f);
|
||||
}
|
||||
|
||||
@Override public void set(@NonNull String key, @NonNull Float value,
|
||||
@NonNull SharedPreferences.Editor editor) {
|
||||
editor.putFloat(key, value);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 Prateek Srivastava
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
This file may have been modified after being copied from it's original source.
|
||||
*/
|
||||
package com.f2prateek.rx.preferences;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
final class IntegerAdapter implements Preference.Adapter<Integer> {
|
||||
static final IntegerAdapter INSTANCE = new IntegerAdapter();
|
||||
|
||||
@Override public Integer get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
||||
return preferences.getInt(key, 0);
|
||||
}
|
||||
|
||||
@Override public void set(@NonNull String key, @NonNull Integer value,
|
||||
@NonNull SharedPreferences.Editor editor) {
|
||||
editor.putInt(key, value);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 Prateek Srivastava
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
This file may have been modified after being copied from it's original source.
|
||||
*/
|
||||
package com.f2prateek.rx.preferences;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
final class LongAdapter implements Preference.Adapter<Long> {
|
||||
static final LongAdapter INSTANCE = new LongAdapter();
|
||||
|
||||
@Override public Long get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
||||
return preferences.getLong(key, 0L);
|
||||
}
|
||||
|
||||
@Override public void set(@NonNull String key, @NonNull Long value,
|
||||
@NonNull SharedPreferences.Editor editor) {
|
||||
editor.putLong(key, value);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 Prateek Srivastava
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
This file has been modified after being copied from it's original source.
|
||||
*/
|
||||
package com.f2prateek.rx.preferences;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.CheckResult;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import rx.Observable;
|
||||
import rx.functions.Action1;
|
||||
|
||||
/** A preference of type {@link T}. Instances can be created from {@link RxSharedPreferences}. */
|
||||
public final class Preference<T> {
|
||||
/** Stores and retrieves instances of {@code T} in {@link SharedPreferences}. */
|
||||
public interface Adapter<T> {
|
||||
/** Retrieve the value for {@code key} from {@code preferences}. */
|
||||
T get(@NonNull String key, @NonNull SharedPreferences preferences);
|
||||
|
||||
/**
|
||||
* Store non-null {@code value} for {@code key} in {@code editor}.
|
||||
* <p>
|
||||
* Note: Implementations <b>must not</b> call {@code commit()} or {@code apply()} on
|
||||
* {@code editor}.
|
||||
*/
|
||||
void set(@NonNull String key, @NonNull T value, @NonNull SharedPreferences.Editor editor);
|
||||
}
|
||||
|
||||
private final SharedPreferences preferences;
|
||||
private final String key;
|
||||
private final T defaultValue;
|
||||
private final Adapter<T> adapter;
|
||||
private final Observable<T> values;
|
||||
|
||||
Preference(SharedPreferences preferences, final String key, T defaultValue, Adapter<T> adapter,
|
||||
Observable<String> keyChanges) {
|
||||
this.preferences = preferences;
|
||||
this.key = key;
|
||||
this.defaultValue = defaultValue;
|
||||
this.adapter = adapter;
|
||||
this.values = keyChanges
|
||||
.filter(key::equals)
|
||||
.startWith("<init>") // Dummy value to trigger initial load.
|
||||
.onBackpressureLatest()
|
||||
.map(ignored -> get());
|
||||
}
|
||||
|
||||
/** The key for which this preference will store and retrieve values. */
|
||||
@NonNull
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/** The value used if none is stored. May be {@code null}. */
|
||||
@Nullable
|
||||
public T defaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current value for this preference. Returns {@link #defaultValue()} if no value is
|
||||
* set.
|
||||
*/
|
||||
@Nullable
|
||||
public T get() {
|
||||
if (!preferences.contains(key)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return adapter.get(key, preferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change this preference's stored value to {@code value}. A value of {@code null} will delete the
|
||||
* preference.
|
||||
*/
|
||||
public void set(@Nullable T value) {
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
if (value == null) {
|
||||
editor.remove(key);
|
||||
} else {
|
||||
adapter.set(key, value, editor);
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/** Returns true if this preference has a stored value. */
|
||||
public boolean isSet() {
|
||||
return preferences.contains(key);
|
||||
}
|
||||
|
||||
/** Delete the stored value for this preference, if any. */
|
||||
public void delete() {
|
||||
set(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe changes to this preference. The current value or {@link #defaultValue()} will be
|
||||
* emitted on first subscribe.
|
||||
*/
|
||||
@CheckResult @NonNull
|
||||
public Observable<T> asObservable() {
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* An action which stores a new value for this preference. Passing {@code null} will delete the
|
||||
* preference.
|
||||
*/
|
||||
@CheckResult @NonNull
|
||||
public Action1<? super T> asAction() {
|
||||
return (Action1<T>) this::set;
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 Prateek Srivastava
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
This file has been modified after being copied from it's original source.
|
||||
*/
|
||||
package com.f2prateek.rx.preferences;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.support.annotation.CheckResult;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import rx.Observable;
|
||||
import rx.subscriptions.Subscriptions;
|
||||
|
||||
import static android.os.Build.VERSION_CODES.HONEYCOMB;
|
||||
import static com.f2prateek.rx.preferences.Preconditions.checkNotNull;
|
||||
|
||||
/** A factory for reactive {@link Preference} objects. */
|
||||
public final class RxSharedPreferences {
|
||||
private static final Float DEFAULT_FLOAT = 0f;
|
||||
private static final Integer DEFAULT_INTEGER = 0;
|
||||
private static final Boolean DEFAULT_BOOLEAN = Boolean.FALSE;
|
||||
private static final Long DEFAULT_LONG = 0L;
|
||||
|
||||
/** Create an instance of {@link RxSharedPreferences} for {@code preferences}. */
|
||||
@CheckResult @NonNull
|
||||
public static RxSharedPreferences create(@NonNull SharedPreferences preferences) {
|
||||
checkNotNull(preferences, "preferences == null");
|
||||
return new RxSharedPreferences(preferences);
|
||||
}
|
||||
|
||||
private final SharedPreferences preferences;
|
||||
private final Observable<String> keyChanges;
|
||||
|
||||
private RxSharedPreferences(final SharedPreferences preferences) {
|
||||
this.preferences = preferences;
|
||||
this.keyChanges = Observable.create((Observable.OnSubscribe<String>) subscriber -> {
|
||||
final OnSharedPreferenceChangeListener listener = (preferences1, key) -> subscriber.onNext(key);
|
||||
|
||||
preferences.registerOnSharedPreferenceChangeListener(listener);
|
||||
|
||||
subscriber.add(Subscriptions.create(() -> preferences.unregisterOnSharedPreferenceChangeListener(listener)));
|
||||
}).share();
|
||||
}
|
||||
|
||||
/** Create a boolean preference for {@code key}. Default is {@code false}. */
|
||||
@CheckResult @NonNull
|
||||
public Preference<Boolean> getBoolean(@NonNull String key) {
|
||||
return getBoolean(key, DEFAULT_BOOLEAN);
|
||||
}
|
||||
|
||||
/** Create a boolean preference for {@code key} with a default of {@code defaultValue}. */
|
||||
@CheckResult @NonNull
|
||||
public Preference<Boolean> getBoolean(@NonNull String key, @Nullable Boolean defaultValue) {
|
||||
checkNotNull(key, "key == null");
|
||||
return new Preference<>(preferences, key, defaultValue, BooleanAdapter.INSTANCE, keyChanges);
|
||||
}
|
||||
|
||||
/** Create an enum preference for {@code key}. Default is {@code null}. */
|
||||
@CheckResult @NonNull
|
||||
public <T extends Enum<T>> Preference<T> getEnum(@NonNull String key,
|
||||
@NonNull Class<T> enumClass) {
|
||||
return getEnum(key, null, enumClass);
|
||||
}
|
||||
|
||||
/** Create an enum preference for {@code key} with a default of {@code defaultValue}. */
|
||||
@CheckResult @NonNull
|
||||
public <T extends Enum<T>> Preference<T> getEnum(@NonNull String key, @Nullable T defaultValue,
|
||||
@NonNull Class<T> enumClass) {
|
||||
checkNotNull(key, "key == null");
|
||||
checkNotNull(enumClass, "enumClass == null");
|
||||
Preference.Adapter<T> adapter = new EnumAdapter<>(enumClass);
|
||||
return new Preference<>(preferences, key, defaultValue, adapter, keyChanges);
|
||||
}
|
||||
|
||||
/** Create a float preference for {@code key}. Default is {@code 0}. */
|
||||
@CheckResult @NonNull
|
||||
public Preference<Float> getFloat(@NonNull String key) {
|
||||
return getFloat(key, DEFAULT_FLOAT);
|
||||
}
|
||||
|
||||
/** Create a float preference for {@code key} with a default of {@code defaultValue}. */
|
||||
@CheckResult @NonNull
|
||||
public Preference<Float> getFloat(@NonNull String key, @Nullable Float defaultValue) {
|
||||
checkNotNull(key, "key == null");
|
||||
return new Preference<>(preferences, key, defaultValue, FloatAdapter.INSTANCE, keyChanges);
|
||||
}
|
||||
|
||||
/** Create an integer preference for {@code key}. Default is {@code 0}. */
|
||||
@CheckResult @NonNull
|
||||
public Preference<Integer> getInteger(@NonNull String key) {
|
||||
//noinspection UnnecessaryBoxing
|
||||
return getInteger(key, DEFAULT_INTEGER);
|
||||
}
|
||||
|
||||
/** Create an integer preference for {@code key} with a default of {@code defaultValue}. */
|
||||
@CheckResult @NonNull
|
||||
public Preference<Integer> getInteger(@NonNull String key, @Nullable Integer defaultValue) {
|
||||
checkNotNull(key, "key == null");
|
||||
return new Preference<>(preferences, key, defaultValue, IntegerAdapter.INSTANCE, keyChanges);
|
||||
}
|
||||
|
||||
/** Create a long preference for {@code key}. Default is {@code 0}. */
|
||||
@CheckResult @NonNull
|
||||
public Preference<Long> getLong(@NonNull String key) {
|
||||
//noinspection UnnecessaryBoxing
|
||||
return getLong(key, DEFAULT_LONG);
|
||||
}
|
||||
|
||||
/** Create a long preference for {@code key} with a default of {@code defaultValue}. */
|
||||
@CheckResult @NonNull
|
||||
public Preference<Long> getLong(@NonNull String key, @Nullable Long defaultValue) {
|
||||
checkNotNull(key, "key == null");
|
||||
return new Preference<>(preferences, key, defaultValue, LongAdapter.INSTANCE, keyChanges);
|
||||
}
|
||||
|
||||
/** Create a preference of type {@code T} for {@code key}. Default is {@code null}. */
|
||||
@CheckResult @NonNull
|
||||
public <T> Preference<T> getObject(@NonNull String key, @NonNull Preference.Adapter<T> adapter) {
|
||||
return getObject(key, null, adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a preference for type {@code T} for {@code key} with a default of {@code defaultValue}.
|
||||
*/
|
||||
@CheckResult @NonNull
|
||||
public <T> Preference<T> getObject(@NonNull String key, @Nullable T defaultValue,
|
||||
@NonNull Preference.Adapter<T> adapter) {
|
||||
checkNotNull(key, "key == null");
|
||||
checkNotNull(adapter, "adapter == null");
|
||||
return new Preference<>(preferences, key, defaultValue, adapter, keyChanges);
|
||||
}
|
||||
|
||||
/** Create a string preference for {@code key}. Default is {@code null}. */
|
||||
@CheckResult @NonNull
|
||||
public Preference<String> getString(@NonNull String key) {
|
||||
return getString(key, null);
|
||||
}
|
||||
|
||||
/** Create a string preference for {@code key} with a default of {@code defaultValue}. */
|
||||
@CheckResult @NonNull
|
||||
public Preference<String> getString(@NonNull String key, @Nullable String defaultValue) {
|
||||
checkNotNull(key, "key == null");
|
||||
return new Preference<>(preferences, key, defaultValue, StringAdapter.INSTANCE, keyChanges);
|
||||
}
|
||||
|
||||
/** Create a string set preference for {@code key}. Default is an empty set. */
|
||||
@TargetApi(HONEYCOMB)
|
||||
@CheckResult @NonNull
|
||||
public Preference<Set<String>> getStringSet(@NonNull String key) {
|
||||
return getStringSet(key, Collections.emptySet());
|
||||
}
|
||||
|
||||
/** Create a string set preference for {@code key} with a default of {@code defaultValue}. */
|
||||
@TargetApi(HONEYCOMB)
|
||||
@CheckResult @NonNull
|
||||
public Preference<Set<String>> getStringSet(@NonNull String key,
|
||||
@NonNull Set<String> defaultValue) {
|
||||
checkNotNull(key, "key == null");
|
||||
return new Preference<>(preferences, key, defaultValue, StringSetAdapter.INSTANCE, keyChanges);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.f2prateek.rx.preferences;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
final class StringAdapter implements Preference.Adapter<String> {
|
||||
static final StringAdapter INSTANCE = new StringAdapter();
|
||||
|
||||
@Override public String get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
||||
return preferences.getString(key, null);
|
||||
}
|
||||
|
||||
@Override public void set(@NonNull String key, @NonNull String value,
|
||||
@NonNull SharedPreferences.Editor editor) {
|
||||
editor.putString(key, value);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.f2prateek.rx.preferences;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
import java.util.Set;
|
||||
|
||||
import static android.os.Build.VERSION_CODES.HONEYCOMB;
|
||||
|
||||
@TargetApi(HONEYCOMB)
|
||||
final class StringSetAdapter implements Preference.Adapter<Set<String>> {
|
||||
static final StringSetAdapter INSTANCE = new StringSetAdapter();
|
||||
|
||||
@Override public Set<String> get(@NonNull String key, @NonNull SharedPreferences preferences) {
|
||||
return preferences.getStringSet(key, null);
|
||||
}
|
||||
|
||||
@Override public void set(@NonNull String key, @NonNull Set<String> value,
|
||||
@NonNull SharedPreferences.Editor editor) {
|
||||
editor.putStringSet(key, value);
|
||||
}
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
package com.github.pwittchen.reactivenetwork.library
|
||||
|
||||
import android.net.NetworkInfo
|
||||
|
||||
class Connectivity {
|
||||
val state = NetworkInfo.State.CONNECTED
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package com.github.pwittchen.reactivenetwork.library
|
||||
|
||||
import android.content.Context
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
* Created by nulldev on 12/29/16.
|
||||
*/
|
||||
|
||||
class ReactiveNetwork {
|
||||
companion object {
|
||||
fun observeNetworkConnectivity(context: Context) = Observable.just(Connectivity())!!
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package kotlinx.coroutines.experimental.android
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
|
||||
val UI = GlobalScope.coroutineContext
|
||||
@@ -2,7 +2,6 @@ package xyz.nulldev.androidcompat
|
||||
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import xyz.nulldev.androidcompat.bytecode.ModApplier
|
||||
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
|
||||
import xyz.nulldev.androidcompat.config.FilesConfigModule
|
||||
import xyz.nulldev.androidcompat.config.SystemConfigModule
|
||||
@@ -12,12 +11,7 @@ import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
* Initializes the Android compatibility module
|
||||
*/
|
||||
class AndroidCompatInitializer {
|
||||
|
||||
val modApplier by lazy { ModApplier() }
|
||||
|
||||
fun init() {
|
||||
modApplier.apply()
|
||||
|
||||
DI.global.addImport(AndroidCompatModule().create())
|
||||
|
||||
//Register config modules
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package xyz.nulldev.androidcompat.bytecode
|
||||
|
||||
import javassist.CtClass
|
||||
import mu.KotlinLogging
|
||||
|
||||
/**
|
||||
* Applies Javassist modifications
|
||||
*/
|
||||
|
||||
class ModApplier {
|
||||
|
||||
val logger = KotlinLogging.logger {}
|
||||
|
||||
fun apply() {
|
||||
logger.info { "Applying Javassist mods..." }
|
||||
val modifiedClasses = mutableListOf<CtClass>()
|
||||
|
||||
modifiedClasses.forEach {
|
||||
it.toClass()
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
package xyz.nulldev.androidcompat.config
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import io.github.config4k.getValue
|
||||
import xyz.nulldev.ts.config.ConfigModule
|
||||
|
||||
/**
|
||||
@@ -8,8 +9,8 @@ import xyz.nulldev.ts.config.ConfigModule
|
||||
*/
|
||||
|
||||
class ApplicationInfoConfigModule(config: Config) : ConfigModule(config) {
|
||||
val packageName = config.getString("packageName")!!
|
||||
val debug = config.getBoolean("debug")
|
||||
val packageName: String by config
|
||||
val debug: Boolean by config
|
||||
|
||||
companion object {
|
||||
fun register(config: Config)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package xyz.nulldev.androidcompat.config
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import io.github.config4k.getValue
|
||||
import xyz.nulldev.ts.config.ConfigModule
|
||||
|
||||
/**
|
||||
@@ -8,23 +9,23 @@ import xyz.nulldev.ts.config.ConfigModule
|
||||
*/
|
||||
|
||||
class FilesConfigModule(config: Config) : ConfigModule(config) {
|
||||
val dataDir = config.getString("dataDir")!!
|
||||
val filesDir = config.getString("filesDir")!!
|
||||
val noBackupFilesDir = config.getString("noBackupFilesDir")!!
|
||||
val externalFilesDirs: MutableList<String> = config.getStringList("externalFilesDirs")!!
|
||||
val obbDirs: MutableList<String> = config.getStringList("obbDirs")!!
|
||||
val cacheDir = config.getString("cacheDir")!!
|
||||
val codeCacheDir = config.getString("codeCacheDir")!!
|
||||
val externalCacheDirs: MutableList<String> = config.getStringList("externalCacheDirs")!!
|
||||
val externalMediaDirs: MutableList<String> = config.getStringList("externalMediaDirs")!!
|
||||
val rootDir = config.getString("rootDir")!!
|
||||
val externalStorageDir = config.getString("externalStorageDir")!!
|
||||
val downloadCacheDir = config.getString("downloadCacheDir")!!
|
||||
val databasesDir = config.getString("databasesDir")!!
|
||||
val dataDir:String by config
|
||||
val filesDir:String by config
|
||||
val noBackupFilesDir:String by config
|
||||
val externalFilesDirs: MutableList<String> by config
|
||||
val obbDirs: MutableList<String> by config
|
||||
val cacheDir:String by config
|
||||
val codeCacheDir:String by config
|
||||
val externalCacheDirs: MutableList<String> by config
|
||||
val externalMediaDirs: MutableList<String> by config
|
||||
val rootDir:String by config
|
||||
val externalStorageDir:String by config
|
||||
val downloadCacheDir:String by config
|
||||
val databasesDir:String by config
|
||||
|
||||
val prefsDir = config.getString("prefsDir")!!
|
||||
val prefsDir:String by config
|
||||
|
||||
val packageDir = config.getString("packageDir")!!
|
||||
val packageDir:String by config
|
||||
|
||||
companion object {
|
||||
fun register(config: Config)
|
||||
|
||||
@@ -2,9 +2,10 @@ package xyz.nulldev.androidcompat.config
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import xyz.nulldev.ts.config.ConfigModule
|
||||
import io.github.config4k.getValue
|
||||
|
||||
class SystemConfigModule(val config: Config) : ConfigModule(config) {
|
||||
val isDebuggable = config.getBoolean("isDebuggable")
|
||||
val isDebuggable: Boolean by config
|
||||
|
||||
val propertyPrefix = "properties."
|
||||
|
||||
|
||||
@@ -4,11 +4,21 @@ import java.io.InputStream
|
||||
import java.io.Reader
|
||||
import java.math.BigDecimal
|
||||
import java.net.URL
|
||||
import java.sql.*
|
||||
import java.sql.Array
|
||||
import java.sql.Blob
|
||||
import java.sql.Clob
|
||||
import java.sql.Date
|
||||
import java.util.*
|
||||
import java.sql.NClob
|
||||
import java.sql.Ref
|
||||
import java.sql.ResultSet
|
||||
import java.sql.ResultSetMetaData
|
||||
import java.sql.RowId
|
||||
import java.sql.SQLXML
|
||||
import java.sql.Time
|
||||
import java.sql.Timestamp
|
||||
import java.util.Calendar
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
|
||||
|
||||
private val cachedContent = mutableListOf<ResultSetEntry>()
|
||||
|
||||
@@ -18,7 +18,7 @@ class ServiceSupport {
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun startService(context: Context, intent: Intent) {
|
||||
fun startService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
|
||||
val name = intentToClassName(intent)
|
||||
|
||||
logger.debug { "Starting service: $name" }
|
||||
@@ -35,7 +35,7 @@ class ServiceSupport {
|
||||
}
|
||||
}
|
||||
|
||||
fun stopService(context: Context, intent: Intent) {
|
||||
fun stopService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
|
||||
val name = intentToClassName(intent)
|
||||
stopService(name)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ object KodeinGlobalHelper {
|
||||
* Get a dependency
|
||||
*/
|
||||
@JvmStatic
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T {
|
||||
return when(type) {
|
||||
AndroidFiles::class.java -> {
|
||||
|
||||
@@ -1,36 +1,12 @@
|
||||
# AndroidComapt Root dir
|
||||
androidcompat.rootDir = androidcompat-root
|
||||
|
||||
# Allow/disallow preference changes (useful for demos)
|
||||
ts.server.allowConfigChanges = true
|
||||
|
||||
# Enable the WebUI? Note: The API and multi-user sync server ui will remain available even if the WebUI is disabled
|
||||
ts.server.enableWebUi = true
|
||||
|
||||
# 'true' to use the old, buggy/memory-leaking WebUI
|
||||
ts.server.useOldWebUi = false
|
||||
|
||||
# 'true' to pretty print all JSON API responses
|
||||
ts.server.prettyPrintApi = false
|
||||
|
||||
# List of blacklisted/whitelisted API endpoints/operation IDs
|
||||
ts.server.disabledApiEndpoints = []
|
||||
ts.server.enabledApiEndpoints = []
|
||||
|
||||
# Message to print in the console when the API has finished booting
|
||||
ts.server.httpInitializedPrintMessage = ""
|
||||
|
||||
# Use external folder for static files
|
||||
ts.server.useExternalStaticFiles = false
|
||||
ts.server.externalStaticFilesFolder = ""
|
||||
|
||||
# Root storage dir
|
||||
ts.server.rootDir = tachiserver-data
|
||||
# Dir to store JVM patches
|
||||
ts.server.patchesDir = ${ts.server.rootDir}/patches
|
||||
####################### `android.files` (FilesConfigModule) #######################
|
||||
|
||||
# Storage dir for the emulated Android app
|
||||
android.files.rootDir = ${ts.server.rootDir}/android-compat/appdata
|
||||
android.files.rootDir = ${androidcompat.rootDir}/appdata
|
||||
# External storage dir for the emulated Android app's
|
||||
android.files.externalStorageDir = ${ts.server.rootDir}/android-compat/extappdata
|
||||
android.files.externalStorageDir = ${androidcompat.rootDir}/extappdata
|
||||
|
||||
# Internal Android directories
|
||||
android.files.dataDir = ${android.files.rootDir}/data
|
||||
@@ -48,37 +24,16 @@ android.files.externalCacheDirs = [${android.files.externalStorageDir}/cache]
|
||||
android.files.externalMediaDirs = [${android.files.externalStorageDir}/media]
|
||||
android.files.downloadCacheDir = ${android.files.externalStorageDir}/downloadCache
|
||||
|
||||
android.files.packageDir = ${ts.server.rootDir}/android-compat/packages
|
||||
android.files.packageDir = ${androidcompat.rootDir}/android-compat/packages
|
||||
|
||||
####################### `android.app` (ApplicationInfoConfigModule) #######################
|
||||
|
||||
# Emulated Android app package name
|
||||
android.app.packageName = eu.kanade.tachiyomi
|
||||
# Debug mode for the emulated Android app
|
||||
android.app.debug = true
|
||||
|
||||
####################### `android.system` (SystemConfigModule) #######################
|
||||
|
||||
# Whether or not the emulated Android system is debuggable
|
||||
android.system.isDebuggable = true
|
||||
|
||||
# Is the multi-user sync server enabled? Does not affect the single-user sync server included in the API.
|
||||
ts.syncd.enable = false
|
||||
|
||||
# The URL of this server (displayed in the sync server web ui)
|
||||
ts.syncd.baseUrl = "http://example.com"
|
||||
|
||||
# 'true' to disable the API and only enable the multi-user sync server
|
||||
ts.syncd.syncOnlyMode = false
|
||||
|
||||
# The root directory to store synchronized data
|
||||
ts.syncd.rootDir = ${ts.server.rootDir}/sync/accounts
|
||||
|
||||
# Location to store config files for the sandbox
|
||||
ts.syncd.sandboxedConfig = ${ts.server.rootDir}/sync/sandboxed_config.config
|
||||
|
||||
# Recaptcha stuff for signup/login
|
||||
ts.syncd.recaptcha.siteKey = ""
|
||||
ts.syncd.recaptcha.secret = ""
|
||||
|
||||
# Sync server display name
|
||||
ts.syncd.name = "Tachiyomi sync server"
|
||||
|
||||
# Header used to forward the IP to the multi-user sync server if the server is behind a reverse proxy
|
||||
ts.syncd.ipHeader = ""
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Server: v0.X.Y-rXXX + WebUI: rXXX
|
||||
## TL;DR
|
||||
<!-- TODO: fill before release -->
|
||||
|
||||
## Tachidesk-Server
|
||||
### Public API
|
||||
#### Non-breaking changes
|
||||
- N/A
|
||||
|
||||
#### Breaking changes
|
||||
- N/A
|
||||
|
||||
#### Bug fixes
|
||||
- N/A
|
||||
|
||||
### Private API
|
||||
- N/A
|
||||
|
||||
|
||||
## Tachidesk-WebUI
|
||||
#### Visible changes
|
||||
- N/A
|
||||
|
||||
#### Bug fixes
|
||||
- N/A
|
||||
|
||||
#### Internal changes
|
||||
- N/A
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
# Server: v0.5.2 + WebUI: r807
|
||||
## TL;DR
|
||||
- Fixed Local source not working on Windows
|
||||
- Fixed Chapter numbers being shown incorrectly
|
||||
|
||||
## Tachidesk-Server
|
||||
### Public API
|
||||
#### Non-breaking changes
|
||||
- N/A
|
||||
|
||||
#### Breaking changes
|
||||
- N/A
|
||||
|
||||
#### Bug fixes
|
||||
- N/A
|
||||
|
||||
### Private API
|
||||
- (r942) Gradle Updates ([#199](https://github.com/Suwayomi/Tachidesk-WebUI/pull/199) by @Syer10)
|
||||
- (r941) Update BytecodeEditor to use Java NIO Paths ([#200](https://github.com/Suwayomi/Tachidesk-WebUI/pull/200) by @Syer10)
|
||||
|
||||
|
||||
## Tachidesk-WebUI
|
||||
#### Visible changes
|
||||
- (r804) update text positioning on Reader and Player ([#35](https://github.com/Suwayomi/Tachidesk-WebUI/pull/35) by @voltrare)
|
||||
- (r806) Source card for Local source is different
|
||||
- (r807) add Local source guide
|
||||
|
||||
#### Bug fixes
|
||||
- (r805) fix chapter name
|
||||
|
||||
#### Internal changes
|
||||
- N/A
|
||||
|
||||
|
||||
|
||||
# Server: v0.5.1 + WebUI: r803
|
||||
## TL;DR
|
||||
- Loading sources' manga list is at least twice as fast
|
||||
- Added support for Tachiyomi's Local source
|
||||
- Added BasicAuth support, now you can protect your Tachidesk instance if you are running it on a public server
|
||||
- Added ability to turn off cache for image requests
|
||||
|
||||
## Tachidesk-Server
|
||||
### Public API
|
||||
#### Non-breaking changes
|
||||
- (r915) add BasicAuth support
|
||||
- (r918) add ability to delete downloaded chapters
|
||||
- (r923-r930) add Local Source
|
||||
- (r938) add ability to turn off cache for image requests
|
||||
|
||||
#### Breaking changes
|
||||
- N/A
|
||||
|
||||
#### Bug fixes
|
||||
- (r917) detect if a downloaded chapter is missing
|
||||
|
||||
### Private API
|
||||
- (r913) remove expand char limit on MangaTable columns
|
||||
- (r914) migrate to Javalin 4
|
||||
- (r921) depricate zero based chapters
|
||||
- (r937) add ChapterRecognition from tachiyomi, closes #10
|
||||
|
||||
|
||||
|
||||
## Tachidesk-WebUI
|
||||
#### Visible changes
|
||||
- (r790) nice looking progress percentage
|
||||
- (r791) show a Delete button for downloaded chapters
|
||||
- (r792) Update hover effect using more of Material-UI color pallete ([#29](https://github.com/Suwayomi/Tachidesk-WebUI/pull/29) by @voltrare)
|
||||
- (r793) Optimize images ([#32](https://github.com/Suwayomi/Tachidesk-WebUI/pull/32) by @phanirithvij)
|
||||
- (r794) try fix #30 ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/31) by @phanirithvij)
|
||||
- (r795) fix viewing page number when the string is long
|
||||
- (r796) show proper display name for source
|
||||
- (r797) fail gracefully when a thumbnail has errors
|
||||
- (r798) fix when a source fails to load mangas
|
||||
- (r800) add Local source ([#31](https://github.com/Suwayomi/Tachidesk-WebUI/pull/31))
|
||||
- (r803) add support for useCache
|
||||
|
||||
#### Bug fixes
|
||||
- N/A
|
||||
|
||||
#### Internal changes
|
||||
- N/A
|
||||
|
||||
|
||||
|
||||
# Server: v0.5.0 + WebUI: r789
|
||||
## TL;DR
|
||||
- You can now install APK extensions from the extensions page
|
||||
- WebUI now comes with an updated Material Design looks and is faster a little bit.
|
||||
- WebUI now shows Nsfw content by default, disable it in settings if you prefer to not see Nsfw stuff
|
||||
- Added support for configuration of sources, this enables MangaDex, Komga, Cubari and many other sources
|
||||
- Chapters in the Manga page and Sources in the source page now look nicer and will glow with mouse hover
|
||||
|
||||
## Tachidesk-Server
|
||||
### Public API
|
||||
#### Non-breaking changes
|
||||
- (r888) add installing APK from external sources endpoint
|
||||
|
||||
#### Breaking changes
|
||||
- (r877 [#188](https://github.com/Suwayomi/Tachidesk-Server/pull/188) by @Syer10) `MangaDataClass.genre` changed type to `List<String>`
|
||||
|
||||
#### Bug fixes
|
||||
- (r899-r901) fix when an external apk is installed and it doesn't have the default tachiyomi-extensions name
|
||||
- (r905) fix a bug where if two sources return the same URL, a false duplicate might be detected
|
||||
|
||||
### Private API
|
||||
- (r887) the `run` task won't call `downloadWebUI` now
|
||||
- (r902) cleanup print/ln instances
|
||||
- (r906) better handling of uninstalling Extensions
|
||||
|
||||
## Tachidesk-WebUI
|
||||
#### Visible changes
|
||||
- (r770) add support for the new genre type
|
||||
- (r771) set the default value of `showNsfw` to `true` so we won't have visual artifacts with a clean install
|
||||
- (r774 [#21](https://github.com/Suwayomi/Tachidesk-WebUI/pull/21) by @voltrare) `ReaderNavbar.jsx`: Swap close and retract Navbar buttons
|
||||
- (r775 [#23](https://github.com/Suwayomi/Tachidesk-WebUI/pull/23) by @voltrare) `yarn.lock`: Fixes version inconsistency after commit 9b866811b
|
||||
- (r776 [#23](https://github.com/Suwayomi/Tachidesk-WebUI/pull/23) by @voltrare) add margin between Source and Extension cards, make the Search button look nicer
|
||||
- (r777) add support for installing external APK files
|
||||
- (r778) fix the makeToaster?
|
||||
- (r779) Action button for installing external extension
|
||||
- (r780 Suwayomi/Tachidesk-WebUI#25) add on hover, active effect to Chapter/Episode card
|
||||
- (r782-r785) updating material-ui to v5 changed the theme
|
||||
- (r785-r788) better `SourceCard` looks on mobile, move `SourceDataClass.isConfigurable` gear button to `SourceMangas`
|
||||
- (r789) implement source configuration
|
||||
|
||||
#### Bug fixes
|
||||
- N/A
|
||||
|
||||
#### Internal changes
|
||||
- (r782-r785) update dependencies, migrate material-ui from v4 to v5
|
||||
|
||||
|
||||
|
||||
# Server: v0.4.9 + WebUI: r769
|
||||
## Tachidesk-Server
|
||||
### Public API
|
||||
#### Non-breaking changes
|
||||
- N/A
|
||||
|
||||
#### Breaking changes
|
||||
- (r857) renamed: `SourceDataClass.isNSFW` -> `SourceDataClass.isNsfw`
|
||||
|
||||
#### Bug fixes
|
||||
- N/A
|
||||
|
||||
### Private API
|
||||
- (r850) Bump WebUI version to r767
|
||||
- (r861) Bump WebUI version to r769
|
||||
|
||||
#### Non-code changes
|
||||
- (r851) Add this changelog file and `CHANGELOG-TEMPLATE.md`
|
||||
- (r852-r853) `CONTRIBUTING.md`: Add a note about this maintaining this file changelog
|
||||
- (r855) `CONTRIBUTING.md`: text cleanup
|
||||
- (r859) `CONTRIBUTING.md`: remove dumb rule
|
||||
- (r862) `windows-bundler.sh`: update jre
|
||||
- (r864) add linux and macOS bundler script and launcher scripts
|
||||
- (r865) fix macOS bundler script and launcher scripts
|
||||
- (r866) bump electron version to v14.0.0
|
||||
- (r868) add linux and macOS bundlers to the publish workflow
|
||||
- (r871) `publish.yml`: remove node module cache, won't need it anymore
|
||||
- (r873) `publish.yml` and `build_push.yml`: fix oopsies
|
||||
|
||||
|
||||
## Tachidesk-WebUI
|
||||
#### Visible changes
|
||||
- (r767-r769) Support for hiding NSFW content in settings screen, extensions screen, sources screen
|
||||
|
||||
#### Bug fixes
|
||||
- N/A
|
||||
|
||||
#### Internal changes
|
||||
- (r767) Remove some duplicate dependency declaration from `package.json`
|
||||
|
||||
#### Non-code changes
|
||||
- (r42-r45) Change `README.md`: some links and stuff
|
||||
- (r45-r765) Add all of the commit history from when WebUI was separated from Server, jumping from r45 to r765 (r45 is exactly the same as r765)
|
||||
- (r766) Steal `.gitattributes` from Tachidesk-Server
|
||||
- (r767) Dependency cleanup in `package.json`
|
||||
|
||||
|
||||
|
||||
|
||||
# Server: v0.4.8 + WebUI: r41
|
||||
## Tachidesk-Server
|
||||
### Public API
|
||||
#### Non-breaking changes
|
||||
- Added support for serializing Search Filters
|
||||
- `SourceDataClass` now has a `isNsfw` key
|
||||
|
||||
#### Breaking changes
|
||||
- N/A
|
||||
|
||||
#### Bug fixes
|
||||
- Fixed a bug where backup restore reversed chapter order
|
||||
- Open Site feature now works properly (https://github.com/Suwayomi/Tachidesk-WebUI/issues/19)
|
||||
|
||||
### Private API
|
||||
- Added `CloudflareInterceptor` from TachiWeb-Server
|
||||
- Restoring backup for mangas in library(merging manga data) is now supported
|
||||
|
||||
## Tachidesk-WebUI
|
||||
#### Visible changes
|
||||
- Better looking manga card titles
|
||||
- Better reader title, next, prev buttons
|
||||
|
||||
#### Bug fixes
|
||||
- Open Site feature now works properly (https://github.com/Suwayomi/Tachidesk-WebUI/issues/19)
|
||||
- Re-ordering categories now works
|
||||
|
||||
#### Internal changes
|
||||
- N/A
|
||||
+22
-35
@@ -2,51 +2,38 @@
|
||||
## Where should I start?
|
||||
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
|
||||
|
||||
**Note to potential contributors:** Notify the developers on Suwayomi discord (#programming channel) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
|
||||
**Note 1:** Notify the developers on [Suwayomi discord](https://discord.gg/DDZdqZWaHA) (#programming channel) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
|
||||
|
||||
## How does Tachidesk work?
|
||||
**Note 2:** Store all changes with each direct commit/PR in [CHANGELOG.md](./CHANGELOG.md).
|
||||
|
||||
## How does Tachidesk-Server work?
|
||||
This project has two components:
|
||||
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
||||
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation.
|
||||
1. **Server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run jar libraries converted from apk extensions. All this concludes to serving a REST API.
|
||||
2. **WebUI:** A React SPA(`create-react-app`) project that works with the server to do the presentation located at https://github.com/Suwayomi/Tachidesk-WebUI
|
||||
|
||||
## Why a web app?
|
||||
This structure is chosen to
|
||||
- Achieve the maximum multi-platform-ness
|
||||
- Gives the ability to acces Tachidesk from a remote web browser e.g. your phone, tablet or smart TV
|
||||
- Eaise development of alternative user intefaces for Tachidesk
|
||||
|
||||
## User Interfaces for Tachidesk server
|
||||
Currently, there are three known interfaces for Tachidesk:
|
||||
1. [webUI](https://github.com/Suwayomi/Tachidesk/tree/master/webUI/react): The react SPA that Tachidesk is traditionally shipped with.
|
||||
2. [TachideskJUI](https://github.com/Suwayomi/TachideskJUI): A Jetbrains Compose Native app, re-uses components made for the upcoming Tachiyomi 1.x
|
||||
3. [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stages of development.
|
||||
- Gives the ability to access Tachidesk-Server from a remote client e.g., your phone, tablet or smart TV
|
||||
- Ease development of user interfaces for Tachidesk
|
||||
|
||||
## Building from source
|
||||
### Prerequisites
|
||||
You need these software packages installed in order to build the project
|
||||
### Server
|
||||
|
||||
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
||||
- Android stubs jar
|
||||
- Manual download: Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||
- Automated download: Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
|
||||
### webUI
|
||||
- Nodejs LTS or latest
|
||||
- Yarn
|
||||
- Git
|
||||
### building the full-blown jar
|
||||
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||
### building without `webUI` bundled(server only)
|
||||
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||
### building the Windows package
|
||||
First Build the jar, then cd into the `scripts` directory and run `./windows<bits>-bundler.sh` (or `./windows<bits>-bundler.ps1` if you are on windows), the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win64.zip`.
|
||||
## Running in development mode
|
||||
First satisfy [the prerequisites](#prerequisites)
|
||||
### server
|
||||
run `./gradlew :server:run --stacktrace` to run the server
|
||||
### webUI
|
||||
How to do it is described in `webUI/react/README.md` but for short,
|
||||
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
||||
then `yarn start` to start the development server, if a new browser window doesn't get opened automatically,
|
||||
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
||||
and supports HMR and all the other goodies you'll need.
|
||||
- **Manual download:** Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||
- **Automated download:** Run `AndroidCompat/getAndroid.sh`(macOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
|
||||
|
||||
### building the full-blown jar (Tachidesk-Server + Tachidesk-WebUI bundle)
|
||||
Run `./gradlew server:downloadWebUI server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`.
|
||||
|
||||
### building without `webUI` bundled (server only)
|
||||
Delete `server/src/main/resources/WebUI.zip` if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx.jar`.
|
||||
|
||||
### building the Windows package
|
||||
First Build the jar, then cd into the `scripts` directory and run `./windows-bundler.sh win32` or `./windows-bundler.sh win64` depending on the target architecture, the resulting built zip package file will be `server/build/Tachidesk-Server-vX.Y.Z-rxxx-winXX.zip`.
|
||||
|
||||
## Running in development mode
|
||||
run `./gradlew :server:run --stacktrace` to run the server
|
||||
|
||||
@@ -3,48 +3,83 @@
|
||||
|-------|----------|---------|---------|
|
||||
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [](https://discord.gg/DDZdqZWaHA) |
|
||||
|
||||
# Tachidesk
|
||||
# Tachidesk-Server is a server app! You may not want to Download Tachidesk-Server directly.
|
||||
Yes, you need a client/user interface app as a front-end for Tachidesk-Server, if you Directly Download Tachidesk-Server you'll get a bundled version of [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI) with it.
|
||||
|
||||
Here's a list of known clients/user interfaces for Tachidesk-Server:
|
||||
- [Tachidesk-JUI](https://github.com/Suwayomi/Tachidesk-JUI): The "official" front-end for Tachidesk-Server, A native desktop Application.
|
||||
- [Tachidesk-WebUI](https://github.com/Suwayomi/Tachidesk-WebUI): The web/electrion front-end that Tachidesk-Server is traditionally shipped with.
|
||||
- [Tachidesk-qtui](https://github.com/Suwayomi/Tachidesk-qtui): A C++/Qt front-end for mobile devices(Android/linux), in super early stage of development.
|
||||
- [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stage of development.
|
||||
|
||||
# What is Tachidesk then?
|
||||
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
||||
|
||||
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||
A free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||
|
||||
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
|
||||
|
||||
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
|
||||
Tachidesk-Server is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
|
||||
|
||||
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
||||
|
||||
**Tachidesk needs serious front-end dev help for it's reader and other parts, if you like the app and want to see it become better please don't hesitate to contribute some code!**
|
||||
|
||||
## Is this application usable? Should I test it?
|
||||
Here is a list of current features:
|
||||
|
||||
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
|
||||
- A library to save your mangas and categories to put them into.
|
||||
- Searching and browsing installed sources.
|
||||
- A decent chapter reader.
|
||||
- Ability to download Manga for offline read
|
||||
- Backup and restore support powered by Tachiyomi Legacy Backups
|
||||
- From Tachiyomi
|
||||
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources
|
||||
- A library to save your mangas and categories to put them into
|
||||
- Searching and browsing installed sources
|
||||
- Ability to download Manga for offline read
|
||||
- Backup and restore support powered by Tachiyomi Backups
|
||||
- From Aniyomi
|
||||
- Installing and executing Aniyomi's Extensions
|
||||
- Searching and browsing installed sources.
|
||||
- Viewing an anime and it's episodes
|
||||
|
||||
**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:** These are capabilities of Tachidesk-Server, the actual working support is provided by each front-end app, checkout their respective readme for more info.
|
||||
|
||||
## Downloading and Running the app
|
||||
### 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.
|
||||
**Note:** Tachidesk-Server is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk-Server/wiki/Troubleshooting) if it happens.
|
||||
|
||||
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).
|
||||
# Downloading and Running the app
|
||||
## General Requirements
|
||||
In order to use the app effectively you need the following:
|
||||
- The jar release of Tachideesk-Server
|
||||
- The Java Runtime Environment(JRE) 8 or newer (included in bundle releases)
|
||||
- A Modern Browser like Google Chrome, Firefox, etc.
|
||||
- ElectronJS (optional) (included in bundle releases)
|
||||
- An internet connection (when you want to use online features)
|
||||
## Using the jar release directly
|
||||
Download the latest `.jar` release from [the releases section](https://github.com/Suwayomi/Tachidesk-Server/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.
|
||||
Make sure you have The Java Runtime Environment installed on your system, 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.
|
||||
|
||||
## Using Operating System Specific Bundles
|
||||
To facilitate the use of Tachidesk we provide bundle releases that include The Java Runtime Environment, ElectronJS and 3 Tachidesk Launcher Scripts.
|
||||
|
||||
#### Launcher Scripts
|
||||
- `Tachidesk Electron Launcher`: Launches Tachidesk inside Electron as a desktop applicaton
|
||||
- `Tachidesk Browser Launcher`: Launches Tachidesk in a browser window
|
||||
- `Tachidesk Debug Launcher`: Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why.
|
||||
|
||||
**Node:** Linux launcher scripts are named a bit differently but work the same.
|
||||
|
||||
### Windows
|
||||
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).
|
||||
Download the latest `win32`(Windows 32-bit) or `win64`(Windows 64-bit) 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 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.
|
||||
Unzip the downloaded file and double click on one of the launcher scripts.
|
||||
|
||||
### macOS
|
||||
Download the latest `macOS-x64`(older macOS systems) or `macOS-arm64`(Apple M1) 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).
|
||||
|
||||
Unzip the downloaded file and double click on one of the launcher scripts.
|
||||
|
||||
### GNU/Linux
|
||||
Download the latest `linux-x64`(x86_64) 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).
|
||||
|
||||
`tar xvf` the downloaded file and double click on one of the launcher scripts or run them using the terminal.
|
||||
|
||||
## Other methods of getting Tachidesk
|
||||
### Arch Linux
|
||||
You can install Tachidesk from the AUR
|
||||
```
|
||||
@@ -52,7 +87,7 @@ yay -S tachidesk
|
||||
```
|
||||
|
||||
### Docker
|
||||
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.
|
||||
Check our Official Docker release [Tachidesk Container](https://github.com/orgs/Suwayomi/packages/container/package/tachidesk) for running Tachidesk Server in a docker container. 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:
|
||||
```
|
||||
@@ -75,9 +110,9 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
## Credit
|
||||
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
|
||||
|
||||
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
||||
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0` and `Copyright 2019 Andy Bao and contributors`.
|
||||
|
||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0` and `Copyright 2015 Javier Tomás`.
|
||||
|
||||
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
+41
-21
@@ -1,8 +1,11 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "1.4.32"
|
||||
kotlin("plugin.serialization") version "1.4.32" apply false
|
||||
kotlin("jvm") version kotlinVersion
|
||||
kotlin("plugin.serialization") version kotlinVersion
|
||||
id("org.jmailen.kotlinter") version "3.6.0"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -12,10 +15,8 @@ allprojects {
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://maven.google.com/")
|
||||
google()
|
||||
maven("https://jitpack.io")
|
||||
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
||||
maven("https://dl.google.com/dl/android/maven2/")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,18 +28,36 @@ val projects = listOf(
|
||||
|
||||
configure(projects) {
|
||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
tasks {
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
withType<LintTask> {
|
||||
source(files("src/kotlin"))
|
||||
}
|
||||
|
||||
withType<FormatTask> {
|
||||
source(files("src/kotlin"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
// Kotlin
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
@@ -46,31 +65,30 @@ configure(projects) {
|
||||
testImplementation(kotlin("test-junit5"))
|
||||
|
||||
// coroutines
|
||||
val coroutinesVersion = "1.4.3"
|
||||
val coroutinesVersion = "1.5.2"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
||||
|
||||
val kotlinSerializationVersion = "1.1.0"
|
||||
val kotlinSerializationVersion = "1.3.0-RC"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
|
||||
|
||||
// Dependency Injection
|
||||
implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0")
|
||||
implementation("org.kodein.di:kodein-di-conf-jvm:7.8.0")
|
||||
|
||||
// Logging
|
||||
implementation("org.slf4j:slf4j-api:1.7.30")
|
||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
||||
implementation("io.github.microutils:kotlin-logging:2.0.6")
|
||||
implementation("org.slf4j:slf4j-api:1.7.32")
|
||||
implementation("ch.qos.logback:logback-classic:1.2.6")
|
||||
implementation("io.github.microutils:kotlin-logging:2.0.11")
|
||||
|
||||
// ReactiveX
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("io.reactivex:rxkotlin:1.0.0")
|
||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
|
||||
// JSoup
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
// dependency both in AndroidCompat and extensions, version locked by Tachiyomi app/extensions
|
||||
implementation("org.jsoup:jsoup:1.14.2")
|
||||
|
||||
// dependency of :AndroidCompat:Config
|
||||
implementation("com.typesafe:config:1.4.1")
|
||||
@@ -79,13 +97,15 @@ configure(projects) {
|
||||
// to get application content root
|
||||
implementation("net.harawata:appdirs:1.2.1")
|
||||
|
||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
||||
// dex2jar
|
||||
val dex2jarVersion = "v26"
|
||||
implementation("com.github.ThexXTURBOXx.dex2jar:dex-translator:$dex2jarVersion")
|
||||
implementation("com.github.ThexXTURBOXx.dex2jar:dex-tools:$dex2jarVersion")
|
||||
|
||||
// APK parser
|
||||
implementation("net.dongliu:apk-parser:2.6.10")
|
||||
|
||||
// Jackson
|
||||
implementation("com.fasterxml.jackson.core:jackson-annotations:2.10.3")
|
||||
// dependency both in AndroidCompat and server, version locked by javalin
|
||||
implementation("com.fasterxml.jackson.core:jackson-annotations:2.12.4")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("net.lingala.zip4j:zip4j:2.9.0")
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import java.io.BufferedReader
|
||||
|
||||
/*
|
||||
* 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/. */
|
||||
|
||||
const val kotlinVersion = "1.5.30"
|
||||
|
||||
const val MainClass = "suwayomi.tachidesk.MainKt"
|
||||
|
||||
// should be bumped with each stable release
|
||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.5.2"
|
||||
|
||||
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r807"
|
||||
|
||||
// counts commits on the master branch
|
||||
val tachideskRevision = runCatching {
|
||||
System.getenv("ProductRevision") ?: Runtime
|
||||
.getRuntime()
|
||||
.exec("git rev-list HEAD --count")
|
||||
.let { process ->
|
||||
process.waitFor()
|
||||
val output = process.inputStream.use {
|
||||
it.bufferedReader().use(BufferedReader::readText)
|
||||
}
|
||||
process.destroy()
|
||||
"r" + output.trim()
|
||||
}
|
||||
}.getOrDefault("r0")
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
cd "`dirname "$0"`"
|
||||
|
||||
./jre/Contents/Home/bin/java -jar Tachidesk.jar
|
||||
@@ -1 +1,7 @@
|
||||
jre\bin\java -Dsuwayomi.tachidesk.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||
:: cleaner output
|
||||
@echo off
|
||||
|
||||
jre\bin\java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||
|
||||
:: Prevent cmd from closing when Tachidesk crashes
|
||||
pause
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
cd "`dirname "$0"`"
|
||||
|
||||
./jre/Contents/Home/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||
@@ -1 +1 @@
|
||||
jre\bin\javaw "-Dsuwayomi.tachidesk.server.webInterface=electron" "-Dsuwayomi.tachidesk.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
|
||||
jre\bin\javaw "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
|
||||
@@ -0,0 +1,3 @@
|
||||
cd "`dirname "$0"`"
|
||||
|
||||
./jre/Contents/Home/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/Electron.app/Contents/MacOS/Electron" -jar Tachidesk.jar
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
./jre/bin/java -jar Tachidesk.jar
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
./jre/bin/java -Dsuwayomi.tachidesk.config.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
./jre/bin/java "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron" -jar Tachidesk.jar
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/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="v14.0.0"
|
||||
|
||||
if [ $1 = "linux-x64" ]; then
|
||||
jre="OpenJDK8U-jre_x64_linux_hotspot_8u302b08.tar.gz"
|
||||
jre_release="jdk8u302-b08"
|
||||
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
|
||||
jre_dir="$jre_release-jre"
|
||||
electron="electron-$electron_version-linux-x64.zip"
|
||||
elif [ $1 = "macOS-x64" ]; then
|
||||
jre="OpenJDK8U-jre_x64_mac_hotspot_8u302b08.tar.gz"
|
||||
jre_release="jdk8u302-b08"
|
||||
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
|
||||
jre_dir="$jre_release-jre"
|
||||
electron="electron-$electron_version-darwin-x64.zip"
|
||||
elif [ $1 = "macOS-arm64" ]; then
|
||||
jre="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64.tar.gz"
|
||||
jre_release="zulu8.56.0.23-ca-jre8.0.302-macosx_aarch64"
|
||||
jre_url="https://cdn.azul.com/zulu/bin/$jre"
|
||||
jre_dir="$jre_release"
|
||||
electron="electron-$electron_version-darwin-arm64.zip"
|
||||
else
|
||||
echo "Unsupported arch value: $1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
arch="$1"
|
||||
os=$(echo $arch | cut -d '-' -f1)
|
||||
|
||||
echo "creating $arch 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 $jre_url -o $jre
|
||||
fi
|
||||
tar xvf $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
|
||||
|
||||
# copy artifacts
|
||||
cp $jar $release_name/Tachidesk.jar
|
||||
if [ $os = linux ]; then
|
||||
cp "resources/tachidesk-browser-launcher.sh" $release_name
|
||||
cp "resources/tachidesk-debug-launcher.sh" $release_name
|
||||
cp "resources/tachidesk-electron-launcher.sh" $release_name
|
||||
elif [ $os = macOS ]; then
|
||||
cp "resources/Tachidesk Browser Launcher.command" $release_name
|
||||
cp "resources/Tachidesk Debug Launcher.command" $release_name
|
||||
cp "resources/Tachidesk Electron Launcher.command" $release_name
|
||||
fi
|
||||
|
||||
archive_name=""
|
||||
if [ $os = linux ]; then
|
||||
archive_name=$release_name.tar.gz
|
||||
GZIP=-9 tar cvzf $archive_name $release_name
|
||||
elif [ $os = macOS ]; then
|
||||
archive_name=$release_name.zip
|
||||
zip -9 -r $archive_name $release_name
|
||||
fi
|
||||
|
||||
rm -rf $release_name
|
||||
|
||||
# clean up from possible previous runs
|
||||
if [ -f ../server/build/$archive_name ]; then
|
||||
rm ../server/build/$archive_name
|
||||
fi
|
||||
|
||||
mv $archive_name ../server/build/
|
||||
@@ -6,19 +6,23 @@
|
||||
# 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"
|
||||
electron_version="v14.0.0"
|
||||
|
||||
if [ $1 = "win32" ]; then
|
||||
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||
jre_release="jdk8u292-b10"
|
||||
jre_url="https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/$jre_release/$jre"
|
||||
arch="win32"
|
||||
electron="electron-$electron_version-win32-ia32.zip"
|
||||
else
|
||||
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||
jre="OpenJDK8U-jre_x64_windows_hotspot_8u302b08.zip"
|
||||
jre_release="jdk8u302-b08"
|
||||
jre_url="https://github.com/adoptium/temurin8-binaries/releases/download/$jre_release/$jre"
|
||||
arch="win64"
|
||||
electron="electron-$electron_version-win32-x64.zip"
|
||||
fi
|
||||
|
||||
jre_dir="jdk8u292-b10-jre"
|
||||
jre_dir="$jre_release-jre"
|
||||
|
||||
echo "creating windows bundle"
|
||||
|
||||
@@ -33,7 +37,7 @@ 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
|
||||
curl -L $jre_url -o $jre
|
||||
fi
|
||||
unzip $jre
|
||||
mv $jre_dir $release_name/jre
|
||||
|
||||
+63
-88
@@ -1,24 +1,11 @@
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||
import java.io.BufferedReader
|
||||
import de.undercouch.gradle.tasks.download.Download
|
||||
import java.time.Instant
|
||||
|
||||
plugins {
|
||||
application
|
||||
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||
id("org.jmailen.kotlinter") version "3.4.3"
|
||||
id("de.fuerstenau.buildconfig") version "1.1.8"
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url = uri("https://repo1.maven.org/maven2/")
|
||||
}
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
id("com.github.gmazzo.buildconfig") version "3.0.3"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -30,13 +17,14 @@ dependencies {
|
||||
implementation("com.squareup.okio:okio:2.10.0")
|
||||
|
||||
// Javalin api
|
||||
implementation("io.javalin:javalin:3.13.6")
|
||||
// jackson version is tied to javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.3")
|
||||
implementation("io.javalin:javalin:4.0.0")
|
||||
// jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
val jacksonVersion = "2.12.4"
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
|
||||
|
||||
// Exposed ORM
|
||||
val exposedVersion = "0.31.1"
|
||||
val exposedVersion = "0.34.1"
|
||||
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||
@@ -45,29 +33,32 @@ dependencies {
|
||||
implementation("com.h2database:h2:1.4.200")
|
||||
|
||||
// Exposed Migrations
|
||||
val exposedMigrationsVersion = "3.1.0"
|
||||
implementation("com.github.Suwayomi:exposed-migrations:$exposedMigrationsVersion")
|
||||
implementation("com.github.Suwayomi:exposed-migrations:3.1.2")
|
||||
|
||||
// tray icon
|
||||
implementation("com.dorkbox:SystemTray:4.1")
|
||||
implementation("com.dorkbox:Utilities:1.9")
|
||||
|
||||
|
||||
implementation("com.dorkbox:Utilities:1.9") // version locked by SystemTray
|
||||
|
||||
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.9.1")
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
implementation("org.jsoup:jsoup:1.14.2")
|
||||
implementation("com.google.code.gson:gson:2.8.8")
|
||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||
|
||||
// Sort
|
||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||
|
||||
// asm for fixing SimpleDateFormat (must match Dex2Jar version)
|
||||
implementation("org.ow2.asm:asm-debug-all:5.0.3")
|
||||
// asm for ByteCodeEditor(fixing SimpleDateFormat) (must match Dex2Jar version)
|
||||
implementation("org.ow2.asm:asm:9.2")
|
||||
|
||||
// extracting zip files
|
||||
// Disk & File
|
||||
implementation("net.lingala.zip4j:zip4j:2.9.0")
|
||||
implementation("com.github.junrar:junrar:7.4.0")
|
||||
|
||||
// CloudflareInterceptor
|
||||
implementation("net.sourceforge.htmlunit:htmlunit:2.52.0")
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
||||
@@ -81,14 +72,13 @@ dependencies {
|
||||
// implementation(fileTree("lib/"))
|
||||
}
|
||||
|
||||
val MainClass = "suwayomi.tachidesk.MainKt"
|
||||
application {
|
||||
mainClass.set(MainClass)
|
||||
|
||||
// for testing electron
|
||||
// uncomment for testing electron
|
||||
// applicationDefaultJvmArgs = listOf(
|
||||
// "-Dsuwayomi.tachidesk.webInterface=electron",
|
||||
// "-Dsuwayomi.tachidesk.electronPath=/usr/bin/electron"
|
||||
// "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
|
||||
// "-Dsuwayomi.tachidesk.config.server.electronPath=/usr/bin/electron"
|
||||
// )
|
||||
}
|
||||
|
||||
@@ -100,71 +90,44 @@ sourceSets {
|
||||
}
|
||||
}
|
||||
|
||||
// should be bumped with each stable release
|
||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.4"
|
||||
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r20"
|
||||
|
||||
// counts commit count on master
|
||||
val tachideskRevision = runCatching {
|
||||
System.getenv("ProductRevision") ?: Runtime
|
||||
.getRuntime()
|
||||
.exec("git rev-list HEAD --count")
|
||||
.let { process ->
|
||||
process.waitFor()
|
||||
val output = process.inputStream.use {
|
||||
it.bufferedReader().use(BufferedReader::readText)
|
||||
}
|
||||
process.destroy()
|
||||
"r" + output.trim()
|
||||
}
|
||||
}.getOrDefault("r0")
|
||||
|
||||
buildConfig {
|
||||
clsName = "BuildConfig"
|
||||
packageName = "suwayomi.tachidesk.server"
|
||||
className("BuildConfig")
|
||||
packageName("suwayomi.tachidesk.server")
|
||||
|
||||
useKotlinOutput()
|
||||
|
||||
buildConfigField("String", "NAME", rootProject.name)
|
||||
buildConfigField("String", "VERSION", tachideskVersion)
|
||||
buildConfigField("String", "REVISION", tachideskRevision)
|
||||
buildConfigField("String", "BUILD_TYPE", if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview")
|
||||
fun quoteWrap(obj: Any): String = """"$obj""""
|
||||
|
||||
buildConfigField("String", "NAME", quoteWrap(rootProject.name))
|
||||
buildConfigField("String", "VERSION", quoteWrap(tachideskVersion))
|
||||
buildConfigField("String", "REVISION", quoteWrap(tachideskRevision))
|
||||
buildConfigField("String", "BUILD_TYPE", quoteWrap(if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview"))
|
||||
buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString())
|
||||
|
||||
|
||||
buildConfigField("String", "WEBUI_REPO", "https://github.com/Suwayomi/Tachidesk-WebUI-preview")
|
||||
buildConfigField("String", "WEBUI_TAG", webUIRevisionTag)
|
||||
buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview"))
|
||||
buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag))
|
||||
|
||||
|
||||
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk")
|
||||
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA")
|
||||
buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk"))
|
||||
buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))
|
||||
}
|
||||
|
||||
tasks {
|
||||
shadowJar {
|
||||
manifest {
|
||||
attributes(
|
||||
mapOf(
|
||||
"Main-Class" to MainClass,
|
||||
"Implementation-Title" to rootProject.name,
|
||||
"Implementation-Vendor" to "The Suwayomi Project",
|
||||
"Specification-Version" to tachideskVersion,
|
||||
"Implementation-Version" to tachideskRevision
|
||||
)
|
||||
"Main-Class" to MainClass,
|
||||
"Implementation-Title" to rootProject.name,
|
||||
"Implementation-Vendor" to "The Suwayomi Project",
|
||||
"Specification-Version" to tachideskVersion,
|
||||
"Implementation-Version" to tachideskRevision
|
||||
)
|
||||
}
|
||||
archiveBaseName.set(rootProject.name)
|
||||
archiveVersion.set(tachideskVersion)
|
||||
archiveClassifier.set(tachideskRevision)
|
||||
}
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnit()
|
||||
@@ -172,11 +135,10 @@ tasks {
|
||||
|
||||
withType<ShadowJar> {
|
||||
destinationDirectory.set(File("$rootDir/server/build"))
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
}
|
||||
|
||||
named("run") {
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
dependsOn(":formatKotlin", ":lintKotlin")
|
||||
}
|
||||
|
||||
named<Copy>("processResources") {
|
||||
@@ -184,16 +146,29 @@ tasks {
|
||||
mustRunAfter("downloadWebUI")
|
||||
}
|
||||
|
||||
register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") {
|
||||
register<Download>("downloadWebUI") {
|
||||
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
|
||||
dest("src/main/resources/WebUI.zip")
|
||||
}
|
||||
|
||||
withType<LintTask> {
|
||||
source(files("src/kotlin"))
|
||||
}
|
||||
|
||||
withType<FormatTask> {
|
||||
source(files("src/kotlin"))
|
||||
fun shouldOverwrite(): Boolean {
|
||||
val zipPath = project.projectDir.absolutePath + "/src/main/resources/WebUI.zip"
|
||||
val zipFile = net.lingala.zip4j.ZipFile(zipPath)
|
||||
|
||||
var shouldOverwrite = true
|
||||
if (zipFile.isValidZipFile) {
|
||||
val zipRevision = zipFile.getInputStream(zipFile.getFileHeader("revision")).bufferedReader().use {
|
||||
it.readText().trim()
|
||||
}
|
||||
|
||||
if (zipRevision == webUIRevisionTag) {
|
||||
shouldOverwrite = false
|
||||
}
|
||||
}
|
||||
|
||||
return shouldOverwrite
|
||||
}
|
||||
|
||||
overwrite(shouldOverwrite())
|
||||
}
|
||||
}
|
||||
|
||||
+2
-19
@@ -1,30 +1,13 @@
|
||||
/*
|
||||
Copyright 2014 Prateek Srivastava
|
||||
Copyright 2015 Javier Tomás
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
This file may have been modified after being copied from it's original source.
|
||||
*/
|
||||
package com.f2prateek.rx.preferences;
|
||||
|
||||
final class Preconditions {
|
||||
static void checkNotNull(Object o, String message) {
|
||||
if (o == null) {
|
||||
throw new NullPointerException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private Preconditions() {
|
||||
throw new AssertionError("No instances");
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ interface AnimeCatalogueSource : AnimeSource {
|
||||
/**
|
||||
* An ISO 639-1 compliant language code (two letters in lower case).
|
||||
*/
|
||||
val lang: String
|
||||
override val lang: String
|
||||
|
||||
/**
|
||||
* Whether the source has support for latest updates.
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.animesource
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
@@ -19,6 +20,9 @@ interface AnimeSource {
|
||||
*/
|
||||
val name: String
|
||||
|
||||
val lang: String
|
||||
get() = ""
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a anime.
|
||||
*
|
||||
@@ -36,12 +40,12 @@ interface AnimeSource {
|
||||
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>>
|
||||
|
||||
/**
|
||||
* Returns an observable with a link for the episode of an anime.
|
||||
* Returns an observable with a list of video 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>
|
||||
fun fetchVideoList(episode: SEpisode): Observable<List<Video>>
|
||||
|
||||
// /**
|
||||
// * [1.x API] Get the updated details for a anime.
|
||||
@@ -74,4 +78,4 @@ interface AnimeSource {
|
||||
|
||||
// fun AnimeSource.icon(): Drawable? = Injekt.get<AnimeExtensionManager>().getAppIconForSource(this)
|
||||
|
||||
// fun AnimeSource.getPreferenceKey(): String = "source_$id"
|
||||
fun AnimeSource.getPreferenceKey(): String = "source_$id"
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animesource.model
|
||||
|
||||
// import tachiyomi.animesource.model.AnimeInfo
|
||||
import java.io.Serializable
|
||||
|
||||
interface SAnime : Serializable {
|
||||
@@ -23,9 +24,7 @@ interface SAnime : Serializable {
|
||||
var initialized: Boolean
|
||||
|
||||
fun copyFrom(other: SAnime) {
|
||||
if (other.title != null) {
|
||||
title = other.title
|
||||
}
|
||||
title = other.title
|
||||
|
||||
if (other.author != null) {
|
||||
author = other.author
|
||||
@@ -65,3 +64,30 @@ interface SAnime : Serializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fun SAnime.toAnimeInfo(): AnimeInfo {
|
||||
// return AnimeInfo(
|
||||
// key = this.url,
|
||||
// title = this.title,
|
||||
// artist = this.artist ?: "",
|
||||
// author = this.author ?: "",
|
||||
// description = this.description ?: "",
|
||||
// genres = this.genre?.split(", ") ?: emptyList(),
|
||||
// status = this.status,
|
||||
// cover = this.thumbnail_url ?: ""
|
||||
// )
|
||||
// }
|
||||
|
||||
// fun AnimeInfo.toSAnime(): SAnime {
|
||||
// val animeInfo = this
|
||||
// return SAnime.create().apply {
|
||||
// url = animeInfo.key
|
||||
// title = animeInfo.title
|
||||
// artist = animeInfo.artist
|
||||
// author = animeInfo.author
|
||||
// description = animeInfo.description
|
||||
// genre = animeInfo.genres.joinToString(", ")
|
||||
// status = animeInfo.status
|
||||
// thumbnail_url = animeInfo.cover
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animesource.model
|
||||
|
||||
// import tachiyomi.animesource.model.EpisodeInfo
|
||||
import java.io.Serializable
|
||||
|
||||
interface SEpisode : Serializable {
|
||||
@@ -28,3 +29,24 @@ interface SEpisode : Serializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fun SEpisode.toEpisodeInfo(): EpisodeInfo {
|
||||
// return EpisodeInfo(
|
||||
// dateUpload = this.date_upload,
|
||||
// key = this.url,
|
||||
// name = this.name,
|
||||
// number = this.episode_number,
|
||||
// scanlator = this.scanlator ?: ""
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// fun EpisodeInfo.toSEpisode(): SEpisode {
|
||||
// val episode = this
|
||||
// return SEpisode.create().apply {
|
||||
// url = episode.key
|
||||
// name = episode.name
|
||||
// date_upload = episode.dateUpload
|
||||
// episode_number = episode.number
|
||||
// scanlator = episode.scanlator
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package eu.kanade.tachiyomi.animesource.model
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.network.ProgressListener
|
||||
import rx.subjects.Subject
|
||||
// import tachiyomi.animesource.model.VideoUrl
|
||||
|
||||
open class Video(
|
||||
val url: String = "",
|
||||
val quality: String = "",
|
||||
var videoUrl: String? = null,
|
||||
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
|
||||
) : ProgressListener {
|
||||
|
||||
@Transient
|
||||
@Volatile
|
||||
var status: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
statusSubject?.onNext(value)
|
||||
statusCallback?.invoke(this)
|
||||
}
|
||||
|
||||
@Transient
|
||||
@Volatile
|
||||
var progress: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
statusCallback?.invoke(this)
|
||||
}
|
||||
|
||||
@Transient
|
||||
private var statusSubject: Subject<Int, Int>? = null
|
||||
|
||||
@Transient
|
||||
private var statusCallback: ((Video) -> Unit)? = null
|
||||
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
progress = if (contentLength > 0) {
|
||||
(100 * bytesRead / contentLength).toInt()
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
fun setStatusSubject(subject: Subject<Int, Int>?) {
|
||||
this.statusSubject = subject
|
||||
}
|
||||
|
||||
fun setStatusCallback(f: ((Video) -> Unit)?) {
|
||||
statusCallback = f
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUEUE = 0
|
||||
const val LOAD_VIDEO = 1
|
||||
const val DOWNLOAD_IMAGE = 2
|
||||
const val READY = 3
|
||||
const val ERROR = 4
|
||||
}
|
||||
}
|
||||
|
||||
// fun Video.toVideoUrl(): VideoUrl {
|
||||
// return VideoUrl(
|
||||
// url = this.videoUrl ?: this.url
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// fun VideoUrl.toVideo(index: Int): Video {
|
||||
// return Video(
|
||||
// videoUrl = this.url
|
||||
// )
|
||||
// }
|
||||
@@ -5,11 +5,11 @@ 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.animesource.model.Video
|
||||
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
|
||||
@@ -54,7 +54,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
* Note the generated id sets the sign bit to 0.
|
||||
*/
|
||||
override val id by lazy {
|
||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
||||
val key = "${name.lowercase()}/$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
|
||||
}
|
||||
@@ -80,7 +80,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
/**
|
||||
* Visible name of the source.
|
||||
*/
|
||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
||||
override fun toString() = "$name (${lang.uppercase()})"
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of anime. Normally it's not needed to
|
||||
@@ -218,14 +218,6 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -236,16 +228,6 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
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.
|
||||
*
|
||||
@@ -254,19 +236,25 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
protected abstract fun episodeListParse(response: Response): List<SEpisode>
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of episodes.
|
||||
* Returns an observable with the page list for a chapter.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
*/
|
||||
protected abstract fun episodeLinkParse(response: Response): String
|
||||
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
|
||||
return client.newCall(videoListRequest(episode))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
videoListParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 whose page list has to be fetched.
|
||||
* @param episode the episode to look for links.
|
||||
*/
|
||||
protected open fun pageListRequest(episode: SEpisode): Request {
|
||||
protected open fun videoListRequest(episode: SEpisode): Request {
|
||||
return GET(baseUrl + episode.url, headers)
|
||||
}
|
||||
|
||||
@@ -275,7 +263,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun pageListParse(response: Response): List<Page>
|
||||
protected abstract fun videoListParse(response: Response): List<Video>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page containing the source url of the image. If there's any
|
||||
@@ -283,20 +271,20 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
*
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
open fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return client.newCall(imageUrlRequest(page))
|
||||
open fun fetchVideoUrl(video: Video): Observable<String> {
|
||||
return client.newCall(videoUrlRequest(video))
|
||||
.asObservableSuccess()
|
||||
.map { imageUrlParse(it) }
|
||||
.map { videoUrlParse(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
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
protected open fun imageUrlRequest(page: Page): Request {
|
||||
return GET(page.url, headers)
|
||||
protected open fun videoUrlRequest(video: Video): Request {
|
||||
return GET(video.url, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,15 +292,15 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
protected abstract fun imageUrlParse(response: Response): String
|
||||
protected abstract fun videoUrlParse(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)
|
||||
fun fetchVideo(video: Video): Observable<Response> {
|
||||
return client.newCallWithProgress(videoRequest(video), video)
|
||||
.asObservableSuccess()
|
||||
}
|
||||
|
||||
@@ -320,10 +308,10 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
* 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
|
||||
* @param video the video whose link has to be fetched
|
||||
*/
|
||||
protected open fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers)
|
||||
protected open fun videoRequest(video: Video): Request {
|
||||
return GET(video.videoUrl!!, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+16
-17
@@ -1,26 +1,25 @@
|
||||
package eu.kanade.tachiyomi.source.online
|
||||
package eu.kanade.tachiyomi.animesource.online
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import rx.Observable
|
||||
|
||||
fun AnimeHttpSource.getImageUrl(page: Page): Observable<Page> {
|
||||
page.status = Page.LOAD_PAGE
|
||||
return fetchImageUrl(page)
|
||||
.doOnError { page.status = Page.ERROR }
|
||||
fun AnimeHttpSource.getVideoUrl(video: Video): Observable<Video> {
|
||||
video.status = Video.LOAD_VIDEO
|
||||
return fetchVideoUrl(video)
|
||||
.doOnError { video.status = Video.ERROR }
|
||||
.onErrorReturn { null }
|
||||
.doOnNext { page.imageUrl = it }
|
||||
.map { page }
|
||||
.doOnNext { video.videoUrl = it }
|
||||
.map { video }
|
||||
}
|
||||
|
||||
fun AnimeHttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { !it.imageUrl.isNullOrEmpty() }
|
||||
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
||||
fun AnimeHttpSource.fetchUrlFromVideo(video: Video): Observable<Video> {
|
||||
return Observable.just(video)
|
||||
.filter { !it.videoUrl.isNullOrEmpty() }
|
||||
.mergeWith(fetchRemainingVideoUrlsFromVideoList(video))
|
||||
}
|
||||
|
||||
fun AnimeHttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { it.imageUrl.isNullOrEmpty() }
|
||||
.concatMap { getImageUrl(it) }
|
||||
fun AnimeHttpSource.fetchRemainingVideoUrlsFromVideoList(video: Video): Observable<Video> {
|
||||
return Observable.just(video)
|
||||
.filter { it.videoUrl.isNullOrEmpty() }
|
||||
.concatMap { getVideoUrl(it) }
|
||||
}
|
||||
|
||||
+17
-33
@@ -3,7 +3,7 @@ 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.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
@@ -159,21 +159,6 @@ abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@@ -181,36 +166,35 @@ abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
|
||||
*/
|
||||
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())
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
return document.select(videoListSelector()).map { videoFromElement(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a page list from the given document.
|
||||
*
|
||||
* @param document the parsed document.
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each video.
|
||||
*/
|
||||
protected abstract fun pageListParse(document: Document): List<Page>
|
||||
protected abstract fun videoListSelector(): String
|
||||
|
||||
/**
|
||||
* Parse the response from the site and returns the absolute url to the source image.
|
||||
* Returns a video from the given element.
|
||||
*
|
||||
* @param element an element obtained from [videoListSelector].
|
||||
*/
|
||||
protected abstract fun videoFromElement(element: Element): Video
|
||||
|
||||
/**
|
||||
* Parse the response from the site and returns the absolute url to the source video.
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
return imageUrlParse(response.asJsoup())
|
||||
override fun videoUrlParse(response: Response): String {
|
||||
return videoUrlParse(response.asJsoup())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,5 +202,5 @@ abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
|
||||
*
|
||||
* @param document the parsed document.
|
||||
*/
|
||||
protected abstract fun imageUrlParse(document: Document): String
|
||||
protected abstract fun videoUrlParse(document: Document): String
|
||||
}
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// import android.annotation.SuppressLint
|
||||
// import android.content.Context
|
||||
// import android.os.Build
|
||||
// import android.os.Handler
|
||||
// import android.os.Looper
|
||||
// import android.webkit.WebSettings
|
||||
// import android.webkit.WebView
|
||||
// import android.widget.Toast
|
||||
// import eu.kanade.tachiyomi.R
|
||||
// import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
// import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
// import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
// import eu.kanade.tachiyomi.util.system.isOutdated
|
||||
// import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
// import eu.kanade.tachiyomi.util.system.toast
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class CloudflareInterceptor() : Interceptor {
|
||||
|
||||
// private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
// private val networkHelper = NetworkHelper()
|
||||
|
||||
/**
|
||||
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
|
||||
* blocking the main thread too much. If used too often we could consider moving it to the
|
||||
* Application class.
|
||||
*/
|
||||
// private val initWebView by lazy {
|
||||
// WebSettings.getDefaultUserAgent(context)
|
||||
// }
|
||||
|
||||
@Synchronized
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
return chain.proceed(originalRequest)
|
||||
|
||||
// if (!WebViewUtil.supportsWebView(context)) {
|
||||
// launchUI {
|
||||
// context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||
// }
|
||||
// return chain.proceed(originalRequest)
|
||||
// }
|
||||
//
|
||||
// initWebView
|
||||
//
|
||||
// val response = chain.proceed(originalRequest)
|
||||
//
|
||||
// // Check if Cloudflare anti-bot is on
|
||||
// if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
|
||||
// return response
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// response.close()
|
||||
// networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
|
||||
// val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
|
||||
// .firstOrNull { it.name == "cf_clearance" }
|
||||
// resolveWithWebView(originalRequest, oldCookie)
|
||||
//
|
||||
// return chain.proceed(originalRequest)
|
||||
// } catch (e: Exception) {
|
||||
// // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||
// // we don't crash the entire app
|
||||
// throw IOException(e)
|
||||
// }
|
||||
}
|
||||
//
|
||||
// // @SuppressLint("SetJavaScriptEnabled")
|
||||
// private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
|
||||
// // We need to lock this thread until the WebView finds the challenge solution url, because
|
||||
// // OkHttp doesn't support asynchronous interceptors.
|
||||
// val latch = CountDownLatch(1)
|
||||
//
|
||||
// var webView: WebView? = null
|
||||
//
|
||||
// var challengeFound = false
|
||||
// var cloudflareBypassed = false
|
||||
// var isWebViewOutdated = false
|
||||
//
|
||||
// val origRequestUrl = request.url.toString()
|
||||
// val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||
// headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
|
||||
//
|
||||
// handler.post {
|
||||
// val webview = WebView(context)
|
||||
// webView = webview
|
||||
// webview.setDefaultSettings()
|
||||
//
|
||||
// // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||
// webview.settings.userAgentString = request.header("User-Agent")
|
||||
// ?: HttpSource.DEFAULT_USERAGENT
|
||||
//
|
||||
// webview.webViewClient = object : WebViewClientCompat() {
|
||||
// override fun onPageFinished(view: WebView, url: String) {
|
||||
// fun isCloudFlareBypassed(): Boolean {
|
||||
// return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl())
|
||||
// .firstOrNull { it.name == "cf_clearance" }
|
||||
// .let { it != null && it != oldCookie }
|
||||
// }
|
||||
//
|
||||
// if (isCloudFlareBypassed()) {
|
||||
// cloudflareBypassed = true
|
||||
// latch.countDown()
|
||||
// }
|
||||
//
|
||||
// // HTTP error codes are only received since M
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
// url == origRequestUrl && !challengeFound
|
||||
// ) {
|
||||
// // The first request didn't return the challenge, abort.
|
||||
// latch.countDown()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onReceivedErrorCompat(
|
||||
// view: WebView,
|
||||
// errorCode: Int,
|
||||
// description: String?,
|
||||
// failingUrl: String,
|
||||
// isMainFrame: Boolean
|
||||
// ) {
|
||||
// if (isMainFrame) {
|
||||
// if (errorCode == 503) {
|
||||
// // Found the Cloudflare challenge page.
|
||||
// challengeFound = true
|
||||
// } else {
|
||||
// // Unlock thread, the challenge wasn't found.
|
||||
// latch.countDown()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// webView?.loadUrl(origRequestUrl, headers)
|
||||
// }
|
||||
//
|
||||
// // Wait a reasonable amount of time to retrieve the solution. The minimum should be
|
||||
// // around 4 seconds but it can take more due to slow networks or server issues.
|
||||
// latch.await(12, TimeUnit.SECONDS)
|
||||
//
|
||||
// handler.post {
|
||||
// if (!cloudflareBypassed) {
|
||||
// isWebViewOutdated = webView?.isOutdated() == true
|
||||
// }
|
||||
//
|
||||
// webView?.stopLoading()
|
||||
// webView?.destroy()
|
||||
// }
|
||||
//
|
||||
// // Throw exception if we failed to bypass Cloudflare
|
||||
// if (!cloudflareBypassed) {
|
||||
// // Prompt user to update WebView if it seems too outdated
|
||||
// if (isWebViewOutdated) {
|
||||
// context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG)
|
||||
// }
|
||||
//
|
||||
// throw Exception(context.getString(R.string.information_cloudflare_bypass_failure))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// companion object {
|
||||
// private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||
// private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
|
||||
// }
|
||||
}
|
||||
@@ -10,69 +10,63 @@ package eu.kanade.tachiyomi.network
|
||||
// import android.content.Context
|
||||
// import eu.kanade.tachiyomi.BuildConfig
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import android.content.Context
|
||||
// import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
// import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
// import okhttp3.logging.HttpLoggingInterceptor
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
class NetworkHelper(context: Context) {
|
||||
|
||||
// private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
// private val cacheDir = File(context.cacheDir, "network_cache")
|
||||
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
// private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
|
||||
val cookieManager = MemoryCookieJar()
|
||||
val cookieManager = PersistentCookieJar(context)
|
||||
|
||||
val client by lazy {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
// .cache(Cache(cacheDir, cacheSize))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(5, TimeUnit.MINUTES)
|
||||
.writeTimeout(5, TimeUnit.MINUTES)
|
||||
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
|
||||
private val baseClientBuilder: OkHttpClient.Builder
|
||||
get() {
|
||||
val builder = OkHttpClient.Builder()
|
||||
.cookieJar(cookieManager)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(UserAgentInterceptor())
|
||||
|
||||
// .addInterceptor(UserAgentInterceptor())
|
||||
if (serverConfig.debugLogsEnabled) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BASIC
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
// if (BuildConfig.DEBUG) {
|
||||
// val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
// level = HttpLoggingInterceptor.Level.HEADERS
|
||||
// when (preferences.dohProvider()) {
|
||||
// PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
// PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
// }
|
||||
// builder.addInterceptor(httpLoggingInterceptor)
|
||||
// }
|
||||
|
||||
// if (preferences.enableDoh()) {
|
||||
// builder.dns(
|
||||
// DnsOverHttps.Builder().client(builder.build())
|
||||
// .url("https://cloudflare-dns.com/dns-query".toHttpUrl())
|
||||
// .bootstrapDnsHosts(
|
||||
// listOf(
|
||||
// InetAddress.getByName("162.159.36.1"),
|
||||
// InetAddress.getByName("162.159.46.1"),
|
||||
// InetAddress.getByName("1.1.1.1"),
|
||||
// InetAddress.getByName("1.0.0.1"),
|
||||
// InetAddress.getByName("162.159.132.53"),
|
||||
// InetAddress.getByName("2606:4700:4700::1111"),
|
||||
// InetAddress.getByName("2606:4700:4700::1001"),
|
||||
// InetAddress.getByName("2606:4700:4700::0064"),
|
||||
// InetAddress.getByName("2606:4700:4700::6400")
|
||||
// )
|
||||
// )
|
||||
// .build()
|
||||
// )
|
||||
// }
|
||||
return builder
|
||||
}
|
||||
|
||||
builder.build()
|
||||
}
|
||||
// val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
||||
val client by lazy { baseClientBuilder.build() }
|
||||
|
||||
val cloudflareClient by lazy {
|
||||
client.newBuilder()
|
||||
.addInterceptor(CloudflareInterceptor())
|
||||
.build()
|
||||
}
|
||||
|
||||
// Tachidesk -->
|
||||
val cookies: PersistentCookieStore
|
||||
get() = cookieManager.store
|
||||
// Tachidesk <--
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
// import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
fun Call.asObservable(): Observable<Response> {
|
||||
return Observable.unsafeCreate { subscriber ->
|
||||
@@ -48,36 +53,38 @@ fun Call.asObservable(): Observable<Response> {
|
||||
}
|
||||
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
// suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
||||
// return suspendCancellableCoroutine { continuation ->
|
||||
// enqueue(
|
||||
// object : Callback {
|
||||
// override fun onResponse(call: Call, response: Response) {
|
||||
// if (assertSuccess && !response.isSuccessful) {
|
||||
// continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// continuation.resume(response)
|
||||
// }
|
||||
//
|
||||
// override fun onFailure(call: Call, e: IOException) {
|
||||
// // Don't bother with resuming the continuation if it is already cancelled.
|
||||
// if (continuation.isCancelled) return
|
||||
// continuation.resumeWithException(e)
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
//
|
||||
// continuation.invokeOnCancellation {
|
||||
// try {
|
||||
// cancel()
|
||||
// } catch (ex: Throwable) {
|
||||
// // Ignore cancel exception
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
suspend fun Call.await(): Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
enqueue(
|
||||
object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (!response.isSuccessful) {
|
||||
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
|
||||
return
|
||||
}
|
||||
|
||||
continuation.resume(response) {
|
||||
response.body?.closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
// Don't bother with resuming the continuation if it is already cancelled.
|
||||
if (continuation.isCancelled) return
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
try {
|
||||
cancel()
|
||||
} catch (ex: Throwable) {
|
||||
// Ignore cancel exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable()
|
||||
@@ -103,6 +110,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
||||
// return progressClient.newCall(request)
|
||||
// }
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
val progressClient = newBuilder()
|
||||
// .cache(null)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
// from TachiWeb-Server
|
||||
class PersistentCookieJar(context: Context) : CookieJar {
|
||||
|
||||
val store = PersistentCookieStore(context)
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
store.addAll(url, cookies)
|
||||
}
|
||||
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return store.get(url)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import android.content.Context
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import java.net.URI
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
// from TachiWeb-Server
|
||||
class PersistentCookieStore(context: Context) {
|
||||
|
||||
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
|
||||
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
|
||||
|
||||
init {
|
||||
for ((key, value) in prefs.all) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val cookies = value as? Set<String>
|
||||
if (cookies != null) {
|
||||
try {
|
||||
val url = "http://$key".toHttpUrlOrNull() ?: continue
|
||||
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
|
||||
.filter { !it.hasExpired() }
|
||||
cookieMap.put(key, nonExpiredCookies)
|
||||
} catch (e: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
|
||||
val key = url.toUri().host
|
||||
|
||||
// Append or replace the cookies for this domain.
|
||||
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
|
||||
for (cookie in cookies) {
|
||||
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
|
||||
val pos = cookiesForDomain.indexOfFirst { it.name == cookie.name }
|
||||
if (pos == -1) {
|
||||
cookiesForDomain.add(cookie)
|
||||
} else {
|
||||
cookiesForDomain[pos] = cookie
|
||||
}
|
||||
}
|
||||
cookieMap.put(key, cookiesForDomain)
|
||||
|
||||
// Get cookies to be stored in disk
|
||||
val newValues = cookiesForDomain.asSequence()
|
||||
.filter { it.persistent && !it.hasExpired() }
|
||||
.map(Cookie::toString)
|
||||
.toSet()
|
||||
|
||||
prefs.edit().putStringSet(key, newValues).apply()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeAll() {
|
||||
prefs.edit().clear().apply()
|
||||
cookieMap.clear()
|
||||
}
|
||||
|
||||
fun remove(uri: URI) {
|
||||
prefs.edit().remove(uri.host).apply()
|
||||
cookieMap.remove(uri.host)
|
||||
}
|
||||
|
||||
fun get(url: HttpUrl) = get(url.toUri().host)
|
||||
|
||||
fun get(uri: URI) = get(uri.host)
|
||||
|
||||
private fun get(url: String): List<Cookie> {
|
||||
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
|
||||
}
|
||||
|
||||
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import com.gargoylesoftware.htmlunit.BrowserVersion
|
||||
import com.gargoylesoftware.htmlunit.WebClient
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import mu.KotlinLogging
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
|
||||
// from TachiWeb-Server
|
||||
class CloudflareInterceptor : Interceptor {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
|
||||
@Synchronized
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
logger.trace { "CloudflareInterceptor is being used." }
|
||||
|
||||
val response = chain.proceed(originalRequest)
|
||||
|
||||
// Check if Cloudflare anti-bot is on
|
||||
if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
|
||||
return response
|
||||
}
|
||||
|
||||
logger.debug { "Cloudflare anti-bot is on, CloudflareInterceptor is kicking in..." }
|
||||
|
||||
return try {
|
||||
response.close()
|
||||
network.cookies.remove(originalRequest.url.toUri())
|
||||
|
||||
chain.proceed(resolveChallenge(response))
|
||||
} catch (e: Exception) {
|
||||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||
// we don't crash the entire app
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveChallenge(response: Response): Request {
|
||||
val browserVersion = BrowserVersion.BrowserVersionBuilder(BrowserVersion.BEST_SUPPORTED)
|
||||
.setUserAgent(response.request.header("User-Agent") ?: BrowserVersion.BEST_SUPPORTED.userAgent)
|
||||
.build()
|
||||
val convertedCookies = WebClient(browserVersion).use { webClient ->
|
||||
webClient.options.isThrowExceptionOnFailingStatusCode = false
|
||||
webClient.options.isThrowExceptionOnScriptError = false
|
||||
webClient.getPage<HtmlPage>(response.request.url.toString())
|
||||
webClient.waitForBackgroundJavaScript(10000)
|
||||
// Challenge solved, process cookies
|
||||
webClient.cookieManager.cookies.filter {
|
||||
// Only include Cloudflare cookies
|
||||
it.name.startsWith("__cf") || it.name.startsWith("cf_")
|
||||
}.map {
|
||||
// Convert cookies -> OkHttp format
|
||||
Cookie.Builder()
|
||||
.domain(it.domain.removePrefix("."))
|
||||
.expiresAt(it.expires?.time ?: Long.MAX_VALUE)
|
||||
.name(it.name)
|
||||
.path(it.path)
|
||||
.value(it.value).apply {
|
||||
if (it.isHttpOnly) httpOnly()
|
||||
if (it.isSecure) secure()
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
// Copy cookies to cookie store
|
||||
convertedCookies.forEach {
|
||||
network.cookies.addAll(
|
||||
HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host(it.domain)
|
||||
.build(),
|
||||
listOf(it)
|
||||
)
|
||||
}
|
||||
// Merge new and existing cookies for this request
|
||||
// Find the cookies that we need to merge into this request
|
||||
val convertedForThisRequest = convertedCookies.filter {
|
||||
it.matches(response.request.url)
|
||||
}
|
||||
// Extract cookies from current request
|
||||
val existingCookies = Cookie.parseAll(
|
||||
response.request.url,
|
||||
response.request.headers
|
||||
)
|
||||
// Filter out existing values of cookies that we are about to merge in
|
||||
val filteredExisting = existingCookies.filter { existing ->
|
||||
convertedForThisRequest.none { converted -> converted.name == existing.name }
|
||||
}
|
||||
val newCookies = filteredExisting + convertedForThisRequest
|
||||
return response.request.newBuilder()
|
||||
.header("Cookie", newCookies.map { it.toString() }.joinToString("; "))
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class UserAgentInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
|
||||
val newRequest = originalRequest
|
||||
.newBuilder()
|
||||
.removeHeader("User-Agent")
|
||||
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
|
||||
.build()
|
||||
chain.proceed(newRequest)
|
||||
} else {
|
||||
chain.proceed(originalRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import rx.Observable
|
||||
|
||||
// import com.github.junrar.Archive
|
||||
// import com.google.gson.JsonParser
|
||||
// import eu.kanade.tachiyomi.R
|
||||
// import eu.kanade.tachiyomi.source.model.Filter
|
||||
// import eu.kanade.tachiyomi.source.model.FilterList
|
||||
// import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
// import eu.kanade.tachiyomi.source.model.Page
|
||||
// import eu.kanade.tachiyomi.source.model.SChapter
|
||||
// import eu.kanade.tachiyomi.source.model.SManga
|
||||
// import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
// import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
// import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
// import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
// import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
// import rx.Observable
|
||||
// import timber.log.Timber
|
||||
// import java.io.File
|
||||
// import java.io.FileInputStream
|
||||
// import java.io.InputStream
|
||||
// import java.util.Locale
|
||||
// import java.util.concurrent.TimeUnit
|
||||
// import java.util.zip.ZipFile
|
||||
|
||||
class LocalSource(private val context: Context) : CatalogueSource {
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
// const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
|
||||
//
|
||||
// private const val COVER_NAME = "cover.jpg"
|
||||
// private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||
//
|
||||
// private val POPULAR_FILTERS = FilterList(OrderBy())
|
||||
// private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
||||
// private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
//
|
||||
// fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
||||
// val dir = getBaseDirectories(context).firstOrNull()
|
||||
// if (dir == null) {
|
||||
// input.close()
|
||||
// return null
|
||||
// }
|
||||
// val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
||||
//
|
||||
// // It might not exist if using the external SD card
|
||||
// cover.parentFile?.mkdirs()
|
||||
// input.use {
|
||||
// cover.outputStream().use {
|
||||
// input.copyTo(it)
|
||||
// }
|
||||
// }
|
||||
// return cover
|
||||
// }
|
||||
//
|
||||
// private fun getBaseDirectories(context: Context): List<File> {
|
||||
// val c = context.getString(R.string.app_name) + File.separator + "local"
|
||||
// return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
|
||||
// }
|
||||
}
|
||||
|
||||
override val id = ID
|
||||
override val name = "Local source"
|
||||
override val lang = ""
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
//
|
||||
// override fun toString() = context.getString(R.string.local_source)
|
||||
//
|
||||
// override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||
//
|
||||
// override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
// val baseDirs = getBaseDirectories(context)
|
||||
//
|
||||
// val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
// var mangaDirs = baseDirs
|
||||
// .asSequence()
|
||||
// .mapNotNull { it.listFiles()?.toList() }
|
||||
// .flatten()
|
||||
// .filter { it.isDirectory }
|
||||
// .filterNot { it.name.startsWith('.') }
|
||||
// .filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||
// .distinctBy { it.name }
|
||||
//
|
||||
// val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
||||
// when (state?.index) {
|
||||
// 0 -> {
|
||||
// mangaDirs = if (state.ascending) {
|
||||
// mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
|
||||
// } else {
|
||||
// mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
|
||||
// }
|
||||
// }
|
||||
// 1 -> {
|
||||
// mangaDirs = if (state.ascending) {
|
||||
// mangaDirs.sortedBy(File::lastModified)
|
||||
// } else {
|
||||
// mangaDirs.sortedByDescending(File::lastModified)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val mangas = mangaDirs.map { mangaDir ->
|
||||
// SManga.create().apply {
|
||||
// title = mangaDir.name
|
||||
// url = mangaDir.name
|
||||
//
|
||||
// // Try to find the cover
|
||||
// for (dir in baseDirs) {
|
||||
// val cover = File("${dir.absolutePath}/$url", COVER_NAME)
|
||||
// if (cover.exists()) {
|
||||
// thumbnail_url = cover.absolutePath
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val chapters = fetchChapterList(this).toBlocking().first()
|
||||
// if (chapters.isNotEmpty()) {
|
||||
// val chapter = chapters.last()
|
||||
// val format = getFormat(chapter)
|
||||
// if (format is Format.Epub) {
|
||||
// EpubFile(format.file).use { epub ->
|
||||
// epub.fillMangaMetadata(this)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Copy the cover from the first chapter found.
|
||||
// if (thumbnail_url == null) {
|
||||
// try {
|
||||
// val dest = updateCover(chapter, this)
|
||||
// thumbnail_url = dest?.absolutePath
|
||||
// } catch (e: Exception) {
|
||||
// Timber.e(e)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return Observable.just(MangasPage(mangas.toList(), false))
|
||||
// }
|
||||
//
|
||||
// override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||
//
|
||||
// override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
// getBaseDirectories(context)
|
||||
// .asSequence()
|
||||
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||
// .flatten()
|
||||
// .firstOrNull { it.extension == "json" }
|
||||
// ?.apply {
|
||||
// val reader = this.inputStream().bufferedReader()
|
||||
// val json = JsonParser.parseReader(reader).asJsonObject
|
||||
//
|
||||
// manga.title = json["title"]?.asString ?: manga.title
|
||||
// manga.author = json["author"]?.asString ?: manga.author
|
||||
// manga.artist = json["artist"]?.asString ?: manga.artist
|
||||
// manga.description = json["description"]?.asString ?: manga.description
|
||||
// manga.genre = json["genre"]?.asJsonArray?.joinToString(", ") { it.asString }
|
||||
// ?: manga.genre
|
||||
// manga.status = json["status"]?.asInt ?: manga.status
|
||||
// }
|
||||
//
|
||||
// return Observable.just(manga)
|
||||
// }
|
||||
//
|
||||
// override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
// val chapters = getBaseDirectories(context)
|
||||
// .asSequence()
|
||||
// .mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
||||
// .flatten()
|
||||
// .filter { it.isDirectory || isSupportedFile(it.extension) }
|
||||
// .map { chapterFile ->
|
||||
// SChapter.create().apply {
|
||||
// url = "${manga.url}/${chapterFile.name}"
|
||||
// name = if (chapterFile.isDirectory) {
|
||||
// chapterFile.name
|
||||
// } else {
|
||||
// chapterFile.nameWithoutExtension
|
||||
// }
|
||||
// date_upload = chapterFile.lastModified()
|
||||
//
|
||||
// val format = getFormat(this)
|
||||
// if (format is Format.Epub) {
|
||||
// EpubFile(format.file).use { epub ->
|
||||
// epub.fillChapterMetadata(this)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val chapNameCut = stripMangaTitle(name, manga.title)
|
||||
// if (chapNameCut.isNotEmpty()) name = chapNameCut
|
||||
// ChapterRecognition.parseChapterNumber(this, manga)
|
||||
// }
|
||||
// }
|
||||
// .sortedWith(
|
||||
// Comparator { c1, c2 ->
|
||||
// val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||
// if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||
// }
|
||||
// )
|
||||
// .toList()
|
||||
//
|
||||
// return Observable.just(chapters)
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace
|
||||
// * characters.
|
||||
// */
|
||||
// private fun stripMangaTitle(chapterName: String, mangaTitle: String): String {
|
||||
// var chapterNameIndex = 0
|
||||
// var mangaTitleIndex = 0
|
||||
// while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
||||
// val chapterChar = chapterName[chapterNameIndex]
|
||||
// val mangaChar = mangaTitle[mangaTitleIndex]
|
||||
// if (!chapterChar.equals(mangaChar, true)) {
|
||||
// val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
||||
// val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
||||
//
|
||||
// if (!invalidChapterChar && !invalidMangaChar) {
|
||||
// return chapterName
|
||||
// }
|
||||
//
|
||||
// if (invalidChapterChar) {
|
||||
// chapterNameIndex++
|
||||
// }
|
||||
//
|
||||
// if (invalidMangaChar) {
|
||||
// mangaTitleIndex++
|
||||
// }
|
||||
// } else {
|
||||
// chapterNameIndex++
|
||||
// mangaTitleIndex++
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
|
||||
// }
|
||||
//
|
||||
// override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
// return Observable.error(Exception("Unused"))
|
||||
// }
|
||||
//
|
||||
// private fun isSupportedFile(extension: String): Boolean {
|
||||
// return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
|
||||
// }
|
||||
//
|
||||
// fun getFormat(chapter: SChapter): Format {
|
||||
// val baseDirs = getBaseDirectories(context)
|
||||
//
|
||||
// for (dir in baseDirs) {
|
||||
// val chapFile = File(dir, chapter.url)
|
||||
// if (!chapFile.exists()) continue
|
||||
//
|
||||
// return getFormat(chapFile)
|
||||
// }
|
||||
// throw Exception("Chapter not found")
|
||||
// }
|
||||
//
|
||||
// private fun getFormat(file: File): Format {
|
||||
// val extension = file.extension
|
||||
// return if (file.isDirectory) {
|
||||
// Format.Directory(file)
|
||||
// } else if (extension.equals("zip", true) || extension.equals("cbz", true)) {
|
||||
// Format.Zip(file)
|
||||
// } else if (extension.equals("rar", true) || extension.equals("cbr", true)) {
|
||||
// Format.Rar(file)
|
||||
// } else if (extension.equals("epub", true)) {
|
||||
// Format.Epub(file)
|
||||
// } else {
|
||||
// throw Exception("Invalid chapter format")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
||||
// return when (val format = getFormat(chapter)) {
|
||||
// is Format.Directory -> {
|
||||
// val entry = format.file.listFiles()
|
||||
// ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
// ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
//
|
||||
// entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||
// }
|
||||
// is Format.Zip -> {
|
||||
// ZipFile(format.file).use { zip ->
|
||||
// val entry = zip.entries().toList()
|
||||
// .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
// .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
//
|
||||
// entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
||||
// }
|
||||
// }
|
||||
// is Format.Rar -> {
|
||||
// Archive(format.file).use { archive ->
|
||||
// val entry = archive.fileHeaders
|
||||
// .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
// .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
//
|
||||
// entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
||||
// }
|
||||
// }
|
||||
// is Format.Epub -> {
|
||||
// EpubFile(format.file).use { epub ->
|
||||
// val entry = epub.getImagesFromPages()
|
||||
// .firstOrNull()
|
||||
// ?.let { epub.getEntry(it) }
|
||||
//
|
||||
// entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private class OrderBy : Filter.Sort("Order by", arrayOf("Title", "Date"), Selection(0, true))
|
||||
//
|
||||
// override fun getFilterList() = FilterList(OrderBy())
|
||||
//
|
||||
// sealed class Format {
|
||||
// data class Directory(val file: File) : Format()
|
||||
// data class Zip(val file: File) : Format()
|
||||
// data class Rar(val file: File) : Format()
|
||||
// data class Epub(val file: File) : Format()
|
||||
// }
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import rx.Observable
|
||||
|
||||
open class SourceManager(private val context: Context) {
|
||||
|
||||
private val sourcesMap = mutableMapOf<Long, Source>()
|
||||
|
||||
private val stubSourcesMap = mutableMapOf<Long, StubSource>()
|
||||
|
||||
init {
|
||||
createInternalSources().forEach { registerSource(it) }
|
||||
}
|
||||
|
||||
open fun get(sourceKey: Long): Source? {
|
||||
return sourcesMap[sourceKey]
|
||||
}
|
||||
|
||||
fun getOrStub(sourceKey: Long): Source {
|
||||
return sourcesMap[sourceKey] ?: stubSourcesMap.getOrPut(sourceKey) {
|
||||
StubSource(sourceKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
|
||||
|
||||
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
|
||||
|
||||
internal fun registerSource(source: Source, overwrite: Boolean = false) {
|
||||
if (overwrite || !sourcesMap.containsKey(source.id)) {
|
||||
sourcesMap[source.id] = source
|
||||
}
|
||||
if (overwrite || !stubSourcesMap.containsKey(source.id)) {
|
||||
stubSourcesMap[source.id] = StubSource(source.id)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun unregisterSource(source: Source) {
|
||||
sourcesMap.remove(source.id)
|
||||
}
|
||||
|
||||
private fun createInternalSources(): List<Source> = listOf(
|
||||
// LocalSource(context)
|
||||
)
|
||||
|
||||
private inner class StubSource(override val id: Long) : Source {
|
||||
|
||||
override val name: String
|
||||
get() = id.toString()
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return Observable.error(getSourceNotInstalledException())
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return Observable.error(getSourceNotInstalledException())
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
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,478 @@
|
||||
package eu.kanade.tachiyomi.source.local
|
||||
|
||||
import com.github.junrar.Archive
|
||||
import eu.kanade.tachiyomi.source.local.FileSystemInterceptor.fakeUrlFrom
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Directory
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Epub
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Rar
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource.Format.Zip
|
||||
import eu.kanade.tachiyomi.source.local.loader.EpubPageLoader
|
||||
import eu.kanade.tachiyomi.source.local.loader.RarPageLoader
|
||||
import eu.kanade.tachiyomi.source.local.loader.ZipPageLoader
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import mu.KotlinLogging
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.asResponseBody
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
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 rx.Observable
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.net.URLDecoder
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class LocalSource : HttpSource() {
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
const val LANG = "localsourcelang"
|
||||
const val NAME = "Local source"
|
||||
|
||||
const val EXTENSION_NAME = "Local Source fake extension"
|
||||
|
||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||
|
||||
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
|
||||
|
||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
|
||||
val pageCache: MutableMap<String, List<() -> InputStream>> = mutableMapOf()
|
||||
|
||||
fun updateCover(manga: SManga, input: InputStream): File? {
|
||||
val cover = getCoverFile(File("${applicationDirs.localMangaRoot}/${manga.url}"))
|
||||
?: File("${applicationDirs.localMangaRoot}/${manga.url}/cover.jpg")
|
||||
|
||||
cover.parentFile?.mkdirs()
|
||||
input.use {
|
||||
cover.outputStream().use {
|
||||
input.copyTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
return cover
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns valid cover file inside [parent] directory.
|
||||
*/
|
||||
private fun getCoverFile(parent: File): File? {
|
||||
return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
|
||||
it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
|
||||
}
|
||||
}
|
||||
|
||||
fun addDbRecords() {
|
||||
transaction {
|
||||
val sourceRecord = SourceTable.select { SourceTable.id eq ID }.firstOrNull()
|
||||
|
||||
if (sourceRecord == null) {
|
||||
// must do this to avoid database integrity errors
|
||||
val extensionId = ExtensionTable.insertAndGetId {
|
||||
it[apkName] = "localSource"
|
||||
it[name] = EXTENSION_NAME
|
||||
it[pkgName] = LocalSource::class.java.`package`.name
|
||||
it[versionName] = "1.2"
|
||||
it[versionCode] = 0
|
||||
it[lang] = LANG
|
||||
it[isNsfw] = false
|
||||
it[isInstalled] = true
|
||||
}
|
||||
|
||||
SourceTable.insert {
|
||||
it[id] = ID
|
||||
it[name] = NAME
|
||||
it[lang] = LANG
|
||||
it[extension] = extensionId
|
||||
it[isNsfw] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val id = ID
|
||||
override val name = NAME
|
||||
override val lang = LANG
|
||||
override val baseUrl: String = ""
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = super.client.newBuilder()
|
||||
.addInterceptor(FileSystemInterceptor)
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override fun toString() = name
|
||||
|
||||
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
|
||||
var mangaDirs = File(applicationDirs.localMangaRoot).listFiles().orEmpty().toList()
|
||||
.filter { it.isDirectory }
|
||||
.filterNot { it.name.startsWith('.') }
|
||||
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||
.distinctBy { it.name }
|
||||
|
||||
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
|
||||
when (state?.index) {
|
||||
0 -> {
|
||||
mangaDirs = if (state.ascending) {
|
||||
mangaDirs.sortedBy { it.name.lowercase(Locale.ENGLISH) }
|
||||
} else {
|
||||
mangaDirs.sortedByDescending { it.name.lowercase(Locale.ENGLISH) }
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
mangaDirs = if (state.ascending) {
|
||||
mangaDirs.sortedBy(File::lastModified)
|
||||
} else {
|
||||
mangaDirs.sortedByDescending(File::lastModified)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mangas = mangaDirs.map { mangaDir ->
|
||||
SManga.create().apply {
|
||||
title = mangaDir.name
|
||||
url = mangaDir.name
|
||||
|
||||
// Try to find the cover
|
||||
val cover = getCoverFile(File("${applicationDirs.localMangaRoot}/$url"))
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = fakeUrlFrom(cover.absolutePath)
|
||||
}
|
||||
|
||||
val chapters = fetchChapterList(this).toBlocking().first()
|
||||
if (chapters.isNotEmpty()) {
|
||||
val chapter = chapters.last()
|
||||
val format = getFormat(chapter)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillMangaMetadata(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the cover from the first chapter found.
|
||||
if (thumbnail_url == null) {
|
||||
try {
|
||||
val dest = updateCover(chapter, this)
|
||||
thumbnail_url = dest?.absolutePath?.let { fakeUrlFrom(it) }
|
||||
} catch (e: Exception) {
|
||||
logger.error { e }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Observable.just(MangasPage(mangas.toList(), false))
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
File(applicationDirs.localMangaRoot, manga.url).listFiles().orEmpty().toList()
|
||||
.firstOrNull { it.extension == "json" }
|
||||
?.apply {
|
||||
val obj = json.decodeFromStream<JsonObject>(inputStream())
|
||||
|
||||
manga.title = obj["title"]?.jsonPrimitive?.contentOrNull ?: manga.title
|
||||
manga.author = obj["author"]?.jsonPrimitive?.contentOrNull ?: manga.author
|
||||
manga.artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: manga.artist
|
||||
manga.description = obj["description"]?.jsonPrimitive?.contentOrNull ?: manga.description
|
||||
manga.genre = obj["genre"]?.jsonArray?.joinToString(", ") { it.jsonPrimitive.content }
|
||||
?: manga.genre
|
||||
manga.status = obj["status"]?.jsonPrimitive?.intOrNull ?: manga.status
|
||||
}
|
||||
|
||||
return Observable.just(manga)
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val chapters = File(applicationDirs.localMangaRoot, manga.url).listFiles().orEmpty().toList()
|
||||
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
||||
.map { chapterFile ->
|
||||
SChapter.create().apply {
|
||||
url = "${manga.url}/${chapterFile.name}"
|
||||
name = if (chapterFile.isDirectory) {
|
||||
chapterFile.name
|
||||
} else {
|
||||
chapterFile.nameWithoutExtension
|
||||
}
|
||||
date_upload = chapterFile.lastModified()
|
||||
|
||||
val format = getFormat(this)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillChapterMetadata(this)
|
||||
}
|
||||
}
|
||||
|
||||
val chapNameCut = stripMangaTitle(name, manga.title)
|
||||
if (chapNameCut.isNotEmpty()) name = chapNameCut
|
||||
ChapterRecognition.parseChapterNumber(this, manga)
|
||||
}
|
||||
}
|
||||
.sortedWith { c1, c2 ->
|
||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||
}
|
||||
.toList()
|
||||
|
||||
return Observable.just(chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the manga title from a chapter name, matching only based on alphanumeric and whitespace
|
||||
* characters.
|
||||
*/
|
||||
private fun stripMangaTitle(chapterName: String, mangaTitle: String): String {
|
||||
var chapterNameIndex = 0
|
||||
var mangaTitleIndex = 0
|
||||
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
|
||||
val chapterChar = chapterName[chapterNameIndex]
|
||||
val mangaChar = mangaTitle[mangaTitleIndex]
|
||||
if (!chapterChar.equals(mangaChar, true)) {
|
||||
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
|
||||
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
|
||||
|
||||
if (!invalidChapterChar && !invalidMangaChar) {
|
||||
return chapterName
|
||||
}
|
||||
|
||||
if (invalidChapterChar) {
|
||||
chapterNameIndex++
|
||||
}
|
||||
|
||||
if (invalidMangaChar) {
|
||||
mangaTitleIndex++
|
||||
}
|
||||
} else {
|
||||
chapterNameIndex++
|
||||
mangaTitleIndex++
|
||||
}
|
||||
}
|
||||
|
||||
return chapterName.substring(chapterNameIndex).trimStart(' ', '-', '_', ',', ':')
|
||||
}
|
||||
|
||||
private fun isSupportedFile(extension: String): Boolean {
|
||||
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
val chapterFile = File(applicationDirs.localMangaRoot + "/" + chapter.url)
|
||||
|
||||
return when (getFormat(chapterFile)) {
|
||||
is Directory -> {
|
||||
Observable.just(
|
||||
chapterFile.listFiles().orEmpty().sortedBy { it.name }.mapIndexed { index, page ->
|
||||
Page(
|
||||
index,
|
||||
imageUrl = fakeUrlFrom(applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
is Zip -> {
|
||||
val pages = ZipPageLoader(chapterFile).getPages()
|
||||
pageCache[chapter.url] = pages.map { it.stream!! }
|
||||
|
||||
Observable.just(pages)
|
||||
}
|
||||
is Rar -> {
|
||||
val pages = RarPageLoader(chapterFile).getPages()
|
||||
pageCache[chapter.url] = pages.map { it.stream!! }
|
||||
|
||||
Observable.just(pages)
|
||||
}
|
||||
is Epub -> {
|
||||
val pages = EpubPageLoader(chapterFile).getPages()
|
||||
pageCache[chapter.url] = pages.map { it.stream!! }
|
||||
|
||||
Observable.just(pages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getFormat(chapter: SChapter): Format {
|
||||
val chapFile = File(applicationDirs.localMangaRoot, chapter.url)
|
||||
if (chapFile.exists())
|
||||
return getFormat(chapFile)
|
||||
|
||||
throw Exception("Chapter not found")
|
||||
}
|
||||
|
||||
private fun getFormat(file: File): Format = with(file) {
|
||||
when {
|
||||
isDirectory -> Format.Directory(this)
|
||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
|
||||
extension.equals("rar", true) || extension.equals("cbr", true) -> Format.Rar(this)
|
||||
extension.equals("epub", true) -> Format.Epub(this)
|
||||
|
||||
else -> throw Exception("Invalid chapter format")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
||||
return when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
val entry = format.file.listFiles()
|
||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(manga, it.inputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
ZipFile(format.file).use { zip ->
|
||||
val entry = zip.entries().toList()
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(manga, zip.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
Archive(format.file).use { archive ->
|
||||
val entry = archive.fileHeaders
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(manga, archive.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file).use { epub ->
|
||||
val entry = epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
|
||||
entry?.let { updateCover(manga, epub.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList() = POPULAR_FILTERS
|
||||
|
||||
private val POPULAR_FILTERS = FilterList(OrderBy())
|
||||
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
|
||||
|
||||
private class OrderBy : Filter.Sort(
|
||||
"Order by",
|
||||
arrayOf("Title", "Date"),
|
||||
Selection(0, true)
|
||||
)
|
||||
|
||||
sealed class Format {
|
||||
data class Directory(val file: File) : Format()
|
||||
data class Zip(val file: File) : Format()
|
||||
data class Rar(val file: File) : Format()
|
||||
data class Epub(val file: File) : Format()
|
||||
}
|
||||
|
||||
// ///////////////////// Not used ///////////////////// //
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga = throw Exception("Not used")
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> = throw Exception("Not used")
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> = throw Exception("Not used")
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw Exception("Not used")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = throw Exception("Not used")
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage = throw Exception("Not used")
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
|
||||
throw Exception("Not used")
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw Exception("Not used")
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = throw Exception("Not used")
|
||||
}
|
||||
|
||||
private object FileSystemInterceptor : Interceptor {
|
||||
fun fakeUrlFrom(path: String): String = "http://$path"
|
||||
|
||||
|
||||
private fun restoreFilePath(url: String): String {
|
||||
val path = URLDecoder.decode(url.replaceFirst("http://", ""), "UTF-8")
|
||||
|
||||
// Windows
|
||||
if (System.getProperty("os.name").lowercase().startsWith("win")) {
|
||||
// convert paths like "c/Users/..." to "c:/Users/..."
|
||||
return StringBuilder(path).insert(1, ":").toString()
|
||||
}
|
||||
|
||||
return "/$path"
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val url = request.url
|
||||
val filePath = restoreFilePath(url.toString())
|
||||
return try {
|
||||
Response.Builder()
|
||||
.body(File(filePath).source().buffer().asResponseBody())
|
||||
.code(200)
|
||||
.message("Some file")
|
||||
.protocol(Protocol.HTTP_1_0)
|
||||
.request(request)
|
||||
.build()
|
||||
} catch (e: FileNotFoundException) {
|
||||
Response.Builder()
|
||||
.body("".toResponseBody())
|
||||
.code(404)
|
||||
.message(e.message ?: "File not found ($filePath)")
|
||||
.protocol(Protocol.HTTP_1_0)
|
||||
.request(request)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package eu.kanade.tachiyomi.source.local.loader
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Loader used to load a chapter from a .epub file.
|
||||
*/
|
||||
class EpubPageLoader(file: File) : PageLoader {
|
||||
|
||||
/**
|
||||
* The epub file.
|
||||
*/
|
||||
private val epub = EpubFile(file)
|
||||
|
||||
/**
|
||||
* Returns an observable containing the pages found on this zip archive ordered with a natural
|
||||
* comparator.
|
||||
*/
|
||||
override fun getPages(): List<ReaderPage> {
|
||||
return epub.getImagesFromPages()
|
||||
.mapIndexed { i, path ->
|
||||
val streamFn = { epub.getInputStream(epub.getEntry(path)!!) }
|
||||
ReaderPage(i).apply {
|
||||
stream = streamFn
|
||||
status = Page.READY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.source.local.loader
|
||||
|
||||
// adapted from eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
interface PageLoader {
|
||||
/**
|
||||
* Returns an observable containing the list of pages of a chapter. Only the first emission
|
||||
* will be used.
|
||||
*/
|
||||
fun getPages(): List<ReaderPage>
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package eu.kanade.tachiyomi.source.local.loader
|
||||
|
||||
import com.github.junrar.Archive
|
||||
import com.github.junrar.rarfile.FileHeader
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.PipedInputStream
|
||||
import java.io.PipedOutputStream
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Loader used to load a chapter from a .rar or .cbr file.
|
||||
*/
|
||||
class RarPageLoader(file: File) : PageLoader {
|
||||
|
||||
/**
|
||||
* The rar archive to load pages from.
|
||||
*/
|
||||
private val archive = Archive(file)
|
||||
|
||||
/**
|
||||
* Pool for copying compressed files to an input stream.
|
||||
*/
|
||||
private val pool = Executors.newFixedThreadPool(1)
|
||||
|
||||
/**
|
||||
* Returns an observable containing the pages found on this rar archive ordered with a natural
|
||||
* comparator.
|
||||
*/
|
||||
override fun getPages(): List<ReaderPage> {
|
||||
return archive.fileHeaders
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.mapIndexed { i, header ->
|
||||
val streamFn = { getStream(header) }
|
||||
|
||||
ReaderPage(i).apply {
|
||||
stream = streamFn
|
||||
status = Page.READY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an input stream for the given [header].
|
||||
*/
|
||||
private fun getStream(header: FileHeader): InputStream {
|
||||
val pipeIn = PipedInputStream()
|
||||
val pipeOut = PipedOutputStream(pipeIn)
|
||||
pool.execute {
|
||||
try {
|
||||
pipeOut.use {
|
||||
archive.extractFile(header, it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
return pipeIn
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package eu.kanade.tachiyomi.source.local.loader
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import java.io.InputStream
|
||||
|
||||
class ReaderPage(
|
||||
index: Int,
|
||||
url: String = "",
|
||||
imageUrl: String? = null,
|
||||
var stream: (() -> InputStream)? = null
|
||||
) : Page(index, url, imageUrl, null)
|
||||
@@ -0,0 +1,31 @@
|
||||
package eu.kanade.tachiyomi.source.local.loader
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class ZipPageLoader(file: File) : PageLoader {
|
||||
/**
|
||||
* The zip file to load pages from.
|
||||
*/
|
||||
private val zip = ZipFile(file)
|
||||
|
||||
/**
|
||||
* Returns an observable containing the pages found on this zip archive ordered with a natural
|
||||
* comparator.
|
||||
*/
|
||||
override fun getPages(): List<ReaderPage> {
|
||||
return zip.entries().toList()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.mapIndexed { i, entry ->
|
||||
val streamFn = { zip.getInputStream(entry) }
|
||||
ReaderPage(i).apply {
|
||||
stream = streamFn
|
||||
status = Page.READY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,3 +61,30 @@ interface SManga : Serializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fun SManga.toMangaInfo(): MangaInfo {
|
||||
// return MangaInfo(
|
||||
// key = this.url,
|
||||
// title = this.title,
|
||||
// artist = this.artist ?: "",
|
||||
// author = this.author ?: "",
|
||||
// description = this.description ?: "",
|
||||
// genres = this.genre?.split(", ") ?: emptyList(),
|
||||
// status = this.status,
|
||||
// cover = this.thumbnail_url ?: ""
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// fun MangaInfo.toSManga(): SManga {
|
||||
// val mangaInfo = this
|
||||
// return SManga.create().apply {
|
||||
// url = mangaInfo.key
|
||||
// title = mangaInfo.title
|
||||
// artist = mangaInfo.artist
|
||||
// author = mangaInfo.author
|
||||
// description = mangaInfo.description
|
||||
// genre = mangaInfo.genres.joinToString(", ")
|
||||
// status = mangaInfo.status
|
||||
// thumbnail_url = mangaInfo.cover
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -55,7 +55,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
* Note the generated id sets the sign bit to 0.
|
||||
*/
|
||||
override val id by lazy {
|
||||
val key = "${name.toLowerCase()}/$lang/$versionId"
|
||||
val key = "${name.lowercase()}/$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
|
||||
}
|
||||
@@ -75,13 +75,13 @@ abstract class HttpSource : CatalogueSource {
|
||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||
*/
|
||||
protected open fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", DEFAULT_USERAGENT)
|
||||
add("User-Agent", DEFAULT_USER_AGENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Visible name of the source.
|
||||
*/
|
||||
override fun toString() = "$name (${lang.toUpperCase()})"
|
||||
override fun toString() = "$name (${lang.uppercase()})"
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||
@@ -372,6 +372,6 @@ abstract class HttpSource : CatalogueSource {
|
||||
override fun getFilterList() = FilterList()
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)"
|
||||
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,152 @@
|
||||
package eu.kanade.tachiyomi.util.chapter
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
/**
|
||||
* -R> = regex conversion.
|
||||
*/
|
||||
object ChapterRecognition {
|
||||
/**
|
||||
* All cases with Ch.xx
|
||||
* Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation -R> 4
|
||||
*/
|
||||
private val basic = Regex("""(?<=ch\.) *([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""")
|
||||
|
||||
/**
|
||||
* Regex used when only one number occurrence
|
||||
* Example: Bleach 567: Down With Snowwhite -R> 567
|
||||
*/
|
||||
private val occurrence = Regex("""([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""")
|
||||
|
||||
/**
|
||||
* Regex used when manga title removed
|
||||
* Example: Solanin 028 Vol. 2 -> 028 Vol.2 -> 028Vol.2 -R> 028
|
||||
*/
|
||||
private val withoutManga = Regex("""^([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?""")
|
||||
|
||||
/**
|
||||
* Regex used to remove unwanted tags
|
||||
* Example Prison School 12 v.1 vol004 version1243 volume64 -R> Prison School 12
|
||||
*/
|
||||
private val unwanted = Regex("""(?<![a-z])(v|ver|vol|version|volume|season|s).?[0-9]+""")
|
||||
|
||||
/**
|
||||
* Regex used to remove unwanted whitespace
|
||||
* Example One Piece 12 special -R> One Piece 12special
|
||||
*/
|
||||
private val unwantedWhiteSpace = Regex("""(\s)(extra|special|omake)""")
|
||||
|
||||
fun parseChapterNumber(chapter: SChapter, manga: SManga) {
|
||||
// If chapter number is known return.
|
||||
if (chapter.chapter_number == -2f || chapter.chapter_number > -1f) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get chapter title with lower case
|
||||
var name = chapter.name.lowercase()
|
||||
|
||||
// Remove comma's from chapter.
|
||||
name = name.replace(',', '.')
|
||||
|
||||
// Remove unwanted white spaces.
|
||||
unwantedWhiteSpace.findAll(name).let {
|
||||
it.forEach { occurrence -> name = name.replace(occurrence.value, occurrence.value.trim()) }
|
||||
}
|
||||
|
||||
// Remove unwanted tags.
|
||||
unwanted.findAll(name).let {
|
||||
it.forEach { occurrence -> name = name.replace(occurrence.value, "") }
|
||||
}
|
||||
|
||||
// Check base case ch.xx
|
||||
if (updateChapter(basic.find(name), chapter)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check one number occurrence.
|
||||
val occurrences: MutableList<MatchResult> = arrayListOf()
|
||||
occurrence.findAll(name).let {
|
||||
it.forEach { occurrence -> occurrences.add(occurrence) }
|
||||
}
|
||||
|
||||
if (occurrences.size == 1) {
|
||||
if (updateChapter(occurrences[0], chapter)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Remove manga title from chapter title.
|
||||
val nameWithoutManga = name.replace(manga.title.lowercase(), "").trim()
|
||||
|
||||
// Check if first value is number after title remove.
|
||||
if (updateChapter(withoutManga.find(nameWithoutManga), chapter)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Take the first number encountered.
|
||||
if (updateChapter(occurrence.find(nameWithoutManga), chapter)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if volume is found and update chapter
|
||||
* @param match result of regex
|
||||
* @param chapter chapter object
|
||||
* @return true if volume is found
|
||||
*/
|
||||
private fun updateChapter(match: MatchResult?, chapter: SChapter): Boolean {
|
||||
match?.let {
|
||||
val initial = it.groups[1]?.value?.toFloat()!!
|
||||
val subChapterDecimal = it.groups[2]?.value
|
||||
val subChapterAlpha = it.groups[3]?.value
|
||||
val addition = checkForDecimal(subChapterDecimal, subChapterAlpha)
|
||||
chapter.chapter_number = initial.plus(addition)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for decimal in received strings
|
||||
* @param decimal decimal value of regex
|
||||
* @param alpha alpha value of regex
|
||||
* @return decimal/alpha float value
|
||||
*/
|
||||
private fun checkForDecimal(decimal: String?, alpha: String?): Float {
|
||||
if (!decimal.isNullOrEmpty()) {
|
||||
return decimal.toFloat()
|
||||
}
|
||||
|
||||
if (!alpha.isNullOrEmpty()) {
|
||||
if (alpha.contains("extra")) {
|
||||
return .99f
|
||||
}
|
||||
|
||||
if (alpha.contains("omake")) {
|
||||
return .98f
|
||||
}
|
||||
|
||||
if (alpha.contains("special")) {
|
||||
return .97f
|
||||
}
|
||||
|
||||
return if (alpha[0] == '.') {
|
||||
// Take value after (.)
|
||||
parseAlphaPostFix(alpha[1])
|
||||
} else {
|
||||
parseAlphaPostFix(alpha[0])
|
||||
}
|
||||
}
|
||||
|
||||
return .0f
|
||||
}
|
||||
|
||||
/**
|
||||
* x.a -> x.1, x.b -> x.2, etc
|
||||
*/
|
||||
private fun parseAlphaPostFix(alpha: Char): Float {
|
||||
return ("0." + (alpha.code - 96).toString()).toFloat()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package eu.kanade.tachiyomi.util.lang
|
||||
|
||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Replaces the given string to have at most [count] characters using [replacement] at its end.
|
||||
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
|
||||
*/
|
||||
fun String.chop(count: Int, replacement: String = "…"): String {
|
||||
return if (length > count) {
|
||||
take(count - replacement.length) + replacement
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the given string to have at most [count] characters using [replacement] near the center.
|
||||
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
|
||||
*/
|
||||
fun String.truncateCenter(count: Int, replacement: String = "..."): String {
|
||||
if (length <= count) {
|
||||
return this
|
||||
}
|
||||
|
||||
val pieceLength: Int = floor((count - replacement.length).div(2.0)).toInt()
|
||||
|
||||
return "${take(pieceLength)}$replacement${takeLast(pieceLength)}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Case-insensitive natural comparator for strings.
|
||||
*/
|
||||
fun String.compareToCaseInsensitiveNaturalOrder(other: String): Int {
|
||||
val comparator = CaseInsensitiveSimpleNaturalComparator.getInstance<String>()
|
||||
return comparator.compare(this, other)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the string as the number of bytes.
|
||||
*/
|
||||
fun String.byteSize(): Int {
|
||||
return toByteArray(Charsets.UTF_8).size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the first [n] bytes from this string, or the entire string if this
|
||||
* string is shorter.
|
||||
*/
|
||||
fun String.takeBytes(n: Int): String {
|
||||
val bytes = toByteArray(Charsets.UTF_8)
|
||||
return if (bytes.size <= n) {
|
||||
this
|
||||
} else {
|
||||
bytes.decodeToString(endIndex = n).replace("\uFFFD", "")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package eu.kanade.tachiyomi.util.storage
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
/**
|
||||
* Wrapper over ZipFile to load files in epub format.
|
||||
*/
|
||||
class EpubFile(file: File) : Closeable {
|
||||
|
||||
/**
|
||||
* Zip file of this epub.
|
||||
*/
|
||||
private val zip = ZipFile(file)
|
||||
|
||||
/**
|
||||
* Path separator used by this epub.
|
||||
*/
|
||||
private val pathSeparator = getPathSeparator()
|
||||
|
||||
/**
|
||||
* Closes the underlying zip file.
|
||||
*/
|
||||
override fun close() {
|
||||
zip.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an input stream for reading the contents of the specified zip file entry.
|
||||
*/
|
||||
fun getInputStream(entry: ZipEntry): InputStream {
|
||||
return zip.getInputStream(entry)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the zip file entry for the specified name, or null if not found.
|
||||
*/
|
||||
fun getEntry(name: String): ZipEntry? {
|
||||
return zip.getEntry(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills manga metadata using this epub file's metadata.
|
||||
*/
|
||||
fun fillMangaMetadata(manga: SManga) {
|
||||
val ref = getPackageHref()
|
||||
val doc = getPackageDocument(ref)
|
||||
|
||||
val creator = doc.getElementsByTag("dc:creator").first()
|
||||
val description = doc.getElementsByTag("dc:description").first()
|
||||
|
||||
manga.author = creator?.text()
|
||||
manga.description = description?.text()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills chapter metadata using this epub file's metadata.
|
||||
*/
|
||||
fun fillChapterMetadata(chapter: SChapter) {
|
||||
val ref = getPackageHref()
|
||||
val doc = getPackageDocument(ref)
|
||||
|
||||
val title = doc.getElementsByTag("dc:title").first()
|
||||
val publisher = doc.getElementsByTag("dc:publisher").first()
|
||||
val creator = doc.getElementsByTag("dc:creator").first()
|
||||
var date = doc.getElementsByTag("dc:date").first()
|
||||
if (date == null) {
|
||||
date = doc.select("meta[property=dcterms:modified]").first()
|
||||
}
|
||||
|
||||
if (title != null) {
|
||||
chapter.name = title.text()
|
||||
}
|
||||
|
||||
if (publisher != null) {
|
||||
chapter.scanlator = publisher.text()
|
||||
} else if (creator != null) {
|
||||
chapter.scanlator = creator.text()
|
||||
}
|
||||
|
||||
if (date != null) {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
|
||||
try {
|
||||
val parsedDate = dateFormat.parse(date.text())
|
||||
if (parsedDate != null) {
|
||||
chapter.date_upload = parsedDate.time
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
// Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of all the images found in the epub file.
|
||||
*/
|
||||
fun getImagesFromPages(): List<String> {
|
||||
val ref = getPackageHref()
|
||||
val doc = getPackageDocument(ref)
|
||||
val pages = getPagesFromDocument(doc)
|
||||
return getImagesFromPages(pages, ref)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the package document.
|
||||
*/
|
||||
private fun getPackageHref(): String {
|
||||
val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml"))
|
||||
if (meta != null) {
|
||||
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") }
|
||||
val path = metaDoc.getElementsByTag("rootfile").first()?.attr("full-path")
|
||||
if (path != null) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return resolveZipPath("OEBPS", "content.opf")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the package document where all the files are listed.
|
||||
*/
|
||||
private fun getPackageDocument(ref: String): Document {
|
||||
val entry = zip.getEntry(ref)
|
||||
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the pages from the epub.
|
||||
*/
|
||||
private fun getPagesFromDocument(document: Document): List<String> {
|
||||
val pages = document.select("manifest > item")
|
||||
.filter { "application/xhtml+xml" == it.attr("media-type") }
|
||||
.associateBy { it.attr("id") }
|
||||
|
||||
val spine = document.select("spine > itemref").map { it.attr("idref") }
|
||||
return spine.mapNotNull { pages[it] }.map { it.attr("href") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the images contained in every page from the epub.
|
||||
*/
|
||||
private fun getImagesFromPages(pages: List<String>, packageHref: String): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
val basePath = getParentDirectory(packageHref)
|
||||
pages.forEach { page ->
|
||||
val entryPath = resolveZipPath(basePath, page)
|
||||
val entry = zip.getEntry(entryPath)
|
||||
val document = zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
|
||||
val imageBasePath = getParentDirectory(entryPath)
|
||||
|
||||
document.allElements.forEach {
|
||||
if (it.tagName() == "img") {
|
||||
result.add(resolveZipPath(imageBasePath, it.attr("src")))
|
||||
} else if (it.tagName() == "image") {
|
||||
result.add(resolveZipPath(imageBasePath, it.attr("xlink:href")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path separator used by the epub file.
|
||||
*/
|
||||
private fun getPathSeparator(): String {
|
||||
val meta = zip.getEntry("META-INF\\container.xml")
|
||||
return if (meta != null) {
|
||||
"\\"
|
||||
} else {
|
||||
"/"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a zip path from base and relative components and a path separator.
|
||||
*/
|
||||
private fun resolveZipPath(basePath: String, relativePath: String): String {
|
||||
if (relativePath.startsWith(pathSeparator)) {
|
||||
// Path is absolute, so return as-is.
|
||||
return relativePath
|
||||
}
|
||||
|
||||
var fixedBasePath = basePath.replace(pathSeparator, File.separator)
|
||||
if (!fixedBasePath.startsWith(File.separator)) {
|
||||
fixedBasePath = "${File.separator}$fixedBasePath"
|
||||
}
|
||||
|
||||
val fixedRelativePath = relativePath.replace(pathSeparator, File.separator)
|
||||
val resolvedPath = File(fixedBasePath, fixedRelativePath).canonicalPath
|
||||
return resolvedPath.replace(File.separator, pathSeparator).substring(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent directory of a path.
|
||||
*/
|
||||
private fun getParentDirectory(path: String): String {
|
||||
val separatorIndex = path.lastIndexOf(pathSeparator)
|
||||
return if (separatorIndex >= 0) {
|
||||
path.substring(0, separatorIndex)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ 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.Search.sourceSearch
|
||||
import suwayomi.tachidesk.anime.impl.Source.getAnimeSource
|
||||
import suwayomi.tachidesk.anime.impl.Source.getSourceList
|
||||
import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIcon
|
||||
@@ -27,7 +28,7 @@ object AnimeAPI {
|
||||
fun defineEndpoints(app: Javalin) {
|
||||
// list all extensions
|
||||
app.get("/api/v1/anime/extension/list") { ctx ->
|
||||
ctx.json(
|
||||
ctx.future(
|
||||
future {
|
||||
getExtensionList()
|
||||
}
|
||||
@@ -35,10 +36,10 @@ object AnimeAPI {
|
||||
}
|
||||
|
||||
// install extension identified with "pkgName"
|
||||
app.get("/api/v1/anime/extension/install/:pkgName") { ctx ->
|
||||
app.get("/api/v1/anime/extension/install/{pkgName}") { ctx ->
|
||||
val pkgName = ctx.pathParam("pkgName")
|
||||
|
||||
ctx.json(
|
||||
ctx.future(
|
||||
future {
|
||||
installExtension(pkgName)
|
||||
}
|
||||
@@ -46,10 +47,10 @@ object AnimeAPI {
|
||||
}
|
||||
|
||||
// update extension identified with "pkgName"
|
||||
app.get("/api/v1/anime/extension/update/:pkgName") { ctx ->
|
||||
app.get("/api/v1/anime/extension/update/{pkgName}") { ctx ->
|
||||
val pkgName = ctx.pathParam("pkgName")
|
||||
|
||||
ctx.json(
|
||||
ctx.future(
|
||||
future {
|
||||
updateExtension(pkgName)
|
||||
}
|
||||
@@ -57,7 +58,7 @@ object AnimeAPI {
|
||||
}
|
||||
|
||||
// uninstall extension identified with "pkgName"
|
||||
app.get("/api/v1/anime/extension/uninstall/:pkgName") { ctx ->
|
||||
app.get("/api/v1/anime/extension/uninstall/{pkgName}") { ctx ->
|
||||
val pkgName = ctx.pathParam("pkgName")
|
||||
|
||||
uninstallExtension(pkgName)
|
||||
@@ -65,10 +66,10 @@ object AnimeAPI {
|
||||
}
|
||||
|
||||
// icon for extension named `apkName`
|
||||
app.get("/api/v1/anime/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
|
||||
app.get("/api/v1/anime/extension/icon/{apkName}") { ctx -> // TODO: move to pkgName
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
|
||||
ctx.result(
|
||||
ctx.future(
|
||||
future { getExtensionIcon(apkName) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
@@ -83,16 +84,16 @@ object AnimeAPI {
|
||||
}
|
||||
|
||||
// fetch source with id `sourceId`
|
||||
app.get("/api/v1/anime/source/:sourceId") { ctx ->
|
||||
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 ->
|
||||
app.get("/api/v1/anime/source/{sourceId}/popular/{pageNum}") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(
|
||||
ctx.future(
|
||||
future {
|
||||
getAnimeList(sourceId, pageNum, popular = true)
|
||||
}
|
||||
@@ -100,10 +101,10 @@ object AnimeAPI {
|
||||
}
|
||||
|
||||
// latest animes from source with id `sourceId`
|
||||
app.get("/api/v1/anime/source/:sourceId/latest/:pageNum") { ctx ->
|
||||
app.get("/api/v1/anime/source/{sourceId}/latest/{pageNum}") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(
|
||||
ctx.future(
|
||||
future {
|
||||
getAnimeList(sourceId, pageNum, popular = false)
|
||||
}
|
||||
@@ -111,11 +112,11 @@ object AnimeAPI {
|
||||
}
|
||||
|
||||
// get anime info
|
||||
app.get("/api/v1/anime/anime/:animeId/") { ctx ->
|
||||
app.get("/api/v1/anime/anime/{animeId}/") { ctx ->
|
||||
val animeId = ctx.pathParam("animeId").toInt()
|
||||
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
|
||||
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean() ?: false
|
||||
|
||||
ctx.json(
|
||||
ctx.future(
|
||||
future {
|
||||
getAnime(animeId, onlineFetch)
|
||||
}
|
||||
@@ -123,10 +124,10 @@ object AnimeAPI {
|
||||
}
|
||||
|
||||
// anime thumbnail
|
||||
app.get("api/v1/anime/anime/:animeId/thumbnail") { ctx ->
|
||||
app.get("api/v1/anime/anime/{animeId}/thumbnail") { ctx ->
|
||||
val animeId = ctx.pathParam("animeId").toInt()
|
||||
|
||||
ctx.result(
|
||||
ctx.future(
|
||||
future { getAnimeThumbnail(animeId) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
@@ -136,13 +137,13 @@ object AnimeAPI {
|
||||
}
|
||||
//
|
||||
// // list manga's categories
|
||||
// app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||
// app.get("api/v1/manga/{mangaId}/category/") { ctx ->
|
||||
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
// ctx.json(getMangaCategories(mangaId))
|
||||
// }
|
||||
//
|
||||
// // adds the manga to category
|
||||
// app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
// app.get("api/v1/manga/{mangaId}/category/{categoryId}") { ctx ->
|
||||
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
// addMangaToCategory(mangaId, categoryId)
|
||||
@@ -150,7 +151,7 @@ object AnimeAPI {
|
||||
// }
|
||||
//
|
||||
// // removes the manga from the category
|
||||
// app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
// app.delete("api/v1/manga/{mangaId}/category/{categoryId}") { ctx ->
|
||||
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
// removeMangaFromCategory(mangaId, categoryId)
|
||||
@@ -158,23 +159,23 @@ object AnimeAPI {
|
||||
// }
|
||||
//
|
||||
// get episode list when showing a anime
|
||||
app.get("/api/v1/anime/anime/:animeId/episodes") { ctx ->
|
||||
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) })
|
||||
ctx.future(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 ->
|
||||
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) })
|
||||
ctx.future(future { getEpisode(episodeIndex, animeId) })
|
||||
}
|
||||
|
||||
// used to modify a episode's parameters
|
||||
app.patch("/api/v1/anime/anime/:animeId/episode/:episodeIndex") { ctx ->
|
||||
app.patch("/api/v1/anime/anime/{animeId}/episode/{episodeIndex}") { ctx ->
|
||||
val episodeIndex = ctx.pathParam("episodeIndex").toInt()
|
||||
val animeId = ctx.pathParam("animeId").toInt()
|
||||
|
||||
@@ -189,7 +190,7 @@ object AnimeAPI {
|
||||
}
|
||||
//
|
||||
// // 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 chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
// val index = ctx.pathParam("index").toInt()
|
||||
@@ -204,49 +205,49 @@ object AnimeAPI {
|
||||
// }
|
||||
//
|
||||
// // submit a chapter for download
|
||||
// app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||
// 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 ->
|
||||
// app.delete("/api/v1/manga/{mangaId}/chapter/{chapterIndex}/download") { ctx ->
|
||||
// // TODO
|
||||
// }
|
||||
//
|
||||
// // global search, Not implemented yet
|
||||
// app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||
// app.get("/api/v1/search/{searchTerm}") { ctx ->
|
||||
// val searchTerm = ctx.pathParam("searchTerm")
|
||||
// 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) })
|
||||
// }
|
||||
// single source search
|
||||
app.get("/api/v1/anime/source/{sourceId}/search/{searchTerm}/{pageNum}") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.future(future { sourceSearch(sourceId, searchTerm, pageNum) })
|
||||
}
|
||||
//
|
||||
// // source filter list
|
||||
// app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||
// 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 ->
|
||||
// app.get("api/v1/manga/{mangaId}/library") { ctx ->
|
||||
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
//
|
||||
// ctx.result(
|
||||
// ctx.future(
|
||||
// JavalinSetup.future { addMangaToLibrary(mangaId) }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // removes the manga from the library
|
||||
// app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||
// app.delete("api/v1/manga/{mangaId}/library") { ctx ->
|
||||
// val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
//
|
||||
// ctx.result(
|
||||
// ctx.future(
|
||||
// JavalinSetup.future { removeMangaFromLibrary(mangaId) }
|
||||
// )
|
||||
// }
|
||||
@@ -274,7 +275,7 @@ object AnimeAPI {
|
||||
// }
|
||||
//
|
||||
// // category modification
|
||||
// app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||
// app.patch("/api/v1/category/{categoryId}") { ctx ->
|
||||
// val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
// val name = ctx.formParam("name")
|
||||
// val isDefault = ctx.formParam("default")?.toBoolean()
|
||||
@@ -283,7 +284,7 @@ object AnimeAPI {
|
||||
// }
|
||||
//
|
||||
// // category re-ordering
|
||||
// app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
|
||||
// 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()
|
||||
@@ -292,21 +293,21 @@ object AnimeAPI {
|
||||
// }
|
||||
//
|
||||
// // category delete
|
||||
// app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||
// 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 ->
|
||||
// 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(
|
||||
// ctx.future(
|
||||
// future {
|
||||
// restoreLegacyBackup(ctx.bodyAsInputStream())
|
||||
// }
|
||||
@@ -315,7 +316,7 @@ object AnimeAPI {
|
||||
//
|
||||
// // 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(
|
||||
// ctx.future(
|
||||
// JavalinSetup.future {
|
||||
// restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
|
||||
// }
|
||||
@@ -325,7 +326,7 @@ object AnimeAPI {
|
||||
// // 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(
|
||||
// ctx.future(
|
||||
// JavalinSetup.future {
|
||||
// createLegacyBackup(
|
||||
// BackupFlags(
|
||||
@@ -347,7 +348,7 @@ object AnimeAPI {
|
||||
// val currentDate = sdf.format(Date())
|
||||
//
|
||||
// ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
|
||||
// ctx.result(
|
||||
// ctx.future(
|
||||
// JavalinSetup.future {
|
||||
// createLegacyBackup(
|
||||
// BackupFlags(
|
||||
|
||||
@@ -23,8 +23,8 @@ 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.manga.impl.util.storage.ImageResponse.clearCachedImage
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getCachedImageResponse
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ 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.dataclass.VideoDataClass
|
||||
import suwayomi.tachidesk.anime.model.table.AnimeTable
|
||||
import suwayomi.tachidesk.anime.model.table.EpisodeTable
|
||||
import suwayomi.tachidesk.anime.model.table.toDataClass
|
||||
@@ -135,7 +136,7 @@ object Episode {
|
||||
|
||||
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
|
||||
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
|
||||
val fetchedLinkUrl = source.fetchEpisodeLink(
|
||||
val fetchedVideos = source.fetchVideoList(
|
||||
SEpisode.create().also {
|
||||
it.url = episode.url
|
||||
it.name = episode.name
|
||||
@@ -154,7 +155,13 @@ object Episode {
|
||||
episode.lastPageRead,
|
||||
episode.index,
|
||||
episode.episodeCount,
|
||||
fetchedLinkUrl
|
||||
fetchedVideos.map {
|
||||
VideoDataClass(
|
||||
it.url,
|
||||
it.quality,
|
||||
it.videoUrl,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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 suwayomi.tachidesk.anime.impl.AnimeList.processEntries
|
||||
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
|
||||
import suwayomi.tachidesk.anime.model.dataclass.PagedAnimeListDataClass
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
|
||||
object Search {
|
||||
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedAnimeListDataClass {
|
||||
val source = getAnimeHttpSource(sourceId)
|
||||
val searchManga = source.fetchSearchAnime(pageNum, searchTerm, source.getFilterList()).awaitSingle()
|
||||
return searchManga.processEntries(sourceId)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ 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.manga.impl.util.storage.ImageResponse.getCachedImageResponse
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
@@ -138,7 +138,7 @@ object Extension {
|
||||
else -> "all"
|
||||
}
|
||||
|
||||
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ")
|
||||
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Aniyomi: ")
|
||||
|
||||
// update extension info
|
||||
transaction {
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ object ExtensionGithubApi {
|
||||
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
|
||||
}
|
||||
.map { element ->
|
||||
val name = element["name"].string.substringAfter("Tachiyomi: ")
|
||||
val name = element["name"].string.substringAfter("Aniyomi: ")
|
||||
val pkgName = element["pkg"].string
|
||||
val apkName = element["apk"].string
|
||||
val versionName = element["version"].string
|
||||
|
||||
@@ -41,8 +41,8 @@ object PackageTools {
|
||||
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
|
||||
const val LIB_VERSION_MIN = 12
|
||||
const val LIB_VERSION_MAX = 12
|
||||
|
||||
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key
|
||||
var trustedSignatures = mutableSetOf<String>() + officialSignature
|
||||
@@ -82,7 +82,7 @@ object PackageTools {
|
||||
)
|
||||
handler.dump(errorFile, emptyArray<String>())
|
||||
} else {
|
||||
BytecodeEditor.fixAndroidClasses(jarFilePath.toFile())
|
||||
BytecodeEditor.fixAndroidClasses(jarFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,5 +31,5 @@ data class EpisodeDataClass(
|
||||
val episodeCount: Int? = null,
|
||||
|
||||
/** used to construct pages in the front-end */
|
||||
val linkUrl: String? = null,
|
||||
val videos: List<VideoDataClass>? = null,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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 VideoDataClass(
|
||||
val url: String,
|
||||
val quality: String,
|
||||
var videoUrl: String?,
|
||||
)
|
||||
@@ -12,6 +12,7 @@ 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.dataclass.toGenreList
|
||||
import suwayomi.tachidesk.manga.model.table.MangaStatus.Companion
|
||||
|
||||
object AnimeTable : IntIdTable() {
|
||||
@@ -48,7 +49,7 @@ fun AnimeTable.toDataClass(mangaEntry: ResultRow) =
|
||||
mangaEntry[artist],
|
||||
mangaEntry[author],
|
||||
mangaEntry[description],
|
||||
mangaEntry[genre],
|
||||
mangaEntry[genre].toGenreList(),
|
||||
Companion.valueOf(mangaEntry[status]).name,
|
||||
mangaEntry[inLibrary]
|
||||
)
|
||||
|
||||
@@ -7,18 +7,15 @@ package suwayomi.tachidesk.global
|
||||
* 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 io.javalin.apibuilder.ApiBuilder.get
|
||||
import io.javalin.apibuilder.ApiBuilder.path
|
||||
import suwayomi.tachidesk.global.controller.SettingsController
|
||||
|
||||
object GlobalAPI {
|
||||
fun defineEndpoints(app: Javalin) {
|
||||
app.routes {
|
||||
path("api/v1/settings") {
|
||||
get("about", SettingsController::about)
|
||||
get("check-update", SettingsController::checkUpdate)
|
||||
}
|
||||
fun defineEndpoints() {
|
||||
path("settings") {
|
||||
get("about", SettingsController::about)
|
||||
get("check-update", SettingsController::checkUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,19 @@ package suwayomi.tachidesk.global.controller
|
||||
import io.javalin.http.Context
|
||||
import suwayomi.tachidesk.global.impl.About
|
||||
import suwayomi.tachidesk.global.impl.AppUpdate
|
||||
import suwayomi.tachidesk.server.JavalinSetup
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
/** Settings Page/Screen */
|
||||
object SettingsController {
|
||||
/** returns some static info about the current app build */
|
||||
fun about(ctx: Context): Context {
|
||||
return ctx.json(About.getAbout())
|
||||
fun about(ctx: Context) {
|
||||
ctx.json(About.getAbout())
|
||||
}
|
||||
|
||||
/** check for app updates */
|
||||
fun checkUpdate(ctx: Context): Context {
|
||||
return ctx.json(
|
||||
JavalinSetup.future { AppUpdate.checkUpdate() }
|
||||
fun checkUpdate(ctx: Context) {
|
||||
ctx.json(
|
||||
future { AppUpdate.checkUpdate() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user