Compare commits

...

15 Commits

Author SHA1 Message Date
Aria Moradi e46e165704 bump to v0.4.5
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-08-11 00:39:08 +04:30
Aria Moradi a6af423fb4 Merge branch 'master' of github.com:Suwayomi/Tachidesk-Server 2021-08-10 09:42:41 +04:30
Aria Moradi 3397e694c0 sync anime lib implementation with 12 (#133)
* sync anime lib implementation with 11

* fix wrong api

* delete unused classes

* adapt to lib 12

* add LICENSE for eu.kanade.tachiyomi

* changes for lib 12

* update to lib 12

* update webUI
2021-08-10 09:42:14 +04:30
Aria Moradi 77ff82505e fix wrong api 2021-08-10 04:01:08 +04:30
Aria Moradi c3f2838270 fix name tag generator 2021-08-10 03:40:49 +04:30
Aria Moradi aed7f205b6 update webUI 2021-08-10 03:39:36 +04:30
Aria Moradi 95b3587f7a update webUI 2021-08-10 02:04:35 +04:30
Aria Moradi a4baeb995a refactor endpoints to the new styles 2021-08-09 23:18:41 +04:30
Aria Moradi 032ab54206 fix tag generator 2021-08-09 23:18:06 +04:30
Aria Moradi 1f9c1eb1c0 only open browser when appropriate 2021-08-09 07:15:41 +04:30
Aria Moradi a213e568ba update property strings 2021-08-09 06:59:16 +04:30
Aria Moradi 7aeaeb4b86 move copyright notice to it's place 2021-08-09 06:49:12 +04:30
Aria Moradi 81aef4b8fa remove un-used files 2021-08-09 06:48:02 +04:30
Aria Moradi 31f0b6a16c ability to override server.conf with java -D arguments 2021-08-09 06:45:49 +04:30
Aria Moradi 44e3a682fd update Project name 2021-08-09 01:27:09 +04:30
56 changed files with 938 additions and 5658 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
id: GenTagName id: GenTagName
run: | run: |
cd master/server/build 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 "$genTag"
echo "::set-output name=value::$genTag" echo "::set-output name=value::$genTag"
@@ -1,14 +1,15 @@
package xyz.nulldev.ts.config package xyz.nulldev.ts.config
import net.harawata.appdirs.AppDirsFactory
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
* This Source Code Form is subject to the terms of the Mozilla Public * This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import net.harawata.appdirs.AppDirsFactory
val ApplicationRootDir: String val ApplicationRootDir: String
get(): String { get(): String {
return System.getProperty( return System.getProperty(
@@ -35,6 +35,7 @@ open class ConfigManager {
/** /**
* Get a config module (Java API) * Get a config module (Java API)
*/ */
@Suppress("UNCHECKED_CAST")
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
/** /**
@@ -1,8 +1,39 @@
package xyz.nulldev.ts.config 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 com.typesafe.config.Config
import io.github.config4k.getValue
import kotlin.reflect.KProperty
/** /**
* Abstract config module. * 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) 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 +0,0 @@
xyz.nulldev.ts.api.v2.java.impl.ServerAPIImpl
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long
@@ -1,22 +0,0 @@
[
{
"label": "Sync",
"icon": "import_export",
"type": "nested",
"prefs": []
},
{
"label": "Server",
"icon": "dns",
"type": "nested",
"prefs": [
{
"label": "Password authentication",
"type": "text-password",
"default": "",
"key": "pref_ts_server_password",
"hint": "Enter a password"
}
]
}
]
@@ -1,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);
}
}
@@ -1,7 +0,0 @@
package com.github.pwittchen.reactivenetwork.library
import android.net.NetworkInfo
class Connectivity {
val state = NetworkInfo.State.CONNECTED
}
@@ -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 +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.webInterface=electron" "-Dsuwayomi.tachidesk.config.server.electronPath=electron/electron.exe" -jar Tachidesk.jar
+2 -3
View File
@@ -101,8 +101,8 @@ sourceSets {
} }
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.4" val tachideskVersion = System.getenv("ProductVersion") ?: "v0.4.5"
val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r20" val webUIRevisionTag = System.getenv("WebUIRevision") ?: "r23"
// counts commit count on master // counts commit count on master
val tachideskRevision = runCatching { val tachideskRevision = runCatching {
@@ -172,7 +172,6 @@ tasks {
withType<ShadowJar> { withType<ShadowJar> {
destinationDirectory.set(File("$rootDir/server/build")) destinationDirectory.set(File("$rootDir/server/build"))
dependsOn("formatKotlin", "lintKotlin")
} }
named("run") { named("run") {
@@ -1,30 +1,13 @@
/* Copyright 2015 Javier Tomás
Copyright 2014 Prateek Srivastava
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at 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 Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. 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). * 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. * 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.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import rx.Observable import rx.Observable
/** /**
@@ -19,6 +20,9 @@ interface AnimeSource {
*/ */
val name: String val name: String
val lang: String
get() = ""
/** /**
* Returns an observable with the updated details for a anime. * Returns an observable with the updated details for a anime.
* *
@@ -36,12 +40,12 @@ interface AnimeSource {
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> 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. * @param episode the episode to get the link for.
*/ */
// @Deprecated("Use getEpisodeList instead") // @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. // * [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.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 package eu.kanade.tachiyomi.animesource.model
// import tachiyomi.animesource.model.AnimeInfo
import java.io.Serializable import java.io.Serializable
interface SAnime : Serializable { interface SAnime : Serializable {
@@ -23,9 +24,7 @@ interface SAnime : Serializable {
var initialized: Boolean var initialized: Boolean
fun copyFrom(other: SAnime) { fun copyFrom(other: SAnime) {
if (other.title != null) { title = other.title
title = other.title
}
if (other.author != null) { if (other.author != null) {
author = other.author 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 package eu.kanade.tachiyomi.animesource.model
// import tachiyomi.animesource.model.EpisodeInfo
import java.io.Serializable import java.io.Serializable
interface SEpisode : 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.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode 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.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.newCallWithProgress import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.source.model.Page
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -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 * 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. * the url, send different headers or request method like POST.
@@ -236,16 +228,6 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
return GET(baseUrl + anime.url, headers) 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. * 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> 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 * Returns the request for getting the episode link. Override only if it's needed to override
* url, send different headers or request method like POST. * 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) return GET(baseUrl + episode.url, headers)
} }
@@ -275,7 +263,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* *
* @param response the response from the site. * @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 * 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. * @param page the page whose source image has to be fetched.
*/ */
open fun fetchImageUrl(page: Page): Observable<String> { open fun fetchVideoUrl(video: Video): Observable<String> {
return client.newCall(imageUrlRequest(page)) return client.newCall(videoUrlRequest(video))
.asObservableSuccess() .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 * 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. * 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 { protected open fun videoUrlRequest(video: Video): Request {
return GET(page.url, headers) return GET(video.url, headers)
} }
/** /**
@@ -304,15 +292,15 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* *
* @param response the response from the site. * @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. * Returns an observable with the response of the source image.
* *
* @param page the page whose source image has to be downloaded. * @param page the page whose source image has to be downloaded.
*/ */
fun fetchImage(page: Page): Observable<Response> { fun fetchVideo(video: Video): Observable<Response> {
return client.newCallWithProgress(imageRequest(page), page) return client.newCallWithProgress(videoRequest(video), video)
.asObservableSuccess() .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 * 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. * 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 { protected open fun videoRequest(video: Video): Request {
return GET(page.imageUrl!!, headers) return GET(video.videoUrl!!, headers)
} }
/** /**
@@ -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.animesource.model.Video
import eu.kanade.tachiyomi.source.model.Page
import rx.Observable import rx.Observable
fun AnimeHttpSource.getImageUrl(page: Page): Observable<Page> { fun AnimeHttpSource.getVideoUrl(video: Video): Observable<Video> {
page.status = Page.LOAD_PAGE video.status = Video.LOAD_VIDEO
return fetchImageUrl(page) return fetchVideoUrl(video)
.doOnError { page.status = Page.ERROR } .doOnError { video.status = Video.ERROR }
.onErrorReturn { null } .onErrorReturn { null }
.doOnNext { page.imageUrl = it } .doOnNext { video.videoUrl = it }
.map { page } .map { video }
} }
fun AnimeHttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> { fun AnimeHttpSource.fetchUrlFromVideo(video: Video): Observable<Video> {
return Observable.from(pages) return Observable.just(video)
.filter { !it.imageUrl.isNullOrEmpty() } .filter { !it.videoUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingImageUrlsFromPageList(pages)) .mergeWith(fetchRemainingVideoUrlsFromVideoList(video))
} }
fun AnimeHttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> { fun AnimeHttpSource.fetchRemainingVideoUrlsFromVideoList(video: Video): Observable<Video> {
return Observable.from(pages) return Observable.just(video)
.filter { it.imageUrl.isNullOrEmpty() } .filter { it.videoUrl.isNullOrEmpty() }
.concatMap { getImageUrl(it) } .concatMap { getVideoUrl(it) }
} }
@@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.animesource.online
import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode 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 eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
@@ -159,21 +159,6 @@ abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
*/ */
protected abstract fun episodeListSelector(): String 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. * Returns a episode from the given element.
* *
@@ -181,36 +166,35 @@ abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
*/ */
protected abstract fun episodeFromElement(element: Element): SEpisode 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. * Parses the response from the site and returns the page list.
* *
* @param response the response from the site. * @param response the response from the site.
*/ */
override fun pageListParse(response: Response): List<Page> { override fun videoListParse(response: Response): List<Video> {
return pageListParse(response.asJsoup()) val document = response.asJsoup()
return document.select(videoListSelector()).map { videoFromElement(it) }
} }
/** /**
* Returns a page list from the given document. * Returns the Jsoup selector that returns a list of [Element] corresponding to each video.
*
* @param document the parsed document.
*/ */
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. * @param response the response from the site.
*/ */
override fun imageUrlParse(response: Response): String { override fun videoUrlParse(response: Response): String {
return imageUrlParse(response.asJsoup()) return videoUrlParse(response.asJsoup())
} }
/** /**
@@ -218,5 +202,5 @@ abstract class ParsedAnimeHttpSource : AnimeHttpSource() {
* *
* @param document the parsed document. * @param document the parsed document.
*/ */
protected abstract fun imageUrlParse(document: Document): String protected abstract fun videoUrlParse(document: Document): String
} }
@@ -1,14 +1,19 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
// import kotlinx.coroutines.suspendCancellableCoroutine // import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resumeWithException
fun Call.asObservable(): Observable<Response> { fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber -> return Observable.unsafeCreate { subscriber ->
@@ -48,36 +53,38 @@ fun Call.asObservable(): Observable<Response> {
} }
// Based on https://github.com/gildor/kotlin-coroutines-okhttp // Based on https://github.com/gildor/kotlin-coroutines-okhttp
// suspend fun Call.await(assertSuccess: Boolean = false): Response { suspend fun Call.await(): Response {
// return suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
// enqueue( enqueue(
// object : Callback { object : Callback {
// override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
// if (assertSuccess && !response.isSuccessful) { if (!response.isSuccessful) {
// continuation.resumeWithException(Exception("HTTP error ${response.code}")) continuation.resumeWithException(Exception("HTTP error ${response.code}"))
// return return
// } }
//
// continuation.resume(response) 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 override fun onFailure(call: Call, e: IOException) {
// continuation.resumeWithException(e) // Don't bother with resuming the continuation if it is already cancelled.
// } if (continuation.isCancelled) return
// } continuation.resumeWithException(e)
// ) }
// }
// continuation.invokeOnCancellation { )
// try {
// cancel() continuation.invokeOnCancellation {
// } catch (ex: Throwable) { try {
// // Ignore cancel exception cancel()
// } } catch (ex: Throwable) {
// } // Ignore cancel exception
// } }
// } }
}
}
fun Call.asObservableSuccess(): Observable<Response> { fun Call.asObservableSuccess(): Observable<Response> {
return asObservable() return asObservable()
@@ -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")
}
}
}
@@ -19,6 +19,7 @@ import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.anime.impl.Anime.getAnime import suwayomi.tachidesk.anime.impl.Anime.getAnime
import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource import suwayomi.tachidesk.anime.impl.util.GetAnimeHttpSource.getAnimeHttpSource
import suwayomi.tachidesk.anime.model.dataclass.EpisodeDataClass 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.AnimeTable
import suwayomi.tachidesk.anime.model.table.EpisodeTable import suwayomi.tachidesk.anime.model.table.EpisodeTable
import suwayomi.tachidesk.anime.model.table.toDataClass import suwayomi.tachidesk.anime.model.table.toDataClass
@@ -135,7 +136,7 @@ object Episode {
val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() } val animeEntry = transaction { AnimeTable.select { AnimeTable.id eq animeId }.first() }
val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference]) val source = getAnimeHttpSource(animeEntry[AnimeTable.sourceReference])
val fetchedLinkUrl = source.fetchEpisodeLink( val fetchedVideos = source.fetchVideoList(
SEpisode.create().also { SEpisode.create().also {
it.url = episode.url it.url = episode.url
it.name = episode.name it.name = episode.name
@@ -154,7 +155,13 @@ object Episode {
episode.lastPageRead, episode.lastPageRead,
episode.index, episode.index,
episode.episodeCount, episode.episodeCount,
fetchedLinkUrl fetchedVideos.map {
VideoDataClass(
it.url,
it.quality,
it.videoUrl,
)
}
) )
} }
@@ -41,8 +41,8 @@ object PackageTools {
const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class" const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory" const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
const val METADATA_NSFW = "tachiyomi.animeextension.nsfw" const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
const val LIB_VERSION_MIN = 10 const val LIB_VERSION_MIN = 12
const val LIB_VERSION_MAX = 10 const val LIB_VERSION_MAX = 12
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key
var trustedSignatures = mutableSetOf<String>() + officialSignature var trustedSignatures = mutableSetOf<String>() + officialSignature
@@ -31,5 +31,5 @@ data class EpisodeDataClass(
val episodeCount: Int? = null, val episodeCount: Int? = null,
/** used to construct pages in the front-end */ /** 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 * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.get import io.javalin.apibuilder.ApiBuilder.get
import io.javalin.apibuilder.ApiBuilder.path import io.javalin.apibuilder.ApiBuilder.path
import suwayomi.tachidesk.global.controller.SettingsController import suwayomi.tachidesk.global.controller.SettingsController
object GlobalAPI { object GlobalAPI {
fun defineEndpoints(app: Javalin) { fun defineEndpoints() {
app.routes { path("settings") {
path("api/v1/settings") { get("about", SettingsController::about)
get("about", SettingsController::about) get("check-update", SettingsController::checkUpdate)
get("check-update", SettingsController::checkUpdate)
}
} }
} }
} }
@@ -10,19 +10,19 @@ package suwayomi.tachidesk.global.controller
import io.javalin.http.Context import io.javalin.http.Context
import suwayomi.tachidesk.global.impl.About import suwayomi.tachidesk.global.impl.About
import suwayomi.tachidesk.global.impl.AppUpdate import suwayomi.tachidesk.global.impl.AppUpdate
import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.future
/** Settings Page/Screen */ /** Settings Page/Screen */
object SettingsController { object SettingsController {
/** returns some static info about the current app build */ /** returns some static info about the current app build */
fun about(ctx: Context): Context { fun about(ctx: Context) {
return ctx.json(About.getAbout()) ctx.json(About.getAbout())
} }
/** check for app updates */ /** check for app updates */
fun checkUpdate(ctx: Context): Context { fun checkUpdate(ctx: Context) {
return ctx.json( ctx.json(
JavalinSetup.future { AppUpdate.checkUpdate() } future { AppUpdate.checkUpdate() }
) )
} }
} }
@@ -8,469 +8,103 @@ package suwayomi.tachidesk.manga
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.Javalin import io.javalin.Javalin
import suwayomi.tachidesk.manga.impl.Category import io.javalin.apibuilder.ApiBuilder.delete
import suwayomi.tachidesk.manga.impl.CategoryManga.addMangaToCategory import io.javalin.apibuilder.ApiBuilder.get
import suwayomi.tachidesk.manga.impl.CategoryManga.getCategoryMangaList import io.javalin.apibuilder.ApiBuilder.patch
import suwayomi.tachidesk.manga.impl.CategoryManga.getMangaCategories import io.javalin.apibuilder.ApiBuilder.path
import suwayomi.tachidesk.manga.impl.CategoryManga.removeMangaFromCategory import io.javalin.apibuilder.ApiBuilder.post
import suwayomi.tachidesk.manga.impl.Chapter.getChapter import io.javalin.apibuilder.ApiBuilder.ws
import suwayomi.tachidesk.manga.impl.Chapter.getChapterList import suwayomi.tachidesk.manga.controller.BackupController
import suwayomi.tachidesk.manga.impl.Chapter.modifyChapter import suwayomi.tachidesk.manga.controller.DownloadController
import suwayomi.tachidesk.manga.impl.Chapter.modifyChapterMeta import suwayomi.tachidesk.manga.controller.ExtensionController
import suwayomi.tachidesk.manga.impl.Library.addMangaToLibrary import suwayomi.tachidesk.manga.controller.LibraryController
import suwayomi.tachidesk.manga.impl.Library.getLibraryMangas import suwayomi.tachidesk.manga.controller.MangaController
import suwayomi.tachidesk.manga.impl.Library.removeMangaFromLibrary import suwayomi.tachidesk.manga.controller.SourceController
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
object MangaAPI { object MangaAPI {
fun defineEndpoints(app: Javalin) { fun defineEndpoints(app: Javalin) {
// list all extensions path("extension") {
app.get("/api/v1/extension/list") { ctx -> get("list", ExtensionController::list)
ctx.json(
future { get("install/:pkgName", ExtensionController::install)
getExtensionList() get("update/:pkgName", ExtensionController::update)
} get("uninstall/:pkgName", ExtensionController::uninstall)
)
get("icon/:apkName", ExtensionController::icon)
} }
// install extension identified with "pkgName" path("source") {
app.get("/api/v1/extension/install/:pkgName") { ctx -> get("list", SourceController::list)
val pkgName = ctx.pathParam("pkgName") get(":sourceId", SourceController::retrieve)
ctx.json( get(":sourceId/popular/:pageNum", SourceController::popular)
future { get(":sourceId/latest/:pageNum", SourceController::latest)
installExtension(pkgName)
} 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" path("manga") {
app.get("/api/v1/extension/update/:pkgName") { ctx -> get(":mangaId", MangaController::retrieve)
val pkgName = ctx.pathParam("pkgName") get(":mangaId/thumbnail", MangaController::thumbnail)
ctx.json( get(":mangaId/category", MangaController::categoryList)
future { get(":mangaId/category/:categoryId", MangaController::addToCategory)
updateExtension(pkgName) 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" path("") {
app.get("/api/v1/extension/uninstall/:pkgName") { ctx -> get("library", LibraryController::list)
val pkgName = ctx.pathParam("pkgName")
uninstallExtension(pkgName) path("category") {
ctx.status(200) get("", LibraryController::categoryList)
} post("", LibraryController::categoryCreate)
// icon for extension named `apkName` get(":categoryId", LibraryController::categoryMangas)
app.get("/api/v1/extension/icon/:apkName") { ctx -> patch(":categoryId", LibraryController::categoryModify)
val apkName = ctx.pathParam("apkName") delete(":categoryId", LibraryController::categoryDelete)
ctx.result( patch(":categoryId/reorder", LibraryController::categoryReorder)
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)
} }
} }
// Start the downloader path("backup") {
app.get("/api/v1/downloads/start") { ctx -> post("legacy/import", BackupController::legacyImport)
DownloadManager.start() post("legacy/import/file", BackupController::legacyImportFile)
ctx.status(200) get("legacy/export", BackupController::legacyExport)
get("legacy/export/file", BackupController::legacyExportFile)
} }
// Stop the downloader path("downloads") {
app.get("/api/v1/downloads/stop") { ctx -> ws("", DownloadController::downloadsWS)
DownloadManager.stop()
ctx.status(200) get("start", DownloadController::start)
get("stop", DownloadController::stop)
get("clear", DownloadController::stop)
} }
// clear download queue path("download") {
app.get("/api/v1/downloads/clear") { ctx -> get(":mangaId/chapter/:chapterIndex", DownloadController::queueChapter)
DownloadManager.clear() delete(":mangaId/chapter/:chapterIndex", DownloadController::unqueueChapter)
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)
} }
} }
} }
@@ -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,63 @@
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 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)
}
}
@@ -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")?.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))
}
}
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.server
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.Javalin import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.path
import io.javalin.http.staticfiles.Location import io.javalin.http.staticfiles.Location
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -50,7 +51,7 @@ object JavalinSetup {
config.enableCorsForAllOrigins() config.enableCorsForAllOrigins()
}.events { event -> }.events { event ->
event.serverStarted { event.serverStarted {
if (serverConfig.webUIEnabled && serverConfig.initialOpenInBrowserEnabled) { if (serverConfig.initialOpenInBrowserEnabled) {
Browser.openInBrowser() Browser.openInBrowser()
} }
} }
@@ -77,8 +78,12 @@ object JavalinSetup {
ctx.result(e.message ?: "Internal Server Error") ctx.result(e.message ?: "Internal Server Error")
} }
GlobalAPI.defineEndpoints(app) app.routes {
MangaAPI.defineEndpoints(app) path("api/v1/") {
AnimeAPI.defineEndpoints(app) GlobalAPI.defineEndpoints()
MangaAPI.defineEndpoints(app)
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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.typesafe.config.Config import com.typesafe.config.Config
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule import xyz.nulldev.ts.config.ConfigModule
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
import xyz.nulldev.ts.config.debugLogsEnabled import xyz.nulldev.ts.config.debugLogsEnabled
class ServerConfig(config: Config) : ConfigModule(config) { class ServerConfig(config: Config, moduleName: String = "") : ConfigModule(config, moduleName) {
val ip: String by config val ip: String by overridableWithSysProperty
val port: Int by config val port: Int by overridableWithSysProperty
val webUIEnabled: Boolean = System.getProperty(
"suwayomi.tachidesk.server.webUIEnabled", config.getString("webUIEnabled")
).toBoolean()
// proxy // proxy
val socksProxyEnabled: Boolean by config val socksProxyEnabled: Boolean by overridableWithSysProperty
val socksProxyHost: String by config
val socksProxyPort: String by config val socksProxyHost: String by overridableWithSysProperty
val socksProxyPort: String by overridableWithSysProperty
// misc // misc
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config) val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
val systemTrayEnabled: Boolean by config val systemTrayEnabled: Boolean by overridableWithSysProperty
val initialOpenInBrowserEnabled: Boolean by config
// webUI
val webUIEnabled: Boolean by overridableWithSysProperty
val initialOpenInBrowserEnabled: Boolean by overridableWithSysProperty
val webUIBrowser: String by overridableWithSysProperty
val electronPath: String by overridableWithSysProperty
companion object { 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>() private val electronInstances = mutableListOf<Any>()
fun openInBrowser() { fun openInBrowser() {
if (serverConfig.webUIEnabled) {
val openInElectron = System.getProperty("suwayomi.tachidesk.server.webInterface")?.equals("electron") if (serverConfig.webUIBrowser == ("electron")) {
try {
if (openInElectron == true) { val electronPath = serverConfig.electronPath
try { electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
val electronPath = System.getProperty("suwayomi.tachidesk.server.electronPath")!! } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start()) e.printStackTrace()
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error }
e.printStackTrace() } else {
} try {
} else { Desktop.browseURL(appBaseUrl)
try { } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
Desktop.browseURL(appBaseUrl) e.printStackTrace()
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error }
e.printStackTrace()
} }
} }
} }
@@ -14,3 +14,5 @@ server.systemTrayEnabled = true
# webUI # webUI
server.webUIEnabled = true server.webUIEnabled = true
server.initialOpenInBrowserEnabled = true server.initialOpenInBrowserEnabled = true
server.webUIBrowser = "browser" # "browser" or "electron"
server.electronPath = ""
+1 -1
View File
@@ -1,4 +1,4 @@
rootProject.name = System.getenv("ProductName") ?: "Tachidesk" rootProject.name = System.getenv("ProductName") ?: "Tachidesk-Server"
include("server") include("server")