Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4b7b5d572 | |||
| 291c2e692d | |||
| 8a9a4f21b1 | |||
| cd31332b39 | |||
| cc8d2162a0 | |||
| e6313cdc67 | |||
| a5578a7ac7 | |||
| fcdda6406e | |||
| d3a6662c60 | |||
| 5474eddf84 | |||
| b666cd47d4 | |||
| 8a986383fe | |||
| 9fa17f617e | |||
| e46e165704 | |||
| a6af423fb4 | |||
| 3397e694c0 | |||
| 77ff82505e | |||
| c3f2838270 | |||
| aed7f205b6 | |||
| 95b3587f7a | |||
| a4baeb995a | |||
| 032ab54206 | |||
| 1f9c1eb1c0 | |||
| a213e568ba | |||
| 7aeaeb4b86 | |||
| 81aef4b8fa | |||
| 31f0b6a16c | |||
| 44e3a682fd |
@@ -74,7 +74,7 @@ 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"
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
dependencies {
|
||||
// Config API, moved to the global build.gradle
|
||||
// implementation("com.typesafe:config:1.4.0")
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
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
|
||||
|
||||
|
||||
val ApplicationRootDir: String
|
||||
get(): String {
|
||||
return System.getProperty(
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,39 @@
|
||||
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.
|
||||
*/
|
||||
abstract class ConfigModule(config: Config)
|
||||
abstract class ConfigModule(config: Config, moduleName: String = "") {
|
||||
val overridableWithSysProperty = SystemPropertyOverrideDelegate(config, moduleName)
|
||||
}
|
||||
|
||||
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(
|
||||
"suwayomi.tachidesk.config.$moduleName.${property.name}",
|
||||
configValue.toString()
|
||||
)
|
||||
|
||||
val asT = when(T::class.simpleName) {
|
||||
"Int" -> combined.toInt()
|
||||
"Boolean" -> combined.toBoolean()
|
||||
// add more types as needed
|
||||
else -> combined
|
||||
}
|
||||
|
||||
return asT 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -20,16 +20,9 @@ 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")
|
||||
|
||||
@@ -43,10 +36,8 @@ dependencies {
|
||||
compileOnly("androidx.annotation:annotation:1.2.0-alpha01")
|
||||
|
||||
// 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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 -> {
|
||||
|
||||
+5
-4
@@ -1,8 +1,8 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -46,12 +46,12 @@ configure(projects) {
|
||||
testImplementation(kotlin("test-junit5"))
|
||||
|
||||
// coroutines
|
||||
val coroutinesVersion = "1.4.3"
|
||||
val coroutinesVersion = "1.5.0"
|
||||
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.2.1"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
|
||||
@@ -80,6 +80,7 @@ configure(projects) {
|
||||
implementation("net.harawata:appdirs:1.2.1")
|
||||
|
||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||
// note: watch https://github.com/ThexXTURBOXx/dex2jar for future development
|
||||
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
||||
|
||||
// APK parser
|
||||
|
||||
@@ -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.21"
|
||||
|
||||
const val MainClass = "suwayomi.tachidesk.MainKt"
|
||||
|
||||
// should be bumped with each stable release
|
||||
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.6"
|
||||
|
||||
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r24"
|
||||
|
||||
// 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")
|
||||
|
||||
+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.1.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1 +1 @@
|
||||
jre\bin\java -Dsuwayomi.tachidesk.server.debugLogsEnabled=true -jar Tachidesk.jar
|
||||
jre\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
|
||||
+49
-48
@@ -1,15 +1,15 @@
|
||||
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||
import java.io.BufferedReader
|
||||
import java.time.Instant
|
||||
|
||||
plugins {
|
||||
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"
|
||||
id("com.github.gmazzo.buildconfig") version "3.0.2"
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -53,7 +53,6 @@ dependencies {
|
||||
implementation("com.dorkbox:Utilities:1.9")
|
||||
|
||||
|
||||
|
||||
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.9.1")
|
||||
@@ -81,14 +80,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,56 +98,40 @@ 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
|
||||
)
|
||||
mapOf(
|
||||
"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)
|
||||
@@ -159,9 +141,9 @@ tasks {
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -172,11 +154,10 @@ tasks {
|
||||
|
||||
withType<ShadowJar> {
|
||||
destinationDirectory.set(File("$rootDir/server/build"))
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
}
|
||||
|
||||
named("run") {
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
dependsOn("formatKotlin", "lintKotlin", "downloadWebUI")
|
||||
}
|
||||
|
||||
named<Copy>("processResources") {
|
||||
@@ -187,6 +168,26 @@ tasks {
|
||||
register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") {
|
||||
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
|
||||
dest("src/main/resources/WebUI.zip")
|
||||
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
withType<LintTask> {
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import okhttp3.OkHttpClient
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
class NetworkHelper(context: Context) {
|
||||
|
||||
// private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -81,7 +81,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -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
|
||||
@@ -219,13 +220,13 @@ object AnimeAPI {
|
||||
// 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.json(future { sourceSearch(sourceId, searchTerm, pageNum) })
|
||||
}
|
||||
//
|
||||
// // source filter list
|
||||
// app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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?,
|
||||
)
|
||||
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,470 +7,103 @@ package suwayomi.tachidesk.manga
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import io.javalin.Javalin
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga.addMangaToCategory
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga.getCategoryMangaList
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga.getMangaCategories
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga.removeMangaFromCategory
|
||||
import suwayomi.tachidesk.manga.impl.Chapter.getChapter
|
||||
import suwayomi.tachidesk.manga.impl.Chapter.getChapterList
|
||||
import suwayomi.tachidesk.manga.impl.Chapter.modifyChapter
|
||||
import suwayomi.tachidesk.manga.impl.Chapter.modifyChapterMeta
|
||||
import suwayomi.tachidesk.manga.impl.Library.addMangaToLibrary
|
||||
import suwayomi.tachidesk.manga.impl.Library.getLibraryMangas
|
||||
import suwayomi.tachidesk.manga.impl.Library.removeMangaFromLibrary
|
||||
import suwayomi.tachidesk.manga.impl.Manga.getManga
|
||||
import suwayomi.tachidesk.manga.impl.Manga.getMangaThumbnail
|
||||
import suwayomi.tachidesk.manga.impl.Manga.modifyMangaMeta
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.getMangaList
|
||||
import suwayomi.tachidesk.manga.impl.Page.getPageImage
|
||||
import suwayomi.tachidesk.manga.impl.Search.sourceFilters
|
||||
import suwayomi.tachidesk.manga.impl.Search.sourceGlobalSearch
|
||||
import suwayomi.tachidesk.manga.impl.Search.sourceSearch
|
||||
import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSource
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSourceList
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSourcePreferences
|
||||
import suwayomi.tachidesk.manga.impl.Source.setSourcePreference
|
||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
||||
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIcon
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import io.javalin.apibuilder.ApiBuilder.delete
|
||||
import io.javalin.apibuilder.ApiBuilder.get
|
||||
import io.javalin.apibuilder.ApiBuilder.patch
|
||||
import io.javalin.apibuilder.ApiBuilder.path
|
||||
import io.javalin.apibuilder.ApiBuilder.post
|
||||
import io.javalin.apibuilder.ApiBuilder.ws
|
||||
import suwayomi.tachidesk.manga.controller.BackupController
|
||||
import suwayomi.tachidesk.manga.controller.DownloadController
|
||||
import suwayomi.tachidesk.manga.controller.ExtensionController
|
||||
import suwayomi.tachidesk.manga.controller.LibraryController
|
||||
import suwayomi.tachidesk.manga.controller.MangaController
|
||||
import suwayomi.tachidesk.manga.controller.SourceController
|
||||
|
||||
object MangaAPI {
|
||||
fun defineEndpoints(app: Javalin) {
|
||||
// list all extensions
|
||||
app.get("/api/v1/extension/list") { ctx ->
|
||||
ctx.json(
|
||||
future {
|
||||
getExtensionList()
|
||||
}
|
||||
)
|
||||
fun defineEndpoints() {
|
||||
path("extension") {
|
||||
get("list", ExtensionController::list)
|
||||
|
||||
get("install/:pkgName", ExtensionController::install)
|
||||
get("update/:pkgName", ExtensionController::update)
|
||||
get("uninstall/:pkgName", ExtensionController::uninstall)
|
||||
|
||||
get("icon/:apkName", ExtensionController::icon)
|
||||
}
|
||||
|
||||
// install extension identified with "pkgName"
|
||||
app.get("/api/v1/extension/install/:pkgName") { ctx ->
|
||||
val pkgName = ctx.pathParam("pkgName")
|
||||
path("source") {
|
||||
get("list", SourceController::list)
|
||||
get(":sourceId", SourceController::retrieve)
|
||||
|
||||
ctx.json(
|
||||
future {
|
||||
installExtension(pkgName)
|
||||
}
|
||||
)
|
||||
get(":sourceId/popular/:pageNum", SourceController::popular)
|
||||
get(":sourceId/latest/:pageNum", SourceController::latest)
|
||||
|
||||
get(":sourceId/preferences", SourceController::getPreferences)
|
||||
post(":sourceId/preferences", SourceController::setPreference)
|
||||
|
||||
post(":sourceId/filters", SourceController::filters) // TODO
|
||||
|
||||
get(":sourceId/search/:searchTerm/:pageNum", SourceController::searchSingle)
|
||||
get("search/:searchTerm/:pageNum", SourceController::searchSingle) // TODO
|
||||
}
|
||||
|
||||
// update extension identified with "pkgName"
|
||||
app.get("/api/v1/extension/update/:pkgName") { ctx ->
|
||||
val pkgName = ctx.pathParam("pkgName")
|
||||
path("manga") {
|
||||
get(":mangaId", MangaController::retrieve)
|
||||
get(":mangaId/thumbnail", MangaController::thumbnail)
|
||||
|
||||
ctx.json(
|
||||
future {
|
||||
updateExtension(pkgName)
|
||||
}
|
||||
)
|
||||
get(":mangaId/category", MangaController::categoryList)
|
||||
get(":mangaId/category/:categoryId", MangaController::addToCategory)
|
||||
delete(":mangaId/category/:categoryId", MangaController::removeFromCategory)
|
||||
|
||||
get(":mangaId/library", MangaController::addToLibrary)
|
||||
delete(":mangaId/library", MangaController::removeFromLibrary)
|
||||
|
||||
patch(":mangaId/meta", MangaController::meta)
|
||||
|
||||
get(":mangaId/chapters", MangaController::chapterList)
|
||||
get(":mangaId/chapter/:chapterIndex", MangaController::chapterRetrieve)
|
||||
patch(":mangaId/chapter/:chapterIndex", MangaController::chapterModify)
|
||||
|
||||
patch(":mangaId/chapter/:chapterIndex/meta", MangaController::chapterMeta)
|
||||
|
||||
get(":mangaId/chapter/:chapterIndex/page/:index", MangaController::pageRetrieve)
|
||||
}
|
||||
|
||||
// uninstall extension identified with "pkgName"
|
||||
app.get("/api/v1/extension/uninstall/:pkgName") { ctx ->
|
||||
val pkgName = ctx.pathParam("pkgName")
|
||||
path("") {
|
||||
get("library", LibraryController::list)
|
||||
|
||||
uninstallExtension(pkgName)
|
||||
ctx.status(200)
|
||||
}
|
||||
path("category") {
|
||||
get("", LibraryController::categoryList)
|
||||
post("", LibraryController::categoryCreate)
|
||||
|
||||
// icon for extension named `apkName`
|
||||
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
get(":categoryId", LibraryController::categoryMangas)
|
||||
patch(":categoryId", LibraryController::categoryModify)
|
||||
delete(":categoryId", LibraryController::categoryDelete)
|
||||
|
||||
ctx.result(
|
||||
future { getExtensionIcon(apkName) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
it.first
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// list of sources
|
||||
app.get("/api/v1/source/list") { ctx ->
|
||||
ctx.json(getSourceList())
|
||||
}
|
||||
|
||||
// fetch source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(getSource(sourceId))
|
||||
}
|
||||
|
||||
// fetch preferences of source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId/preferences") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(getSourcePreferences(sourceId))
|
||||
}
|
||||
|
||||
// fetch preferences of source with id `sourceId`
|
||||
app.post("/api/v1/source/:sourceId/preferences") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
|
||||
ctx.json(setSourcePreference(sourceId, preferenceChange))
|
||||
}
|
||||
|
||||
// popular mangas from source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(
|
||||
future {
|
||||
getMangaList(sourceId, pageNum, popular = true)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// latest mangas from source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(
|
||||
future {
|
||||
getMangaList(sourceId, pageNum, popular = false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// get manga info
|
||||
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
|
||||
|
||||
ctx.json(
|
||||
future {
|
||||
getManga(mangaId, onlineFetch)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// manga thumbnail
|
||||
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
ctx.result(
|
||||
future { getMangaThumbnail(mangaId) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
it.first
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// list manga's categories
|
||||
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getMangaCategories(mangaId))
|
||||
}
|
||||
|
||||
// adds the manga to category
|
||||
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
addMangaToCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// removes the manga from the category
|
||||
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
removeMangaFromCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// get chapter list when showing a manga
|
||||
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean()
|
||||
|
||||
ctx.json(future { getChapterList(mangaId, onlineFetch) })
|
||||
}
|
||||
|
||||
// used to modify a manga's meta parameters
|
||||
app.patch("/api/v1/manga/:mangaId/meta") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
val key = ctx.formParam("key")!!
|
||||
val value = ctx.formParam("value")!!
|
||||
|
||||
modifyMangaMeta(mangaId, key, value)
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// used to display a chapter, get a chapter in order to show it's pages
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(future { getChapter(chapterIndex, mangaId) })
|
||||
}
|
||||
|
||||
// used to modify a chapter's parameters
|
||||
app.patch("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
val read = ctx.formParam("read")?.toBoolean()
|
||||
val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
|
||||
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
|
||||
val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
|
||||
|
||||
modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// used to modify a chapter's meta parameters
|
||||
app.patch("/api/v1/manga/:mangaId/chapter/:chapterIndex/meta") { ctx ->
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
val key = ctx.formParam("key")!!
|
||||
val value = ctx.formParam("value")!!
|
||||
|
||||
modifyChapterMeta(mangaId, chapterIndex, key, value)
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// get page at index "index"
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val index = ctx.pathParam("index").toInt()
|
||||
|
||||
ctx.result(
|
||||
future { getPageImage(mangaId, chapterIndex, index) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
it.first
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// submit a chapter for download
|
||||
app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||
// TODO
|
||||
}
|
||||
|
||||
// cancel a chapter download
|
||||
app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||
// TODO
|
||||
}
|
||||
|
||||
// global search, Not implemented yet
|
||||
app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
ctx.json(sourceGlobalSearch(searchTerm))
|
||||
}
|
||||
|
||||
// single source search
|
||||
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(future { sourceSearch(sourceId, searchTerm, pageNum) })
|
||||
}
|
||||
|
||||
// source filter list
|
||||
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(sourceFilters(sourceId))
|
||||
}
|
||||
|
||||
// adds the manga to library
|
||||
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
ctx.result(
|
||||
future { addMangaToLibrary(mangaId) }
|
||||
)
|
||||
}
|
||||
|
||||
// removes the manga from the library
|
||||
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
ctx.result(
|
||||
future { removeMangaFromLibrary(mangaId) }
|
||||
)
|
||||
}
|
||||
|
||||
// lists mangas that have no category assigned
|
||||
app.get("/api/v1/library/") { ctx ->
|
||||
ctx.json(getLibraryMangas())
|
||||
}
|
||||
|
||||
// category list
|
||||
app.get("/api/v1/category/") { ctx ->
|
||||
ctx.json(Category.getCategoryList())
|
||||
}
|
||||
|
||||
// category create
|
||||
app.post("/api/v1/category/") { ctx ->
|
||||
val name = ctx.formParam("name")!!
|
||||
Category.createCategory(name)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category modification
|
||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
val name = ctx.formParam("name")
|
||||
val isDefault = ctx.formParam("default")?.toBoolean()
|
||||
Category.updateCategory(categoryId, name, isDefault)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category re-ordering
|
||||
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
val from = ctx.formParam("from")!!.toInt()
|
||||
val to = ctx.formParam("to")!!.toInt()
|
||||
Category.reorderCategory(categoryId, from, to)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category delete
|
||||
app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
Category.removeCategory(categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// returns the manga list associated with a category
|
||||
app.get("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
ctx.json(getCategoryMangaList(categoryId))
|
||||
}
|
||||
|
||||
// expects a Tachiyomi legacy backup json in the body
|
||||
app.post("/api/v1/backup/legacy/import") { ctx ->
|
||||
ctx.result(
|
||||
future {
|
||||
restoreLegacyBackup(ctx.bodyAsInputStream())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json"
|
||||
app.post("/api/v1/backup/legacy/import/file") { ctx ->
|
||||
ctx.result(
|
||||
future {
|
||||
restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// returns a Tachiyomi legacy backup json created from the current database as a json body
|
||||
app.get("/api/v1/backup/legacy/export") { ctx ->
|
||||
ctx.contentType("application/json")
|
||||
ctx.result(
|
||||
future {
|
||||
createLegacyBackup(
|
||||
BackupFlags(
|
||||
includeManga = true,
|
||||
includeCategories = true,
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// returns a Tachiyomi legacy backup json created from the current database as a file
|
||||
app.get("/api/v1/backup/legacy/export/file") { ctx ->
|
||||
ctx.contentType("application/json")
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm")
|
||||
val currentDate = sdf.format(Date())
|
||||
|
||||
ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
|
||||
ctx.result(
|
||||
future {
|
||||
createLegacyBackup(
|
||||
BackupFlags(
|
||||
includeManga = true,
|
||||
includeCategories = true,
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Download queue stats
|
||||
app.ws("/api/v1/downloads") { ws ->
|
||||
ws.onConnect { ctx ->
|
||||
DownloadManager.addClient(ctx)
|
||||
DownloadManager.notifyClient(ctx)
|
||||
}
|
||||
ws.onMessage { ctx ->
|
||||
DownloadManager.handleRequest(ctx)
|
||||
}
|
||||
ws.onClose { ctx ->
|
||||
DownloadManager.removeClient(ctx)
|
||||
patch(":categoryId/reorder", LibraryController::categoryReorder) // TODO: the underlying code doesn't need `:categoryId`, remove it
|
||||
}
|
||||
}
|
||||
|
||||
// Start the downloader
|
||||
app.get("/api/v1/downloads/start") { ctx ->
|
||||
DownloadManager.start()
|
||||
path("backup") {
|
||||
post("legacy/import", BackupController::legacyImport)
|
||||
post("legacy/import/file", BackupController::legacyImportFile)
|
||||
|
||||
ctx.status(200)
|
||||
get("legacy/export", BackupController::legacyExport)
|
||||
get("legacy/export/file", BackupController::legacyExportFile)
|
||||
}
|
||||
|
||||
// Stop the downloader
|
||||
app.get("/api/v1/downloads/stop") { ctx ->
|
||||
DownloadManager.stop()
|
||||
path("downloads") {
|
||||
ws("", DownloadController::downloadsWS)
|
||||
|
||||
ctx.status(200)
|
||||
get("start", DownloadController::start)
|
||||
get("stop", DownloadController::stop)
|
||||
get("clear", DownloadController::stop)
|
||||
}
|
||||
|
||||
// clear download queue
|
||||
app.get("/api/v1/downloads/clear") { ctx ->
|
||||
DownloadManager.clear()
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// Queue chapter for download
|
||||
app.get("/api/v1/download/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
DownloadManager.enqueue(chapterIndex, mangaId)
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// delete chapter from download queue
|
||||
app.delete("/api/v1/download/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
DownloadManager.unqueue(chapterIndex, mangaId)
|
||||
|
||||
ctx.status(200)
|
||||
path("download") {
|
||||
get(":mangaId/chapter/:chapterIndex", DownloadController::queueChapter)
|
||||
delete(":mangaId/chapter/:chapterIndex", DownloadController::unqueueChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package suwayomi.tachidesk.manga.controller
|
||||
|
||||
import io.javalin.http.Context
|
||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupExport
|
||||
import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupImport
|
||||
import suwayomi.tachidesk.server.JavalinSetup
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
object BackupController {
|
||||
/** expects a Tachiyomi legacy backup json in the body */
|
||||
fun legacyImport(ctx: Context) {
|
||||
ctx.result(
|
||||
JavalinSetup.future {
|
||||
LegacyBackupImport.restoreLegacyBackup(ctx.bodyAsInputStream())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json" */
|
||||
fun legacyImportFile(ctx: Context) {
|
||||
ctx.result(
|
||||
JavalinSetup.future {
|
||||
LegacyBackupImport.restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** returns a Tachiyomi legacy backup json created from the current database as a json body */
|
||||
fun legacyExport(ctx: Context) {
|
||||
ctx.contentType("application/json")
|
||||
ctx.result(
|
||||
JavalinSetup.future {
|
||||
LegacyBackupExport.createLegacyBackup(
|
||||
BackupFlags(
|
||||
includeManga = true,
|
||||
includeCategories = true,
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** returns a Tachiyomi legacy backup json created from the current database as a file */
|
||||
fun legacyExportFile(ctx: Context) {
|
||||
ctx.contentType("application/json")
|
||||
val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm")
|
||||
val currentDate = sdf.format(Date())
|
||||
|
||||
ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"")
|
||||
ctx.result(
|
||||
JavalinSetup.future {
|
||||
LegacyBackupExport.createLegacyBackup(
|
||||
BackupFlags(
|
||||
includeManga = true,
|
||||
includeCategories = true,
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package suwayomi.tachidesk.manga.controller
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import io.javalin.http.Context
|
||||
import io.javalin.websocket.WsHandler
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||
|
||||
object DownloadController {
|
||||
/** Download queue stats */
|
||||
fun downloadsWS(ws: WsHandler) {
|
||||
ws.onConnect { ctx ->
|
||||
DownloadManager.addClient(ctx)
|
||||
DownloadManager.notifyClient(ctx)
|
||||
}
|
||||
ws.onMessage { ctx ->
|
||||
DownloadManager.handleRequest(ctx)
|
||||
}
|
||||
ws.onClose { ctx ->
|
||||
DownloadManager.removeClient(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/** Start the downloader */
|
||||
fun start(ctx: Context) {
|
||||
DownloadManager.start()
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** Stop the downloader */
|
||||
fun stop(ctx: Context) {
|
||||
DownloadManager.stop()
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** clear download queue */
|
||||
fun clear(ctx: Context) {
|
||||
DownloadManager.clear()
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** Queue chapter for download */
|
||||
fun queueChapter(ctx: Context) {
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
DownloadManager.enqueue(chapterIndex, mangaId)
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** delete chapter from download queue */
|
||||
fun unqueueChapter(ctx: Context) {
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
DownloadManager.unqueue(chapterIndex, mangaId)
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package suwayomi.tachidesk.manga.controller
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import io.javalin.http.Context
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
object ExtensionController {
|
||||
/** list all extensions */
|
||||
fun list(ctx: Context) {
|
||||
ctx.json(
|
||||
future {
|
||||
ExtensionsList.getExtensionList()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** install extension identified with "pkgName" */
|
||||
fun install(ctx: Context) {
|
||||
val pkgName = ctx.pathParam("pkgName")
|
||||
|
||||
ctx.json(
|
||||
future {
|
||||
Extension.installExtension(pkgName)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** update extension identified with "pkgName" */
|
||||
fun update(ctx: Context) {
|
||||
val pkgName = ctx.pathParam("pkgName")
|
||||
|
||||
ctx.json(
|
||||
future {
|
||||
Extension.updateExtension(pkgName)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** uninstall extension identified with "pkgName" */
|
||||
fun uninstall(ctx: Context) {
|
||||
val pkgName = ctx.pathParam("pkgName")
|
||||
|
||||
Extension.uninstallExtension(pkgName)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** icon for extension named `apkName` */
|
||||
fun icon(ctx: Context) {
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
|
||||
ctx.result(
|
||||
future { Extension.getExtensionIcon(apkName) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
it.first
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package suwayomi.tachidesk.manga.controller
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import io.javalin.http.Context
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
import suwayomi.tachidesk.manga.impl.Library
|
||||
|
||||
object LibraryController {
|
||||
/** lists mangas that have no category assigned */
|
||||
fun list(ctx: Context) {
|
||||
ctx.json(Library.getLibraryMangas())
|
||||
}
|
||||
|
||||
/** category list */
|
||||
fun categoryList(ctx: Context) {
|
||||
ctx.json(Category.getCategoryList())
|
||||
}
|
||||
|
||||
/** category create */
|
||||
fun categoryCreate(ctx: Context) {
|
||||
val name = ctx.formParam("name")!!
|
||||
Category.createCategory(name)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** category modification */
|
||||
fun categoryModify(ctx: Context) {
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
val name = ctx.formParam("name")
|
||||
val isDefault = ctx.formParam("default")?.toBoolean()
|
||||
Category.updateCategory(categoryId, name, isDefault)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** category delete */
|
||||
fun categoryDelete(ctx: Context) {
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
Category.removeCategory(categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** returns the manga list associated with a category */
|
||||
fun categoryMangas(ctx: Context) {
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
ctx.json(CategoryManga.getCategoryMangaList(categoryId))
|
||||
}
|
||||
|
||||
/** category re-ordering */
|
||||
fun categoryReorder(ctx: Context) {
|
||||
val from = ctx.formParam("from")!!.toInt()
|
||||
val to = ctx.formParam("to")!!.toInt()
|
||||
Category.reorderCategory(from, to)
|
||||
ctx.status(200)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package suwayomi.tachidesk.manga.controller
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import io.javalin.http.Context
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.Library
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.impl.Page
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
object MangaController {
|
||||
/** get manga info */
|
||||
fun retrieve(ctx: Context) {
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
|
||||
|
||||
ctx.json(
|
||||
future {
|
||||
Manga.getManga(mangaId, onlineFetch)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** manga thumbnail */
|
||||
fun thumbnail(ctx: Context) {
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
ctx.result(
|
||||
future { Manga.getMangaThumbnail(mangaId) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
it.first
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** adds the manga to library */
|
||||
fun addToLibrary(ctx: Context) {
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
ctx.result(
|
||||
future { Library.addMangaToLibrary(mangaId) }
|
||||
)
|
||||
}
|
||||
|
||||
/** removes the manga from the library */
|
||||
fun removeFromLibrary(ctx: Context) {
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
ctx.result(
|
||||
future { Library.removeMangaFromLibrary(mangaId) }
|
||||
)
|
||||
}
|
||||
|
||||
/** list manga's categories */
|
||||
fun categoryList(ctx: Context) {
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(CategoryManga.getMangaCategories(mangaId))
|
||||
}
|
||||
|
||||
/** adds the manga to category */
|
||||
fun addToCategory(ctx: Context) {
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
CategoryManga.addMangaToCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** removes the manga from the category */
|
||||
fun removeFromCategory(ctx: Context) {
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
CategoryManga.removeMangaFromCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** used to modify a manga's meta parameters */
|
||||
fun meta(ctx: Context) {
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
val key = ctx.formParam("key")!!
|
||||
val value = ctx.formParam("value")!!
|
||||
|
||||
Manga.modifyMangaMeta(mangaId, key, value)
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** get chapter list when showing a manga */
|
||||
fun chapterList(ctx: Context) {
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
|
||||
|
||||
ctx.json(future { Chapter.getChapterList(mangaId, onlineFetch) })
|
||||
}
|
||||
|
||||
/** used to display a chapter, get a chapter in order to show its pages */
|
||||
fun chapterRetrieve(ctx: Context) {
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(future { Chapter.getChapter(chapterIndex, mangaId) })
|
||||
}
|
||||
|
||||
/** used to modify a chapter's parameters */
|
||||
fun chapterModify(ctx: Context) {
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
val read = ctx.formParam("read")?.toBoolean()
|
||||
val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
|
||||
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
|
||||
val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
|
||||
|
||||
Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** used to modify a chapter's meta parameters */
|
||||
fun chapterMeta(ctx: Context) {
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
|
||||
val key = ctx.formParam("key")!!
|
||||
val value = ctx.formParam("value")!!
|
||||
|
||||
Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value)
|
||||
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
/** get page at index "index" */
|
||||
fun pageRetrieve(ctx: Context) {
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val index = ctx.pathParam("index").toInt()
|
||||
|
||||
ctx.result(
|
||||
future { Page.getPageImage(mangaId, chapterIndex, index) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
it.first
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package suwayomi.tachidesk.manga.controller
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import io.javalin.http.Context
|
||||
import suwayomi.tachidesk.manga.impl.MangaList
|
||||
import suwayomi.tachidesk.manga.impl.Search
|
||||
import suwayomi.tachidesk.manga.impl.Source
|
||||
import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange
|
||||
import suwayomi.tachidesk.server.JavalinSetup
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
object SourceController {
|
||||
/** list of sources */
|
||||
fun list(ctx: Context) {
|
||||
ctx.json(Source.getSourceList())
|
||||
}
|
||||
|
||||
/** fetch source with id `sourceId` */
|
||||
fun retrieve(ctx: Context) {
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(Source.getSource(sourceId))
|
||||
}
|
||||
|
||||
/** popular mangas from source with id `sourceId` */
|
||||
fun popular(ctx: Context) {
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(
|
||||
future {
|
||||
MangaList.getMangaList(sourceId, pageNum, popular = true)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** latest mangas from source with id `sourceId` */
|
||||
fun latest(ctx: Context) {
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(
|
||||
future {
|
||||
MangaList.getMangaList(sourceId, pageNum, popular = false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** fetch preferences of source with id `sourceId` */
|
||||
fun getPreferences(ctx: Context) {
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(Source.getSourcePreferences(sourceId))
|
||||
}
|
||||
|
||||
/** fetch preferences of source with id `sourceId` */
|
||||
fun setPreference(ctx: Context) {
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
|
||||
ctx.json(Source.setSourcePreference(sourceId, preferenceChange))
|
||||
}
|
||||
|
||||
/** fetch filters of source with id `sourceId` */
|
||||
fun filters(ctx: Context) { // TODO
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(Search.sourceFilters(sourceId))
|
||||
}
|
||||
|
||||
/** single source search */
|
||||
fun searchSingle(ctx: Context) {
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(JavalinSetup.future { Search.sourceSearch(sourceId, searchTerm, pageNum) })
|
||||
}
|
||||
|
||||
/** all source search */
|
||||
fun searchAll(ctx: Context) { // TODO
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
ctx.json(Search.sourceGlobalSearch(searchTerm))
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ object Category {
|
||||
/**
|
||||
* Move the category from position `from` to `to`
|
||||
*/
|
||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
||||
fun reorderCategory(from: Int, to: Int) {
|
||||
transaction {
|
||||
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||
categories.add(to - 1, categories.removeAt(from - 1))
|
||||
|
||||
@@ -30,8 +30,8 @@ import java.time.Instant
|
||||
|
||||
object Chapter {
|
||||
/** get chapter list when showing a manga */
|
||||
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean?): List<ChapterDataClass> {
|
||||
return if (onlineFetch == true) {
|
||||
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean = false): List<ChapterDataClass> {
|
||||
return if (onlineFetch) {
|
||||
getSourceChapters(mangaId)
|
||||
} else {
|
||||
transaction {
|
||||
@@ -40,10 +40,7 @@ object Chapter {
|
||||
ChapterTable.toDataClass(it)
|
||||
}
|
||||
}.ifEmpty {
|
||||
// If it was explicitly set to offline dont grab chapters
|
||||
if (onlineFetch == null) {
|
||||
getSourceChapters(mangaId)
|
||||
} else emptyList()
|
||||
getSourceChapters(mangaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +160,10 @@ object Chapter {
|
||||
// update page list for this chapter
|
||||
transaction {
|
||||
pageList.forEach { page ->
|
||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||
val pageEntry = transaction {
|
||||
PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }
|
||||
.firstOrNull()
|
||||
}
|
||||
if (pageEntry == null) {
|
||||
PageTable.insert {
|
||||
it[index] = page.index
|
||||
@@ -210,7 +210,14 @@ object Chapter {
|
||||
}
|
||||
}
|
||||
|
||||
fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
|
||||
fun modifyChapter(
|
||||
mangaId: Int,
|
||||
chapterIndex: Int,
|
||||
isRead: Boolean?,
|
||||
isBookmarked: Boolean?,
|
||||
markPrevRead: Boolean?,
|
||||
lastPageRead: Int?
|
||||
) {
|
||||
transaction {
|
||||
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
|
||||
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) { update ->
|
||||
@@ -244,9 +251,11 @@ object Chapter {
|
||||
|
||||
fun modifyChapterMeta(mangaId: Int, chapterIndex: Int, key: String, value: String) {
|
||||
transaction {
|
||||
val chapter = ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }
|
||||
.first()[ChapterTable.id]
|
||||
val meta = transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapter) and (ChapterMetaTable.key eq key) } }.firstOrNull()
|
||||
val chapter =
|
||||
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }
|
||||
.first()[ChapterTable.id]
|
||||
val meta =
|
||||
transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapter) and (ChapterMetaTable.key eq key) } }.firstOrNull()
|
||||
if (meta == null) {
|
||||
ChapterMetaTable.insert {
|
||||
it[ChapterMetaTable.key] = key
|
||||
|
||||
@@ -13,22 +13,25 @@ import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
|
||||
|
||||
object Search {
|
||||
// TODO
|
||||
fun sourceFilters(sourceId: Long) {
|
||||
val source = getHttpSource(sourceId)
|
||||
// source.getFilterList().toItems()
|
||||
}
|
||||
|
||||
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
|
||||
val source = getHttpSource(sourceId)
|
||||
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).awaitSingle()
|
||||
return searchManga.processEntries(sourceId)
|
||||
}
|
||||
|
||||
// TODO
|
||||
@Suppress("UNUSED_PARAMETER", "UNUSED_VARIABLE")
|
||||
fun sourceFilters(sourceId: Long) {
|
||||
val source = getHttpSource(sourceId)
|
||||
// source.getFilterList().toItems()
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun sourceGlobalSearch(searchTerm: String) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
data class FilterWrapper(
|
||||
val type: String,
|
||||
val filter: Any
|
||||
|
||||
+2
@@ -141,6 +141,7 @@ object LegacyBackupImport : LegacyBackupBase() {
|
||||
* @param history history data from json
|
||||
* @param tracks tracking data from json
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private suspend fun restoreMangaData(
|
||||
manga: Manga,
|
||||
source: Source,
|
||||
@@ -204,6 +205,7 @@ object LegacyBackupImport : LegacyBackupBase() {
|
||||
return fetchedManga
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER") // TODO: remove this suppress when update Chapters is written
|
||||
private fun updateChapters(source: Source, fetchedManga: SManga, chapters: List<Chapter>) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
+5
-5
@@ -42,11 +42,11 @@ object LegacyBackupValidator {
|
||||
.sorted()
|
||||
}
|
||||
|
||||
val trackers = mangas
|
||||
.filter { it.asJsonObject.has("track") }
|
||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
||||
.map { it.asJsonObject["s"].asInt }
|
||||
.distinct()
|
||||
// val trackers = mangas
|
||||
// .filter { it.asJsonObject.has("track") }
|
||||
// .flatMap { it.asJsonObject["track"].asJsonArray }
|
||||
// .map { it.asJsonObject["s"].asInt }
|
||||
// .distinct()
|
||||
|
||||
val missingTrackers = listOf("")
|
||||
// val missingTrackers = trackers
|
||||
|
||||
@@ -85,7 +85,7 @@ object BytecodeEditor {
|
||||
bytes[2],
|
||||
bytes[3]
|
||||
)
|
||||
if (cafebabe.toLowerCase() != "cafebabe") {
|
||||
if (cafebabe.lowercase() != "cafebabe") {
|
||||
// Corrupted class
|
||||
return@use null
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.server
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import io.javalin.Javalin
|
||||
import io.javalin.apibuilder.ApiBuilder.path
|
||||
import io.javalin.http.staticfiles.Location
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -50,7 +51,7 @@ object JavalinSetup {
|
||||
config.enableCorsForAllOrigins()
|
||||
}.events { event ->
|
||||
event.serverStarted {
|
||||
if (serverConfig.webUIEnabled && serverConfig.initialOpenInBrowserEnabled) {
|
||||
if (serverConfig.initialOpenInBrowserEnabled) {
|
||||
Browser.openInBrowser()
|
||||
}
|
||||
}
|
||||
@@ -77,8 +78,12 @@ object JavalinSetup {
|
||||
ctx.result(e.message ?: "Internal Server Error")
|
||||
}
|
||||
|
||||
GlobalAPI.defineEndpoints(app)
|
||||
MangaAPI.defineEndpoints(app)
|
||||
AnimeAPI.defineEndpoints(app)
|
||||
app.routes {
|
||||
path("api/v1/") {
|
||||
GlobalAPI.defineEndpoints()
|
||||
MangaAPI.defineEndpoints()
|
||||
AnimeAPI.defineEndpoints(app) // TODO: migrate Anime endpoints
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,30 +8,31 @@ package suwayomi.tachidesk.server
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import io.github.config4k.getValue
|
||||
import xyz.nulldev.ts.config.ConfigModule
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import xyz.nulldev.ts.config.debugLogsEnabled
|
||||
|
||||
class ServerConfig(config: Config) : ConfigModule(config) {
|
||||
val ip: String by config
|
||||
val port: Int by config
|
||||
|
||||
val webUIEnabled: Boolean = System.getProperty(
|
||||
"suwayomi.tachidesk.server.webUIEnabled", config.getString("webUIEnabled")
|
||||
).toBoolean()
|
||||
class ServerConfig(config: Config, moduleName: String = "") : ConfigModule(config, moduleName) {
|
||||
val ip: String by overridableWithSysProperty
|
||||
val port: Int by overridableWithSysProperty
|
||||
|
||||
// proxy
|
||||
val socksProxyEnabled: Boolean by config
|
||||
val socksProxyHost: String by config
|
||||
val socksProxyPort: String by config
|
||||
val socksProxyEnabled: Boolean by overridableWithSysProperty
|
||||
|
||||
val socksProxyHost: String by overridableWithSysProperty
|
||||
val socksProxyPort: String by overridableWithSysProperty
|
||||
|
||||
// misc
|
||||
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
|
||||
val systemTrayEnabled: Boolean by config
|
||||
val initialOpenInBrowserEnabled: Boolean by config
|
||||
val systemTrayEnabled: Boolean by overridableWithSysProperty
|
||||
|
||||
// webUI
|
||||
val webUIEnabled: Boolean by overridableWithSysProperty
|
||||
val initialOpenInBrowserEnabled: Boolean by overridableWithSysProperty
|
||||
val webUIInterface: String by overridableWithSysProperty
|
||||
val electronPath: String by overridableWithSysProperty
|
||||
|
||||
companion object {
|
||||
fun register(config: Config) = ServerConfig(config.getConfig("server"))
|
||||
fun register(config: Config) = ServerConfig(config.getConfig("server"), "server")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,21 +17,20 @@ object Browser {
|
||||
private val electronInstances = mutableListOf<Any>()
|
||||
|
||||
fun openInBrowser() {
|
||||
|
||||
val openInElectron = System.getProperty("suwayomi.tachidesk.server.webInterface")?.equals("electron")
|
||||
|
||||
if (openInElectron == true) {
|
||||
try {
|
||||
val electronPath = System.getProperty("suwayomi.tachidesk.server.electronPath")!!
|
||||
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
|
||||
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||
e.printStackTrace()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Desktop.browseURL(appBaseUrl)
|
||||
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||
e.printStackTrace()
|
||||
if (serverConfig.webUIEnabled) {
|
||||
if (serverConfig.webUIInterface == ("electron")) {
|
||||
try {
|
||||
val electronPath = serverConfig.electronPath
|
||||
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
|
||||
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||
e.printStackTrace()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Desktop.browseURL(appBaseUrl)
|
||||
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
import suwayomi.tachidesk.server.BuildConfig
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
@@ -58,9 +59,12 @@ fun setupWebUI() {
|
||||
val webUIZipFile = File(webUIZipPath)
|
||||
|
||||
// try with resources first
|
||||
val resourceWebUI = try {
|
||||
val resourceWebUI: InputStream? = try {
|
||||
BuildConfig::class.java.getResourceAsStream("/WebUI.zip")
|
||||
} catch (e: NullPointerException) { null }
|
||||
} catch (e: NullPointerException) {
|
||||
logger.info { "No bundled WebUI.zip found!" }
|
||||
null
|
||||
}
|
||||
|
||||
if (resourceWebUI == null) { // is not bundled
|
||||
// download webUI zip
|
||||
@@ -71,18 +75,25 @@ fun setupWebUI() {
|
||||
val data = ByteArray(1024)
|
||||
|
||||
webUIZipFile.outputStream().use { webUIZipFileOut ->
|
||||
BufferedInputStream(URL(webUIZipURL).openStream()).use { inp ->
|
||||
|
||||
val connection = URL(webUIZipURL).openConnection() as HttpURLConnection
|
||||
connection.connect()
|
||||
val contentLength = connection.contentLength
|
||||
|
||||
connection.inputStream.buffered().use { inp ->
|
||||
var totalCount = 0
|
||||
var tresh = 0
|
||||
|
||||
print("Download progress: % 00")
|
||||
while (true) {
|
||||
val count = inp.read(data, 0, 1024)
|
||||
totalCount += count
|
||||
if (totalCount > tresh + 10 * 1024) {
|
||||
tresh = totalCount
|
||||
print(" *")
|
||||
}
|
||||
|
||||
if (count == -1)
|
||||
break
|
||||
|
||||
totalCount += count
|
||||
val percentage = (totalCount.toFloat() / contentLength * 100).toInt().toString().padStart(2, '0')
|
||||
print("\b\b$percentage")
|
||||
|
||||
webUIZipFileOut.write(data, 0, count)
|
||||
}
|
||||
println()
|
||||
|
||||
@@ -14,3 +14,5 @@ server.systemTrayEnabled = true
|
||||
# webUI
|
||||
server.webUIEnabled = true
|
||||
server.initialOpenInBrowserEnabled = true
|
||||
server.webUIInterface = "browser" # "browser" or "electron"
|
||||
server.electronPath = ""
|
||||
|
||||
@@ -72,7 +72,7 @@ class TestExtensions {
|
||||
sources = getSourceList().map { getHttpSource(it.id.toLong()) }
|
||||
}
|
||||
setLoggingEnabled(true)
|
||||
File("tmp/TestDesk/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.toUpperCase()} - ${it.id}" })
|
||||
File("tmp/TestDesk/sources.txt").writeText(sources.joinToString("\n") { "${it.name} - ${it.lang.uppercase()} - ${it.id}" })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,7 +99,7 @@ class TestExtensions {
|
||||
}.awaitAll()
|
||||
File("tmp/TestDesk/failedToFetch.txt").writeText(
|
||||
failedToFetch.joinToString("\n") { (source, exception) ->
|
||||
"${source.name} (${source.lang.toUpperCase()}, ${source.id}):" +
|
||||
"${source.name} (${source.lang.uppercase()}, ${source.id}):" +
|
||||
" ${exception.message}"
|
||||
}
|
||||
)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
rootProject.name = System.getenv("ProductName") ?: "Tachidesk"
|
||||
rootProject.name = System.getenv("ProductName") ?: "Tachidesk-Server"
|
||||
|
||||
include("server")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user