Initial commit
This commit is contained in:
commit
21bfbfb139
520 changed files with 47819 additions and 0 deletions
224
app/src/main/java/ani/dantotsu/parsers/AnimeParser.kt
Normal file
224
app/src/main/java/ani/dantotsu/parsers/AnimeParser.kt
Normal file
|
@ -0,0 +1,224 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import android.net.Uri
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.FileUrl
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import ani.dantotsu.asyncMap
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.others.MalSyncBackup
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* An abstract class for creating a new Source
|
||||
*
|
||||
* Most of the functions & variables that need to be overridden are abstract
|
||||
* **/
|
||||
abstract class AnimeParser : BaseParser() {
|
||||
|
||||
/**
|
||||
* Takes ShowResponse.link & ShowResponse.extra (if you added any) as arguments & gives a list of total episodes present on the site.
|
||||
* **/
|
||||
abstract suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode>
|
||||
|
||||
/**
|
||||
* Takes ShowResponse.link, ShowResponse.extra & the Last Largest Episode Number known by app as arguments
|
||||
*
|
||||
* Returns the latest episode (If overriding, Make sure the episode is actually the latest episode)
|
||||
* Returns null, if no latest episode is found.
|
||||
* **/
|
||||
open suspend fun getLatestEpisode(animeLink: String, extra: Map<String, String>?, sAnime: SAnime, latest: Float): Episode?{
|
||||
return loadEpisodes(animeLink, extra, sAnime)
|
||||
.maxByOrNull { it.number.toFloatOrNull()?:0f }
|
||||
?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes Episode.link as a parameter
|
||||
*
|
||||
* This returns a Map of "Video Server's Name" & "Link/Data" of all the Video Servers present on the site, which can be further used by loadVideoServers() & loadSingleVideoServer()
|
||||
* **/
|
||||
abstract suspend fun loadVideoServers(episodeLink: String, extra: Map<String,String>?, sEpisode: SEpisode): List<VideoServer>
|
||||
|
||||
|
||||
/**
|
||||
* This function will receive **url of the embed** & **name** of a Video Server present on the site to host the episode.
|
||||
*
|
||||
*
|
||||
* Create a new VideoExtractor for the video server you are trying to scrape, if there's not one already.
|
||||
*
|
||||
*
|
||||
* (Some sites might not have separate video hosts. In that case, just create a new VideoExtractor for that particular site)
|
||||
*
|
||||
*
|
||||
* returns a **VideoExtractor** containing **`server`**, the app will further load the videos using `extract()` function inside it
|
||||
*
|
||||
* **Example for Site with multiple Video Servers**
|
||||
* ```
|
||||
val domain = Uri.parse(server.embed.url).host ?: ""
|
||||
val extractor: VideoExtractor? = when {
|
||||
"fembed" in domain -> FPlayer(server)
|
||||
"sb" in domain -> StreamSB(server)
|
||||
"streamta" in domain -> StreamTape(server)
|
||||
else -> null
|
||||
}
|
||||
return extractor
|
||||
```
|
||||
* You can use your own way to get the Extractor for reliability.
|
||||
* if there's only extractor, you can directly return it.
|
||||
* **/
|
||||
open suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? {
|
||||
var domain = Uri.parse(server.embed.url).host ?: return null
|
||||
if (domain.startsWith("www.")) {domain = domain.substring(4)}
|
||||
|
||||
val extractor: VideoExtractor? = when (domain) {
|
||||
else -> {
|
||||
println("$name : No extractor found for: $domain | ${server.embed.url}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return extractor
|
||||
}
|
||||
|
||||
/**
|
||||
* If the Video Servers support preloading links for the videos
|
||||
* typically depends on what Video Extractor is being used
|
||||
* **/
|
||||
open val allowsPreloading = true
|
||||
|
||||
/**
|
||||
* This Function used when there "isn't" a default Server set by the user, or when user wants to switch the Server
|
||||
*
|
||||
* Doesn't need to be overridden, if the parser is following the norm.
|
||||
* **/
|
||||
open suspend fun loadByVideoServers(episodeUrl: String, extra: Map<String,String>?, sEpisode: SEpisode, callback: (VideoExtractor) -> Unit) {
|
||||
tryWithSuspend(true) {
|
||||
loadVideoServers(episodeUrl, extra, sEpisode).asyncMap {
|
||||
getVideoExtractor(it)?.apply {
|
||||
tryWithSuspend(true) {
|
||||
load()
|
||||
}
|
||||
callback.invoke(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This Function used when there "is" a default Server set by the user, only loads a Single Server for faster response.
|
||||
*
|
||||
* Doesn't need to be overridden, if the parser is following the norm.
|
||||
* **/
|
||||
open suspend fun loadSingleVideoServer(serverName: String, episodeUrl: String, extra: Map<String,String>?, sEpisode: SEpisode, post: Boolean): VideoExtractor? {
|
||||
return tryWithSuspend(post) {
|
||||
loadVideoServers(episodeUrl, extra, sEpisode).apply {
|
||||
find { it.name == serverName }?.also {
|
||||
return@tryWithSuspend getVideoExtractor(it)?.apply {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Many sites have Dub & Sub anime as separate Shows
|
||||
*
|
||||
* make this `true`, if they are separated else `false`
|
||||
*
|
||||
* **NOTE : do not forget to override `search` if the site does not support only dub search**
|
||||
* **/
|
||||
open val isDubAvailableSeparately by Delegates.notNull<Boolean>()
|
||||
|
||||
/**
|
||||
* The app changes this, depending on user's choice.
|
||||
* **/
|
||||
open var selectDub = false
|
||||
|
||||
/**
|
||||
* Name used to get Shows Directly from MALSyncBackup's github dump
|
||||
*
|
||||
* Do not override if the site is not present on it.
|
||||
* **/
|
||||
open val malSyncBackupName = ""
|
||||
|
||||
/**
|
||||
* Overridden to add MalSyncBackup support for Anime Sites
|
||||
* **/
|
||||
override suspend fun loadSavedShowResponse(mediaId: Int): ShowResponse? {
|
||||
checkIfVariablesAreEmpty()
|
||||
val dub = if (isDubAvailableSeparately) "_${if (selectDub) "dub" else "sub"}" else ""
|
||||
var loaded = loadData<ShowResponse>("${saveName}${dub}_$mediaId")
|
||||
if (loaded == null && malSyncBackupName.isNotEmpty())
|
||||
loaded = MalSyncBackup.get(mediaId, malSyncBackupName, selectDub)?.also { saveShowResponse(mediaId, it, true) }
|
||||
return loaded
|
||||
}
|
||||
|
||||
override fun saveShowResponse(mediaId: Int, response: ShowResponse?, selected: Boolean) {
|
||||
if (response != null) {
|
||||
checkIfVariablesAreEmpty()
|
||||
setUserText("${if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString(R.string.found)} : ${response.name}")
|
||||
val dub = if (isDubAvailableSeparately) "_${if (selectDub) "dub" else "sub"}" else ""
|
||||
saveData("${saveName}${dub}_$mediaId", response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for containing Episode data of a particular parser
|
||||
* **/
|
||||
data class Episode(
|
||||
/**
|
||||
* Number of the Episode in "String",
|
||||
*
|
||||
* useful in cases where episode is not a number
|
||||
* **/
|
||||
val number: String,
|
||||
|
||||
/**
|
||||
* Link that links to the episode page containing videos
|
||||
* **/
|
||||
val link: String,
|
||||
|
||||
//Self-Descriptive
|
||||
val title: String? = null,
|
||||
val thumbnail: FileUrl? = null,
|
||||
val description: String? = null,
|
||||
val isFiller: Boolean = false,
|
||||
|
||||
/**
|
||||
* In case, you want to pass extra data
|
||||
* **/
|
||||
val extra: Map<String,String>? = null,
|
||||
|
||||
//SEpisode from Aniyomi
|
||||
val sEpisode: SEpisode? = null
|
||||
) {
|
||||
constructor(
|
||||
number: String,
|
||||
link: String,
|
||||
title: String? = null,
|
||||
thumbnail: String,
|
||||
description: String? = null,
|
||||
isFiller: Boolean = false,
|
||||
extra: Map<String,String>? = null
|
||||
) : this(number, link, title, FileUrl(thumbnail), description, isFiller, extra)
|
||||
|
||||
constructor(
|
||||
number: String,
|
||||
link: String,
|
||||
title: String? = null,
|
||||
thumbnail: String,
|
||||
description: String? = null,
|
||||
isFiller: Boolean = false,
|
||||
extra: Map<String,String>? = null,
|
||||
sEpisode: SEpisode? = null
|
||||
) : this(number, link, title, FileUrl(thumbnail), description, isFiller, extra, sEpisode)
|
||||
}
|
65
app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
Normal file
65
app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt
Normal file
|
@ -0,0 +1,65 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.Lazier
|
||||
import ani.dantotsu.aniyomi.anime.model.AnimeExtension
|
||||
import ani.dantotsu.lazyList
|
||||
//import ani.dantotsu.parsers.anime.AllAnime
|
||||
//import ani.dantotsu.parsers.anime.AnimeDao
|
||||
//import ani.dantotsu.parsers.anime.AnimePahe
|
||||
//import ani.dantotsu.parsers.anime.Gogo
|
||||
//import ani.dantotsu.parsers.anime.Haho
|
||||
//import ani.dantotsu.parsers.anime.HentaiFF
|
||||
//import ani.dantotsu.parsers.anime.HentaiMama
|
||||
//import ani.dantotsu.parsers.anime.HentaiStream
|
||||
//import ani.dantotsu.parsers.anime.Marin
|
||||
//import ani.dantotsu.parsers.anime.AniWave
|
||||
//import ani.dantotsu.parsers.anime.Kaido
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
/*
|
||||
object AnimeSources_old : WatchSources() {
|
||||
override val list: List<Lazier<BaseParser>> = lazyList(
|
||||
"AllAnime" to ::AllAnime,
|
||||
"Gogo" to ::Gogo,
|
||||
"Kaido" to ::Kaido,
|
||||
"Marin" to ::Marin,
|
||||
"AnimePahe" to ::AnimePahe,
|
||||
"AniWave" to ::AniWave,
|
||||
"AnimeDao" to ::AnimeDao,
|
||||
)
|
||||
}
|
||||
*/
|
||||
object AnimeSources : WatchSources() {
|
||||
override var list: List<Lazier<BaseParser>> = emptyList()
|
||||
|
||||
suspend fun init(fromExtensions: StateFlow<List<AnimeExtension.Installed>>) {
|
||||
// Initialize with the first value from StateFlow
|
||||
val initialExtensions = fromExtensions.first()
|
||||
list = createParsersFromExtensions(initialExtensions)
|
||||
|
||||
// Update as StateFlow emits new values
|
||||
fromExtensions.collect { extensions ->
|
||||
list = createParsersFromExtensions(extensions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createParsersFromExtensions(extensions: List<AnimeExtension.Installed>): List<Lazier<BaseParser>> {
|
||||
return extensions.map { extension ->
|
||||
val name = extension.name
|
||||
Lazier({ DynamicAnimeParser(extension) }, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
object HAnimeSources : WatchSources() {
|
||||
private val aList: List<Lazier<BaseParser>> = lazyList(
|
||||
//"HentaiMama" to ::HentaiMama,
|
||||
//"Haho" to ::Haho,
|
||||
//"HentaiStream" to ::HentaiStream,
|
||||
//"HentaiFF" to ::HentaiFF,
|
||||
)
|
||||
|
||||
override val list = listOf(aList,AnimeSources.list).flatten()
|
||||
}
|
175
app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
Normal file
175
app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt
Normal file
|
@ -0,0 +1,175 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.aniyomi.anime.model.AnimeExtension
|
||||
import ani.dantotsu.aniyomi.animesource.AnimeCatalogueSource
|
||||
import ani.dantotsu.logger
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
|
||||
class AniyomiAdapter {
|
||||
fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser {
|
||||
return DynamicAnimeParser(extension)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||
val extension: AnimeExtension.Installed
|
||||
init {
|
||||
this.extension = extension
|
||||
}
|
||||
override val name = extension.name
|
||||
override val saveName = extension.name
|
||||
override val hostUrl = extension.sources.first().name
|
||||
override val isDubAvailableSeparately = false
|
||||
override suspend fun loadEpisodes(animeLink: String, extra: Map<String, String>?, sAnime: SAnime): List<Episode> {
|
||||
val source = extension.sources.first()
|
||||
if (source is AnimeCatalogueSource) {
|
||||
var res: SEpisode? = null
|
||||
try {
|
||||
val res = source.getEpisodeList(sAnime)
|
||||
var EpisodeList: List<Episode> = emptyList()
|
||||
for (episode in res) {
|
||||
println("episode: $episode")
|
||||
EpisodeList += SEpisodeToEpisode(episode)
|
||||
}
|
||||
return EpisodeList
|
||||
}
|
||||
catch (e: Exception) {
|
||||
println("Exception: $e")
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
|
||||
}
|
||||
override suspend fun loadVideoServers(episodeLink: String, extra: Map<String, String>?, sEpisode: SEpisode): List<VideoServer> {
|
||||
val source = extension.sources.first()
|
||||
if (source is AnimeCatalogueSource) {
|
||||
val video = source.getVideoList(sEpisode)
|
||||
var VideoList: List<VideoServer> = emptyList()
|
||||
for (videoServer in video) {
|
||||
VideoList += VideoToVideoServer(videoServer)
|
||||
}
|
||||
return VideoList
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? {
|
||||
return VideoServerPassthrough(server)
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
val source = extension.sources.first()
|
||||
if (source is AnimeCatalogueSource) {
|
||||
|
||||
var res: AnimesPage? = null
|
||||
try {
|
||||
res = source.fetchSearchAnime(0, query, AnimeFilterList()).toBlocking().first()
|
||||
println("res: $res")
|
||||
}
|
||||
catch (e: Exception) {
|
||||
logger("Exception: $e")
|
||||
}
|
||||
|
||||
val conv = convertAnimesPageToShowResponse(res!!)
|
||||
return conv
|
||||
}
|
||||
return emptyList() // Return an empty list if source is not an AnimeCatalogueSource
|
||||
}
|
||||
|
||||
fun convertAnimesPageToShowResponse(animesPage: AnimesPage): List<ShowResponse> {
|
||||
return animesPage.animes.map { sAnime ->
|
||||
// Extract required fields from sAnime
|
||||
val name = sAnime.title
|
||||
val link = sAnime.url
|
||||
val coverUrl = sAnime.thumbnail_url ?: ""
|
||||
val otherNames = emptyList<String>() // Populate as needed
|
||||
val total = 1
|
||||
val extra: Map<String, String>? = null // Populate as needed
|
||||
|
||||
// Create a new ShowResponse
|
||||
ShowResponse(name, link, coverUrl, sAnime)
|
||||
}
|
||||
}
|
||||
|
||||
fun SEpisodeToEpisode(sEpisode: SEpisode): Episode {
|
||||
val episode = Episode(
|
||||
sEpisode.episode_number.toString(),
|
||||
sEpisode.url,
|
||||
sEpisode.name,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
sEpisode)
|
||||
return episode
|
||||
}
|
||||
|
||||
fun VideoToVideoServer(video: Video): VideoServer {
|
||||
val videoServer = VideoServer(
|
||||
video.quality,
|
||||
video.url,
|
||||
null,
|
||||
video)
|
||||
return videoServer
|
||||
}
|
||||
}
|
||||
|
||||
class VideoServerPassthrough : VideoExtractor{
|
||||
val videoServer: VideoServer
|
||||
constructor(videoServer: VideoServer) {
|
||||
this.videoServer = videoServer
|
||||
}
|
||||
override val server: VideoServer
|
||||
get() {
|
||||
return videoServer
|
||||
}
|
||||
|
||||
override suspend fun extract(): VideoContainer {
|
||||
val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) })
|
||||
var subList: List<Subtitle> = emptyList()
|
||||
for(sub in videoServer.video?.subtitleTracks ?: emptyList()) {
|
||||
subList += TrackToSubtitle(sub)
|
||||
}
|
||||
if(vidList.isEmpty()) {
|
||||
throw Exception("No videos found")
|
||||
}else{
|
||||
return VideoContainer(vidList, subList)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video) : ani.dantotsu.parsers.Video {
|
||||
//try to find the number value from the .quality string
|
||||
val regex = Regex("""\d+""")
|
||||
val result = regex.find(aniVideo.quality)
|
||||
val number = result?.value?.toInt() ?: 0
|
||||
val videoUrl = aniVideo.videoUrl ?: throw Exception("Video URL is null")
|
||||
|
||||
val format = when {
|
||||
videoUrl.endsWith(".mp4", ignoreCase = true) || videoUrl.endsWith(".mkv", ignoreCase = true) -> VideoType.CONTAINER
|
||||
videoUrl.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
|
||||
videoUrl.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
||||
else -> throw Exception("Unknown video format")
|
||||
}
|
||||
val headersMap: Map<String, String> = aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
|
||||
|
||||
|
||||
return ani.dantotsu.parsers.Video(
|
||||
number,
|
||||
format,
|
||||
FileUrl(videoUrl, headersMap),
|
||||
aniVideo.totalContentLength.toDouble()
|
||||
)
|
||||
}
|
||||
|
||||
private fun TrackToSubtitle(track: Track, type: SubtitleType = SubtitleType.VTT): Subtitle {
|
||||
return Subtitle(track.lang, track.url, type)
|
||||
}
|
||||
}
|
162
app/src/main/java/ani/dantotsu/parsers/BaseParser.kt
Normal file
162
app/src/main/java/ani/dantotsu/parsers/BaseParser.kt
Normal file
|
@ -0,0 +1,162 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.media.Media
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import java.io.Serializable
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
|
||||
abstract class BaseParser {
|
||||
|
||||
/**
|
||||
* Name that will be shown in Source Selection
|
||||
* **/
|
||||
open val name: String = ""
|
||||
|
||||
/**
|
||||
* Name used to save the ShowResponse selected by user or by autoSearch
|
||||
* **/
|
||||
open val saveName: String = ""
|
||||
|
||||
/**
|
||||
* The main URL of the Site
|
||||
* **/
|
||||
open val hostUrl: String = ""
|
||||
|
||||
/**
|
||||
* override as `true` if the site **only** has NSFW media
|
||||
* **/
|
||||
open val isNSFW = false
|
||||
|
||||
/**
|
||||
* mostly redundant for official app, But override if you want to add different languages
|
||||
* **/
|
||||
open val language = "English"
|
||||
|
||||
/**
|
||||
* Search for Anime/Manga/Novel, returns a List of Responses
|
||||
*
|
||||
* use `encode(query)` to encode the query for making requests
|
||||
* **/
|
||||
abstract suspend fun search(query: String): List<ShowResponse>
|
||||
|
||||
/**
|
||||
* The function app uses to auto find the anime/manga using Media data provided by anilist
|
||||
*
|
||||
* Isn't necessary to override, but recommended, if you want to improve auto search results
|
||||
* **/
|
||||
open suspend fun autoSearch(mediaObj: Media): ShowResponse? {
|
||||
var response = loadSavedShowResponse(mediaObj.id)
|
||||
if (response != null) {
|
||||
saveShowResponse(mediaObj.id, response, true)
|
||||
} else {
|
||||
setUserText("Searching : ${mediaObj.mainName()}")
|
||||
val results = search(mediaObj.mainName())
|
||||
val sortedResults = if (results.isNotEmpty()) {
|
||||
StringMatcher.closestShowMovedToTop(mediaObj.mainName(), results)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
response = sortedResults.firstOrNull()
|
||||
|
||||
if (response == null) {
|
||||
setUserText("Searching : ${mediaObj.nameRomaji}")
|
||||
val romajiResults = search(mediaObj.nameRomaji)
|
||||
val sortedRomajiResults = if (romajiResults.isNotEmpty()) {
|
||||
StringMatcher.closestShowMovedToTop(mediaObj.nameRomaji, romajiResults)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
response = sortedRomajiResults.firstOrNull()
|
||||
}
|
||||
saveShowResponse(mediaObj.id, response)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to get an existing Search Response which was selected by the user.
|
||||
* **/
|
||||
open suspend fun loadSavedShowResponse(mediaId: Int): ShowResponse? {
|
||||
checkIfVariablesAreEmpty()
|
||||
return loadData("${saveName}_$mediaId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to save Shows Response using `saveName`.
|
||||
* **/
|
||||
open fun saveShowResponse(mediaId: Int, response: ShowResponse?, selected: Boolean = false) {
|
||||
if (response != null) {
|
||||
checkIfVariablesAreEmpty()
|
||||
setUserText("${if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString(R.string.found)} : ${response.name}")
|
||||
saveData("${saveName}_$mediaId", response)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkIfVariablesAreEmpty() {
|
||||
if (hostUrl.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `hostUrl` for the Parser")
|
||||
if (name.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `name` for the Parser")
|
||||
if (saveName.isEmpty()) throw UninitializedPropertyAccessException("Please provide a `saveName` for the Parser")
|
||||
}
|
||||
|
||||
open var showUserText = ""
|
||||
open var showUserTextListener: ((String) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Used to show messages & errors to the User, a useful way to convey what's currently happening or what was done.
|
||||
* **/
|
||||
fun setUserText(string: String) {
|
||||
showUserText = string
|
||||
showUserTextListener?.invoke(showUserText)
|
||||
}
|
||||
|
||||
fun encode(input: String): String = URLEncoder.encode(input, "utf-8").replace("+", "%20")
|
||||
fun decode(input: String): String = URLDecoder.decode(input, "utf-8")
|
||||
|
||||
val defaultImage = "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/default.jpg"
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A single show which contains some episodes/chapters which is sent by the site using their search function.
|
||||
*
|
||||
* You might wanna include `otherNames` & `total` too, to further improve user experience.
|
||||
*
|
||||
* You can also store a Map of Strings if you want to save some extra data.
|
||||
* **/
|
||||
data class ShowResponse(
|
||||
val name: String,
|
||||
val link: String,
|
||||
val coverUrl: FileUrl,
|
||||
|
||||
//would be Useful for custom search, ig
|
||||
val otherNames: List<String> = listOf(),
|
||||
|
||||
//Total number of Episodes/Chapters in the show.
|
||||
val total: Int? = null,
|
||||
|
||||
//In case you want to sent some extra data
|
||||
val extra : Map<String,String>?=null,
|
||||
|
||||
//SAnime object from Aniyomi
|
||||
val sAnime: SAnime?=null
|
||||
) : Serializable {
|
||||
constructor(name: String, link: String, coverUrl: String, otherNames: List<String> = listOf(), total: Int? = null, extra: Map<String, String>?=null)
|
||||
: this(name, link, FileUrl(coverUrl), otherNames, total, extra)
|
||||
|
||||
constructor(name: String, link: String, coverUrl: String, otherNames: List<String> = listOf(), total: Int? = null)
|
||||
: this(name, link, FileUrl(coverUrl), otherNames, total)
|
||||
|
||||
constructor(name: String, link: String, coverUrl: String, otherNames: List<String> = listOf())
|
||||
: this(name, link, FileUrl(coverUrl), otherNames)
|
||||
|
||||
constructor(name: String, link: String, coverUrl: String)
|
||||
: this(name, link, FileUrl(coverUrl))
|
||||
|
||||
constructor(name: String, link: String, coverUrl: String, sAnime: SAnime)
|
||||
: this(name, link, FileUrl(coverUrl), sAnime = sAnime)
|
||||
}
|
||||
|
||||
|
110
app/src/main/java/ani/dantotsu/parsers/BaseSources.kt
Normal file
110
app/src/main/java/ani/dantotsu/parsers/BaseSources.kt
Normal file
|
@ -0,0 +1,110 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.Lazier
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
|
||||
abstract class WatchSources : BaseSources() {
|
||||
|
||||
override operator fun get(i: Int): AnimeParser {
|
||||
return (list.getOrNull(i)?:list[0]).get.value as AnimeParser
|
||||
}
|
||||
|
||||
suspend fun loadEpisodesFromMedia(i: Int, media: Media): MutableMap<String, Episode> {
|
||||
return tryWithSuspend(true) {
|
||||
val res = get(i).autoSearch(media) ?: return@tryWithSuspend mutableMapOf()
|
||||
loadEpisodes(i, res.link, res.extra, res.sAnime)
|
||||
} ?: mutableMapOf()
|
||||
}
|
||||
|
||||
suspend fun loadEpisodes(i: Int, showLink: String, extra: Map<String, String>?, sAnime: SAnime?): MutableMap<String, Episode> {
|
||||
println("finder333 $showLink")
|
||||
val map = mutableMapOf<String, Episode>()
|
||||
val parser = get(i)
|
||||
tryWithSuspend(true) {
|
||||
if (sAnime != null) {
|
||||
parser.loadEpisodes(showLink,extra, sAnime).forEach {
|
||||
map[it.number] = Episode(it.number, it.link, it.title, it.description, it.thumbnail, it.isFiller, extra = it.extra, sEpisode = it.sEpisode)
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class MangaReadSources : BaseSources() {
|
||||
|
||||
override operator fun get(i: Int): MangaParser {
|
||||
return (list.getOrNull(i)?:list[0]).get.value as MangaParser
|
||||
}
|
||||
|
||||
suspend fun loadChaptersFromMedia(i: Int, media: Media): MutableMap<String, MangaChapter> {
|
||||
return tryWithSuspend(true) {
|
||||
val res = get(i).autoSearch(media) ?: return@tryWithSuspend mutableMapOf()
|
||||
loadChapters(i, res)
|
||||
} ?: mutableMapOf()
|
||||
}
|
||||
|
||||
suspend fun loadChapters(i: Int, show: ShowResponse): MutableMap<String, MangaChapter> {
|
||||
val map = mutableMapOf<String, MangaChapter>()
|
||||
val parser = get(i)
|
||||
tryWithSuspend(true) {
|
||||
parser.loadChapters(show.link, show.extra).forEach {
|
||||
map[it.number] = MangaChapter(it)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
}
|
||||
|
||||
abstract class NovelReadSources : BaseSources(){
|
||||
override operator fun get(i: Int): NovelParser? {
|
||||
return if (list.isNotEmpty()) {
|
||||
(list.getOrNull(i) ?: list[0]).get.value as NovelParser
|
||||
} else {
|
||||
return EmptyNovelParser()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EmptyNovelParser : NovelParser() {
|
||||
|
||||
override val volumeRegex: Regex = Regex("")
|
||||
|
||||
override suspend fun loadBook(link: String, extra: Map<String, String>?): Book {
|
||||
return Book("","", null, emptyList()) // Return an empty Book object or some default value
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<ShowResponse> {
|
||||
return listOf() // Return an empty list or some default value
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BaseSources {
|
||||
abstract val list: List<Lazier<BaseParser>>
|
||||
|
||||
val names: List<String> get() = list.map { it.name }
|
||||
|
||||
fun flushText() {
|
||||
list.forEach {
|
||||
if (it.get.isInitialized())
|
||||
it.get.value?.showUserText = ""
|
||||
}
|
||||
}
|
||||
|
||||
open operator fun get(i: Int): BaseParser? {
|
||||
return list[i].get.value
|
||||
}
|
||||
|
||||
fun saveResponse(i: Int, mediaId: Int, response: ShowResponse) {
|
||||
get(i)?.saveShowResponse(mediaId, response, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
82
app/src/main/java/ani/dantotsu/parsers/MangaParser.kt
Normal file
82
app/src/main/java/ani/dantotsu/parsers/MangaParser.kt
Normal file
|
@ -0,0 +1,82 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.media.Media
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import java.io.Serializable
|
||||
|
||||
abstract class MangaParser : BaseParser() {
|
||||
|
||||
/**
|
||||
* Takes ShowResponse.link and ShowResponse.extra (if any) as arguments & gives a list of total chapters present on the site.
|
||||
* **/
|
||||
abstract suspend fun loadChapters(mangaLink: String, extra: Map<String, String>?): List<MangaChapter>
|
||||
|
||||
/**
|
||||
* Takes ShowResponse.link, ShowResponse.extra & the Last Largest Chapter Number known by app as arguments
|
||||
*
|
||||
* Returns the latest chapter (If overriding, Make sure the chapter is actually the latest chapter)
|
||||
* Returns null, if no latest chapter is found.
|
||||
* **/
|
||||
open suspend fun getLatestChapter(mangaLink: String, extra: Map<String, String>?, latest: Float): MangaChapter? {
|
||||
return loadChapters(mangaLink, extra)
|
||||
.maxByOrNull { it.number.toFloatOrNull() ?: 0f }
|
||||
?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes MangaChapter.link as an argument & returns a list of MangaImages with their Url (with headers & transformations, if needed)
|
||||
* **/
|
||||
abstract suspend fun loadImages(chapterLink: String): List<MangaImage>
|
||||
|
||||
override suspend fun autoSearch(mediaObj: Media): ShowResponse? {
|
||||
var response = loadSavedShowResponse(mediaObj.id)
|
||||
if (response != null) {
|
||||
saveShowResponse(mediaObj.id, response, true)
|
||||
} else {
|
||||
setUserText("Searching : ${mediaObj.mangaName()}")
|
||||
response = search(mediaObj.mangaName()).let { if (it.isNotEmpty()) it[0] else null }
|
||||
|
||||
if (response == null) {
|
||||
setUserText("Searching : ${mediaObj.nameRomaji}")
|
||||
response = search(mediaObj.nameRomaji).let { if (it.isNotEmpty()) it[0] else null }
|
||||
}
|
||||
saveShowResponse(mediaObj.id, response)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
open fun getTransformation(): BitmapTransformation? = null
|
||||
}
|
||||
|
||||
data class MangaChapter(
|
||||
/**
|
||||
* Number of the Chapter in "String",
|
||||
*
|
||||
* useful in cases where chapter is not a number
|
||||
* **/
|
||||
val number: String,
|
||||
|
||||
/**
|
||||
* Link that links to the chapter page containing videos
|
||||
* **/
|
||||
val link: String,
|
||||
|
||||
//Self-Descriptive
|
||||
val title: String? = null,
|
||||
val description: String? = null,
|
||||
)
|
||||
|
||||
data class MangaImage(
|
||||
/**
|
||||
* The direct url to the Image of a page in a chapter
|
||||
*
|
||||
* Supports jpeg,jpg,png & gif(non animated) afaik
|
||||
* **/
|
||||
val url: FileUrl,
|
||||
|
||||
val useTransformation: Boolean = false
|
||||
) : Serializable{
|
||||
constructor(url: String,useTransformation: Boolean=false)
|
||||
: this(FileUrl(url),useTransformation)
|
||||
}
|
15
app/src/main/java/ani/dantotsu/parsers/MangaSources.kt
Normal file
15
app/src/main/java/ani/dantotsu/parsers/MangaSources.kt
Normal file
|
@ -0,0 +1,15 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.Lazier
|
||||
import ani.dantotsu.lazyList
|
||||
|
||||
object MangaSources : MangaReadSources() {
|
||||
override val list: List<Lazier<BaseParser>> = lazyList(
|
||||
)
|
||||
}
|
||||
|
||||
object HMangaSources : MangaReadSources() {
|
||||
val aList: List<Lazier<BaseParser>> = lazyList(
|
||||
)
|
||||
override val list = listOf(aList,MangaSources.list).flatten()
|
||||
}
|
51
app/src/main/java/ani/dantotsu/parsers/NovelParser.kt
Normal file
51
app/src/main/java/ani/dantotsu/parsers/NovelParser.kt
Normal file
|
@ -0,0 +1,51 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.media.Media
|
||||
|
||||
abstract class NovelParser : BaseParser() {
|
||||
|
||||
abstract val volumeRegex: Regex
|
||||
|
||||
abstract suspend fun loadBook(link: String, extra: Map<String, String>?): Book
|
||||
|
||||
fun List<ShowResponse>.sortByVolume(query:String) : List<ShowResponse> {
|
||||
val sorted = groupBy { res ->
|
||||
val match = volumeRegex.find(res.name)?.groupValues
|
||||
?.firstOrNull { it.isNotEmpty() }
|
||||
?.substringAfter(" ")
|
||||
?.toDoubleOrNull() ?: Double.MAX_VALUE
|
||||
match
|
||||
}.toSortedMap().values
|
||||
|
||||
val volumes = sorted.map { showList ->
|
||||
val nonDefaultCoverShows = showList.filter { it.coverUrl.url != defaultImage }
|
||||
val bestShow = nonDefaultCoverShows.firstOrNull { it.name.contains(query) }
|
||||
?: nonDefaultCoverShows.firstOrNull()
|
||||
?: showList.first()
|
||||
bestShow
|
||||
}
|
||||
val remainingShows = sorted.flatten() - volumes.toSet()
|
||||
|
||||
return volumes + remainingShows
|
||||
}
|
||||
|
||||
suspend fun sortedSearch(mediaObj: Media): List<ShowResponse> {
|
||||
val query = mediaObj.name ?: mediaObj.nameRomaji
|
||||
return search(query).sortByVolume(query)
|
||||
}
|
||||
}
|
||||
|
||||
data class Book(
|
||||
val name: String,
|
||||
val img: FileUrl,
|
||||
val description: String? = null,
|
||||
val links: List<FileUrl>
|
||||
) {
|
||||
constructor (name: String, img: String, description: String? = null, links: List<String>) : this(
|
||||
name,
|
||||
FileUrl(img),
|
||||
description,
|
||||
links.map { FileUrl(it) }
|
||||
)
|
||||
}
|
9
app/src/main/java/ani/dantotsu/parsers/NovelSources.kt
Normal file
9
app/src/main/java/ani/dantotsu/parsers/NovelSources.kt
Normal file
|
@ -0,0 +1,9 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.Lazier
|
||||
import ani.dantotsu.lazyList
|
||||
|
||||
object NovelSources : NovelReadSources() {
|
||||
override val list: List<Lazier<BaseParser>> = lazyList(
|
||||
)
|
||||
}
|
77
app/src/main/java/ani/dantotsu/parsers/StringMatcher.kt
Normal file
77
app/src/main/java/ani/dantotsu/parsers/StringMatcher.kt
Normal file
|
@ -0,0 +1,77 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
class StringMatcher {
|
||||
companion object {
|
||||
private fun levenshteinDistance(s1: String, s2: String): Int {
|
||||
val dp = Array(s1.length + 1) { IntArray(s2.length + 1) }
|
||||
for (i in 0..s1.length) {
|
||||
for (j in 0..s2.length) {
|
||||
when {
|
||||
i == 0 -> dp[i][j] = j
|
||||
j == 0 -> dp[i][j] = i
|
||||
else -> dp[i][j] = minOf(
|
||||
dp[i - 1][j - 1] + if (s1[i - 1] == s2[j - 1]) 0 else 1,
|
||||
dp[i - 1][j] + 1,
|
||||
dp[i][j - 1] + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[s1.length][s2.length]
|
||||
}
|
||||
|
||||
fun closestString(target: String, list: List<String>): Pair<String, Int> {
|
||||
var minDistance = Int.MAX_VALUE
|
||||
var closestString = ""
|
||||
var closestIndex = -1
|
||||
|
||||
for ((index, str) in list.withIndex()) {
|
||||
val distance = levenshteinDistance(target, str)
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance
|
||||
closestString = str
|
||||
closestIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(closestString, closestIndex)
|
||||
}
|
||||
|
||||
fun closestStringMovedToTop(target: String, list: List<String>): List<String> {
|
||||
val (_, closestIndex) = closestString(target, list)
|
||||
if (closestIndex == -1) {
|
||||
return list // Return original list if no closest string found
|
||||
}
|
||||
return listOf(list[closestIndex]) + list.subList(0, closestIndex) + list.subList(
|
||||
closestIndex + 1,
|
||||
list.size
|
||||
)
|
||||
}
|
||||
|
||||
fun closestShowMovedToTop(target: String, shows: List<ShowResponse>): List<ShowResponse> {
|
||||
val closestShowAndIndex = closestShow(target, shows)
|
||||
val closestIndex = closestShowAndIndex.second
|
||||
if (closestIndex == -1) {
|
||||
return shows // Return original list if no closest show found
|
||||
}
|
||||
return listOf(shows[closestIndex]) + shows.subList(0, closestIndex) + shows.subList(closestIndex + 1, shows.size)
|
||||
}
|
||||
|
||||
private fun closestShow(target: String, shows: List<ShowResponse>): Pair<ShowResponse, Int> {
|
||||
var minDistance = Int.MAX_VALUE
|
||||
var closestShow = ShowResponse("", "", "")
|
||||
var closestIndex = -1
|
||||
|
||||
for ((index, show) in shows.withIndex()) {
|
||||
val distance = levenshteinDistance(target, show.name)
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance
|
||||
closestShow = show
|
||||
closestIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(closestShow, closestIndex)
|
||||
}
|
||||
}
|
||||
}
|
163
app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt
Normal file
163
app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt
Normal file
|
@ -0,0 +1,163 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import ani.dantotsu.FileUrl
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Used to extract videos from a specific video host,
|
||||
*
|
||||
* A new instance is created for every embeds/iframes of that Episode
|
||||
* **/
|
||||
abstract class VideoExtractor : Serializable {
|
||||
abstract val server: VideoServer
|
||||
var videos: List<Video> = listOf()
|
||||
var subtitles: List<Subtitle> = listOf()
|
||||
|
||||
/**
|
||||
* Extracts videos & subtitles from the `embed`
|
||||
*
|
||||
* returns a container containing both videos & subtitles (optional)
|
||||
* **/
|
||||
abstract suspend fun extract(): VideoContainer
|
||||
|
||||
/**
|
||||
* Loads videos & subtitles from a given Url
|
||||
*
|
||||
* & returns itself with the data loaded
|
||||
* **/
|
||||
open suspend fun load(): VideoExtractor {
|
||||
extract().also {
|
||||
videos = it.videos
|
||||
subtitles = it.subtitles
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when a Video from this extractor starts playing
|
||||
*
|
||||
* Useful for Extractor that require Polling
|
||||
* **/
|
||||
open suspend fun onVideoPlayed(video: Video?) {}
|
||||
|
||||
/**
|
||||
* Called when a particular video has been stopped playing
|
||||
**/
|
||||
open suspend fun onVideoStopped(video: Video?) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple class containing name, link & extraData(in case you want to give some to it) of the embed which shows the video present on the site
|
||||
*
|
||||
* `name` variable is used when checking if there was a Default Server Selected with the same name
|
||||
*
|
||||
*
|
||||
* **/
|
||||
data class VideoServer(
|
||||
val name: String,
|
||||
val embed: FileUrl,
|
||||
val extraData : Map<String,String>?=null,
|
||||
val video: eu.kanade.tachiyomi.animesource.model.Video? = null
|
||||
) : Serializable {
|
||||
constructor(name: String, embedUrl: String,extraData: Map<String,String>?=null)
|
||||
: this(name, FileUrl(embedUrl),extraData)
|
||||
|
||||
constructor(name: String, embedUrl: String,extraData: Map<String,String>?=null, video: eu.kanade.tachiyomi.animesource.model.Video?)
|
||||
: this(name, FileUrl(embedUrl),extraData, video)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Container for keeping video & subtitles, so you dont need to check backend
|
||||
* **/
|
||||
data class VideoContainer(
|
||||
val videos: List<Video>,
|
||||
val subtitles: List<Subtitle> = listOf()
|
||||
) : Serializable
|
||||
|
||||
/**
|
||||
* The Class which contains all the information about a Video
|
||||
* **/
|
||||
data class Video(
|
||||
/**
|
||||
* Will represent quality to user in form of `"${quality}p"` (1080p)
|
||||
*
|
||||
* If quality is null, shows "Unknown Quality"
|
||||
*
|
||||
* If isM3U8 is true, shows "Multi Quality"
|
||||
* **/
|
||||
val quality: Int?,
|
||||
|
||||
/**
|
||||
* Mime type / Format of the video,
|
||||
*
|
||||
* If not a "CONTAINER" format, the app show video as a "Multi Quality" Link
|
||||
* "CONTAINER" formats are Mp4 & Mkv
|
||||
* **/
|
||||
val format: VideoType,
|
||||
|
||||
/**
|
||||
* The direct url to the Video
|
||||
*
|
||||
* Supports mp4, mkv, dash & m3u8, afaik
|
||||
* **/
|
||||
val file: FileUrl,
|
||||
|
||||
/**
|
||||
* use getSize(url) to get this size,
|
||||
*
|
||||
* no need to set it on M3U8 links
|
||||
* **/
|
||||
val size: Double? = null,
|
||||
|
||||
/**
|
||||
* In case, you want to show some extra notes to the User
|
||||
*
|
||||
* Ex: "Backup" which could be used if the site provides some
|
||||
* **/
|
||||
val extraNote: String? = null,
|
||||
) : Serializable {
|
||||
|
||||
constructor(quality: Int? = null, videoType: VideoType, url: String, size: Double?, extraNote: String? = null)
|
||||
: this(quality, videoType, FileUrl(url), size, extraNote)
|
||||
|
||||
constructor(quality: Int? = null, videoType: VideoType, url: String, size: Double?)
|
||||
: this(quality, videoType, FileUrl(url), size)
|
||||
|
||||
constructor(quality: Int? = null, videoType: VideoType, url: String)
|
||||
: this(quality, videoType, FileUrl(url))
|
||||
}
|
||||
|
||||
/**
|
||||
* The Class which contains the link to a subtitle file of a specific language
|
||||
* **/
|
||||
data class Subtitle(
|
||||
/**
|
||||
* Language of the Subtitle
|
||||
*
|
||||
* for now app will directly try to select "English".
|
||||
* Probably in rework we can add more subtitles support
|
||||
* **/
|
||||
val language: String,
|
||||
|
||||
/**
|
||||
* The direct url to the Subtitle
|
||||
* **/
|
||||
val file: FileUrl,
|
||||
|
||||
/**
|
||||
* format of the Subtitle
|
||||
*
|
||||
* Supports VTT, SRT & ASS
|
||||
* **/
|
||||
val type:SubtitleType = SubtitleType.VTT,
|
||||
) : Serializable {
|
||||
constructor(language: String, url: String, type: SubtitleType = SubtitleType.VTT) : this(language, FileUrl(url), type)
|
||||
}
|
||||
|
||||
enum class VideoType{
|
||||
CONTAINER, M3U8, DASH
|
||||
}
|
||||
|
||||
enum class SubtitleType{
|
||||
VTT, ASS, SRT
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue