fuck it we ball, m3u8 download is now fixed

This commit is contained in:
LagradOst 2023-08-17 23:10:21 +02:00
parent c2b951a078
commit 590c74111c
3 changed files with 237 additions and 276 deletions

View file

@ -1,13 +1,11 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.Qualities
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import android.util.Log
import java.net.URLDecoder import java.net.URLDecoder
open class Cda: ExtractorApi() { open class Cda: ExtractorApi() {

View file

@ -1,17 +1,16 @@
package com.lagradost.cloudstream3.utils package com.lagradost.cloudstream3.utils
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlin.math.pow import kotlin.math.pow
/** backwards api surface */
class M3u8Helper { class M3u8Helper {
companion object { companion object {
private val generator = M3u8Helper()
suspend fun generateM3u8( suspend fun generateM3u8(
source: String, source: String,
streamUrl: String, streamUrl: String,
@ -20,34 +19,59 @@ class M3u8Helper {
headers: Map<String, String> = mapOf(), headers: Map<String, String> = mapOf(),
name: String = source name: String = source
): List<ExtractorLink> { ): List<ExtractorLink> {
return generator.m3u8Generation( return M3u8Helper2.generateM3u8(source, streamUrl, referer, quality, headers, name)
M3u8Stream(
streamUrl = streamUrl,
quality = quality,
headers = headers,
), null
)
.map { stream ->
ExtractorLink(
source,
name = name,
stream.streamUrl,
referer,
stream.quality ?: Qualities.Unknown.value,
true,
stream.headers,
)
}
} }
} }
data class M3u8Stream(
val streamUrl: String,
val quality: Int? = null,
val headers: Map<String, String> = mapOf()
)
suspend fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean? = true): List<M3u8Stream> {
return M3u8Helper2.m3u8Generation(m3u8, returnThis)
}
}
object M3u8Helper2 {
suspend fun generateM3u8(
source: String,
streamUrl: String,
referer: String,
quality: Int? = null,
headers: Map<String, String> = mapOf(),
name: String = source
): List<ExtractorLink> {
return m3u8Generation(
M3u8Helper.M3u8Stream(
streamUrl = streamUrl,
quality = quality,
headers = headers,
), null
)
.map { stream ->
ExtractorLink(
source,
name = name,
stream.streamUrl,
referer,
stream.quality ?: Qualities.Unknown.value,
true,
stream.headers,
)
}
}
private val ENCRYPTION_DETECTION_REGEX = Regex("#EXT-X-KEY:METHOD=([^,]+),") private val ENCRYPTION_DETECTION_REGEX = Regex("#EXT-X-KEY:METHOD=([^,]+),")
private val ENCRYPTION_URL_IV_REGEX = private val ENCRYPTION_URL_IV_REGEX =
Regex("#EXT-X-KEY:METHOD=([^,]+),URI=\"([^\"]+)\"(?:,IV=(.*))?") Regex("#EXT-X-KEY:METHOD=([^,]+),URI=\"([^\"]+)\"(?:,IV=(.*))?")
private val QUALITY_REGEX = private val QUALITY_REGEX =
Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""") Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""")
private val TS_EXTENSION_REGEX = private val TS_EXTENSION_REGEX =
Regex("""(.*\.ts.*|.*\.jpg.*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways
//Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts
private fun absoluteExtensionDetermination(url: String): String? { private fun absoluteExtensionDetermination(url: String): String? {
val split = url.split("/") val split = url.split("/")
@ -73,7 +97,7 @@ class M3u8Helper {
} }
}.iterator() }.iterator()
private fun getDecrypter( fun getDecrypter(
secretKey: ByteArray, secretKey: ByteArray,
data: ByteArray, data: ByteArray,
iv: ByteArray = "".toByteArray() iv: ByteArray = "".toByteArray()
@ -91,13 +115,8 @@ class M3u8Helper {
return st != null && (st.value.isNotEmpty() || st.destructured.component1() != "NONE") return st != null && (st.value.isNotEmpty() || st.destructured.component1() != "NONE")
} }
data class M3u8Stream(
val streamUrl: String,
val quality: Int? = null,
val headers: Map<String, String> = mapOf()
)
private fun selectBest(qualities: List<M3u8Stream>): M3u8Stream? { private fun selectBest(qualities: List<M3u8Helper.M3u8Stream>): M3u8Helper.M3u8Stream? {
val result = qualities.sortedBy { val result = qualities.sortedBy {
if (it.quality != null && it.quality <= 1080) it.quality else 0 if (it.quality != null && it.quality <= 1080) it.quality else 0
}.filter { }.filter {
@ -113,19 +132,16 @@ class M3u8Helper {
} }
private fun isNotCompleteUrl(url: String): Boolean { private fun isNotCompleteUrl(url: String): Boolean {
return !url.contains("https://") && !url.contains("http://") return !url.startsWith("https://") && !url.startsWith("http://")
} }
suspend fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean? = true): List<M3u8Stream> { suspend fun m3u8Generation(m3u8: M3u8Helper.M3u8Stream, returnThis: Boolean? = true): List<M3u8Helper.M3u8Stream> {
// return listOf(m3u8) val list = mutableListOf<M3u8Helper.M3u8Stream>()
val list = mutableListOf<M3u8Stream>()
val m3u8Parent = getParentLink(m3u8.streamUrl) val m3u8Parent = getParentLink(m3u8.streamUrl)
val response = app.get(m3u8.streamUrl, headers = m3u8.headers, verify = false).text val response = app.get(m3u8.streamUrl, headers = m3u8.headers, verify = false).text
// var hasAnyContent = false
for (match in QUALITY_REGEX.findAll(response)) { for (match in QUALITY_REGEX.findAll(response)) {
// hasAnyContent = true
var (quality, m3u8Link, m3u8Link2) = match.destructured var (quality, m3u8Link, m3u8Link2) = match.destructured
if (m3u8Link.isEmpty()) m3u8Link = m3u8Link2 if (m3u8Link.isEmpty()) m3u8Link = m3u8Link2
if (absoluteExtensionDetermination(m3u8Link) == "m3u8") { if (absoluteExtensionDetermination(m3u8Link) == "m3u8") {
@ -136,21 +152,21 @@ class M3u8Helper {
println(m3u8.streamUrl) println(m3u8.streamUrl)
} }
list += m3u8Generation( list += m3u8Generation(
M3u8Stream( M3u8Helper.M3u8Stream(
m3u8Link, m3u8Link,
quality.toIntOrNull(), quality.toIntOrNull(),
m3u8.headers m3u8.headers
), false ), false
) )
} }
list += M3u8Stream( list += M3u8Helper.M3u8Stream(
m3u8Link, m3u8Link,
quality.toIntOrNull(), quality.toIntOrNull(),
m3u8.headers m3u8.headers
) )
} }
if (returnThis != false) { if (returnThis != false) {
list += M3u8Stream( list += M3u8Helper.M3u8Stream(
m3u8.streamUrl, m3u8.streamUrl,
Qualities.Unknown.value, Qualities.Unknown.value,
m3u8.headers m3u8.headers
@ -160,113 +176,111 @@ class M3u8Helper {
return list return list
} }
data class LazyHlsDownloadData(
private val encryptionData: ByteArray,
private val encryptionIv: ByteArray,
private val isEncrypted: Boolean,
private val allTsLinks: List<String>,
private val relativeUrl: String,
private val headers: Map<String, String>,
) {
val size get() = allTsLinks.size
data class HlsDownloadData( suspend fun resolveLinkSafe(
val bytes: ByteArray, index: Int,
val currentIndex: Int, tries: Int = 3,
val totalTs: Int, failDelay: Long = 3000
val errored: Boolean = false ): ByteArray? {
) for (i in 0 until tries) {
try {
suspend fun hlsYield( return resolveLink(index)
qualities: List<M3u8Stream>, } catch (e: IllegalArgumentException) {
startIndex: Int = 0 return null
): Iterator<HlsDownloadData> { } catch (t: Throwable) {
if (qualities.isEmpty()) return listOf( delay(failDelay)
HlsDownloadData( }
byteArrayOf(), }
1, return null
1,
true
)
).iterator()
var selected = selectBest(qualities)
if (selected == null) {
selected = qualities[0]
} }
@Throws
suspend fun resolveLink(index: Int): ByteArray {
if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts")
val url = allTsLinks[index]
val tsResponse = app.get(url, headers = headers, verify = false)
val tsData = tsResponse.body.bytes()
if (tsData.isEmpty()) throw ErrorLoadingException("no data")
return if (isEncrypted) {
getDecrypter(encryptionData, tsData, encryptionIv)
} else {
tsData
}
}
}
@Throws
suspend fun hslLazy(
qualities: List<M3u8Helper.M3u8Stream>
): LazyHlsDownloadData {
if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty")
val selected = selectBest(qualities) ?: qualities.first()
val headers = selected.headers val headers = selected.headers
val streams = qualities.map { m3u8Generation(it, false) }.flatten() val streams = qualities.map { m3u8Generation(it, false) }.flatten()
//val sslVerification = if (headers.containsKey("ssl_verification")) headers["ssl_verification"].toBoolean() else true // this selects the best quality of the qualities offered,
// due to the recursive nature of m3u8, we only go 2 depth
val secondSelection = selectBest(streams.ifEmpty { listOf(selected) }) val secondSelection = selectBest(streams.ifEmpty { listOf(selected) })
if (secondSelection != null) { ?: throw IllegalArgumentException("qualities has no streams")
val m3u8Response =
runBlocking {
app.get(
secondSelection.streamUrl,
headers = headers,
verify = false
).text
}
var encryptionUri: String? val m3u8Response =
var encryptionIv = byteArrayOf() app.get(
var encryptionData = byteArrayOf() secondSelection.streamUrl,
headers = headers,
verify = false
).text
val encryptionState = isEncrypted(m3u8Response) println("m3u8Response=$m3u8Response")
if (encryptionState) { // encryption, this is because crunchy uses it
val match = var encryptionIv = byteArrayOf()
ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.destructured // its safe to assume that its not going to be null var encryptionData = byteArrayOf()
encryptionUri = match.component2()
if (isNotCompleteUrl(encryptionUri)) { val encryptionState = isEncrypted(m3u8Response)
encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri"
}
encryptionIv = match.component3().toByteArray() if (encryptionState) {
val encryptionKeyResponse = // its safe to assume that its not going to be null
runBlocking { app.get(encryptionUri, headers = headers, verify = false) } val match =
encryptionData = encryptionKeyResponse.body?.bytes() ?: byteArrayOf() ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.groupValues
var encryptionUri = match[1]
if (isNotCompleteUrl(encryptionUri)) {
encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri"
} }
val allTs = TS_EXTENSION_REGEX.findAll(m3u8Response) encryptionIv = match[2].toByteArray()
val allTsList = allTs.toList() val encryptionKeyResponse = app.get(encryptionUri, headers = headers, verify = false)
val totalTs = allTsList.size encryptionData = encryptionKeyResponse.body.bytes()
if (totalTs == 0) {
return listOf(HlsDownloadData(byteArrayOf(), 1, 1, true)).iterator()
}
var lastYield = 0
val relativeUrl = getParentLink(secondSelection.streamUrl)
var retries = 0
val tsByteGen = sequence {
loop@ for ((index, ts) in allTs.withIndex()) {
val url = if (
isNotCompleteUrl(ts.destructured.component1())
) "$relativeUrl/${ts.destructured.component1()}" else ts.destructured.component1()
val c = index + 1 + startIndex
while (lastYield != c) {
try {
val tsResponse =
runBlocking { app.get(url, headers = headers, verify = false) }
var tsData = tsResponse.body?.bytes() ?: byteArrayOf()
if (encryptionState) {
tsData = getDecrypter(encryptionData, tsData, encryptionIv)
yield(HlsDownloadData(tsData, c, totalTs))
lastYield = c
break
}
yield(HlsDownloadData(tsData, c, totalTs))
lastYield = c
} catch (e: Exception) {
logError(e)
if (retries == 3) {
yield(HlsDownloadData(byteArrayOf(), c, totalTs, true))
break@loop
}
++retries
Thread.sleep(2_000)
}
}
}
}
return tsByteGen.iterator()
} }
return listOf(HlsDownloadData(byteArrayOf(), 1, 1, true)).iterator() val relativeUrl = getParentLink(secondSelection.streamUrl)
val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts ->
val value = ts.groupValues[1]
if (isNotCompleteUrl(value)) {
"$relativeUrl/${value}"
} else {
value
}
}.toList()
if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty")
return LazyHlsDownloadData(
encryptionData = encryptionData,
encryptionIv = encryptionIv,
isEncrypted = encryptionState,
allTsLinks = allTsList,
relativeUrl = relativeUrl,
headers = headers
)
} }
} }

View file

@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.Data import androidx.work.Data
@ -32,18 +31,15 @@ import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.services.VideoDownloadService
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import java.io.BufferedInputStream import java.io.BufferedInputStream
@ -51,11 +47,9 @@ import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.lang.Thread.sleep import java.lang.Thread.sleep
import java.net.URI
import java.net.URL import java.net.URL
import java.net.URLConnection import java.net.URLConnection
import java.util.* import java.util.*
import kotlin.math.roundToInt
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
const val DOWNLOAD_CHANNEL_NAME = "Downloads" const val DOWNLOAD_CHANNEL_NAME = "Downloads"
@ -92,7 +86,7 @@ object VideoDownloadManager {
@DrawableRes @DrawableRes
const val pressToStopIcon = R.drawable.exo_icon_stop const val pressToStopIcon = R.drawable.exo_icon_stop
private var updateCount : Int = 0 private var updateCount: Int = 0
private val downloadDataUpdateCount = MutableLiveData<Int>() private val downloadDataUpdateCount = MutableLiveData<Int>()
enum class DownloadType { enum class DownloadType {
@ -687,7 +681,8 @@ object VideoDownloadManager {
return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream)
} }
fun downloadThing( @Throws
suspend fun downloadThing(
context: Context, context: Context,
link: IDownloadableMinimum, link: IDownloadableMinimum,
name: String, name: String,
@ -696,9 +691,9 @@ object VideoDownloadManager {
tryResume: Boolean, tryResume: Boolean,
parentId: Int?, parentId: Int?,
createNotificationCallback: (CreateNotificationMetadata) -> Unit, createNotificationCallback: (CreateNotificationMetadata) -> Unit,
): Int { ): Int = withContext(Dispatchers.IO) {
if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) {
return ERROR_UNKNOWN return@withContext ERROR_UNKNOWN
} }
val basePath = context.getBasePath() val basePath = context.getBasePath()
@ -714,7 +709,7 @@ object VideoDownloadManager {
} }
val stream = setupStream(context, name, relativePath, extension, tryResume) val stream = setupStream(context, name, relativePath, extension, tryResume)
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode
val resume = stream.resume!! val resume = stream.resume!!
val fileStream = stream.fileStream!! val fileStream = stream.fileStream!!
@ -766,7 +761,7 @@ object VideoDownloadManager {
} }
val bytesTotal = contentLength + resumeLength val bytesTotal = contentLength + resumeLength
if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG if (extension == "mp4" && bytesTotal < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
parentId?.let { parentId?.let {
setKey( setKey(
@ -845,11 +840,13 @@ object VideoDownloadManager {
DownloadActionType.Pause -> { DownloadActionType.Pause -> {
isPaused = true; updateNotification() isPaused = true; updateNotification()
} }
DownloadActionType.Stop -> { DownloadActionType.Stop -> {
isStopped = true; updateNotification() isStopped = true; updateNotification()
removeKey(KEY_RESUME_PACKAGES, event.first.toString()) removeKey(KEY_RESUME_PACKAGES, event.first.toString())
saveQueue() saveQueue()
} }
DownloadActionType.Resume -> { DownloadActionType.Resume -> {
isPaused = false; updateNotification() isPaused = false; updateNotification()
} }
@ -917,15 +914,17 @@ object VideoDownloadManager {
} }
// RETURN MESSAGE // RETURN MESSAGE
return when { return@withContext when {
isFailed -> { isFailed -> {
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
ERROR_CONNECTION_ERROR ERROR_CONNECTION_ERROR
} }
isStopped -> { isStopped -> {
parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) }
deleteFile() deleteFile()
} }
else -> { else -> {
parentId?.let { id -> parentId?.let { id ->
downloadProgressEvent.invoke( downloadProgressEvent.invoke(
@ -989,6 +988,7 @@ object VideoDownloadManager {
found.delete() found.delete()
this.createDirectory(directoryName) this.createDirectory(directoryName)
} }
this.isDirectory -> this.createDirectory(directoryName) this.isDirectory -> this.createDirectory(directoryName)
else -> this.parentFile?.createDirectory(directoryName) else -> this.parentFile?.createDirectory(directoryName)
} }
@ -1107,7 +1107,8 @@ object VideoDownloadManager {
return SUCCESS_STOPPED return SUCCESS_STOPPED
} }
private fun downloadHLS( @Throws
private suspend fun downloadHLS(
context: Context, context: Context,
link: ExtractorLink, link: ExtractorLink,
name: String, name: String,
@ -1115,16 +1116,8 @@ object VideoDownloadManager {
parentId: Int?, parentId: Int?,
startIndex: Int?, startIndex: Int?,
createNotificationCallback: (CreateNotificationMetadata) -> Unit createNotificationCallback: (CreateNotificationMetadata) -> Unit
): Int { ): Int = withContext(Dispatchers.IO) {
val extension = "mp4" val extension = "mp4"
fun logcatPrint(vararg items: Any?) {
items.forEach {
println("[HLS]: $it")
}
}
val m3u8Helper = M3u8Helper()
logcatPrint("initialised the HLS downloader.")
val m3u8 = M3u8Helper.M3u8Stream( val m3u8 = M3u8Helper.M3u8Stream(
link.url, link.quality, mapOf("referer" to link.referer) link.url, link.quality, mapOf("referer" to link.referer)
@ -1139,54 +1132,40 @@ object VideoDownloadManager {
) else folder ) else folder
val stream = setupStream(context, name, relativePath, extension, realIndex > 0) val stream = setupStream(context, name, relativePath, extension, realIndex > 0)
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode
if (!stream.resume!!) realIndex = 0 if (stream.resume != true) realIndex = 0
val fileLengthAdd = stream.fileLength!! val fileLengthAdd = stream.fileLength ?: 0
val tsIterator = runBlocking { val items = M3u8Helper2.hslLazy(listOf(m3u8))
m3u8Helper.hlsYield(listOf(m3u8), realIndex)
}
val displayName = getDisplayName(name, extension) val displayName = getDisplayName(name, extension)
val fileStream = stream.fileStream!! val fileStream = stream.fileStream!!
val firstTs = tsIterator.next()
var isDone = false var isDone = false
var isFailed = false var isFailed = false
var isPaused = false var isPaused = false
var bytesDownloaded = firstTs.bytes.size.toLong() + fileLengthAdd var bytesDownloaded = fileLengthAdd
var tsProgress = 1L + realIndex var tsProgress: Long = realIndex.toLong() + 1 // we don't want div by zero
val totalTs = firstTs.totalTs.toLong() val totalTs: Long = items.size.toLong()
fun deleteFile(): Int { fun deleteFile(): Int {
return delete(context, name, relativePath, extension, parentId, basePath.first) return delete(context, name, relativePath, extension, parentId, basePath.first)
} }
/*
Most of the auto generated m3u8 out there have TS of the same size.
And only the last TS might have a different size.
But oh well, in cases of handmade m3u8 streams this will go all over the place ¯\_()_/¯
So ya, this calculates an estimate of how many bytes the file is going to be.
> (bytesDownloaded/tsProgress)*totalTs
*/
fun updateInfo() { fun updateInfo() {
parentId?.let { setKey(
setKey( KEY_DOWNLOAD_INFO,
KEY_DOWNLOAD_INFO, (parentId ?: return).toString(),
it.toString(), DownloadedFileInfo(
DownloadedFileInfo( // approx bytes
(bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), totalBytes = (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(),
relativePath ?: "", relativePath = relativePath ?: "",
displayName, displayName = displayName,
tsProgress.toString(), extraInfo = tsProgress.toString(),
basePath = basePath.second basePath = basePath.second
)
) )
} )
} }
updateInfo() updateInfo()
@ -1210,9 +1189,7 @@ object VideoDownloadManager {
(bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(),
) )
) )
} catch (e: Exception) { } catch (_: Throwable) {}
// IDK MIGHT ERROR
}
} }
createNotificationCallback.invoke( createNotificationCallback.invoke(
@ -1226,24 +1203,6 @@ object VideoDownloadManager {
) )
} }
fun stopIfError(ts: M3u8Helper.HlsDownloadData): Int? {
if (ts.errored || ts.bytes.isEmpty()) {
val error: Int = if (!ts.errored) {
logcatPrint("Error: No stream was found.")
ERROR_UNKNOWN
} else {
logcatPrint("Error: Failed to fetch data.")
ERROR_CONNECTION_ERROR
}
isFailed = true
fileStream.close()
deleteFile()
updateNotification()
return error
}
return null
}
val notificationCoroutine = main { val notificationCoroutine = main {
while (true) { while (true) {
if (!isDone) { if (!isDone) {
@ -1261,11 +1220,11 @@ object VideoDownloadManager {
DownloadActionType.Stop -> { DownloadActionType.Stop -> {
isFailed = true isFailed = true
} }
DownloadActionType.Pause -> { DownloadActionType.Pause -> {
isPaused = isPaused = true
true // Pausing is not supported since well...I need to know the index of the ts it was paused at
// it may be possible to store it in a variable, but when the app restarts it will be lost
} }
DownloadActionType.Resume -> { DownloadActionType.Resume -> {
isPaused = false isPaused = false
} }
@ -1278,32 +1237,22 @@ object VideoDownloadManager {
try { try {
if (parentId != null) if (parentId != null)
downloadEvent -= downloadEventListener downloadEvent -= downloadEventListener
} catch (e: Exception) { } catch (t: Throwable) {
logError(e) logError(t)
} }
try { try {
parentId?.let { parentId?.let {
downloadStatus.remove(it) downloadStatus.remove(it)
} }
} catch (e: Exception) { } catch (t: Throwable) {
logError(e) logError(t)
// IDK MIGHT ERROR
} }
notificationCoroutine.cancel() notificationCoroutine.cancel()
} }
stopIfError(firstTs).let {
if (it != null) {
closeAll()
return it
}
}
if (parentId != null) if (parentId != null)
downloadEvent += downloadEventListener downloadEvent += downloadEventListener
fileStream.write(firstTs.bytes)
fun onFailed() { fun onFailed() {
fileStream.close() fileStream.close()
deleteFile() deleteFile()
@ -1311,31 +1260,29 @@ object VideoDownloadManager {
closeAll() closeAll()
} }
for (ts in tsIterator) { for (idx in realIndex until items.size) {
while (isPaused) { while (isPaused) {
if (isFailed) { if (isFailed) {
onFailed() onFailed()
return SUCCESS_STOPPED return@withContext SUCCESS_STOPPED
} }
sleep(100) delay(100)
} }
if (isFailed) { if (isFailed) {
onFailed() onFailed()
return SUCCESS_STOPPED return@withContext SUCCESS_STOPPED
} }
stopIfError(ts).let { val bytes = items.resolveLinkSafe(idx) ?: run {
if (it != null) { isFailed = true
closeAll() onFailed()
return it return@withContext ERROR_CONNECTION_ERROR
}
} }
fileStream.write(ts.bytes) fileStream.write(bytes)
tsProgress = ts.currentIndex.toLong() tsProgress = idx.toLong() + 1
bytesDownloaded += ts.bytes.size.toLong() bytesDownloaded += bytes.size.toLong()
logcatPrint("Download progress ${((tsProgress.toFloat() / totalTs.toFloat()) * 100).roundToInt()}%")
updateInfo() updateInfo()
} }
isDone = true isDone = true
@ -1344,7 +1291,7 @@ object VideoDownloadManager {
closeAll() closeAll()
updateInfo() updateInfo()
return SUCCESS_DOWNLOAD_DONE return@withContext SUCCESS_DOWNLOAD_DONE
} }
fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String {
@ -1379,7 +1326,7 @@ object VideoDownloadManager {
) )
} }
private fun downloadSingleEpisode( private suspend fun downloadSingleEpisode(
context: Context, context: Context,
source: String?, source: String?,
folder: String?, folder: String?,
@ -1405,25 +1352,29 @@ object VideoDownloadManager {
null null
)?.extraInfo?.toIntOrNull() )?.extraInfo?.toIntOrNull()
} else null } else null
return downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> return suspendSafeApiCall {
main { downloadHLS(context, link, name, folder, ep.id, startIndex) { meta ->
createNotification( main {
context, createNotification(
source, context,
link.name, source,
ep, link.name,
meta.type, ep,
meta.bytesDownloaded, meta.type,
meta.bytesTotal, meta.bytesDownloaded,
notificationCallback, meta.bytesTotal,
meta.hlsProgress, notificationCallback,
meta.hlsTotal meta.hlsProgress,
) meta.hlsTotal
)
}
} }
}.also { extractorJob.cancel() } }.also {
extractorJob.cancel()
} ?: ERROR_UNKNOWN
} }
return normalSafeApiCall { return suspendSafeApiCall {
downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta -> downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta ->
main { main {
createNotification( createNotification(
@ -1468,17 +1419,15 @@ object VideoDownloadManager {
DownloadResumePackage(item, index) DownloadResumePackage(item, index)
) )
val connectionResult = withContext(Dispatchers.IO) { val connectionResult = withContext(Dispatchers.IO) {
normalSafeApiCall { downloadSingleEpisode(
downloadSingleEpisode( context,
context, item.source,
item.source, item.folder,
item.folder, item.ep,
item.ep, link,
link, notificationCallback,
notificationCallback, resume
resume ).also { println("Single episode finished with return code: $it") }
).also { println("Single episode finished with return code: $it") }
}
} }
if (connectionResult != null && connectionResult > 0) { // SUCCESS if (connectionResult != null && connectionResult > 0) { // SUCCESS
removeKey(KEY_RESUME_PACKAGES, id.toString()) removeKey(KEY_RESUME_PACKAGES, id.toString())