mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
fuck it we ball, m3u8 download is now fixed
This commit is contained in:
parent
c2b951a078
commit
590c74111c
3 changed files with 237 additions and 276 deletions
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in a new issue