fix(downloader): media identifier & dash chapter selector
- fix ffmpeg crashes - fix close resources - perf http server
This commit is contained in:
@@ -26,9 +26,9 @@ class DownloadManagerClient (
|
||||
DownloadRequest(
|
||||
inputMedias = arrayOf(
|
||||
InputMedia(
|
||||
content = playlistUrl,
|
||||
type = DownloadMediaType.REMOTE_MEDIA
|
||||
)
|
||||
content = playlistUrl,
|
||||
type = DownloadMediaType.REMOTE_MEDIA
|
||||
)
|
||||
),
|
||||
dashOptions = DashOptions(offsetTime, duration),
|
||||
flags = DownloadRequest.Flags.IS_DASH_PLAYLIST
|
||||
|
||||
@@ -17,8 +17,11 @@ class Stories : Feature("Stories", loadParams = FeatureLoadParams.ACTIVITY_CREAT
|
||||
fun cancelRequest() {
|
||||
runBlocking {
|
||||
suspendCoroutine {
|
||||
context.httpServer.ensureServerStarted {
|
||||
event.url = "http://127.0.0.1:${context.httpServer.port}"
|
||||
context.httpServer.ensureServerStarted()?.let { server ->
|
||||
event.url = "http://127.0.0.1:${server.port}"
|
||||
it.resumeWith(Result.success(Unit))
|
||||
} ?: run {
|
||||
event.canceled = true
|
||||
it.resumeWith(Result.success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import me.rhunk.snapenhance.common.data.download.MediaDownloadSource
|
||||
import me.rhunk.snapenhance.common.data.download.SplitMediaAssetType
|
||||
import me.rhunk.snapenhance.common.database.impl.ConversationMessage
|
||||
import me.rhunk.snapenhance.common.database.impl.FriendInfo
|
||||
import me.rhunk.snapenhance.common.util.ktx.longHashCode
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
|
||||
import me.rhunk.snapenhance.common.util.snap.MediaDownloaderHelper
|
||||
@@ -54,9 +55,11 @@ import java.io.ByteArrayInputStream
|
||||
import java.nio.file.Paths
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
private fun String.sanitizeForPath(): String {
|
||||
return this.replace(" ", "_")
|
||||
@@ -85,7 +88,11 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
downloadSource: MediaDownloadSource,
|
||||
friendInfo: FriendInfo? = null
|
||||
): DownloadManagerClient {
|
||||
val generatedHash = mediaIdentifier.hashCode().toString(16).replaceFirst("-", "")
|
||||
val generatedHash = (
|
||||
if (!context.config.downloader.allowDuplicate.get()) mediaIdentifier
|
||||
else UUID.randomUUID().toString()
|
||||
).longHashCode().absoluteValue.toString(16)
|
||||
|
||||
val iconUrl = BitmojiSelfie.getBitmojiSelfie(friendInfo?.bitmojiSelfieId, friendInfo?.bitmojiAvatarId, BitmojiSelfie.BitmojiSelfieType.THREE_D)
|
||||
|
||||
val downloadLogging by context.config.downloader.logging
|
||||
@@ -98,9 +105,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
return DownloadManagerClient(
|
||||
context = context,
|
||||
metadata = DownloadMetadata(
|
||||
mediaIdentifier = if (!context.config.downloader.allowDuplicate.get()) {
|
||||
generatedHash
|
||||
} else null,
|
||||
mediaIdentifier = generatedHash,
|
||||
mediaAuthor = mediaAuthor,
|
||||
downloadSource = downloadSource.key,
|
||||
iconUrl = iconUrl,
|
||||
@@ -161,7 +166,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
finalPath.append(downloadSource.pathName).append("/")
|
||||
}
|
||||
if (pathFormat.contains("append_hash")) {
|
||||
appendFileName(hexHash)
|
||||
appendFileName(hexHash.substring(0, hexHash.length.coerceAtMost(8)))
|
||||
}
|
||||
if (pathFormat.contains("append_source")) {
|
||||
appendFileName(downloadSource.pathName)
|
||||
@@ -228,10 +233,12 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
Uri.parse(path).let { uri ->
|
||||
if (uri.scheme == "file") {
|
||||
return@let suspendCoroutine<String> { continuation ->
|
||||
context.httpServer.ensureServerStarted {
|
||||
context.httpServer.ensureServerStarted()?.let { server ->
|
||||
val file = Paths.get(uri.path).toFile()
|
||||
val url = putDownloadableContent(file.inputStream(), file.length())
|
||||
val url = server.putDownloadableContent(file.inputStream(), file.length())
|
||||
continuation.resumeWith(Result.success(url))
|
||||
} ?: run {
|
||||
continuation.resumeWith(Result.failure(Exception("Failed to start http server")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,7 +433,12 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
setTitle("Download dash media")
|
||||
setMultiChoiceItems(
|
||||
chapters.map { "Segment ${prettyPrintTime(it.offset)} - ${prettyPrintTime(it.offset + (it.duration ?: 0))}" }.toTypedArray(),
|
||||
List(chapters.size) { index -> currentChapterIndex == index }.toBooleanArray()
|
||||
List(chapters.size) { index ->
|
||||
if (currentChapterIndex == index) {
|
||||
selectedChapters.add(index)
|
||||
true
|
||||
} else false
|
||||
}.toBooleanArray()
|
||||
) { _, which, isChecked ->
|
||||
if (isChecked) {
|
||||
selectedChapters.add(which)
|
||||
@@ -444,22 +456,19 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
|
||||
}
|
||||
setPositiveButton("Download") { _, _ ->
|
||||
val groups = mutableListOf<MutableList<SnapChapterInfo>>()
|
||||
var currentGroup = mutableListOf<SnapChapterInfo>()
|
||||
|
||||
var lastChapterIndex = -1
|
||||
|
||||
//check for consecutive chapters
|
||||
chapters.filterIndexed { index, _ -> selectedChapters.contains(index) }
|
||||
.forEachIndexed { index, pair ->
|
||||
if (lastChapterIndex != -1 && index != lastChapterIndex + 1) {
|
||||
groups.add(currentGroup)
|
||||
currentGroup = mutableListOf()
|
||||
// group consecutive chapters
|
||||
chapters.forEachIndexed { index, snapChapter ->
|
||||
lastChapterIndex = if (selectedChapters.contains(index)) {
|
||||
if (lastChapterIndex == -1) {
|
||||
groups.add(mutableListOf())
|
||||
}
|
||||
currentGroup.add(pair)
|
||||
lastChapterIndex = index
|
||||
}
|
||||
|
||||
if (currentGroup.isNotEmpty()) {
|
||||
groups.add(currentGroup)
|
||||
groups.last().add(snapChapter)
|
||||
index
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
groups.forEach { group ->
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package me.rhunk.snapenhance.core.util.media
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import me.rhunk.snapenhance.common.logger.AbstractLogger
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStream
|
||||
@@ -16,12 +12,16 @@ import java.net.SocketException
|
||||
import java.util.Locale
|
||||
import java.util.StringTokenizer
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.random.Random
|
||||
|
||||
class HttpServer(
|
||||
private val timeout: Int = 10000
|
||||
) {
|
||||
val port = Random.nextInt(10000, 65535)
|
||||
private fun newRandomPort() = Random.nextInt(10000, 65535)
|
||||
|
||||
var port = newRandomPort()
|
||||
private set
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
private var timeoutJob: Job? = null
|
||||
@@ -30,42 +30,56 @@ class HttpServer(
|
||||
private val cachedData = ConcurrentHashMap<String, Pair<InputStream, Long>>()
|
||||
private var serverSocket: ServerSocket? = null
|
||||
|
||||
fun ensureServerStarted(callback: HttpServer.() -> Unit) {
|
||||
if (serverSocket != null && !serverSocket!!.isClosed) {
|
||||
callback(this)
|
||||
return
|
||||
}
|
||||
fun ensureServerStarted(): HttpServer? {
|
||||
if (serverSocket != null && serverSocket?.isClosed != true) return this
|
||||
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
AbstractLogger.directDebug("starting http server on port $port")
|
||||
serverSocket = ServerSocket(port)
|
||||
callback(this@HttpServer)
|
||||
while (!serverSocket!!.isClosed) {
|
||||
try {
|
||||
val socket = serverSocket!!.accept()
|
||||
timeoutJob?.cancel()
|
||||
launch {
|
||||
handleRequest(socket)
|
||||
timeoutJob = launch {
|
||||
delay(timeout.toLong())
|
||||
AbstractLogger.directDebug("http server closed due to timeout")
|
||||
runCatching {
|
||||
socketJob?.cancel()
|
||||
socket.close()
|
||||
serverSocket?.close()
|
||||
}.onFailure {
|
||||
AbstractLogger.directError("failed to close socket", it)
|
||||
return runBlocking {
|
||||
withTimeoutOrNull(5000L) {
|
||||
suspendCoroutine { continuation ->
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
AbstractLogger.directDebug("Starting http server on port $port")
|
||||
for (i in 0..5) {
|
||||
try {
|
||||
serverSocket = ServerSocket(port)
|
||||
break
|
||||
} catch (e: Throwable) {
|
||||
AbstractLogger.directError("failed to start http server on port $port", e)
|
||||
port = newRandomPort()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: SocketException) {
|
||||
AbstractLogger.directDebug("http server timed out")
|
||||
break;
|
||||
} catch (e: Throwable) {
|
||||
AbstractLogger.directError("failed to handle request", e)
|
||||
continuation.resumeWith(Result.success(if (serverSocket == null) null.also {
|
||||
return@launch
|
||||
} else this@HttpServer))
|
||||
|
||||
while (!serverSocket!!.isClosed) {
|
||||
try {
|
||||
val socket = serverSocket!!.accept()
|
||||
timeoutJob?.cancel()
|
||||
launch {
|
||||
handleRequest(socket)
|
||||
timeoutJob = launch {
|
||||
delay(timeout.toLong())
|
||||
AbstractLogger.directDebug("http server closed due to timeout")
|
||||
runCatching {
|
||||
socketJob?.cancel()
|
||||
socket.close()
|
||||
serverSocket?.close()
|
||||
}.onFailure {
|
||||
AbstractLogger.directError("failed to close socket", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: SocketException) {
|
||||
AbstractLogger.directDebug("http server timed out")
|
||||
break;
|
||||
} catch (e: Throwable) {
|
||||
AbstractLogger.directError("failed to handle request", e)
|
||||
}
|
||||
}
|
||||
}.also { socketJob = it }
|
||||
}
|
||||
}
|
||||
}.also { socketJob = it }
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
@@ -112,18 +126,15 @@ class HttpServer(
|
||||
if (fileRequested.startsWith("/")) {
|
||||
fileRequested = fileRequested.substring(1)
|
||||
}
|
||||
if (!cachedData.containsKey(fileRequested)) {
|
||||
with(writer) {
|
||||
println("HTTP/1.1 404 Not Found")
|
||||
println("Content-type: " + "application/octet-stream")
|
||||
println("Content-length: " + 0)
|
||||
println()
|
||||
flush()
|
||||
}
|
||||
val requestedData = cachedData[fileRequested] ?: writer.run {
|
||||
println("HTTP/1.1 404 Not Found")
|
||||
println("Content-type: " + "application/octet-stream")
|
||||
println("Content-length: " + 0)
|
||||
println()
|
||||
flush()
|
||||
close()
|
||||
return
|
||||
}
|
||||
val requestedData = cachedData[fileRequested]!!
|
||||
with(writer) {
|
||||
println("HTTP/1.1 200 OK")
|
||||
println("Content-type: " + "application/octet-stream")
|
||||
@@ -131,9 +142,11 @@ class HttpServer(
|
||||
println()
|
||||
flush()
|
||||
}
|
||||
requestedData.first.copyTo(outputStream)
|
||||
outputStream.flush()
|
||||
cachedData.remove(fileRequested)
|
||||
requestedData.first.use {
|
||||
it.copyTo(outputStream)
|
||||
}
|
||||
outputStream.flush()
|
||||
close()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user