chore: bump extension interface
This commit is contained in:
parent
dec2ed7959
commit
7d0894cd92
6 changed files with 311 additions and 83 deletions
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -226,8 +226,18 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|||
?: return emptyList())
|
||||
|
||||
return try {
|
||||
val videos = source.getVideoList(sEpisode)
|
||||
videos.map { videoToVideoServer(it) }
|
||||
// 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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
@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 = "",
|
||||
) {
|
||||
|
||||
@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)
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue