fix: unique hash (#81)

* fix: media hash reference

* fix: download manager receiver
longToast -> shortToast

* fix: media downloader
sanitize file name
fix playlistUrl bug
fix dash download duration

---------

Co-authored-by: rhunk <101876869+rhunk@users.noreply.github.com>
This commit is contained in:
auth 2023-06-22 00:57:51 +02:00 committed by GitHub
parent cc59f9d060
commit c501682a2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 150 additions and 41 deletions

View File

@ -87,6 +87,7 @@
"conversation_info": "\uD83D\uDC64 Conversation Info"
},
"download_options": {
"allow_duplicate": "Allow duplicate downloads",
"format_user_folder": "Create folder for each user",
"format_hash": "Add a unique hash to the file path",
"format_username": "Add the username to the file path",
@ -232,6 +233,8 @@
}
},
"download_manager_receiver": {
"already_queued_toast": "Media already in queue!",
"already_downloaded_toast": "Media already downloaded!",
"saved_toast": "Saved to {path}",
"download_toast": "Downloading {path}...",
"processing_toast": "Processing {path}...",

View File

@ -16,7 +16,7 @@ object Logger {
}
fun error(throwable: Throwable) {
Log.e(TAG, "",throwable)
Log.e(TAG, "", throwable)
}
fun error(message: Any?) {

View File

@ -115,8 +115,16 @@ enum class ConfigProperty(
"download_options",
ConfigCategory.MEDIA_MANAGEMENT,
ConfigStateListValue(
listOf("format_user_folder", "format_hash", "format_date_time", "format_username", "merge_overlay"),
listOf(
"allow_duplicate",
"format_user_folder",
"format_hash",
"format_date_time",
"format_username",
"merge_overlay"
),
mutableMapOf(
"allow_duplicate" to false,
"format_user_folder" to true,
"format_hash" to true,
"format_date_time" to true,

View File

@ -13,7 +13,8 @@ class DownloadManagerClient (
private val outputPath: String,
private val mediaDisplaySource: String?,
private val mediaDisplayType: String?,
private val iconUrl: String?
private val iconUrl: String?,
private val uniqueHash: String?
) {
private fun sendToBroadcastReceiver(bundle: Bundle) {
val intent = Intent()
@ -32,10 +33,11 @@ class DownloadManagerClient (
putString("mediaDisplaySource", mediaDisplaySource)
putString("mediaDisplayType", mediaDisplayType)
putString("iconUrl", iconUrl)
putString("uniqueHash", uniqueHash)
}.apply(extras))
}
fun downloadDashMedia(playlistUrl: String, offsetTime: Long, duration: Long) {
fun downloadDashMedia(playlistUrl: String, offsetTime: Long, duration: Long?) {
sendToBroadcastReceiver(
DownloadRequest(
inputMedias = arrayOf(playlistUrl),
@ -44,8 +46,8 @@ class DownloadManagerClient (
)
) {
putBundle("dashOptions", Bundle().apply {
putLong("offsetTime", offsetTime)
putLong("duration", duration)
putString("offsetTime", offsetTime.toString())
duration?.let { putString("duration", it.toString()) }
})
}
}

View File

@ -128,7 +128,7 @@ class DownloadManagerReceiver : BroadcastReceiver() {
if (!it.endsWith("/")) "$it/" else it
}
longToast(
shortToast(
translation.format("saved_toast", "path" to outputFile.absolutePath.replace(parentName ?: "", ""))
)
@ -255,9 +255,19 @@ class DownloadManagerReceiver : BroadcastReceiver() {
this.context = context
SharedContext.ensureInitialized(context)
val downloadRequest = DownloadRequest.fromBundle(intent.extras!!)
SharedContext.downloadTaskManager.canDownloadMedia(downloadRequest.getUniqueHash())?.let { downloadStage ->
shortToast(
translation[if (downloadStage.isFinalStage) {
"already_downloaded_toast"
} else {
"already_queued_toast"
}]
)
return
}
GlobalScope.launch(Dispatchers.IO) {
val pendingDownloadObject = PendingDownload.fromBundle(intent.extras!!)

View File

@ -20,6 +20,7 @@ class DownloadTaskManager {
SQLiteDatabaseHelper.createTablesFromSchema(this, mapOf(
"tasks" to listOf(
"id INTEGER PRIMARY KEY AUTOINCREMENT",
"hash VARCHAR UNIQUE",
"outputPath TEXT",
"outputFile TEXT",
"mediaDisplayType TEXT",
@ -32,8 +33,9 @@ class DownloadTaskManager {
}
fun addTask(task: PendingDownload): Int {
taskDatabase.execSQL("INSERT INTO tasks (outputPath, outputFile, mediaDisplayType, mediaDisplaySource, iconUrl, downloadStage) VALUES (?, ?, ?, ?, ?, ?)",
taskDatabase.execSQL("INSERT INTO tasks (hash, outputPath, outputFile, mediaDisplayType, mediaDisplaySource, iconUrl, downloadStage) VALUES (?, ?, ?, ?, ?, ?, ?)",
arrayOf(
task.uniqueHash,
task.outputPath,
task.outputFile,
task.mediaDisplayType,
@ -51,8 +53,9 @@ class DownloadTaskManager {
}
fun updateTask(task: PendingDownload) {
taskDatabase.execSQL("UPDATE tasks SET outputPath = ?, outputFile = ?, mediaDisplayType = ?, mediaDisplaySource = ?, iconUrl = ?, downloadStage = ? WHERE id = ?",
taskDatabase.execSQL("UPDATE tasks SET hash = ?, outputPath = ?, outputFile = ?, mediaDisplayType = ?, mediaDisplaySource = ?, iconUrl = ?, downloadStage = ? WHERE id = ?",
arrayOf(
task.uniqueHash,
task.outputPath,
task.outputFile,
task.mediaDisplayType,
@ -71,6 +74,28 @@ class DownloadTaskManager {
}
}
@SuppressLint("Range")
fun canDownloadMedia(mediaIdentifier: String?): DownloadStage? {
if (mediaIdentifier == null) return null
val cursor = taskDatabase.rawQuery("SELECT * FROM tasks WHERE hash = ?", arrayOf(mediaIdentifier))
if (cursor.count > 0) {
cursor.moveToFirst()
val downloadStage = DownloadStage.valueOf(cursor.getString(cursor.getColumnIndex("downloadStage")))
cursor.close()
//if the stage has reached a final stage and is not in a saved state, remove the task
if (downloadStage.isFinalStage && downloadStage != DownloadStage.SAVED) {
taskDatabase.execSQL("DELETE FROM tasks WHERE hash = ?", arrayOf(mediaIdentifier))
return null
}
return downloadStage
}
cursor.close()
return null
}
fun isEmpty(): Boolean {
return cachedTasks.isEmpty() && pendingTasks.isEmpty()
}
@ -127,6 +152,7 @@ class DownloadTaskManager {
outputPath = cursor.getString(cursor.getColumnIndex("outputPath")),
mediaDisplayType = cursor.getString(cursor.getColumnIndex("mediaDisplayType")),
mediaDisplaySource = cursor.getString(cursor.getColumnIndex("mediaDisplaySource")),
uniqueHash = cursor.getString(cursor.getColumnIndex("hash")),
iconUrl = cursor.getString(cursor.getColumnIndex("iconUrl"))
).apply {
downloadStage = DownloadStage.valueOf(cursor.getString(cursor.getColumnIndex("downloadStage")))

View File

@ -19,7 +19,8 @@ class DownloadRequest(
private val flags: Int = 0,
private val dashOptions: Map<String, String?>? = null,
private val mediaDisplaySource: String? = null,
private val mediaDisplayType: String? = null
private val mediaDisplayType: String? = null,
private val uniqueHash: String? = null
) {
companion object {
fun fromBundle(bundle: Bundle): DownloadRequest {
@ -39,7 +40,8 @@ class DownloadRequest(
options.getString(key)
}
},
flags = bundle.getInt("flags", 0)
flags = bundle.getInt("flags", 0),
uniqueHash = bundle.getString("uniqueHash")
)
}
}
@ -62,6 +64,7 @@ class DownloadRequest(
}
})
putInt("flags", flags)
putString("uniqueHash", uniqueHash)
}
}
@ -85,10 +88,6 @@ class DownloadRequest(
}
}
fun getInputMedia(index: Int): String? {
return inputMedias.getOrNull(index)
}
fun getInputMedias(): List<InputMedia> {
return inputMedias.mapIndexed { index, uri ->
InputMedia(
@ -102,4 +101,6 @@ class DownloadRequest(
fun getInputType(index: Int): DownloadMediaType? {
return inputTypes.getOrNull(index)?.let { DownloadMediaType.valueOf(it) }
}
fun getUniqueHash() = uniqueHash
}

View File

@ -13,7 +13,8 @@ data class PendingDownload(
val outputPath: String,
val mediaDisplayType: String?,
val mediaDisplaySource: String?,
val iconUrl: String?
val iconUrl: String?,
val uniqueHash: String?
) {
companion object {
fun fromBundle(bundle: Bundle): PendingDownload {
@ -21,7 +22,8 @@ data class PendingDownload(
outputPath = bundle.getString("outputPath")!!,
mediaDisplayType = bundle.getString("mediaDisplayType"),
mediaDisplaySource = bundle.getString("mediaDisplaySource"),
iconUrl = bundle.getString("iconUrl")
iconUrl = bundle.getString("iconUrl"),
uniqueHash = bundle.getString("uniqueHash")
)
}
}

View File

@ -30,13 +30,14 @@ import me.rhunk.snapenhance.hook.HookAdapter
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.ui.download.MediaFilter
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.protobuf.ProtoReader
import me.rhunk.snapenhance.util.snap.BitmojiSelfie
import me.rhunk.snapenhance.util.snap.EncryptionHelper
import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper
import me.rhunk.snapenhance.util.snap.MediaType
import me.rhunk.snapenhance.util.snap.PreviewUtils
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.protobuf.ProtoReader
import me.rhunk.snapenhance.util.snap.BitmojiSelfie
import java.io.File
import java.nio.file.Paths
import java.text.SimpleDateFormat
@ -56,10 +57,13 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
private fun provideClientDownloadManager(
pathSuffix: String,
mediaIdentifier: String,
mediaDisplaySource: String? = null,
mediaDisplayType: String? = null,
friendInfo: FriendInfo? = null
): DownloadManagerClient {
val generatedHash = mediaIdentifier.hashCode().toString(16)
val iconUrl = friendInfo?.takeIf {
it.bitmojiAvatarId != null && it.bitmojiSelfieId != null
}?.let {
@ -68,7 +72,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
val outputPath = File(
context.config.string(ConfigProperty.SAVE_FOLDER),
createNewFilePath(pathSuffix.hashCode(), pathSuffix)
createNewFilePath(generatedHash, pathSuffix)
).absolutePath
return DownloadManagerClient(
@ -76,6 +80,11 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
mediaDisplaySource = mediaDisplaySource,
mediaDisplayType = mediaDisplayType,
iconUrl = iconUrl,
uniqueHash =
// If duplicate is allowed, we don't need to pass the hash
if (context.config.options(ConfigProperty.DOWNLOAD_OPTIONS)["allow_duplicate"] == false) {
generatedHash
} else null,
outputPath = outputPath
)
}
@ -85,9 +94,12 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
return isFFmpegPresent
}
private fun createNewFilePath(hash: Int, pathPrefix: String): String {
val hexHash = Integer.toHexString(hash)
private fun createNewFilePath(hexHash: String, pathPrefix: String): String {
val downloadOptions = context.config.options(ConfigProperty.DOWNLOAD_OPTIONS)
val sanitizedPathPrefix = pathPrefix
.replace(" ", "_")
.replace(Regex("[\\\\/:*?\"<>|]"), "")
.ifEmpty { hexHash }
val currentDateTime = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH).format(System.currentTimeMillis())
@ -102,13 +114,13 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
}
if (downloadOptions["format_user_folder"] == true) {
finalPath.append(pathPrefix).append("/")
finalPath.append(sanitizedPathPrefix).append("/")
}
if (downloadOptions["format_hash"] == true) {
appendFileName(hexHash)
}
if (downloadOptions["format_username"] == true) {
appendFileName(pathPrefix)
appendFileName(sanitizedPathPrefix)
}
if (downloadOptions["format_date_time"] == true) {
appendFileName(currentDateTime)
@ -186,7 +198,10 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
//messages
paramMap["MESSAGE_ID"]?.toString()?.takeIf { forceDownload || canAutoDownload("friend_snaps") }?.let { id ->
val messageId = id.substring(id.lastIndexOf(":") + 1).toLong()
val senderId: String = context.database.getConversationMessageFromId(messageId)!!.sender_id!!
val conversationMessage = context.database.getConversationMessageFromId(messageId)!!
val senderId = conversationMessage.sender_id!!
val conversationId = conversationMessage.client_conversation_id!!
if (!forceDownload && context.feature(AntiAutoDownload::class).isUserIgnored(senderId)) {
return
@ -194,7 +209,15 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
val author = context.database.getFriendInfo(senderId) ?: return
val authorUsername = author.usernameForSorting!!
downloadOperaMedia(provideClientDownloadManager(authorUsername, authorUsername, MediaFilter.CHAT_MEDIA.mediaDisplayType, friendInfo = author), mediaInfoMap)
downloadOperaMedia(provideClientDownloadManager(
pathSuffix = authorUsername,
mediaIdentifier = "$conversationId$senderId$messageId",
mediaDisplaySource = authorUsername,
mediaDisplayType = MediaFilter.CHAT_MEDIA.mediaDisplayType,
friendInfo = author
), mediaInfoMap)
return
}
@ -202,15 +225,20 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
paramMap["PLAYLIST_V2_GROUP"]?.toString()?.takeIf {
it.contains("storyUserId=") && (forceDownload || canAutoDownload("friend_stories"))
}?.let { playlistGroup ->
val storyIdStartIndex = playlistGroup.indexOf("storyUserId=") + 12
val storyUserId = playlistGroup.substring(
storyIdStartIndex,
playlistGroup.indexOf(",", storyIdStartIndex)
)
val storyUserId = (playlistGroup.indexOf("storyUserId=") + 12).let {
playlistGroup.substring(it, playlistGroup.indexOf(",", it))
}
val author = context.database.getFriendInfo(if (storyUserId == "null") context.database.getMyUserId()!! else storyUserId) ?: return
val authorName = author.usernameForSorting!!
downloadOperaMedia(provideClientDownloadManager(authorName, authorName, MediaFilter.STORY.mediaDisplayType, friendInfo = author), mediaInfoMap, )
downloadOperaMedia(provideClientDownloadManager(
pathSuffix = authorName,
mediaIdentifier = paramMap["MEDIA_ID"].toString(),
mediaDisplaySource = authorName,
mediaDisplayType = MediaFilter.STORY.mediaDisplayType,
friendInfo = author
), mediaInfoMap)
return
}
@ -222,13 +250,24 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
val userDisplayName = (if (paramMap.containsKey("USER_DISPLAY_NAME")) paramMap["USER_DISPLAY_NAME"].toString() else "").replace(
"[^\\x00-\\x7F]".toRegex(),
"")
downloadOperaMedia(provideClientDownloadManager("Public-Stories/$userDisplayName", userDisplayName, "Public Story"), mediaInfoMap)
downloadOperaMedia(provideClientDownloadManager(
pathSuffix = "Public-Stories/$userDisplayName",
mediaIdentifier = paramMap["SNAP_ID"].toString(),
mediaDisplayType = userDisplayName,
mediaDisplaySource = "Public Story"
), mediaInfoMap)
return
}
//spotlight
if (snapSource == "SINGLE_SNAP_STORY" && (forceDownload || canAutoDownload("spotlight"))) {
downloadOperaMedia(provideClientDownloadManager("Spotlight", mediaDisplayType = MediaFilter.SPOTLIGHT.mediaDisplayType, mediaDisplaySource = paramMap["TIME_STAMP"].toString()), mediaInfoMap)
downloadOperaMedia(provideClientDownloadManager(
pathSuffix = "Spotlight",
mediaIdentifier = paramMap["SNAP_ID"].toString(),
mediaDisplayType = MediaFilter.SPOTLIGHT.mediaDisplayType,
mediaDisplaySource = paramMap["TIME_STAMP"].toString()
), mediaInfoMap)
return
}
@ -256,12 +295,24 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
//add 100ms to the start time to prevent the video from starting too early
val snapChapterTimestamp = snapChapter.startTimeMs.plus(100)
val duration = nextChapter?.startTimeMs?.minus(snapChapterTimestamp) ?: 0
val duration: Long? = nextChapter?.startTimeMs?.minus(snapChapterTimestamp)
//get the mpd playlist and append the cdn url to baseurl nodes
val playlistUrl = paramMap["MEDIA_ID"].toString().let { it.substring(it.indexOf("https://cf-st.sc-cdn.net")) }
val clientDownloadManager = provideClientDownloadManager("Pro-Stories/${storyName}", storyName, "Pro Story")
clientDownloadManager.downloadDashMedia(
val playlistUrl = paramMap["MEDIA_ID"].toString().let {
val urlIndex = it.indexOf("https://cf-st.sc-cdn.net")
if (urlIndex == -1) {
"${RemoteMediaResolver.CF_ST_CDN_D}$it"
} else {
it.substring(urlIndex)
}
}
provideClientDownloadManager(
pathSuffix = "Pro-Stories/${storyName}",
mediaIdentifier = "${paramMap["STORY_ID"]}-${snapItem.snapId}",
mediaDisplaySource = storyName,
mediaDisplayType = "Pro Story"
).downloadDashMedia(
playlistUrl,
snapChapterTimestamp,
duration
@ -384,7 +435,13 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
runCatching {
if (!isPreviewMode) {
val encryptionKeys = EncryptionHelper.getEncryptionKeys(contentType, messageReader, isArroyo = isArroyoMessage)
provideClientDownloadManager(authorName, authorName, MediaFilter.CHAT_MEDIA.mediaDisplayType, friendInfo = friendInfo).downloadMedia(
provideClientDownloadManager(
pathSuffix = authorName,
mediaIdentifier = "${message.client_conversation_id}${message.sender_id}${message.client_message_id}",
mediaDisplaySource = authorName,
mediaDisplayType = MediaFilter.CHAT_MEDIA.mediaDisplayType,
friendInfo = friendInfo
).downloadMedia(
Base64.UrlSafe.encode(urlProto),
DownloadMediaType.PROTO_MEDIA,
encryption = encryptionKeys?.toKeyPair()