Merge remote-tracking branch 'upstream/master'

This commit is contained in:
jmir1 2021-11-29 20:59:28 +01:00
commit 737c5c9889
29 changed files with 124 additions and 62 deletions

View file

@ -28,9 +28,6 @@ jobs:
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build app
uses: gradle/gradle-command-action@v1
uses: gradle/gradle-command-action@v2
with:
arguments: assembleStandardRelease
distributions-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true

View file

@ -34,12 +34,9 @@ jobs:
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build app
uses: gradle/gradle-command-action@v1
uses: gradle/gradle-command-action@v2
with:
arguments: assembleStandardRelease
distributions-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
# Sign APK and create release for tags

View file

@ -9,6 +9,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Moderate issues
uses: tachiyomiorg/issue-moderator-action@v1.1
uses: tachiyomiorg/issue-moderator-action@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -149,10 +149,10 @@ dependencies {
// AndroidX libraries
implementation("androidx.annotation:annotation:1.3.0")
implementation("androidx.appcompat:appcompat:1.4.0-rc01")
implementation("androidx.appcompat:appcompat:1.4.0")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha04")
implementation("androidx.browser:browser:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.2")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0-beta01")
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")

View file

@ -41,23 +41,32 @@ interface AnimeSource : tachiyomi.animesource.AnimeSource {
*
* @param anime the anime to update.
*/
@Deprecated("Use getAnimeDetails instead")
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = Observable.empty()
@Deprecated(
"Use the 1.x API instead",
ReplaceWith("getAnimeDetails")
)
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = throw IllegalStateException("Not used")
/**
* Returns an observable with all the available episodes for an anime.
*
* @param anime the anime to update.
*/
@Deprecated("Use getEpisodeList instead")
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> = Observable.empty()
@Deprecated(
"Use the 1.x API instead",
ReplaceWith("getEpisodeList")
)
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> = throw IllegalStateException("Not used")
/**
* Returns an observable with a list of video for the episode of an anime.
*
* @param episode the episode to get the link for.
*/
@Deprecated("Use getVideoList instead")
@Deprecated(
"Use the 1.x API instead",
ReplaceWith("getVideoList")
)
fun fetchVideoList(episode: SEpisode): Observable<List<Video>> = Observable.empty()
/**

View file

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.toEpisodeInfo
import eu.kanade.tachiyomi.animesource.model.toSAnime
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.util.episode.EpisodeRecognition
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.DiskUtil
@ -30,7 +31,7 @@ import java.io.InputStream
import java.util.Locale
import java.util.concurrent.TimeUnit
class LocalAnimeSource(private val context: Context) : AnimeCatalogueSource {
class LocalAnimeSource(private val context: Context) : AnimeCatalogueSource, UnmeteredSource {
companion object {
const val ID = 0L
const val HELP_URL = "https://aniyomi.jmir.xyz/help/guides/local-anime/"

View file

@ -28,6 +28,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.util.episode.NoEpisodesException
import eu.kanade.tachiyomi.util.episode.syncEpisodesWithSource
import eu.kanade.tachiyomi.util.episode.syncEpisodesWithTrackServiceTwoWay
@ -267,7 +268,10 @@ class AnimelibUpdateService(
.sortedWith(rankingScheme[selectedScheme])
// Warn when excessively checking a single source
val maxUpdatesFromSource = animeToUpdate.groupBy { it.source }.maxOfOrNull { it.value.size } ?: 0
val maxUpdatesFromSource = animeToUpdate
.groupBy { it.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (maxUpdatesFromSource > ANIME_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
toast(R.string.notification_size_warning, Toast.LENGTH_LONG)
}

View file

@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.data.database.models.Episode
import eu.kanade.tachiyomi.data.download.model.AnimeDownload
import eu.kanade.tachiyomi.data.download.model.AnimeDownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.plusAssign
@ -297,7 +298,10 @@ class AnimeDownloader(
// Start downloader if needed
if (autoStart && wasEmpty) {
val maxDownloadsFromSource = queue.groupBy { it.source }.maxOf { it.value.size }
val maxDownloadsFromSource = queue
.groupBy { it.source }
.filterKeys { it !is UnmeteredSource }
.maxOf { it.value.size }
if (maxDownloadsFromSource > EPISODES_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
withUIContext {
context.toast(R.string.download_queue_size_warning, Toast.LENGTH_LONG)

View file

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
@ -266,7 +267,10 @@ class Downloader(
// Start downloader if needed
if (autoStart && wasEmpty) {
val maxDownloadsFromSource = queue.groupBy { it.source }.maxOf { it.value.size }
val maxDownloadsFromSource = queue
.groupBy { it.source }
.filterKeys { it !is UnmeteredSource }
.maxOf { it.value.size }
if (maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
withUIContext {
context.toast(R.string.download_queue_size_warning, Toast.LENGTH_LONG)

View file

@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga
@ -267,7 +268,10 @@ class LibraryUpdateService(
.sortedWith(rankingScheme[selectedScheme])
// Warn when excessively checking a single source
val maxUpdatesFromSource = mangaToUpdate.groupBy { it.source }.maxOfOrNull { it.value.size } ?: 0
val maxUpdatesFromSource = mangaToUpdate
.groupBy { it.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
toast(R.string.notification_size_warning, Toast.LENGTH_LONG)
}

View file

@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.data.track.model.AnimeTrackSearch
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptor
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext
@ -27,13 +27,13 @@ import kotlinx.serialization.json.putJsonObject
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Calendar
import java.util.concurrent.TimeUnit.MINUTES
import java.util.concurrent.TimeUnit
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val authClient = client.newBuilder()
.addInterceptor(interceptor)
.addInterceptor(RateLimitInterceptor(85, 1, MINUTES))
.rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES)
.build()
suspend fun addLibManga(track: Track): Track {

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context
import android.graphics.drawable.Drawable
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -15,8 +16,10 @@ import eu.kanade.tachiyomi.extension.util.AnimeExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.AnimeExtensionInstaller
import eu.kanade.tachiyomi.extension.util.AnimeExtensionLoader
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async
import logcat.LogPriority
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -160,7 +163,8 @@ class AnimeExtensionManager(
val extensions: List<AnimeExtension.Available> = try {
api.findExtensions()
} catch (e: Exception) {
context.toast(e.message)
logcat(LogPriority.ERROR, e)
context.toast(R.string.extension_api_error)
emptyList()
}

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context
import android.graphics.drawable.Drawable
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
@ -15,8 +16,10 @@ import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async
import logcat.LogPriority
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -160,7 +163,8 @@ class ExtensionManager(
val extensions: List<Extension.Available> = try {
api.findExtensions()
} catch (e: Exception) {
context.toast(e.message)
logcat(LogPriority.ERROR, e)
context.toast(R.string.extension_api_error)
emptyList()
}

View file

@ -22,11 +22,19 @@ internal class AnimeExtensionGithubApi {
suspend fun findExtensions(): List<AnimeExtension.Available> {
return withIOContext {
networkService.client
val extensions = networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.await()
.parseAs<List<AnimeExtensionJsonObject>>()
.toExtensions()
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
if (extensions.size < 10) {
throw Exception()
}
extensions
}
}

View file

@ -22,11 +22,19 @@ internal class ExtensionGithubApi {
suspend fun findExtensions(): List<Extension.Available> {
return withIOContext {
networkService.client
val extensions = networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.await()
.parseAs<List<ExtensionJsonObject>>()
.toExtensions()
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
if (extensions.size < 100) {
throw Exception()
}
extensions
}
}

View file

@ -35,7 +35,7 @@ internal object AnimeExtensionLoader {
private const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
private const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
const val LIB_VERSION_MIN = 12
const val LIB_VERSION_MAX = 12
const val LIB_VERSION_MAX = 13
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES

View file

@ -35,7 +35,7 @@ internal object ExtensionLoader {
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2
const val LIB_VERSION_MAX = 1.3
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network.interceptor
import android.os.SystemClock
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.util.concurrent.TimeUnit
@ -17,10 +18,16 @@ import java.util.concurrent.TimeUnit
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
*/
class RateLimitInterceptor(
fun OkHttpClient.Builder.rateLimit(
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(permits, period, unit))
private class RateLimitInterceptor(
private val permits: Int,
private val period: Long = 1,
private val unit: TimeUnit = TimeUnit.SECONDS
period: Long,
unit: TimeUnit,
) : Interceptor {
private val requestQueue = ArrayList<Long>(permits)

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.network.interceptor
import android.os.SystemClock
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.util.concurrent.TimeUnit
@ -19,11 +20,18 @@ import java.util.concurrent.TimeUnit
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl,
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(SpecificHostRateLimitInterceptor(httpUrl, permits, period, unit))
class SpecificHostRateLimitInterceptor(
private val httpUrl: HttpUrl,
httpUrl: HttpUrl,
private val permits: Int,
private val period: Long = 1,
private val unit: TimeUnit = TimeUnit.SECONDS
period: Long,
unit: TimeUnit,
) : Interceptor {
private val requestQueue = ArrayList<Long>(permits)

View file

@ -38,7 +38,8 @@ import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
class LocalSource(private val context: Context) : CatalogueSource {
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
companion object {
const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"

View file

@ -40,23 +40,33 @@ interface Source : tachiyomi.source.Source {
*
* @param manga the manga to update.
*/
@Deprecated("Use getMangaDetails instead")
fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.empty()
@Deprecated(
"Use the 1.x API instead",
ReplaceWith("getMangaDetails")
)
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
/**
* Returns an observable with all the available chapters for a manga.
*
* @param manga the manga to update.
*/
@Deprecated("Use getChapterList instead")
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.empty()
@Deprecated(
"Use the 1.x API instead",
ReplaceWith("getChapterList")
)
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
// TODO: remove direct usages on this method
/**
* Returns an observable with the list of pages a chapter has.
*
* @param chapter the chapter.
*/
@Deprecated("Use getPageList instead")
@Deprecated(
"Use the 1.x API instead",
ReplaceWith("getPageList")
)
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
/**

View file

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.source
/**
* A source that explicitly doesn't require traffic considerations.
*
* This typically applies for self-hosted sources.
*/
interface UnmeteredSource

View file

@ -222,7 +222,7 @@ open class GlobalAnimeSearchPresenter(
Observable.from(first)
.filter { it.thumbnail_url == null && !it.initialized }
.map { Pair(it, source) }
.concatMap { runAsObservable({ getAnimeDetails(it.first, it.second) }) }
.concatMap { runAsObservable { getAnimeDetails(it.first, it.second) } }
.map { Pair(source as AnimeCatalogueSource, it) }
}
.onBackpressureBuffer()

View file

@ -222,7 +222,7 @@ open class GlobalSearchPresenter(
Observable.from(first)
.filter { it.thumbnail_url == null && !it.initialized }
.map { Pair(it, source) }
.concatMap { runAsObservable({ getMangaDetails(it.first, it.second) }) }
.concatMap { runAsObservable { getMangaDetails(it.first, it.second) } }
.map { Pair(source as CatalogueSource, it) }
}
.onBackpressureBuffer()

View file

@ -60,8 +60,8 @@ internal fun <T> CancellableContinuation<T>.unsubscribeOnCancellation(sub: Subsc
invokeOnCancellation { sub.unsubscribe() }
fun <T> runAsObservable(
backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE,
block: suspend () -> T,
backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE
): Observable<T> {
return Observable.create(
{ emitter ->

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/card_radius" />
<solid android:color="?attr/colorSurface" />
</shape>

View file

@ -301,6 +301,7 @@
<string name="untrusted_extension_message">This extension was signed with an untrusted certificate and wasn\'t activated.\n\nA malicious extension could read any login credentials stored in Tachiyomi or execute arbitrary code.\n\nBy trusting this certificate you accept these risks.</string>
<string name="obsolete_extension_message">This extension is no longer available.</string>
<string name="unofficial_extension_message">This extension is not from the official Tachiyomi extensions list.</string>
<string name="extension_api_error">Failed to get extensions list</string>
<string name="ext_version_info">Version: %1$s</string>
<string name="ext_language_info">Language: %1$s</string>
<string name="ext_nsfw_short">18+</string>

View file

@ -102,15 +102,6 @@
</style>
<style name="Widget.Tachiyomi.Snackbar" parent="Widget.Material3.Snackbar">
<item name="android:background">@drawable/snackbar_background</item>
<item name="actionTextColorAlpha">1</item>
</style>
<style name="Widget.Tachiyomi.Snackbar.TextView" parent="Widget.Material3.Snackbar.TextView">
<item name="android:textColor">?attr/colorOnSurface</item>
</style>
<style name="Widget.Tachiyomi.Chip.Action" parent="Widget.Material3.Chip.Suggestion">
<item name="chipBackgroundColor">?attr/chipBackgroundColor</item>
<item name="android:textColor">?attr/chipTextColor</item>

View file

@ -74,8 +74,6 @@
<item name="chipStyle">@style/Widget.Tachiyomi.Chip.Action</item>
<item name="chipTextColor">?android:attr/textColorPrimary</item>
<item name="chipBackgroundColor">?android:attr/divider</item>
<item name="snackbarStyle">@style/Widget.Tachiyomi.Snackbar</item>
<item name="snackbarTextViewStyle">@style/Widget.Tachiyomi.Snackbar.TextView</item>
<item name="textInputStyle">@style/Widget.Material3.TextInputLayout.OutlinedBox</item>
<item name="appBarLayoutStyle">@style/Widget.Material3.AppBarLayout</item>
<item name="toolbarStyle">@style/Widget.Material3.Toolbar.Surface</item>