commit c501682a2fd185f41eb8d117e28ddb2c1fccf210
parent cc59f9d060b685eda27ccb013673dc016270a906
Author: auth <64337177+authorisation@users.noreply.github.com>
Date: Thu, 22 Jun 2023 00:57:51 +0200
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>
Diffstat:
9 files changed, 150 insertions(+), 41 deletions(-)
diff --git a/app/src/main/assets/lang/en_US.json b/app/src/main/assets/lang/en_US.json
@@ -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}...",
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/Logger.kt b/app/src/main/kotlin/me/rhunk/snapenhance/Logger.kt
@@ -16,7 +16,7 @@ object Logger {
}
fun error(throwable: Throwable) {
- Log.e(TAG, "",throwable)
+ Log.e(TAG, "", throwable)
}
fun error(message: Any?) {
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt b/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt
@@ -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,
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt
@@ -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()) }
})
}
}
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerReceiver.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerReceiver.kt
@@ -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!!)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt
@@ -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")))
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt
@@ -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
}
\ No newline at end of file
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt
@@ -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")
)
}
}
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt
@@ -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()