chore: bump extension interface
This commit is contained in:
parent
dec2ed7959
commit
7d0894cd92
6 changed files with 311 additions and 83 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
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)
|
@kotlinx.serialization.Serializable
|
||||||
private fun writeObject(out: ObjectOutputStream) {
|
data class SerializableVideo(
|
||||||
out.defaultWriteObject()
|
val videoUrl: String = "",
|
||||||
val headersMap: Map<String, List<String>> = headers?.toMultimap() ?: emptyMap()
|
val videoTitle: String = "",
|
||||||
out.writeObject(headersMap)
|
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")
|
companion object {
|
||||||
@Throws(IOException::class, ClassNotFoundException::class)
|
fun List<Video>.serialize(): String =
|
||||||
private fun readObject(input: ObjectInputStream) {
|
Json.encodeToString(
|
||||||
input.defaultReadObject()
|
this.map { vid ->
|
||||||
val headersMap = input.readObject() as? Map<String, List<String>>
|
SerializableVideo(
|
||||||
headers = headersMap?.let { map ->
|
vid.videoUrl,
|
||||||
val builder = Headers.Builder()
|
vid.videoTitle,
|
||||||
for ((key, values) in map) {
|
vid.resolution,
|
||||||
for (value in values) {
|
vid.bitrate,
|
||||||
builder.add(key, value)
|
vid.headers?.toList(),
|
||||||
}
|
vid.preferred,
|
||||||
}
|
vid.subtitleTracks,
|
||||||
builder.build()
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue