719 lines
No EOL
26 KiB
Kotlin
719 lines
No EOL
26 KiB
Kotlin
package ani.dantotsu.parsers
|
|
|
|
import android.content.ContentResolver
|
|
import android.content.ContentValues
|
|
import android.content.Context
|
|
import android.graphics.Bitmap
|
|
import android.graphics.BitmapFactory
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Environment
|
|
import android.provider.MediaStore
|
|
import ani.dantotsu.FileUrl
|
|
import ani.dantotsu.logger
|
|
import ani.dantotsu.media.anime.AnimeNameAdapter
|
|
import ani.dantotsu.media.manga.ImageData
|
|
import ani.dantotsu.media.manga.MangaCache
|
|
import ani.dantotsu.snackString
|
|
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
|
import eu.kanade.tachiyomi.animesource.model.Track
|
|
import eu.kanade.tachiyomi.animesource.model.Video
|
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException
|
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
import eu.kanade.tachiyomi.source.model.Page
|
|
import eu.kanade.tachiyomi.source.model.SChapter
|
|
import eu.kanade.tachiyomi.source.model.SManga
|
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.async
|
|
import kotlinx.coroutines.awaitAll
|
|
import kotlinx.coroutines.coroutineScope
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.runBlocking
|
|
import kotlinx.coroutines.sync.Semaphore
|
|
import kotlinx.coroutines.sync.withPermit
|
|
import kotlinx.coroutines.withContext
|
|
import okhttp3.Request
|
|
import uy.kohesive.injekt.Injekt
|
|
import uy.kohesive.injekt.api.get
|
|
import java.io.File
|
|
import java.io.FileOutputStream
|
|
import java.io.UnsupportedEncodingException
|
|
import java.net.URL
|
|
import java.net.URLDecoder
|
|
import java.util.regex.Pattern
|
|
|
|
class AniyomiAdapter {
|
|
fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser {
|
|
return DynamicAnimeParser(extension)
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
|
val extension: AnimeExtension.Installed
|
|
var sourceLanguage = 0
|
|
|
|
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 val isNSFW = extension.isNsfw
|
|
override suspend fun loadEpisodes(
|
|
animeLink: String,
|
|
extra: Map<String, String>?,
|
|
sAnime: SAnime
|
|
): List<Episode> {
|
|
val source = try {
|
|
extension.sources[sourceLanguage]
|
|
} catch (e: Exception) {
|
|
sourceLanguage = 0
|
|
extension.sources[sourceLanguage]
|
|
} as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
|
|
?: return emptyList())
|
|
try {
|
|
val res = source.getEpisodeList(sAnime)
|
|
|
|
val sortedEpisodes = if (res[0].episode_number == -1f) {
|
|
// Find the number in the string and sort by that number
|
|
val sortedByStringNumber = res.sortedBy {
|
|
val matchResult = AnimeNameAdapter.findEpisodeNumber(it.name)
|
|
val number = matchResult ?: Float.MAX_VALUE
|
|
it.episode_number = number // Store the found number in episode_number
|
|
number
|
|
}
|
|
|
|
// If there is no number, reverse the order and give them an incrementing number
|
|
var incrementingNumber = 1f
|
|
sortedByStringNumber.map {
|
|
if (it.episode_number == Float.MAX_VALUE) {
|
|
it.episode_number =
|
|
incrementingNumber++ // Update episode_number with the incrementing number
|
|
}
|
|
it
|
|
}
|
|
} else {
|
|
var episodeCounter = 1f
|
|
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is
|
|
val seasonGroups =
|
|
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
|
|
seasonGroups.keys.sortedBy { it.toInt() }
|
|
.flatMap { season ->
|
|
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
|
|
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
|
|
val potentialNumber =
|
|
AnimeNameAdapter.findEpisodeNumber(episode.name)
|
|
if (potentialNumber != null) {
|
|
episode.episode_number = potentialNumber
|
|
} else {
|
|
episode.episode_number = episodeCounter
|
|
}
|
|
episodeCounter++
|
|
}
|
|
episode
|
|
} ?: emptyList()
|
|
}
|
|
}
|
|
return sortedEpisodes.map { SEpisodeToEpisode(it) }
|
|
} catch (e: Exception) {
|
|
logger("Exception: $e")
|
|
}
|
|
return emptyList()
|
|
}
|
|
|
|
override suspend fun loadVideoServers(
|
|
episodeLink: String,
|
|
extra: Map<String, String>?,
|
|
sEpisode: SEpisode
|
|
): List<VideoServer> {
|
|
val source = try {
|
|
extension.sources[sourceLanguage]
|
|
} catch (e: Exception) {
|
|
sourceLanguage = 0
|
|
extension.sources[sourceLanguage]
|
|
} as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
|
|
?: return emptyList())
|
|
|
|
return try {
|
|
val videos = source.getVideoList(sEpisode)
|
|
videos.map { VideoToVideoServer(it) }
|
|
} catch (e: Exception) {
|
|
logger("Exception occurred: ${e.message}")
|
|
emptyList()
|
|
}
|
|
}
|
|
|
|
|
|
override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor {
|
|
return VideoServerPassthrough(server)
|
|
}
|
|
|
|
override suspend fun search(query: String): List<ShowResponse> {
|
|
val source = try {
|
|
extension.sources[sourceLanguage]
|
|
} catch (e: Exception) {
|
|
sourceLanguage = 0
|
|
extension.sources[sourceLanguage]
|
|
} as? AnimeHttpSource ?: (extension.sources[sourceLanguage] as? AnimeCatalogueSource
|
|
?: return emptyList())
|
|
return try {
|
|
val res = source.fetchSearchAnime(1, query, source.getFilterList()).awaitSingle()
|
|
logger("query: $query")
|
|
convertAnimesPageToShowResponse(res)
|
|
} catch (e: CloudflareBypassException) {
|
|
logger("Exception in search: $e")
|
|
withContext(Dispatchers.Main) {
|
|
snackString( "Failed to bypass Cloudflare")
|
|
}
|
|
emptyList()
|
|
} catch (e: Exception) {
|
|
logger("General exception in search: $e")
|
|
emptyList()
|
|
}
|
|
}
|
|
|
|
|
|
private 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)
|
|
}
|
|
}
|
|
|
|
private fun SEpisodeToEpisode(sEpisode: SEpisode): Episode {
|
|
//if the float episode number is a whole number, convert it to an int
|
|
val episodeNumberInt =
|
|
if (sEpisode.episode_number % 1 == 0f) {
|
|
sEpisode.episode_number.toInt()
|
|
} else {
|
|
sEpisode.episode_number
|
|
}
|
|
return Episode(
|
|
if (episodeNumberInt.toInt() != -1) {
|
|
if (sEpisode.episode_number % 1 == 0f) {
|
|
episodeNumberInt.toInt().toString()
|
|
} else {
|
|
sEpisode.episode_number.toString()
|
|
}
|
|
} else {
|
|
sEpisode.name
|
|
},
|
|
sEpisode.url,
|
|
sEpisode.name,
|
|
null,
|
|
null,
|
|
false,
|
|
null,
|
|
sEpisode
|
|
)
|
|
}
|
|
|
|
private fun VideoToVideoServer(video: Video): VideoServer {
|
|
return VideoServer(
|
|
video.quality,
|
|
video.url,
|
|
null,
|
|
video
|
|
)
|
|
}
|
|
}
|
|
|
|
class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
|
val mangaCache = Injekt.get<MangaCache>()
|
|
val extension: MangaExtension.Installed
|
|
var sourceLanguage = 0
|
|
|
|
init {
|
|
this.extension = extension
|
|
}
|
|
|
|
override val name = extension.name
|
|
override val saveName = extension.name
|
|
override val hostUrl = extension.sources.first().name
|
|
override val isNSFW = extension.isNsfw
|
|
|
|
override suspend fun loadChapters(
|
|
mangaLink: String,
|
|
extra: Map<String, String>?,
|
|
sManga: SManga
|
|
): List<MangaChapter> {
|
|
val source = try {
|
|
extension.sources[sourceLanguage]
|
|
} catch (e: Exception) {
|
|
sourceLanguage = 0
|
|
extension.sources[sourceLanguage]
|
|
} as? HttpSource ?: return emptyList()
|
|
|
|
return try {
|
|
val res = source.getChapterList(sManga)
|
|
val reversedRes = res.reversed()
|
|
val chapterList = reversedRes.map { SChapterToMangaChapter(it) }
|
|
logger("chapterList size: ${chapterList.size}")
|
|
logger("chapterList: ${chapterList[1].title}")
|
|
logger("chapterList: ${chapterList[1].description}")
|
|
chapterList
|
|
} catch (e: Exception) {
|
|
logger("loadChapters Exception: $e")
|
|
emptyList()
|
|
}
|
|
}
|
|
|
|
|
|
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
|
|
val source = try {
|
|
extension.sources[sourceLanguage]
|
|
} catch (e: Exception) {
|
|
sourceLanguage = 0
|
|
extension.sources[sourceLanguage]
|
|
} as? HttpSource ?: return emptyList()
|
|
var imageDataList: List<ImageData> = listOf()
|
|
val ret = coroutineScope {
|
|
try {
|
|
logger("source.name " + source.name)
|
|
val res = source.getPageList(sChapter)
|
|
val reIndexedPages =
|
|
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
|
|
|
val deferreds = reIndexedPages.map { page ->
|
|
async(Dispatchers.IO) {
|
|
mangaCache.put(page.imageUrl ?: "", ImageData(page, source))
|
|
imageDataList += ImageData(page, source)
|
|
logger("put page: ${page.imageUrl}")
|
|
pageToMangaImage(page)
|
|
}
|
|
}
|
|
|
|
deferreds.awaitAll()
|
|
|
|
} catch (e: Exception) {
|
|
logger("loadImages Exception: $e")
|
|
snackString("Failed to load images: $e")
|
|
emptyList()
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
suspend fun imageList(chapterLink: String, sChapter: SChapter): List<ImageData> {
|
|
val source = try {
|
|
extension.sources[sourceLanguage]
|
|
} catch (e: Exception) {
|
|
sourceLanguage = 0
|
|
extension.sources[sourceLanguage]
|
|
} as? HttpSource ?: return emptyList()
|
|
|
|
return coroutineScope {
|
|
try {
|
|
logger("source.name " + source.name)
|
|
val res = source.getPageList(sChapter)
|
|
val reIndexedPages =
|
|
res.mapIndexed { index, page -> Page(index, page.url, page.imageUrl, page.uri) }
|
|
|
|
val semaphore = Semaphore(5)
|
|
val deferreds = reIndexedPages.map { page ->
|
|
async(Dispatchers.IO) {
|
|
semaphore.withPermit {
|
|
ImageData(page, source)
|
|
}
|
|
}
|
|
}
|
|
|
|
deferreds.awaitAll()
|
|
} catch (e: Exception) {
|
|
logger("loadImages Exception: $e")
|
|
snackString("Failed to load images: $e")
|
|
emptyList()
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun fetchAndProcessImage(
|
|
page: Page,
|
|
httpSource: HttpSource,
|
|
context: Context
|
|
): Bitmap? {
|
|
return withContext(Dispatchers.IO) {
|
|
try {
|
|
// Fetch the image
|
|
val response = httpSource.getImage(page)
|
|
logger("Response: ${response.code}")
|
|
logger("Response: ${response.message}")
|
|
|
|
// Convert the Response to an InputStream
|
|
val inputStream = response.body.byteStream()
|
|
|
|
// Convert InputStream to Bitmap
|
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
|
|
inputStream.close()
|
|
ani.dantotsu.media.manga.saveImage(
|
|
bitmap,
|
|
context.contentResolver,
|
|
page.imageUrl!!,
|
|
Bitmap.CompressFormat.JPEG,
|
|
100
|
|
)
|
|
|
|
return@withContext bitmap
|
|
} catch (e: Exception) {
|
|
// Handle any exceptions
|
|
logger("An error occurred: ${e.message}")
|
|
return@withContext null
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
|
|
CoroutineScope(Dispatchers.IO).launch {
|
|
try {
|
|
// Fetch the image
|
|
val response = httpSource.getImage(page)
|
|
|
|
// Convert the Response to an InputStream
|
|
val inputStream = response.body.byteStream()
|
|
|
|
// Convert InputStream to Bitmap
|
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
|
|
withContext(Dispatchers.IO) {
|
|
// Save the Bitmap using MediaStore API
|
|
saveImage(
|
|
bitmap,
|
|
contentResolver,
|
|
"image_${System.currentTimeMillis()}.jpg",
|
|
Bitmap.CompressFormat.JPEG,
|
|
100
|
|
)
|
|
}
|
|
|
|
inputStream.close()
|
|
} catch (e: Exception) {
|
|
// Handle any exceptions
|
|
logger("An error occurred: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
fun saveImage(
|
|
bitmap: Bitmap,
|
|
contentResolver: ContentResolver,
|
|
filename: String,
|
|
format: Bitmap.CompressFormat,
|
|
quality: Int
|
|
) {
|
|
try {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
val contentValues = ContentValues().apply {
|
|
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
|
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
|
|
put(
|
|
MediaStore.MediaColumns.RELATIVE_PATH,
|
|
"${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime"
|
|
)
|
|
}
|
|
|
|
val uri: Uri? = contentResolver.insert(
|
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
|
contentValues
|
|
)
|
|
|
|
uri?.let {
|
|
contentResolver.openOutputStream(it)?.use { os ->
|
|
bitmap.compress(format, quality, os)
|
|
}
|
|
}
|
|
} else {
|
|
val directory =
|
|
File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
|
|
if (!directory.exists()) {
|
|
directory.mkdirs()
|
|
}
|
|
|
|
val file = File(directory, filename)
|
|
FileOutputStream(file).use { outputStream ->
|
|
bitmap.compress(format, quality, outputStream)
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
// Handle exception here
|
|
logger("Exception while saving image: ${e.message}")
|
|
}
|
|
}
|
|
|
|
|
|
override suspend fun search(query: String): List<ShowResponse> {
|
|
val source = try {
|
|
extension.sources[sourceLanguage]
|
|
} catch (e: Exception) {
|
|
sourceLanguage = 0
|
|
extension.sources[sourceLanguage]
|
|
} as? HttpSource ?: return emptyList()
|
|
|
|
return try {
|
|
val res = source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle()
|
|
logger("res observable: $res")
|
|
convertMangasPageToShowResponse(res)
|
|
} catch (e: CloudflareBypassException) {
|
|
logger("Exception in search: $e")
|
|
withContext(Dispatchers.Main) {
|
|
snackString("Failed to bypass Cloudflare")
|
|
}
|
|
emptyList()
|
|
} catch (e: Exception) {
|
|
logger("General exception in search: $e")
|
|
emptyList()
|
|
}
|
|
}
|
|
|
|
|
|
private fun convertMangasPageToShowResponse(mangasPage: MangasPage): List<ShowResponse> {
|
|
return mangasPage.mangas.map { sManga ->
|
|
// Extract required fields from sManga
|
|
val name = sManga.title
|
|
val link = sManga.url
|
|
val coverUrl = sManga.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, sManga)
|
|
}
|
|
}
|
|
|
|
private fun pageToMangaImage(page: Page): MangaImage {
|
|
var headersMap = mapOf<String, String>()
|
|
var urlWithoutHeaders = ""
|
|
var url = ""
|
|
|
|
page.imageUrl?.let {
|
|
val splitUrl = it.split("&")
|
|
urlWithoutHeaders = splitUrl.getOrNull(0) ?: ""
|
|
url = it
|
|
|
|
headersMap = splitUrl.mapNotNull { part ->
|
|
val idx = part.indexOf("=")
|
|
if (idx != -1) {
|
|
try {
|
|
val key = URLDecoder.decode(part.substring(0, idx), "UTF-8")
|
|
val value = URLDecoder.decode(part.substring(idx + 1), "UTF-8")
|
|
Pair(key, value)
|
|
} catch (e: UnsupportedEncodingException) {
|
|
null
|
|
}
|
|
} else {
|
|
null
|
|
}
|
|
}.toMap()
|
|
}
|
|
|
|
return MangaImage(
|
|
FileUrl(url, headersMap),
|
|
false,
|
|
page
|
|
)
|
|
}
|
|
|
|
|
|
private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter {
|
|
return MangaChapter(
|
|
sChapter.name,
|
|
sChapter.url,
|
|
sChapter.name,
|
|
null,
|
|
sChapter.scanlator,
|
|
sChapter
|
|
)
|
|
}
|
|
|
|
fun parseChapterTitle(title: String): Triple<String?, String?, String> {
|
|
val volumePattern =
|
|
Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
|
|
val chapterPattern =
|
|
Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
|
|
|
|
val volumeMatcher = volumePattern.matcher(title)
|
|
val chapterMatcher = chapterPattern.matcher(title)
|
|
|
|
val volumeNumber = if (volumeMatcher.find()) volumeMatcher.group(1) else null
|
|
val chapterNumber = if (chapterMatcher.find()) chapterMatcher.group(1) else null
|
|
|
|
var remainingTitle = title
|
|
if (volumeNumber != null) {
|
|
remainingTitle =
|
|
volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
|
}
|
|
if (chapterNumber != null) {
|
|
remainingTitle =
|
|
chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
|
|
}
|
|
|
|
return Triple(volumeNumber, chapterNumber, remainingTitle.trim())
|
|
}
|
|
|
|
}
|
|
|
|
class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
|
override val server: VideoServer
|
|
get() = videoServer
|
|
|
|
override suspend fun extract(): VideoContainer {
|
|
val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) })
|
|
val subList = videoServer.video?.subtitleTracks?.map { TrackToSubtitle(it) } ?: emptyList()
|
|
|
|
return if (vidList.isNotEmpty()) {
|
|
VideoContainer(vidList, subList)
|
|
} else {
|
|
throw Exception("No videos found")
|
|
}
|
|
}
|
|
|
|
private fun AniVideoToSaiVideo(aniVideo: Video): ani.dantotsu.parsers.Video {
|
|
// Find the number value from the .quality string
|
|
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
|
|
|
|
// Check for null video URL
|
|
val videoUrl = aniVideo.videoUrl ?: throw Exception("Video URL is null")
|
|
|
|
val urlObj = URL(videoUrl)
|
|
val path = urlObj.path
|
|
val query = urlObj.query
|
|
|
|
var format = getVideoType(path)
|
|
|
|
if (format == null && query != null) {
|
|
val queryPairs: List<Pair<String, String>> = query.split("&").map {
|
|
val idx = it.indexOf("=")
|
|
val key = URLDecoder.decode(it.substring(0, idx), "UTF-8")
|
|
val value = URLDecoder.decode(it.substring(idx + 1), "UTF-8")
|
|
Pair(key, value)
|
|
}
|
|
|
|
// Assume the file is named under the "file" query parameter
|
|
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
|
|
|
|
format = getVideoType(fileName)
|
|
// this solves a problem no one has, so I'm commenting it out for now
|
|
//if (format == null) {
|
|
// val networkHelper = Injekt.get<NetworkHelper>()
|
|
// format = headRequest(videoUrl, networkHelper)
|
|
//}
|
|
}
|
|
|
|
// If the format is still undetermined, log an error
|
|
if (format == null) {
|
|
logger("Unknown video format: $videoUrl")
|
|
//FirebaseCrashlytics.getInstance()
|
|
// .recordException(Exception("Unknown video format: $videoUrl"))
|
|
format = VideoType.CONTAINER
|
|
}
|
|
val headersMap: Map<String, String> =
|
|
aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
|
|
|
|
|
|
return Video(
|
|
number,
|
|
format,
|
|
FileUrl(videoUrl, headersMap),
|
|
if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
|
|
)
|
|
}
|
|
|
|
private fun getVideoType(fileName: String): VideoType? {
|
|
val type = when {
|
|
fileName.endsWith(".mp4", ignoreCase = true) || fileName.endsWith(
|
|
".mkv",
|
|
ignoreCase = true
|
|
) -> VideoType.CONTAINER
|
|
|
|
fileName.endsWith(".m3u8", ignoreCase = true) -> VideoType.M3U8
|
|
fileName.endsWith(".mpd", ignoreCase = true) -> VideoType.DASH
|
|
else -> null
|
|
}
|
|
|
|
return type
|
|
}
|
|
|
|
private fun headRequest(fileName: String, networkHelper: NetworkHelper): VideoType? {
|
|
return try {
|
|
logger("attempting head request for $fileName")
|
|
val request = Request.Builder()
|
|
.url(fileName)
|
|
.head()
|
|
.build()
|
|
|
|
networkHelper.client.newCall(request).execute().use { response ->
|
|
val contentType = response.header("Content-Type")
|
|
val contentDisposition = response.header("Content-Disposition")
|
|
|
|
if (contentType != null) {
|
|
when {
|
|
contentType.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8
|
|
contentType.contains("dash", ignoreCase = true) -> VideoType.DASH
|
|
contentType.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER
|
|
else -> null
|
|
}
|
|
} else if (contentDisposition != null) {
|
|
when {
|
|
contentDisposition.contains("mpegurl", ignoreCase = true) -> VideoType.M3U8
|
|
contentDisposition.contains("dash", ignoreCase = true) -> VideoType.DASH
|
|
contentDisposition.contains("mp4", ignoreCase = true) -> VideoType.CONTAINER
|
|
else -> null
|
|
}
|
|
} else {
|
|
logger("failed head request for $fileName")
|
|
null
|
|
}
|
|
|
|
}
|
|
} catch (e: Exception) {
|
|
logger("Exception in headRequest: $e")
|
|
null
|
|
}
|
|
|
|
}
|
|
|
|
private fun TrackToSubtitle(track: Track): Subtitle {
|
|
//use Dispatchers.IO to make a HTTP request to determine the subtitle type
|
|
var type: SubtitleType? = null
|
|
runBlocking {
|
|
type = findSubtitleType(track.url)
|
|
}
|
|
return Subtitle(track.lang, track.url, type ?: SubtitleType.SRT)
|
|
}
|
|
|
|
private fun findSubtitleType(url: String): SubtitleType {
|
|
// First, try to determine the type based on the URL file extension
|
|
val type: SubtitleType = when {
|
|
url.endsWith(".vtt", true) -> SubtitleType.VTT
|
|
url.endsWith(".ass", true) -> SubtitleType.ASS
|
|
url.endsWith(".srt", true) -> SubtitleType.SRT
|
|
else -> SubtitleType.UNKNOWN
|
|
}
|
|
|
|
return type
|
|
}
|
|
} |