idk stuff not working

This commit is contained in:
LagradOst 2021-09-01 23:30:21 +02:00
parent 09a0e8a6c1
commit dffa7a39c4
5 changed files with 211 additions and 241 deletions

View file

@ -19,8 +19,6 @@ class AsiaFlixProvider : MainAPI() {
get() = false get() = false
override val hasMainPage: Boolean override val hasMainPage: Boolean
get() = true get() = true
override val hasDownloadSupport: Boolean
get() = false
override val hasChromecastSupport: Boolean override val hasChromecastSupport: Boolean
get() = false get() = false

View file

@ -10,8 +10,6 @@ class HDMProvider : MainAPI() {
get() = "HD Movies" get() = "HD Movies"
override val mainUrl: String override val mainUrl: String
get() = "https://hdm.to" get() = "https://hdm.to"
override val hasDownloadSupport: Boolean
get() = false
override val supportedTypes: Set<TvType> override val supportedTypes: Set<TvType>
get() = setOf( get() = setOf(

View file

@ -197,7 +197,7 @@ class TrailersToProvider : MainAPI() {
return false return false
} }
override fun load(url: String): LoadResponse? { override fun load(url: String): LoadResponse {
val response = khttp.get(url) val response = khttp.get(url)
val document = Jsoup.parse(response.text) val document = Jsoup.parse(response.text)
val metaInfo = document.select("div.post-info-meta > ul.post-info-meta-list > li") val metaInfo = document.select("div.post-info-meta > ul.post-info-meta-list > li")
@ -225,7 +225,7 @@ class TrailersToProvider : MainAPI() {
val isTvShow = url.contains("/tvshow/") val isTvShow = url.contains("/tvshow/")
if (isTvShow) { if (isTvShow) {
val episodes = document.select("#seasons-accordion .card-body > .tour-modern") ?: return null val episodes = document.select("#seasons-accordion .card-body > .tour-modern") ?: throw ErrorLoadingException("No Episodes found")
val parsedEpisodes = episodes.withIndex().map { (index, item) -> val parsedEpisodes = episodes.withIndex().map { (index, item) ->
val epPoster = item.selectFirst("img").attr("src") val epPoster = item.selectFirst("img").attr("src")
val main = item.selectFirst(".tour-modern-main") val main = item.selectFirst(".tour-modern-main")
@ -283,7 +283,7 @@ class TrailersToProvider : MainAPI() {
} else "" } else ""
val data = mapper.writeValueAsString( val data = mapper.writeValueAsString(
Pair(subUrl, fixUrl(document.selectFirst("content")?.attr("data-url") ?: return null)) Pair(subUrl, fixUrl(document?.selectFirst("content")?.attr("data-url") ?: throw ErrorLoadingException("Link not found")))
) )
return MovieLoadResponse( return MovieLoadResponse(
title, title,

View file

@ -60,9 +60,11 @@ class M3u8Helper {
) )
private fun selectBest(qualities: List<M3u8Stream>): M3u8Stream? { private fun selectBest(qualities: List<M3u8Stream>): M3u8Stream? {
val result = qualities.sortedBy { if (it.quality != null && it.quality <= 1080) it.quality else 0 val result = qualities.sortedBy {
if (it.quality != null && it.quality <= 1080) it.quality else 0
}.reversed().filter { }.reversed().filter {
listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) it.streamUrl.contains(".m3u8")
// listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl))
} }
return result.getOrNull(0) return result.getOrNull(0)
} }
@ -80,7 +82,7 @@ class M3u8Helper {
public fun m3u8Generation(m3u8: M3u8Stream): List<M3u8Stream> { public fun m3u8Generation(m3u8: M3u8Stream): List<M3u8Stream> {
val generate = sequence { val generate = sequence {
val m3u8Parent = getParentLink(m3u8.streamUrl) val m3u8Parent = getParentLink(m3u8.streamUrl)
val response = khttp.get(m3u8.streamUrl, headers=m3u8.headers) val response = khttp.get(m3u8.streamUrl, headers = m3u8.headers)
for (match in QUALITY_REGEX.findAll(response.text)) { for (match in QUALITY_REGEX.findAll(response.text)) {
var (quality, m3u8Link) = match.destructured var (quality, m3u8Link) = match.destructured
@ -117,7 +119,7 @@ class M3u8Helper {
val errored: Boolean = false val errored: Boolean = false
) )
public fun hlsYield(qualities: List<M3u8Stream>): Iterator<HlsDownloadData> { fun hlsYield(qualities: List<M3u8Stream>, startIndex: Int = 0): Iterator<HlsDownloadData> {
if (qualities.isEmpty()) return listOf<HlsDownloadData>(HlsDownloadData(byteArrayOf(), 0, 0, true)).iterator() if (qualities.isEmpty()) return listOf<HlsDownloadData>(HlsDownloadData(byteArrayOf(), 0, 0, true)).iterator()
var selected = selectBest(qualities) var selected = selectBest(qualities)
@ -127,21 +129,22 @@ class M3u8Helper {
val headers = selected.headers val headers = selected.headers
val streams = qualities.map { m3u8Generation(it) }.flatten() val streams = qualities.map { m3u8Generation(it) }.flatten()
val sslVerification = if (headers.containsKey("ssl_verification")) headers["ssl_verification"].toBoolean() else true //val sslVerification = if (headers.containsKey("ssl_verification")) headers["ssl_verification"].toBoolean() else true
val secondSelection = selectBest(streams.ifEmpty { listOf(selected) }) val secondSelection = selectBest(streams.ifEmpty { listOf(selected) })
if (secondSelection != null) { if (secondSelection != null) {
val m3u8Response = khttp.get(secondSelection.streamUrl, headers=headers) val m3u8Response = khttp.get(secondSelection.streamUrl, headers = headers)
val m3u8Data = m3u8Response.text val m3u8Data = m3u8Response.text
var encryptionUri: String? = null var encryptionUri: String? = null
var encryptionIv = byteArrayOf() var encryptionIv = byteArrayOf()
var encryptionData= byteArrayOf() var encryptionData = byteArrayOf()
val encryptionState = isEncrypted(m3u8Data) val encryptionState = isEncrypted(m3u8Data)
if (encryptionState) { if (encryptionState) {
val match = ENCRYPTION_URL_IV_REGEX.find(m3u8Data)!!.destructured // its safe to assume that its not going to be null val match =
ENCRYPTION_URL_IV_REGEX.find(m3u8Data)!!.destructured // its safe to assume that its not going to be null
encryptionUri = match.component2() encryptionUri = match.component2()
if (!isCompleteUrl(encryptionUri)) { if (!isCompleteUrl(encryptionUri)) {
@ -149,29 +152,30 @@ class M3u8Helper {
} }
encryptionIv = match.component3().toByteArray() encryptionIv = match.component3().toByteArray()
val encryptionKeyResponse = khttp.get(encryptionUri, headers=headers) val encryptionKeyResponse = khttp.get(encryptionUri, headers = headers)
encryptionData = encryptionKeyResponse.content encryptionData = encryptionKeyResponse.content
} }
val allTs = TS_EXTENSION_REGEX.findAll(m3u8Data) val allTs = TS_EXTENSION_REGEX.findAll(m3u8Data)
val totalTs = allTs.toList().size val allTsList = allTs.toList()
val totalTs =allTsList .size
if (totalTs == 0) { if (totalTs == 0) {
return listOf<HlsDownloadData>(HlsDownloadData(byteArrayOf(), 0, 0, true)).iterator() return listOf(HlsDownloadData(byteArrayOf(), 0, 0, true)).iterator()
} }
var lastYield = 0 var lastYield = 0
val relativeUrl = getParentLink(secondSelection.streamUrl) val relativeUrl = getParentLink(secondSelection.streamUrl)
var retries = 0 var retries = 0
val tsByteGen = sequence<HlsDownloadData> { val tsByteGen = sequence {
loop@ for ((index, ts) in allTs.withIndex()) { loop@ for ((index, ts) in allTs.withIndex()) {
val url = if ( val url = if (
isCompleteUrl(ts.destructured.component1()) isCompleteUrl(ts.destructured.component1())
) ts.destructured.component1() else "$relativeUrl/${ts.destructured.component1()}" ) ts.destructured.component1() else "$relativeUrl/${ts.destructured.component1()}"
val c = index+1 val c = index + 1 + startIndex
while (lastYield != c) { while (lastYield != c) {
try { try {
val tsResponse = khttp.get(url, headers=headers) val tsResponse = khttp.get(url, headers = headers)
var tsData = tsResponse.content var tsData = tsResponse.content
if (encryptionState) { if (encryptionState) {
@ -196,6 +200,6 @@ class M3u8Helper {
} }
return tsByteGen.iterator() return tsByteGen.iterator()
} }
return listOf<HlsDownloadData>(HlsDownloadData(byteArrayOf(), 0, 0, true)).iterator() return listOf(HlsDownloadData(byteArrayOf(), 0, 0, true)).iterator()
} }
} }

View file

@ -1,7 +1,10 @@
package com.lagradost.cloudstream3.utils package com.lagradost.cloudstream3.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.* import android.app.Activity
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.* import android.content.*
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
@ -31,8 +34,6 @@ 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.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.M3u8Helper
import kotlin.math.roundToInt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -41,6 +42,7 @@ import java.lang.Thread.sleep
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"
@ -114,7 +116,7 @@ object VideoDownloadManager {
val source: String?, val source: String?,
val folder: String?, val folder: String?,
val ep: DownloadEpisodeMetadata, val ep: DownloadEpisodeMetadata,
val links: List<ExtractorLink> val links: List<ExtractorLink>,
) )
data class DownloadResumePackage( data class DownloadResumePackage(
@ -126,7 +128,7 @@ object VideoDownloadManager {
val totalBytes: Long, val totalBytes: Long,
val relativePath: String, val relativePath: String,
val displayName: String, val displayName: String,
val extraData : String? = null, val extraInfo: String? = null
) )
data class DownloadedFileInfoResult( data class DownloadedFileInfoResult(
@ -141,6 +143,7 @@ object VideoDownloadManager {
) )
private const val SUCCESS_DOWNLOAD_DONE = 1 private const val SUCCESS_DOWNLOAD_DONE = 1
private const val SUCCESS_STREAM = 3
private const val SUCCESS_STOPPED = 2 private const val SUCCESS_STOPPED = 2
private const val ERROR_DELETING_FILE = 3 // will not download the next one, but is still classified as an error private const val ERROR_DELETING_FILE = 3 // will not download the next one, but is still classified as an error
private const val ERROR_CREATE_FILE = -2 private const val ERROR_CREATE_FILE = -2
@ -514,25 +517,27 @@ object VideoDownloadManager {
} }
} }
private fun downloadTorrent( data class StreamData(
val errorCode: Int,
val resume: Boolean? = null,
val fileLength: Long? = null,
val fileStream: OutputStream? = null,
)
private fun setupStream(
context: Context, context: Context,
link: String,
name: String, name: String,
folder: String?, folder: String?,
extension: String, extension: String,
//tryResume: Boolean = false, tryResume: Boolean,
parentId: Int?, ): StreamData {
createNotificationCallback: (CreateNotificationMetadata) -> Unit val relativePath = getRelativePath(folder)
): Int { val displayName = getDisplayName(name, extension)
val relativePath = (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
val displayName = "$name.$extension"
val fileStream: OutputStream val fileStream: OutputStream
val fileLength: Long val fileLength: Long
val resume = false var resume = tryResume
val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
if (isScopedStorage()) { if (isScopedStorage()) {
val cr = context.contentResolver ?: return ERROR_CONTENT_RESOLVER_NOT_FOUND val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
val currentExistingFile = val currentExistingFile =
cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH
@ -573,30 +578,47 @@ object VideoDownloadManager {
cr.insert( cr.insert(
contentUri, contentUri,
newFile newFile
) ?: return ERROR_MEDIA_STORE_URI_CANT_BE_CREATED ) ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
} }
fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else "")) fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else ""))
?: return ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
} else { } else {
val normalPath = getNormalPath(relativePath, displayName)
// NORMAL NON SCOPED STORAGE FILE CREATION // NORMAL NON SCOPED STORAGE FILE CREATION
val rFile = File(normalPath) val rFile = File(normalPath)
if (!rFile.exists()) { if (!rFile.exists()) {
fileLength = 0 fileLength = 0
rFile.parentFile?.mkdirs() rFile.parentFile?.mkdirs()
if (!rFile.createNewFile()) return ERROR_CREATE_FILE if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
} else { } else {
if (resume) { if (resume) {
fileLength = rFile.length() fileLength = rFile.length()
} else { } else {
fileLength = 0 fileLength = 0
rFile.parentFile?.mkdirs() rFile.parentFile?.mkdirs()
if (!rFile.delete()) return ERROR_DELETING_FILE if (!rFile.delete()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
if (!rFile.createNewFile()) return ERROR_CREATE_FILE if (!rFile.createNewFile()) return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND)
} }
} }
fileStream = FileOutputStream(rFile, false) fileStream = FileOutputStream(rFile, false)
} }
if (fileLength == 0L) resume = false
return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream)
}
private fun downloadTorrent(
context: Context,
link: String,
name: String,
folder: String?,
extension: String,
//tryResume: Boolean = false,
parentId: Int?,
createNotificationCallback: (CreateNotificationMetadata) -> Unit
): Int {
val stream = setupStream(context, name, folder, extension, false)
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode
val torrentOptions: TorrentOptions = TorrentOptions.Builder() val torrentOptions: TorrentOptions = TorrentOptions.Builder()
.saveLocation(context.cacheDir.absolutePath) .saveLocation(context.cacheDir.absolutePath)
@ -737,8 +759,10 @@ object VideoDownloadManager {
SUCCESS_STOPPED SUCCESS_STOPPED
} }
isDone -> { isDone -> {
stream.fileStream?.let { fileStream ->
torrentStream?.currentTorrent?.videoStream?.copyTo(fileStream) torrentStream?.currentTorrent?.videoStream?.copyTo(fileStream)
torrentStream?.currentTorrent?.videoFile?.delete() torrentStream?.currentTorrent?.videoFile?.delete()
}
SUCCESS_DOWNLOAD_DONE SUCCESS_DOWNLOAD_DONE
} }
@ -760,100 +784,33 @@ object VideoDownloadManager {
createNotificationCallback: (CreateNotificationMetadata) -> Unit createNotificationCallback: (CreateNotificationMetadata) -> Unit
): Int { ): Int {
if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) {
return normalSafeApiCall { downloadTorrent(context, link.url, name, folder, extension, parentId, createNotificationCallback) } ?: ERROR_UNKNOWN return normalSafeApiCall {
downloadTorrent(
context,
link.url,
name,
folder,
extension,
parentId,
createNotificationCallback
)
} ?: ERROR_UNKNOWN
} }
val relativePath = (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar) val relativePath = getRelativePath(folder)
val displayName = "$name.$extension" val displayName = getDisplayName(name, extension)
val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
var resume = tryResume
val fileStream: OutputStream
val fileLength: Long
fun deleteFile(): Int { fun deleteFile(): Int {
if (isScopedStorage()) { return delete(context, name, folder, extension, parentId)
val lastContent = context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
if (lastContent != null) {
context.contentResolver.delete(lastContent, null, null)
}
} else {
if (!File(normalPath).delete()) return ERROR_DELETING_FILE
}
parentId?.let {
downloadDeleteEvent.invoke(parentId)
}
return SUCCESS_STOPPED
} }
if (isScopedStorage()) { val stream = setupStream(context, name, folder, extension, tryResume)
val cr = context.contentResolver ?: return ERROR_CONTENT_RESOLVER_NOT_FOUND if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode
val currentExistingFile = val resume = stream.resume!!
cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH val fileStream = stream.fileStream!!
val fileLength = stream.fileLength!!
fileLength =
if (currentExistingFile == null || !resume) 0 else (cr.getFileLength(currentExistingFile)
?: 0)// IF NOT RESUME THEN 0, OTHERWISE THE CURRENT FILE SIZE
if (!resume && currentExistingFile != null) { // DELETE FILE IF FILE EXITS AND NOT RESUME
val rowsDeleted = context.contentResolver.delete(currentExistingFile, null, null)
if (rowsDeleted < 1) {
println("ERROR DELETING FILE!!!")
}
}
var appendFile = false
val newFileUri = if (resume && currentExistingFile != null) {
appendFile = true
currentExistingFile
} else {
val contentUri =
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
//val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
val currentMimeType = when (extension) {
"vtt" -> "text/vtt"
"mp4" -> "video/mp4"
"srt" -> "text/plain"
else -> null
}
val newFile = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.TITLE, name)
if (currentMimeType != null)
put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
}
cr.insert(
contentUri,
newFile
) ?: return ERROR_MEDIA_STORE_URI_CANT_BE_CREATED
}
fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else ""))
?: return ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM
} else {
// NORMAL NON SCOPED STORAGE FILE CREATION
val rFile = File(normalPath)
if (!rFile.exists()) {
fileLength = 0
rFile.parentFile?.mkdirs()
if (!rFile.createNewFile()) return ERROR_CREATE_FILE
} else {
if (resume) {
fileLength = rFile.length()
} else {
fileLength = 0
rFile.parentFile?.mkdirs()
if (!rFile.delete()) return ERROR_DELETING_FILE
if (!rFile.createNewFile()) return ERROR_CREATE_FILE
}
}
fileStream = FileOutputStream(rFile, false)
}
if (fileLength == 0L) resume = false
// CONNECT // CONNECT
val connection: URLConnection = URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK val connection: URLConnection = URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK
@ -1049,14 +1006,52 @@ object VideoDownloadManager {
} }
} }
private fun getRelativePath(folder: String?): String {
return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
}
private fun getDisplayName(name: String, extension: String): String {
return "$name.$extension"
}
private fun getNormalPath(relativePath: String, displayName: String): String {
return "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
}
private fun delete(
context: Context,
name: String,
folder: String?,
extension: String,
parentId: Int?,
): Int {
val relativePath = getRelativePath(folder)
val displayName = getDisplayName(name, extension)
if (isScopedStorage()) {
val lastContent = context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
if (lastContent != null) {
context.contentResolver.delete(lastContent, null, null)
}
} else {
if (!File(getNormalPath(relativePath, displayName)).delete()) return ERROR_DELETING_FILE
}
parentId?.let {
downloadDeleteEvent.invoke(parentId)
}
return SUCCESS_STOPPED
}
private fun downloadHLS( private fun downloadHLS(
context: Context, context: Context,
link: ExtractorLink, link: ExtractorLink,
name: String, name: String,
folder: String?, folder: String?,
parentId: Int?, parentId: Int?,
startIndex: Int?,
createNotificationCallback: (CreateNotificationMetadata) -> Unit createNotificationCallback: (CreateNotificationMetadata) -> Unit
): Int { ): Int {
val extension = "mp4"
fun logcatPrint(vararg items: Any?) { fun logcatPrint(vararg items: Any?) {
items.forEach { items.forEach {
println("[HLS]: $it") println("[HLS]: $it")
@ -1066,93 +1061,40 @@ object VideoDownloadManager {
val m3u8Helper = M3u8Helper() val m3u8Helper = M3u8Helper()
logcatPrint("initialised the HLS downloader.") logcatPrint("initialised the HLS downloader.")
val m3u8 = M3u8Helper.M3u8Stream(link.url, when (link.quality) { val m3u8 = M3u8Helper.M3u8Stream(
link.url, when (link.quality) {
-2 -> 360 -2 -> 360
-1 -> 480 -1 -> 480
1 -> 720 1 -> 720
2 -> 1080 2 -> 1080
else -> null else -> null
}, mapOf("referer" to link.referer)) }, mapOf("referer" to link.referer)
val tsIterator = m3u8Helper.hlsYield(listOf(m3u8)) )
val relativePath = (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar) var realIndex = startIndex ?: 0
val displayName = "$name.ts" val stream = setupStream(context, name, folder, extension, realIndex > 0)
if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode
val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName" if (!stream.resume!!) realIndex = 0
val tsIterator = m3u8Helper.hlsYield(listOf(m3u8), realIndex)
val fileStream: OutputStream val relativePath = getRelativePath(folder)
val fileLength: Long val displayName = getDisplayName(name, extension)
fun deleteFile(): Int { val fileStream = stream.fileStream!!
if (isScopedStorage()) {
val lastContent = context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
if (lastContent != null) {
context.contentResolver.delete(lastContent, null, null)
}
} else {
if (!File(normalPath).delete()) return ERROR_DELETING_FILE
}
parentId?.let {
downloadDeleteEvent.invoke(parentId)
}
return SUCCESS_STOPPED
}
if (isScopedStorage()) {
val cr = context.contentResolver ?: return ERROR_CONTENT_RESOLVER_NOT_FOUND
val currentExistingFile =
cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH
if (currentExistingFile != null) { // DELETE FILE IF FILE EXITS
val rowsDeleted = context.contentResolver.delete(currentExistingFile, null, null)
if (rowsDeleted < 1) {
println("ERROR DELETING FILE!!!")
}
}
val newFileUri = if (currentExistingFile != null) {
currentExistingFile
} else {
val contentUri =
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
//val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
val currentMimeType = "video/mp2t"
val newFile = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.TITLE, name)
put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
}
cr.insert(
contentUri,
newFile
) ?: return ERROR_MEDIA_STORE_URI_CANT_BE_CREATED
}
fileStream = cr.openOutputStream(newFileUri, "a")
?: return ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM
} else {
// NORMAL NON SCOPED STORAGE FILE CREATION
val rFile = File(normalPath)
if (!rFile.exists()) {
rFile.parentFile?.mkdirs()
if (!rFile.createNewFile()) return ERROR_CREATE_FILE
} else {
rFile.parentFile?.mkdirs()
if (!rFile.delete()) return ERROR_DELETING_FILE
if (!rFile.createNewFile()) return ERROR_CREATE_FILE
}
fileStream = FileOutputStream(rFile, false)
}
val firstTs = tsIterator.next() val firstTs = tsIterator.next()
var isDone = false var isDone = false
var isFailed = false var isFailed = false
var isPaused = false
var bytesDownloaded = firstTs.bytes.size.toLong() var bytesDownloaded = firstTs.bytes.size.toLong()
var tsProgress = 1L var tsProgress = 1L + realIndex
val totalTs = firstTs.totalTs.toLong() val totalTs = firstTs.totalTs.toLong()
fun deleteFile(): Int {
return delete(context, name, folder, extension, parentId)
}
/* /*
Most of the auto generated m3u8 out there have TS of the same size. Most of the auto generated m3u8 out there have TS of the same size.
And only the last TS might have a different size. And only the last TS might have a different size.
@ -1163,15 +1105,27 @@ object VideoDownloadManager {
> (bytesDownloaded/tsProgress)*totalTs > (bytesDownloaded/tsProgress)*totalTs
*/ */
fun updateInfo() {
parentId?.let { parentId?.let {
context.setKey(KEY_DOWNLOAD_INFO, it.toString(), DownloadedFileInfo((bytesDownloaded/tsProgress)*totalTs, relativePath, displayName)) context.setKey(
KEY_DOWNLOAD_INFO,
it.toString(),
DownloadedFileInfo(
(bytesDownloaded / tsProgress) * totalTs,
relativePath,
displayName,
tsProgress.toString()
)
)
} }
}
updateInfo()
fun updateNotification() { fun updateNotification() {
val type = when { val type = when {
isDone -> DownloadType.IsDone isDone -> DownloadType.IsDone
isFailed -> DownloadType.IsFailed isFailed -> DownloadType.IsFailed
isPaused -> DownloadType.IsPaused
else -> DownloadType.IsDownloading else -> DownloadType.IsDownloading
} }
@ -1179,19 +1133,24 @@ object VideoDownloadManager {
try { try {
downloadStatus[id] = type downloadStatus[id] = type
downloadStatusEvent.invoke(Pair(id, type)) downloadStatusEvent.invoke(Pair(id, type))
downloadProgressEvent.invoke(Triple(id, bytesDownloaded, (bytesDownloaded/tsProgress)*totalTs)) downloadProgressEvent.invoke(Triple(id, bytesDownloaded, (bytesDownloaded / tsProgress) * totalTs))
} catch (e: Exception) { } catch (e: Exception) {
// IDK MIGHT ERROR // IDK MIGHT ERROR
} }
} }
createNotificationCallback.invoke(CreateNotificationMetadata(type, bytesDownloaded, (bytesDownloaded/tsProgress)*totalTs)) createNotificationCallback.invoke(
CreateNotificationMetadata(
type,
bytesDownloaded,
(bytesDownloaded / tsProgress) * totalTs
)
)
} }
fun stopIfError(ts: M3u8Helper.HlsDownloadData): Int? { fun stopIfError(ts: M3u8Helper.HlsDownloadData): Int? {
if (ts.errored || ts.bytes.isEmpty()) { if (ts.errored || ts.bytes.isEmpty()) {
val error: Int val error: Int = if (!ts.errored) {
error = if (!ts.errored) {
logcatPrint("Error: No stream was found.") logcatPrint("Error: No stream was found.")
ERROR_UNKNOWN ERROR_UNKNOWN
} else { } else {
@ -1225,12 +1184,16 @@ object VideoDownloadManager {
isFailed = true isFailed = true
} }
DownloadActionType.Pause -> { DownloadActionType.Pause -> {
isFailed = true // Pausing is not supported since well...I need to know the index of the ts it was paused at isPaused =
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 // it may be possible to store it in a variable, but when the app restarts it will be lost
} }
else -> updateNotification() // do nothing, since well...I don't support anything else DownloadActionType.Resume -> {
isPaused = false
} }
} }
updateNotification()
}
} }
fun closeAll() { fun closeAll() {
@ -1263,14 +1226,28 @@ object VideoDownloadManager {
fileStream.write(firstTs.bytes) fileStream.write(firstTs.bytes)
for (ts in tsIterator) { fun onFailed() {
if (isFailed) {
fileStream.close() fileStream.close()
deleteFile() deleteFile()
updateNotification() updateNotification()
closeAll() closeAll()
}
for (ts in tsIterator) {
while (isPaused) {
if (isFailed) {
onFailed()
return SUCCESS_STOPPED return SUCCESS_STOPPED
} }
sleep(100)
}
if (isFailed) {
onFailed()
return SUCCESS_STOPPED
}
stopIfError(ts).let { stopIfError(ts).let {
if (it != null) { if (it != null) {
closeAll() closeAll()
@ -1281,17 +1258,15 @@ object VideoDownloadManager {
fileStream.write(ts.bytes) fileStream.write(ts.bytes)
tsProgress = ts.currentIndex.toLong() tsProgress = ts.currentIndex.toLong()
bytesDownloaded += ts.bytes.size.toLong() bytesDownloaded += ts.bytes.size.toLong()
logcatPrint("Download progress ${((tsProgress.toFloat()/totalTs.toFloat())*100).roundToInt()}%") logcatPrint("Download progress ${((tsProgress.toFloat() / totalTs.toFloat()) * 100).roundToInt()}%")
updateInfo()
} }
isDone = true isDone = true
fileStream.close() fileStream.close()
updateNotification() updateNotification()
closeAll() closeAll()
parentId?.let { updateInfo()
context.setKey(KEY_DOWNLOAD_INFO, it.toString(), DownloadedFileInfo(bytesDownloaded, relativePath, displayName))
}
return SUCCESS_DOWNLOAD_DONE return SUCCESS_DOWNLOAD_DONE
} }
@ -1305,8 +1280,11 @@ object VideoDownloadManager {
): Int { ): Int {
val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}") val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}")
if (link.isM3u8) { if (link.isM3u8 || link.url.endsWith(".m3u8")) {
return downloadHLS(context, link, name, folder, ep.id) { meta -> val startIndex = if (tryResume) {
context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, ep.id.toString(), null)?.extraInfo?.toIntOrNull()
} else null
return downloadHLS(context, link, name, folder, ep.id, startIndex) { meta ->
createNotification( createNotification(
context, context,
source, source,
@ -1390,11 +1368,7 @@ object VideoDownloadManager {
if (fileLength == 0L) return null if (fileLength == 0L) return null
return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri) return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri)
} else { } else {
val normalPath = val normalPath = getNormalPath(info.relativePath, info.displayName)
"${Environment.getExternalStorageDirectory()}${File.separatorChar}${info.relativePath}${info.displayName}".replace(
'/',
File.separatorChar
)
val dFile = File(normalPath) val dFile = File(normalPath)
if (!dFile.exists()) return null if (!dFile.exists()) return null
return DownloadedFileInfoResult(dFile.length(), info.totalBytes, dFile.toUri()) return DownloadedFileInfoResult(dFile.length(), info.totalBytes, dFile.toUri())
@ -1422,11 +1396,7 @@ object VideoDownloadManager {
return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0 return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0
} else { } else {
val normalPath = val normalPath = getNormalPath(info.relativePath, info.displayName)
"${Environment.getExternalStorageDirectory()}${File.separatorChar}${info.relativePath}${info.displayName}".replace(
'/',
File.separatorChar
)
val dFile = File(normalPath) val dFile = File(normalPath)
if (!dFile.exists()) return true if (!dFile.exists()) return true
return dFile.delete() return dFile.delete()