Compare commits

...

28 Commits

Author SHA1 Message Date
Aria Moradi b4b7b5d572 bump to v0.4.6
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-08-18 04:29:14 +04:30
Aria Moradi 291c2e692d clean up build.gradle files, move constants to buildSrc 2021-08-18 04:24:58 +04:30
Aria Moradi 8a9a4f21b1 remove some stuff we don't use 2021-08-18 04:06:13 +04:30
Aria Moradi cd31332b39 better download progress 2021-08-18 03:55:52 +04:30
Aria Moradi cc8d2162a0 fix compile issue 2021-08-18 02:59:07 +04:30
Aria Moradi e6313cdc67 yeet improvments from jui 2021-08-18 01:21:17 +04:30
Aria Moradi a5578a7ac7 fix compile warnings 2021-08-17 23:54:02 +04:30
Aria Moradi fcdda6406e update dependencies 2021-08-17 23:53:41 +04:30
Aria Moradi d3a6662c60 make it compile 2021-08-15 03:16:13 +04:30
Aria Moradi 5474eddf84 fix some inconsitencies 2021-08-15 02:41:23 +04:30
Aria Moradi b666cd47d4 fix shouldOverwrite 2021-08-15 00:25:08 +04:30
Aria Moradi 8a986383fe fixes #175, better webUI download task 2021-08-14 17:10:41 +04:30
Aria Moradi 9fa17f617e add anime seach functionality 2021-08-11 08:47:07 +04:30
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
82 changed files with 1150 additions and 5811 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"
-4
View File
@@ -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 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"
}
]
}
]
+2 -11
View File
@@ -20,16 +20,9 @@ dependencies {
// Android stub library // Android stub library
implementation(fileTree("lib/")) implementation(fileTree("lib/"))
// Android JAR libs
// compileOnly( fileTree(dir: new File(rootProject.rootDir, "libs/other"), include: "*.jar")
// JSON // JSON
compileOnly("com.google.code.gson:gson:2.8.6") compileOnly("com.google.code.gson:gson:2.8.6")
// Javassist
compileOnly("org.javassist:javassist:3.27.0-GA")
// XML // XML
compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1") compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
@@ -43,10 +36,8 @@ dependencies {
compileOnly("androidx.annotation:annotation:1.2.0-alpha01") compileOnly("androidx.annotation:annotation:1.2.0-alpha01")
// substitute for duktape-android // 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") // slimmer version of 'org.mozilla:rhino'
implementation("org.mozilla:rhino-runtime:1.7.13") implementation("org.mozilla:rhino-engine:1.7.13") // provides the same interface as 'javax.script' a.k.a Nashorn
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
implementation("org.mozilla:rhino-engine:1.7.13")
// Kotlin wrapper around Java Preferences, makes certain things easier // Kotlin wrapper around Java Preferences, makes certain things easier
val multiplatformSettingsVersion = "0.7.7" 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);
}
}
@@ -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,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.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.bytecode.ModApplier
import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule import xyz.nulldev.androidcompat.config.ApplicationInfoConfigModule
import xyz.nulldev.androidcompat.config.FilesConfigModule import xyz.nulldev.androidcompat.config.FilesConfigModule
import xyz.nulldev.androidcompat.config.SystemConfigModule import xyz.nulldev.androidcompat.config.SystemConfigModule
@@ -12,12 +11,7 @@ import xyz.nulldev.ts.config.GlobalConfigManager
* Initializes the Android compatibility module * Initializes the Android compatibility module
*/ */
class AndroidCompatInitializer { class AndroidCompatInitializer {
val modApplier by lazy { ModApplier() }
fun init() { fun init() {
modApplier.apply()
DI.global.addImport(AndroidCompatModule().create()) DI.global.addImport(AndroidCompatModule().create())
//Register config modules //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.io.Reader
import java.math.BigDecimal import java.math.BigDecimal
import java.net.URL import java.net.URL
import java.sql.*
import java.sql.Array import java.sql.Array
import java.sql.Blob
import java.sql.Clob
import java.sql.Date 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 { class ScrollableResultSet(val parent: ResultSet) : ResultSet by parent {
private val cachedContent = mutableListOf<ResultSetEntry>() private val cachedContent = mutableListOf<ResultSetEntry>()
@@ -18,7 +18,7 @@ class ServiceSupport {
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
fun startService(context: Context, intent: Intent) { fun startService(@Suppress("UNUSED_PARAMETER") context: Context, intent: Intent) {
val name = intentToClassName(intent) val name = intentToClassName(intent)
logger.debug { "Starting service: $name" } 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) val name = intentToClassName(intent)
stopService(name) stopService(name)
} }
@@ -25,6 +25,7 @@ object KodeinGlobalHelper {
* Get a dependency * Get a dependency
*/ */
@JvmStatic @JvmStatic
@Suppress("UNCHECKED_CAST")
fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T { fun <T : Any> instance(type: Class<T>, kodein: DI? = null): T {
return when(type) { return when(type) {
AndroidFiles::class.java -> { AndroidFiles::class.java -> {
+5 -4
View File
@@ -1,8 +1,8 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") version "1.4.32" kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version "1.4.32" apply false kotlin("plugin.serialization") version kotlinVersion
} }
allprojects { allprojects {
@@ -46,12 +46,12 @@ configure(projects) {
testImplementation(kotlin("test-junit5")) testImplementation(kotlin("test-junit5"))
// coroutines // 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-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
val kotlinSerializationVersion = "1.1.0" val kotlinSerializationVersion = "1.2.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
@@ -80,6 +80,7 @@ configure(projects) {
implementation("net.harawata:appdirs:1.2.1") implementation("net.harawata:appdirs:1.2.1")
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon // 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") implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
// APK parser // APK parser
+11
View File
@@ -0,0 +1,11 @@
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
dependencies {
implementation("net.lingala.zip4j:zip4j:2.9.0")
}
+33
View File
@@ -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
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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
View File
@@ -1,15 +1,15 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jmailen.gradle.kotlinter.tasks.FormatTask import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask import org.jmailen.gradle.kotlinter.tasks.LintTask
import java.io.BufferedReader
import java.time.Instant import java.time.Instant
plugins { plugins {
application application
id("com.github.johnrengelman.shadow") version "7.0.0" id("com.github.johnrengelman.shadow") version "7.0.0"
id("org.jmailen.kotlinter") version "3.4.3" 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 { repositories {
@@ -53,7 +53,6 @@ dependencies {
implementation("com.dorkbox:Utilities:1.9") implementation("com.dorkbox:Utilities:1.9")
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference // dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation("com.squareup.okhttp3:okhttp:4.9.1") implementation("com.squareup.okhttp3:okhttp:4.9.1")
@@ -81,14 +80,13 @@ dependencies {
// implementation(fileTree("lib/")) // implementation(fileTree("lib/"))
} }
val MainClass = "suwayomi.tachidesk.MainKt"
application { application {
mainClass.set(MainClass) mainClass.set(MainClass)
// for testing electron // uncomment for testing electron
// applicationDefaultJvmArgs = listOf( // applicationDefaultJvmArgs = listOf(
// "-Dsuwayomi.tachidesk.webInterface=electron", // "-Dsuwayomi.tachidesk.config.server.webUIInterface=electron",
// "-Dsuwayomi.tachidesk.electronPath=/usr/bin/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 { buildConfig {
clsName = "BuildConfig" className("BuildConfig")
packageName = "suwayomi.tachidesk.server" packageName("suwayomi.tachidesk.server")
useKotlinOutput()
buildConfigField("String", "NAME", rootProject.name) fun quoteWrap(obj: Any): String = """"$obj""""
buildConfigField("String", "VERSION", tachideskVersion)
buildConfigField("String", "REVISION", tachideskRevision) buildConfigField("String", "NAME", quoteWrap(rootProject.name))
buildConfigField("String", "BUILD_TYPE", if (System.getenv("ProductBuildType") == "Stable") "Stable" else "Preview") 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("long", "BUILD_TIME", Instant.now().epochSecond.toString())
buildConfigField("String", "WEBUI_REPO", "https://github.com/Suwayomi/Tachidesk-WebUI-preview") buildConfigField("String", "WEBUI_REPO", quoteWrap("https://github.com/Suwayomi/Tachidesk-WebUI-preview"))
buildConfigField("String", "WEBUI_TAG", webUIRevisionTag) buildConfigField("String", "WEBUI_TAG", quoteWrap(webUIRevisionTag))
buildConfigField("String", "GITHUB", "https://github.com/Suwayomi/Tachidesk") buildConfigField("String", "GITHUB", quoteWrap("https://github.com/Suwayomi/Tachidesk"))
buildConfigField("String", "DISCORD", "https://discord.gg/DDZdqZWaHA") buildConfigField("String", "DISCORD", quoteWrap("https://discord.gg/DDZdqZWaHA"))
} }
tasks { tasks {
shadowJar { shadowJar {
manifest { manifest {
attributes( attributes(
mapOf( mapOf(
"Main-Class" to MainClass, "Main-Class" to MainClass,
"Implementation-Title" to rootProject.name, "Implementation-Title" to rootProject.name,
"Implementation-Vendor" to "The Suwayomi Project", "Implementation-Vendor" to "The Suwayomi Project",
"Specification-Version" to tachideskVersion, "Specification-Version" to tachideskVersion,
"Implementation-Version" to tachideskRevision "Implementation-Version" to tachideskRevision
) )
) )
} }
archiveBaseName.set(rootProject.name) archiveBaseName.set(rootProject.name)
@@ -159,9 +141,9 @@ tasks {
withType<KotlinCompile> { withType<KotlinCompile> {
kotlinOptions { kotlinOptions {
freeCompilerArgs = listOf( freeCompilerArgs = listOf(
"-Xopt-in=kotlin.RequiresOptIn", "-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi" "-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
) )
} }
} }
@@ -172,11 +154,10 @@ tasks {
withType<ShadowJar> { withType<ShadowJar> {
destinationDirectory.set(File("$rootDir/server/build")) destinationDirectory.set(File("$rootDir/server/build"))
dependsOn("formatKotlin", "lintKotlin")
} }
named("run") { named("run") {
dependsOn("formatKotlin", "lintKotlin") dependsOn("formatKotlin", "lintKotlin", "downloadWebUI")
} }
named<Copy>("processResources") { named<Copy>("processResources") {
@@ -187,6 +168,26 @@ tasks {
register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") { register<de.undercouch.gradle.tasks.download.Download>("downloadWebUI") {
src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip") src("https://github.com/Suwayomi/Tachidesk-WebUI-preview/releases/download/$webUIRevisionTag/Tachidesk-WebUI-$webUIRevisionTag.zip")
dest("src/main/resources/WebUI.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> { withType<LintTask> {
@@ -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
@@ -54,7 +54,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* Note the generated id sets the sign bit to 0. * Note the generated id sets the sign bit to 0.
*/ */
override val id by lazy { 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()) 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 (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. * 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 * 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 * 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
} }
@@ -18,6 +18,7 @@ import okhttp3.OkHttpClient
// import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@Suppress("UNUSED_PARAMETER")
class NetworkHelper(context: Context) { class NetworkHelper(context: Context) {
// private val preferences: PreferencesHelper by injectLazy() // private val preferences: PreferencesHelper by injectLazy()
@@ -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()
@@ -103,6 +110,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
// return progressClient.newCall(request) // return progressClient.newCall(request)
// } // }
@Suppress("UNUSED_PARAMETER")
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder() val progressClient = newBuilder()
// .cache(null) // .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. * Note the generated id sets the sign bit to 0.
*/ */
override val id by lazy { 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()) 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 (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. * 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 * 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.getEpisode
import suwayomi.tachidesk.anime.impl.Episode.getEpisodeList import suwayomi.tachidesk.anime.impl.Episode.getEpisodeList
import suwayomi.tachidesk.anime.impl.Episode.modifyEpisode 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.getAnimeSource
import suwayomi.tachidesk.anime.impl.Source.getSourceList import suwayomi.tachidesk.anime.impl.Source.getSourceList
import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIcon import suwayomi.tachidesk.anime.impl.extension.Extension.getExtensionIcon
@@ -219,13 +220,13 @@ object AnimeAPI {
// ctx.json(sourceGlobalSearch(searchTerm)) // ctx.json(sourceGlobalSearch(searchTerm))
// } // }
// //
// // single source search // single source search
// app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx -> app.get("/api/v1/anime/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
// val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
// val searchTerm = ctx.pathParam("searchTerm") val searchTerm = ctx.pathParam("searchTerm")
// val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
// ctx.json(JavalinSetup.future { sourceSearch(sourceId, searchTerm, pageNum) }) ctx.json(future { sourceSearch(sourceId, searchTerm, pageNum) })
// } }
// //
// // source filter list // // source filter list
// app.get("/api/v1/source/:sourceId/filters/") { ctx -> // 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.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,
)
}
) )
} }
@@ -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" else -> "all"
} }
val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Tachiyomi: ") val extensionName = packageInfo.applicationInfo.nonLocalizedLabel.toString().substringAfter("Aniyomi: ")
// update extension info // update extension info
transaction { transaction {
@@ -32,7 +32,7 @@ object ExtensionGithubApi {
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
} }
.map { element -> .map { element ->
val name = element["name"].string.substringAfter("Tachiyomi: ") val name = element["name"].string.substringAfter("Aniyomi: ")
val pkgName = element["pkg"].string val pkgName = element["pkg"].string
val apkName = element["apk"].string val apkName = element["apk"].string
val versionName = element["version"].string val versionName = element["version"].string
@@ -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() }
) )
} }
} }
@@ -7,470 +7,103 @@ package suwayomi.tachidesk.manga
* 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.delete
import suwayomi.tachidesk.manga.impl.Category import io.javalin.apibuilder.ApiBuilder.get
import suwayomi.tachidesk.manga.impl.CategoryManga.addMangaToCategory import io.javalin.apibuilder.ApiBuilder.patch
import suwayomi.tachidesk.manga.impl.CategoryManga.getCategoryMangaList import io.javalin.apibuilder.ApiBuilder.path
import suwayomi.tachidesk.manga.impl.CategoryManga.getMangaCategories import io.javalin.apibuilder.ApiBuilder.post
import suwayomi.tachidesk.manga.impl.CategoryManga.removeMangaFromCategory import io.javalin.apibuilder.ApiBuilder.ws
import suwayomi.tachidesk.manga.impl.Chapter.getChapter import suwayomi.tachidesk.manga.controller.BackupController
import suwayomi.tachidesk.manga.impl.Chapter.getChapterList import suwayomi.tachidesk.manga.controller.DownloadController
import suwayomi.tachidesk.manga.impl.Chapter.modifyChapter import suwayomi.tachidesk.manga.controller.ExtensionController
import suwayomi.tachidesk.manga.impl.Chapter.modifyChapterMeta import suwayomi.tachidesk.manga.controller.LibraryController
import suwayomi.tachidesk.manga.impl.Library.addMangaToLibrary import suwayomi.tachidesk.manga.controller.MangaController
import suwayomi.tachidesk.manga.impl.Library.getLibraryMangas import suwayomi.tachidesk.manga.controller.SourceController
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
object MangaAPI { object MangaAPI {
fun defineEndpoints(app: Javalin) { fun defineEndpoints() {
// 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) // TODO: the underlying code doesn't need `:categoryId`, remove it
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,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` * Move the category from position `from` to `to`
*/ */
fun reorderCategory(categoryId: Int, from: Int, to: Int) { fun reorderCategory(from: Int, to: Int) {
transaction { transaction {
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList() val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
categories.add(to - 1, categories.removeAt(from - 1)) categories.add(to - 1, categories.removeAt(from - 1))
@@ -30,8 +30,8 @@ import java.time.Instant
object Chapter { object Chapter {
/** get chapter list when showing a manga */ /** get chapter list when showing a manga */
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean?): List<ChapterDataClass> { suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean = false): List<ChapterDataClass> {
return if (onlineFetch == true) { return if (onlineFetch) {
getSourceChapters(mangaId) getSourceChapters(mangaId)
} else { } else {
transaction { transaction {
@@ -40,10 +40,7 @@ object Chapter {
ChapterTable.toDataClass(it) ChapterTable.toDataClass(it)
} }
}.ifEmpty { }.ifEmpty {
// If it was explicitly set to offline dont grab chapters getSourceChapters(mangaId)
if (onlineFetch == null) {
getSourceChapters(mangaId)
} else emptyList()
} }
} }
} }
@@ -163,7 +160,10 @@ object Chapter {
// update page list for this chapter // update page list for this chapter
transaction { transaction {
pageList.forEach { page -> 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) { if (pageEntry == null) {
PageTable.insert { PageTable.insert {
it[index] = page.index 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 { transaction {
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) { if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) { update -> 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) { fun modifyChapterMeta(mangaId: Int, chapterIndex: Int, key: String, value: String) {
transaction { transaction {
val chapter = ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) } val chapter =
.first()[ChapterTable.id] ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }
val meta = transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapter) and (ChapterMetaTable.key eq key) } }.firstOrNull() .first()[ChapterTable.id]
val meta =
transaction { ChapterMetaTable.select { (ChapterMetaTable.ref eq chapter) and (ChapterMetaTable.key eq key) } }.firstOrNull()
if (meta == null) { if (meta == null) {
ChapterMetaTable.insert { ChapterMetaTable.insert {
it[ChapterMetaTable.key] = key it[ChapterMetaTable.key] = key
@@ -13,22 +13,25 @@ import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
object Search { object Search {
// TODO
fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId)
// source.getFilterList().toItems()
}
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).awaitSingle() val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).awaitSingle()
return searchManga.processEntries(sourceId) 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) { fun sourceGlobalSearch(searchTerm: String) {
// TODO // TODO
} }
@Suppress("unused")
data class FilterWrapper( data class FilterWrapper(
val type: String, val type: String,
val filter: Any val filter: Any
@@ -141,6 +141,7 @@ object LegacyBackupImport : LegacyBackupBase() {
* @param history history data from json * @param history history data from json
* @param tracks tracking data from json * @param tracks tracking data from json
*/ */
@Suppress("UNUSED_PARAMETER")
private suspend fun restoreMangaData( private suspend fun restoreMangaData(
manga: Manga, manga: Manga,
source: Source, source: Source,
@@ -204,6 +205,7 @@ object LegacyBackupImport : LegacyBackupBase() {
return fetchedManga return fetchedManga
} }
@Suppress("UNUSED_PARAMETER") // TODO: remove this suppress when update Chapters is written
private fun updateChapters(source: Source, fetchedManga: SManga, chapters: List<Chapter>) { private fun updateChapters(source: Source, fetchedManga: SManga, chapters: List<Chapter>) {
// TODO("Not yet implemented") // TODO("Not yet implemented")
} }
@@ -42,11 +42,11 @@ object LegacyBackupValidator {
.sorted() .sorted()
} }
val trackers = mangas // val trackers = mangas
.filter { it.asJsonObject.has("track") } // .filter { it.asJsonObject.has("track") }
.flatMap { it.asJsonObject["track"].asJsonArray } // .flatMap { it.asJsonObject["track"].asJsonArray }
.map { it.asJsonObject["s"].asInt } // .map { it.asJsonObject["s"].asInt }
.distinct() // .distinct()
val missingTrackers = listOf("") val missingTrackers = listOf("")
// val missingTrackers = trackers // val missingTrackers = trackers
@@ -85,7 +85,7 @@ object BytecodeEditor {
bytes[2], bytes[2],
bytes[3] bytes[3]
) )
if (cafebabe.toLowerCase() != "cafebabe") { if (cafebabe.lowercase() != "cafebabe") {
// Corrupted class // Corrupted class
return@use null return@use null
} }
@@ -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()
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 webUIInterface: 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.webUIInterface == ("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,8 +14,9 @@ import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.BuildConfig import suwayomi.tachidesk.server.BuildConfig
import java.io.BufferedInputStream
import java.io.File import java.io.File
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.MessageDigest import java.security.MessageDigest
@@ -58,9 +59,12 @@ fun setupWebUI() {
val webUIZipFile = File(webUIZipPath) val webUIZipFile = File(webUIZipPath)
// try with resources first // try with resources first
val resourceWebUI = try { val resourceWebUI: InputStream? = try {
BuildConfig::class.java.getResourceAsStream("/WebUI.zip") 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 if (resourceWebUI == null) { // is not bundled
// download webUI zip // download webUI zip
@@ -71,18 +75,25 @@ fun setupWebUI() {
val data = ByteArray(1024) val data = ByteArray(1024)
webUIZipFile.outputStream().use { webUIZipFileOut -> 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 totalCount = 0
var tresh = 0
print("Download progress: % 00")
while (true) { while (true) {
val count = inp.read(data, 0, 1024) val count = inp.read(data, 0, 1024)
totalCount += count
if (totalCount > tresh + 10 * 1024) {
tresh = totalCount
print(" *")
}
if (count == -1) if (count == -1)
break break
totalCount += count
val percentage = (totalCount.toFloat() / contentLength * 100).toInt().toString().padStart(2, '0')
print("\b\b$percentage")
webUIZipFileOut.write(data, 0, count) webUIZipFileOut.write(data, 0, count)
} }
println() println()
@@ -14,3 +14,5 @@ server.systemTrayEnabled = true
# webUI # webUI
server.webUIEnabled = true server.webUIEnabled = true
server.initialOpenInBrowserEnabled = 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()) } sources = getSourceList().map { getHttpSource(it.id.toLong()) }
} }
setLoggingEnabled(true) 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 @Test
@@ -99,7 +99,7 @@ class TestExtensions {
}.awaitAll() }.awaitAll()
File("tmp/TestDesk/failedToFetch.txt").writeText( File("tmp/TestDesk/failedToFetch.txt").writeText(
failedToFetch.joinToString("\n") { (source, exception) -> failedToFetch.joinToString("\n") { (source, exception) ->
"${source.name} (${source.lang.toUpperCase()}, ${source.id}):" + "${source.name} (${source.lang.uppercase()}, ${source.id}):" +
" ${exception.message}" " ${exception.message}"
} }
) )
+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")