chore: update extension api

This commit is contained in:
rebelonion 2024-04-14 23:30:37 -05:00
parent bf33f5d9c8
commit 126bc6134e
6 changed files with 229 additions and 97 deletions

View file

@ -88,9 +88,9 @@ android {
dependencies {
// FireBase
googleImplementation platform('com.google.firebase:firebase-bom:32.7.4')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.1'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.2'
googleImplementation platform('com.google.firebase:firebase-bom:32.8.1')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.6.2'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.4'
// Core
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.8.0'
@ -118,7 +118,7 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
ext.exo_version = '1.3.0'
ext.exo_version = '1.3.1'
implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
@ -138,7 +138,7 @@ dependencies {
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:93972bc'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
// Markwon
ext.markwon_version = '4.6.2'

View file

@ -245,17 +245,19 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
?: return emptyList())
return try {
val res = source.fetchSearchAnime(1, query, source.getFilterList()).awaitSingle()
val res = source.getSearchAnime(1, query, source.getFilterList())
Logger.log("query: $query")
convertAnimesPageToShowResponse(res)
} catch (e: CloudflareBypassException) {
Logger.log("Exception in search: $e")
Logger.log(e)
withContext(Dispatchers.Main) {
snackString("Failed to bypass Cloudflare")
}
emptyList()
} catch (e: Exception) {
Logger.log("General exception in search: $e")
Logger.log(e)
emptyList()
}
}

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.util.lang.awaitSingle
import rx.Observable
interface AnimeCatalogueSource : AnimeSource {
@ -17,30 +18,63 @@ interface AnimeCatalogueSource : AnimeSource {
val supportsLatest: Boolean
/**
* Returns an observable containing a page with a list of anime.
* Get a page with a list of anime.
*
* @since extensions-lib 1.5
* @param page the page number to retrieve.
*/
fun fetchPopularAnime(page: Int): Observable<AnimesPage>
@Suppress("DEPRECATION")
suspend fun getPopularAnime(page: Int): AnimesPage {
return fetchPopularAnime(page).awaitSingle()
}
/**
* Returns an observable containing a page with a list of anime.
* Get a page with a list of anime.
*
* @since extensions-lib 1.5
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage>
@Suppress("DEPRECATION")
suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
return fetchSearchAnime(page, query, filters).awaitSingle()
}
/**
* Returns an observable containing a page with a list of latest anime updates.
* Get a page with a list of latest anime updates.
*
* @since extensions-lib 1.5
* @param page the page number to retrieve.
*/
fun fetchLatestUpdates(page: Int): Observable<AnimesPage>
@Suppress("DEPRECATION")
suspend fun getLatestUpdates(page: Int): AnimesPage {
return fetchLatestUpdates(page).awaitSingle()
}
/**
* Returns the list of filters for the source.
*/
fun getFilterList(): AnimeFilterList
// Should be replaced as soon as Anime Extension reach 1.5
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getPopularAnime"),
)
fun fetchPopularAnime(page: Int): Observable<AnimesPage>
// Should be replaced as soon as Anime Extension reach 1.5
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getSearchAnime"),
)
fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage>
// Should be replaced as soon as Anime Extension reach 1.5
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getLatestUpdates"),
)
fun fetchLatestUpdates(page: Int): Observable<AnimesPage>
}

View file

@ -9,8 +9,11 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.util.lang.awaitSingle
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
@ -25,8 +28,8 @@ import java.util.concurrent.TimeUnit
/**
* A simple implementation for sources from a website.
*/
@Suppress("unused")
abstract class AnimeHttpSource : AnimeCatalogueSource {
/**
* Network service.
*/
@ -44,16 +47,16 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
open val versionId = 1
/**
* Id of the source. By default it uses a generated id using the first 16 characters (64 bits)
* of the MD5 of the string: sourcename/language/versionId
* Note the generated id sets the sign bit to 0.
* ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
* of the MD5 of the string `"${name.lowercase()}/$lang/$versionId"`.
*
* The ID is generated by the [generateId] function, which can be reused if needed
* to generate outdated IDs for cases where the source name or language needs to
* be changed but migrations can be avoided.
*
* Note: the generated ID sets the sign bit to `0`.
*/
override val id by lazy {
val key = "${name.lowercase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }
.reduce(Long::or) and Long.MAX_VALUE
}
override val id by lazy { generateId(name, lang, versionId) }
/**
* Headers used for requests.
@ -66,11 +69,34 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
open val client: OkHttpClient
get() = network.client
/**
* Generates a unique ID for the source based on the provided [name], [lang] and
* [versionId]. It will use the first 16 characters (64 bits) of the MD5 of the string
* `"${name.lowercase()}/$lang/$versionId"`.
*
* Note: the generated ID sets the sign bit to `0`.
*
* Can be used to generate outdated IDs, such as when the source name or language
* needs to be changed but migrations can be avoided.
*
* @since extensions-lib 1.5
* @param name [String] the name of the source
* @param lang [String] the language of the source
* @param versionId [Int] the version ID of the source
* @return a unique ID for the source
*/
@Suppress("MemberVisibilityCanBePrivate")
protected fun generateId(name: String, lang: String, versionId: Int): Long {
val key = "${name.lowercase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}
/**
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", defaultUserAgentProvider())
add("User-Agent", NetworkHelper.defaultUserAgentProvider())
}
/**
@ -84,6 +110,10 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
*
* @param page the page number to retrieve.
*/
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getPopularAnime"),
)
override fun fetchPopularAnime(page: Int): Observable<AnimesPage> {
return client.newCall(popularAnimeRequest(page))
.asObservableSuccess()
@ -114,11 +144,11 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun fetchSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList
): Observable<AnimesPage> {
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getSearchAnime"),
)
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return Observable.defer {
try {
client.newCall(searchAnimeRequest(page, query, filters)).asObservableSuccess()
@ -140,11 +170,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* @param query the search query.
* @param filters the list of filters to apply.
*/
protected abstract fun searchAnimeRequest(
page: Int,
query: String,
filters: AnimeFilterList
): Request
protected abstract fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request
/**
* Parses the response from the site and returns a [AnimesPage] object.
@ -158,6 +184,10 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
*
* @param page the page number to retrieve.
*/
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getLatestUpdates"),
)
override fun fetchLatestUpdates(page: Int): Observable<AnimesPage> {
return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
@ -181,11 +211,18 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
protected abstract fun latestUpdatesParse(response: Response): AnimesPage
/**
* Returns an observable with the updated details for a nanime. Normally it's not needed to
* override this method.
* Get the updated details for a anime.
* Normally it's not needed to override this method.
*
* @param anime the anime to be updated.
* @return the updated anime.
*/
@Suppress("DEPRECATION")
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
return fetchAnimeDetails(anime).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getAnimeDetails"))
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
return client.newCall(animeDetailsRequest(anime))
.asObservableSuccess()
@ -212,11 +249,23 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
protected abstract fun animeDetailsParse(response: Response): SAnime
/**
* Returns an observable with the updated episode list for an anime. Normally it's not needed to
* override this method. If an anime is licensed an empty episode list observable is returned
* Get all the available episodes for an anime.
* Normally it's not needed to override this method.
*
* @param anime the anime to look for episodes.
* @param anime the anime to update.
* @return the chapters for the manga.
* @throws LicensedEntryItemsException if a anime is licensed and therefore no episodes are available.
*/
@Suppress("DEPRECATION")
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
if (anime.status == SAnime.LICENSED) {
throw LicensedEntryItemsException()
}
return fetchEpisodeList(anime).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getEpisodeList"))
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
return if (anime.status != SAnime.LICENSED) {
client.newCall(episodeListRequest(anime))
@ -225,7 +274,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
episodeListParse(response)
}
} else {
Observable.error(Exception("Licensed - No episodes to show"))
Observable.error(LicensedEntryItemsException())
}
}
@ -247,10 +296,25 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
protected abstract fun episodeListParse(response: Response): List<SEpisode>
/**
* Returns an observable with the page list for a chapter.
* Parses the response from the site and returns a SEpisode Object.
*
* @param episode the episode whose video list has to be fetched.
* @param response the response from the site.
*/
protected abstract fun episodeVideoParse(response: Response): SEpisode
/**
* Get the list of videos a episode has. Videos should be returned
* in the expected order; the index is ignored.
*
* @param episode the episode.
* @return the videos for the episode.
*/
@Suppress("DEPRECATION")
override suspend fun getVideoList(episode: SEpisode): List<Video> {
return fetchVideoList(episode).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getVideoList"))
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
return client.newCall(videoListRequest(episode))
.asObservableSuccess()
@ -287,8 +351,15 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* Returns an observable with the page containing the source url of the image. If there's any
* error, it will return null instead of throwing an exception.
*
* @since extensions-lib 1.5
* @param video the video whose source image has to be fetched.
*/
@Suppress("DEPRECATION")
open suspend fun getVideoUrl(video: Video): String {
return fetchVideoUrl(video).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getVideoUrl"))
open fun fetchVideoUrl(video: Video): Observable<String> {
return client.newCall(videoUrlRequest(video))
.asObservableSuccess()
@ -313,37 +384,80 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
protected abstract fun videoUrlParse(response: Response): String
/**
* Returns an observable with the response of the source image.
* Returns the response of the source video.
* Typically does not need to be overridden.
*
* @param video the page whose source image has to be downloaded.
* @since extensions-lib 1.5
* @param request the http request for the video that has to be downloaded.
* @param listener the progress listener that has to be attached to the http request
*/
fun fetchVideo(video: Video): Observable<Response> {
val animeDownloadClient = client.newBuilder()
.callTimeout(30, TimeUnit.MINUTES)
.build()
return animeDownloadClient.newCachelessCallWithProgress(
videoRequest(
video,
video.totalBytesDownloaded
), video
)
.asObservableSuccess()
suspend fun getVideo(
request: Request,
listener: ProgressListener,
): Response {
return client.newCachelessCallWithProgress(request, listener)
.awaitSuccess()
}
fun getVideoSize(
video: Video,
tries: Int,
): Long {
val headers = Headers.Builder().addAll(video.headers ?: headers).add("Range", "bytes=0-1").build()
val request = GET(video.videoUrl!!, headers)
val response = client.newCall(request).execute()
// parse the response headers to get the size of the video, in particular the content-range header
val contentRange = response.header("Content-Range")
if (contentRange != null) {
return contentRange.split("/")[1].toLong()
}
if (tries > 0) {
return getVideoSize(video, tries - 1)
}
return -1L
}
/**
* Returns the request for getting the source image. Override only if it's needed to override
* Returns the request for getting the source image, with range header attributes. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* If end is over start than the request is a range request
* If end if equal or less than start then the request is initial-point request
*
* @param video the video whose link has to be fetched
* @param start starting byte of chunk
* @param end ending byte of chunk
*/
fun videoRequest(
video: Video,
start: Long,
end: Long,
): Request {
val headers = video.headers ?: headers
val newHeaders =
if (end - start > 0L) {
Headers.Builder().addAll(headers).add("Range", "bytes=$start-$end").build()
} else if (start >= 0L) {
Headers.Builder().addAll(headers).add("Range", "bytes=$start-").build()
} else {
// logcat(LogPriority.ERROR) { "Error: end-start is less than 0" }
null
}
return GET(video.videoUrl!!, newHeaders ?: headers)
}
/**
* Returns the request for getting the source image without range header attributes. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
*
* @param video the video whose link has to be fetched
*/
protected open fun videoRequest(video: Video, bytes: Long = 0L): Request {
val headers = video.headers ?: headers
val newHeaders = if (bytes > 0L) {
Headers.Builder().addAll(headers).add("Range", "bytes=$bytes-").build()
} else {
null
}
return GET(video.videoUrl!!, newHeaders ?: headers)
fun safeVideoRequest(
video: Video,
): Request {
return GET(video.videoUrl!!, video.headers ?: headers)
}
/**
@ -405,8 +519,8 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* @param episode the episode
* @return url of the episode
*/
open fun getChapterUrl(episode: SEpisode): String {
return episode.url.toString()
open fun getEpisodeUrl(episode: SEpisode): String {
return episode.url
}
/**
@ -423,3 +537,5 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
*/
override fun getFilterList() = AnimeFilterList()
}
class LicensedEntryItemsException : RuntimeException()

View file

@ -1,25 +0,0 @@
package eu.kanade.tachiyomi.animesource.online
import eu.kanade.tachiyomi.animesource.model.Video
import rx.Observable
fun AnimeHttpSource.fetchUrlFromVideo(video: Video): Observable<Video> {
return Observable.just(video)
.filter { !it.videoUrl.isNullOrEmpty() }
.mergeWith(fetchRemainingVideoUrlsFromVideoList(video))
}
private fun AnimeHttpSource.fetchRemainingVideoUrlsFromVideoList(video: Video): Observable<Video> {
return Observable.just(video)
.filter { it.videoUrl.isNullOrEmpty() }
.concatMap { getVideoUrl(it) }
}
private fun AnimeHttpSource.getVideoUrl(video: Video): Observable<Video> {
video.status = Video.State.LOAD_VIDEO
return fetchVideoUrl(video)
.doOnError { video.status = Video.State.ERROR }
.onErrorReturn { null }
.doOnNext { video.videoUrl = it }
.map { video }
}

View file

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.util.storage.DiskUtil
import kotlinx.coroutines.runBlocking
import rx.Observable
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.entries.anime.model.Anime
@ -35,17 +36,21 @@ class LocalAnimeSource(
override val supportsLatest = true
// Browse related
override suspend fun getPopularAnime(page: Int) = getSearchAnime(page, "", POPULAR_FILTERS)
override suspend fun getLatestUpdates(page: Int) = getSearchAnime(page, "", LATEST_FILTERS)
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularAnime"))
override fun fetchPopularAnime(page: Int) = fetchSearchAnime(page, "", POPULAR_FILTERS)
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
override fun fetchLatestUpdates(page: Int) = fetchSearchAnime(page, "", LATEST_FILTERS)
override fun fetchSearchAnime(
page: Int,
query: String,
filters: AnimeFilterList
): Observable<AnimesPage> {
//return emptyObservable()
return Observable.just(AnimesPage(emptyList(), false))
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchAnime"))
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return runBlocking {
Observable.just(getSearchAnime(page, query, filters))
}
}
// Anime details related