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!
<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
By downloading, installing, or using this application, you agree to:
- Use the application in compliance with all applicable laws

View file

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

View file

@ -226,8 +226,18 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
?: return emptyList())
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)
videos.map { videoToVideoServer(it) }
}
} catch (e: Exception) {
Logger.log("Exception occurred: ${e.message}")
emptyList()
@ -576,7 +586,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
number,
format!!,
FileUrl(videoUrl, headersMap),
if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
null
)
}

View file

@ -1,5 +1,6 @@
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.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
@ -48,6 +49,25 @@ interface AnimeSource {
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
* 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
import android.net.Uri
import eu.kanade.tachiyomi.network.ProgressListener
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import rx.subjects.Subject
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
@kotlinx.serialization.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(
val url: String = "",
val quality: String = "",
var videoUrl: String? = null,
headers: Headers? = null,
// "url", "language-label-2", "url2", "language-label-2"
var videoUrl: String = "",
val videoTitle: String = "",
val resolution: Int? = null,
val bitrate: Int? = null,
val headers: Headers? = null,
val preferred: Boolean = false,
val subtitleTracks: 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
var headers: Headers? = headers
// TODO(1.6): Remove after ext lib bump
@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")
constructor(
url: String,
@ -38,83 +108,132 @@ open class Video(
@Transient
@Volatile
var status: State = State.QUEUE
@Transient
private val _progressFlow = MutableStateFlow(0)
@Transient
val progressFlow = _progressFlow.asStateFlow()
var progress: Int
get() = _progressFlow.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
}
@Transient
var progressSubject: Subject<State, State>? = null
fun copy(
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) {
bytesDownloaded = bytesRead
if (contentLength > totalContentLength) {
totalContentLength = contentLength
}
val newProgress = if (totalContentLength > 0) {
(100 * totalBytesDownloaded / totalContentLength).toInt()
} else {
-1
}
if (progress != newProgress) progress = newProgress
fun copy(
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,
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 {
QUEUE,
LOAD_VIDEO,
DOWNLOAD_IMAGE,
READY,
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")
@Throws(IOException::class, ClassNotFoundException::class)
private fun readObject(input: ObjectInputStream) {
input.defaultReadObject()
val headersMap = input.readObject() as? Map<String, List<String>>
headers = headersMap?.let { map ->
val builder = Headers.Builder()
for ((key, values) in map) {
for (value in values) {
builder.add(key, value)
@kotlinx.serialization.Serializable
data class SerializableVideo(
val videoUrl: String = "",
val videoTitle: String = "",
val resolution: Int? = null,
val bitrate: Int? = null,
val headers: List<Pair<String, String>>? = null,
val preferred: Boolean = false,
val subtitleTracks: List<Track> = emptyList(),
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()
}
}
}