chore: bump extension interface

This commit is contained in:
rebel onion 2025-05-14 22:35:50 -05:00
parent dec2ed7959
commit 7d0894cd92
6 changed files with 311 additions and 83 deletions

View file

@ -14,8 +14,6 @@ Dantotsu is an [Anilist](https://anilist.co/) only client.
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge! > **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff" /></a>
## Terms of Use ## Terms of Use
By downloading, installing, or using this application, you agree to: By downloading, installing, or using this application, you agree to:
- Use the application in compliance with all applicable laws - Use the application in compliance with all applicable laws

View file

@ -19,7 +19,7 @@ android {
targetSdk 35 targetSdk 35
versionCode((System.currentTimeMillis() / 60000).toInteger()) versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "3.2.1" versionName "3.2.1"
versionCode 300200100 versionCode versionName.split("\\.").collect { it.toInteger() * 100 }.join("") as Integer
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }

View file

@ -226,8 +226,18 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
?: return emptyList()) ?: return emptyList())
return try { return try {
// TODO(1.6): Remove else block when dropping support for ext lib <1.6
if ((source as AnimeHttpSource).javaClass.declaredMethods.any { it.name == "getHosterList" }){
val hosters = source.getHosterList(sEpisode)
val allVideos = hosters.flatMap { hoster ->
val videos = source.getVideoList(hoster)
videos.map { it.copy(videoTitle = "${hoster.hosterName} - ${it.videoTitle}") }
}
allVideos.map { videoToVideoServer(it) }
} else {
val videos = source.getVideoList(sEpisode) val videos = source.getVideoList(sEpisode)
videos.map { videoToVideoServer(it) } videos.map { videoToVideoServer(it) }
}
} catch (e: Exception) { } catch (e: Exception) {
Logger.log("Exception occurred: ${e.message}") Logger.log("Exception occurred: ${e.message}")
emptyList() emptyList()
@ -576,7 +586,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
number, number,
format!!, format!!,
FileUrl(videoUrl, headersMap), FileUrl(videoUrl, headersMap),
if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble() null
) )
} }

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.animesource package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.animesource.model.Hoster
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.animesource.model.Video
@ -48,6 +49,25 @@ interface AnimeSource {
return fetchEpisodeList(anime).awaitSingle() return fetchEpisodeList(anime).awaitSingle()
} }
/**
* Get the list of hoster for an episode. The first hoster in the list should
* be the preferred hoster.
*
* @since extensions-lib 16
* @param episode the episode.
* @return the hosters for the episode.
*/
suspend fun getHosterList(episode: SEpisode): List<Hoster> = throw IllegalStateException("Not used")
/**
* Get the list of videos for a hoster.
*
* @since extensions-lib 16
* @param hoster the hoster.
* @return the videos for the hoster.
*/
suspend fun getVideoList(hoster: Hoster): List<Video> = throw IllegalStateException("Not used")
/** /**
* Get the list of videos a episode has. Pages should be returned * Get the list of videos a episode has. Pages should be returned
* in the expected order; the index is ignored. * in the expected order; the index is ignored.

View file

@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.animesource.model
import eu.kanade.tachiyomi.animesource.model.SerializableVideo.Companion.serialize
import eu.kanade.tachiyomi.animesource.model.SerializableVideo.Companion.toVideoList
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
open class Hoster(
val hosterUrl: String = "",
val hosterName: String = "",
val videoList: List<Video>? = null,
val internalData: String = "",
) {
@Transient
@Volatile
var status: State = State.IDLE
enum class State {
IDLE,
LOADING,
READY,
ERROR,
}
fun copy(
hosterUrl: String = this.hosterUrl,
hosterName: String = this.hosterName,
videoList: List<Video>? = this.videoList,
internalData: String = this.internalData,
): Hoster {
return Hoster(hosterUrl, hosterName, videoList, internalData)
}
companion object {
const val NO_HOSTER_LIST = "no_hoster_list"
fun List<Video>.toHosterList(): List<Hoster> {
return listOf(
Hoster(
hosterUrl = "",
hosterName = NO_HOSTER_LIST,
videoList = this,
),
)
}
}
}
@Serializable
data class SerializableHoster(
val hosterUrl: String = "",
val hosterName: String = "",
val videoList: String? = null,
val internalData: String = "",
) {
companion object {
fun List<Hoster>.serialize(): String =
Json.encodeToString(
this.map { host ->
SerializableHoster(
host.hosterUrl,
host.hosterName,
host.videoList?.serialize(),
host.internalData,
)
},
)
fun String.toHosterList(): List<Hoster> =
Json.decodeFromString<List<SerializableHoster>>(this)
.map { sHost ->
Hoster(
sHost.hosterUrl,
sHost.hosterName,
sHost.videoList?.toVideoList(),
sHost.internalData,
)
}
}
}

View file

@ -1,31 +1,101 @@
package eu.kanade.tachiyomi.animesource.model package eu.kanade.tachiyomi.animesource.model
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.ProgressListener import kotlinx.serialization.encodeToString
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.json.Json
import kotlinx.coroutines.flow.asStateFlow
import okhttp3.Headers import okhttp3.Headers
import rx.subjects.Subject
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable import java.io.Serializable
@kotlinx.serialization.Serializable
data class Track(val url: String, val lang: String) : Serializable data class Track(val url: String, val lang: String) : Serializable
@kotlinx.serialization.Serializable
enum class ChapterType {
Opening,
Ending,
Recap,
MixedOp,
Other,
}
@kotlinx.serialization.Serializable
data class TimeStamp(
val start: Double,
val end: Double,
val name: String,
val type: ChapterType = ChapterType.Other,
)
open class Video( open class Video(
val url: String = "", var videoUrl: String = "",
val quality: String = "", val videoTitle: String = "",
var videoUrl: String? = null, val resolution: Int? = null,
headers: Headers? = null, val bitrate: Int? = null,
// "url", "language-label-2", "url2", "language-label-2" val headers: Headers? = null,
val preferred: Boolean = false,
val subtitleTracks: List<Track> = emptyList(), val subtitleTracks: List<Track> = emptyList(),
val audioTracks: List<Track> = emptyList(), val audioTracks: List<Track> = emptyList(),
) : Serializable, ProgressListener { val timestamps: List<TimeStamp> = emptyList(),
val internalData: String = "",
val initialized: Boolean = false,
// TODO(1.6): Remove after ext lib bump
val videoPageUrl: String = "",
) {
@Transient // TODO(1.6): Remove after ext lib bump
var headers: Headers? = headers @Deprecated("Use videoTitle instead", ReplaceWith("videoTitle"))
val quality: String
get() = videoTitle
// TODO(1.6): Remove after ext lib bump
@Deprecated("Use videoPageUrl instead", ReplaceWith("videoPageUrl"))
val url: String
get() = videoPageUrl
// TODO(1.6): Remove after ext lib bump
constructor(
url: String,
quality: String,
videoUrl: String?,
headers: Headers? = null,
subtitleTracks: List<Track> = emptyList(),
audioTracks: List<Track> = emptyList(),
) : this(
videoPageUrl = url,
videoTitle = quality,
videoUrl = videoUrl ?: "null",
headers = headers,
subtitleTracks = subtitleTracks,
audioTracks = audioTracks,
)
// TODO(1.6): Remove after ext lib bump
constructor(
videoUrl: String = "",
videoTitle: String = "",
resolution: Int? = null,
bitrate: Int? = null,
headers: Headers? = null,
preferred: Boolean = false,
subtitleTracks: List<Track> = emptyList(),
audioTracks: List<Track> = emptyList(),
timestamps: List<TimeStamp> = emptyList(),
internalData: String = "",
) : this(
videoUrl = videoUrl,
videoTitle = videoTitle,
resolution = resolution,
bitrate = bitrate,
headers = headers,
preferred = preferred,
subtitleTracks = subtitleTracks,
audioTracks = audioTracks,
timestamps = timestamps,
internalData = internalData,
videoPageUrl = "",
)
// TODO(1.6): Remove after ext lib bump
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
constructor( constructor(
url: String, url: String,
@ -38,83 +108,132 @@ open class Video(
@Transient @Transient
@Volatile @Volatile
var status: State = State.QUEUE var status: State = State.QUEUE
@Transient
private val _progressFlow = MutableStateFlow(0)
@Transient
val progressFlow = _progressFlow.asStateFlow()
var progress: Int
get() = _progressFlow.value
set(value) { set(value) {
_progressFlow.value = value
}
@Transient
@Volatile
var totalBytesDownloaded: Long = 0L
@Transient
@Volatile
var totalContentLength: Long = 0L
@Transient
@Volatile
var bytesDownloaded: Long = 0L
set(value) {
totalBytesDownloaded += if (value < field) {
value
} else {
value - field
}
field = value field = value
} }
@Transient fun copy(
var progressSubject: Subject<State, State>? = null videoUrl: String = this.videoUrl,
videoTitle: String = this.videoTitle,
resolution: Int? = this.resolution,
bitrate: Int? = this.bitrate,
headers: Headers? = this.headers,
preferred: Boolean = this.preferred,
subtitleTracks: List<Track> = this.subtitleTracks,
audioTracks: List<Track> = this.audioTracks,
timestamps: List<TimeStamp> = this.timestamps,
internalData: String = this.internalData,
): Video {
return Video(
videoUrl = videoUrl,
videoTitle = videoTitle,
resolution = resolution,
bitrate = bitrate,
headers = headers,
preferred = preferred,
subtitleTracks = subtitleTracks,
audioTracks = audioTracks,
timestamps = timestamps,
internalData = internalData,
)
}
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { fun copy(
bytesDownloaded = bytesRead videoUrl: String = this.videoUrl,
if (contentLength > totalContentLength) { videoTitle: String = this.videoTitle,
totalContentLength = contentLength resolution: Int? = this.resolution,
} bitrate: Int? = this.bitrate,
val newProgress = if (totalContentLength > 0) { headers: Headers? = this.headers,
(100 * totalBytesDownloaded / totalContentLength).toInt() preferred: Boolean = this.preferred,
} else { subtitleTracks: List<Track> = this.subtitleTracks,
-1 audioTracks: List<Track> = this.audioTracks,
} timestamps: List<TimeStamp> = this.timestamps,
if (progress != newProgress) progress = newProgress internalData: String = this.internalData,
initialized: Boolean = this.initialized,
videoPageUrl: String = this.videoPageUrl,
): Video {
return Video(
videoUrl = videoUrl,
videoTitle = videoTitle,
resolution = resolution,
bitrate = bitrate,
headers = headers,
preferred = preferred,
subtitleTracks = subtitleTracks,
audioTracks = audioTracks,
timestamps = timestamps,
internalData = internalData,
initialized = initialized,
videoPageUrl = videoPageUrl,
)
} }
enum class State { enum class State {
QUEUE, QUEUE,
LOAD_VIDEO, LOAD_VIDEO,
DOWNLOAD_IMAGE,
READY, READY,
ERROR, ERROR,
} }
@Throws(IOException::class)
private fun writeObject(out: ObjectOutputStream) {
out.defaultWriteObject()
val headersMap: Map<String, List<String>> = headers?.toMultimap() ?: emptyMap()
out.writeObject(headersMap)
} }
@Suppress("UNCHECKED_CAST") @kotlinx.serialization.Serializable
@Throws(IOException::class, ClassNotFoundException::class) data class SerializableVideo(
private fun readObject(input: ObjectInputStream) { val videoUrl: String = "",
input.defaultReadObject() val videoTitle: String = "",
val headersMap = input.readObject() as? Map<String, List<String>> val resolution: Int? = null,
headers = headersMap?.let { map -> val bitrate: Int? = null,
val builder = Headers.Builder() val headers: List<Pair<String, String>>? = null,
for ((key, values) in map) { val preferred: Boolean = false,
for (value in values) { val subtitleTracks: List<Track> = emptyList(),
builder.add(key, value) val audioTracks: List<Track> = emptyList(),
val timestamps: List<TimeStamp> = emptyList(),
val internalData: String = "",
val initialized: Boolean = false,
// TODO(1.6): Remove after ext lib bump
val videoPageUrl: String = "",
) {
companion object {
fun List<Video>.serialize(): String =
Json.encodeToString(
this.map { vid ->
SerializableVideo(
vid.videoUrl,
vid.videoTitle,
vid.resolution,
vid.bitrate,
vid.headers?.toList(),
vid.preferred,
vid.subtitleTracks,
vid.audioTracks,
vid.timestamps,
vid.internalData,
vid.initialized,
vid.videoPageUrl,
)
},
)
fun String.toVideoList(): List<Video> =
Json.decodeFromString<List<SerializableVideo>>(this)
.map { sVid ->
Video(
sVid.videoUrl,
sVid.videoTitle,
sVid.resolution,
sVid.bitrate,
sVid.headers
?.flatMap { it.toList() }
?.let { Headers.headersOf(*it.toTypedArray()) },
sVid.preferred,
sVid.subtitleTracks,
sVid.audioTracks,
sVid.timestamps,
sVid.internalData,
sVid.initialized,
sVid.videoPageUrl,
)
} }
} }
builder.build()
} }
}
}