From 5776d4411103691e0f09a2e902cb6e0591a8ccb5 Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:50:42 +0200 Subject: [PATCH] fix(media_downloader): story voice note reply - refactor media author and download source - optimize download section --- .../kotlin/me/rhunk/snapenhance/LogManager.kt | 2 +- .../me/rhunk/snapenhance/RemoteSideContext.kt | 2 +- .../snapenhance/download}/DownloadObject.kt | 10 +- .../snapenhance/download/DownloadProcessor.kt | 8 +- .../download/DownloadTaskManager.kt | 41 ++++---- .../sections/downloads/DownloadsSection.kt | 67 ++++++++----- .../snapenhance/ui/util/ComposeImageHelper.kt | 2 + core/src/main/assets/lang/en_US.json | 7 +- .../core/config/impl/DownloaderConfig.kt | 7 +- .../core/download/data/DownloadMetadata.kt | 4 +- .../core/download/data/MediaDownloadSource.kt | 28 ++++++ .../core/download/data/MediaFilter.kt | 18 ---- .../impl/downloader/MediaDownloader.kt | 96 +++++++++---------- .../snapenhance/util/snap/EncryptionHelper.kt | 6 +- .../util/snap/MediaDownloaderHelper.kt | 11 ++- 15 files changed, 170 insertions(+), 139 deletions(-) rename {core/src/main/kotlin/me/rhunk/snapenhance/core/download/data => app/src/main/kotlin/me/rhunk/snapenhance/download}/DownloadObject.kt (72%) rename {core/src/main/kotlin/me/rhunk/snapenhance/core => app/src/main/kotlin/me/rhunk/snapenhance}/download/DownloadTaskManager.kt (79%) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaDownloadSource.kt delete mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaFilter.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt index e99616ed..1649e257 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt @@ -180,7 +180,7 @@ class LogManager( fun error(message: Any?, throwable: Throwable, tag: String = TAG) { internalLog(tag, LogLevel.ERROR, message) - internalLog(tag, LogLevel.ERROR, throwable) + internalLog(tag, LogLevel.ERROR, throwable.stackTraceToString()) } fun info(message: Any?, tag: String = TAG) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt index 75c50c4c..4a1c0dbd 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -20,7 +20,7 @@ import me.rhunk.snapenhance.core.BuildConfig import me.rhunk.snapenhance.core.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.core.bridge.wrapper.MappingsWrapper import me.rhunk.snapenhance.core.config.ModConfig -import me.rhunk.snapenhance.core.download.DownloadTaskManager +import me.rhunk.snapenhance.download.DownloadTaskManager import me.rhunk.snapenhance.messaging.ModDatabase import me.rhunk.snapenhance.messaging.StreaksReminder import me.rhunk.snapenhance.ui.manager.MainActivity diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadObject.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadObject.kt similarity index 72% rename from core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadObject.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadObject.kt index 4c40b3ac..73fdc8ad 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadObject.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadObject.kt @@ -1,17 +1,19 @@ -package me.rhunk.snapenhance.core.download.data +package me.rhunk.snapenhance.download import kotlinx.coroutines.Job -import me.rhunk.snapenhance.core.download.DownloadTaskManager +import me.rhunk.snapenhance.core.download.data.DownloadMetadata +import me.rhunk.snapenhance.core.download.data.DownloadStage data class DownloadObject( var downloadId: Int = 0, var outputFile: String? = null, val metadata : DownloadMetadata ) { - lateinit var downloadTaskManager: DownloadTaskManager var job: Job? = null var changeListener = { _: DownloadStage, _: DownloadStage -> } + lateinit var updateTaskCallback: (DownloadObject) -> Unit + private var _stage: DownloadStage = DownloadStage.PENDING var downloadStage: DownloadStage get() = synchronized(this) { @@ -20,7 +22,7 @@ data class DownloadObject( set(value) = synchronized(this) { changeListener(_stage, value) _stage = value - downloadTaskManager.updateTask(this) + updateTaskCallback(this) } fun isJobActive() = job?.isActive == true diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt index 7aeec42e..b92b88ea 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt @@ -16,14 +16,12 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.bridge.DownloadCallback import me.rhunk.snapenhance.core.download.DownloadManagerClient import me.rhunk.snapenhance.data.FileType import me.rhunk.snapenhance.core.download.data.DownloadMediaType import me.rhunk.snapenhance.core.download.data.DownloadMetadata -import me.rhunk.snapenhance.core.download.data.DownloadObject import me.rhunk.snapenhance.core.download.data.DownloadRequest import me.rhunk.snapenhance.core.download.data.DownloadStage import me.rhunk.snapenhance.core.download.data.InputMedia @@ -320,7 +318,11 @@ class DownloadProcessor ( val downloadObjectObject = DownloadObject( metadata = downloadMetadata - ).apply { downloadTaskManager = remoteSideContext.downloadTaskManager } + ).apply { + updateTaskCallback = { + remoteSideContext.downloadTaskManager.updateTask(it) + } + } downloadObjectObject.also { remoteSideContext.downloadTaskManager.addTask(it) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/DownloadTaskManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt similarity index 79% rename from core/src/main/kotlin/me/rhunk/snapenhance/core/download/DownloadTaskManager.kt rename to app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt index 5360191a..916fc83d 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/DownloadTaskManager.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt @@ -1,12 +1,11 @@ -package me.rhunk.snapenhance.core.download +package me.rhunk.snapenhance.download import android.annotation.SuppressLint import android.content.Context import android.database.sqlite.SQLiteDatabase import me.rhunk.snapenhance.core.download.data.DownloadMetadata -import me.rhunk.snapenhance.core.download.data.DownloadObject import me.rhunk.snapenhance.core.download.data.DownloadStage -import me.rhunk.snapenhance.core.download.data.MediaFilter +import me.rhunk.snapenhance.core.download.data.MediaDownloadSource import me.rhunk.snapenhance.util.SQLiteDatabaseHelper import me.rhunk.snapenhance.util.ktx.getIntOrNull import me.rhunk.snapenhance.util.ktx.getStringOrNull @@ -26,8 +25,8 @@ class DownloadTaskManager { "hash VARCHAR UNIQUE", "outputPath TEXT", "outputFile TEXT", - "mediaDisplayType TEXT", - "mediaDisplaySource TEXT", + "mediaAuthor TEXT", + "downloadSource TEXT", "iconUrl TEXT", "downloadStage TEXT" ) @@ -36,13 +35,13 @@ class DownloadTaskManager { } fun addTask(task: DownloadObject): Int { - taskDatabase.execSQL("INSERT INTO tasks (hash, outputPath, outputFile, mediaDisplayType, mediaDisplaySource, iconUrl, downloadStage) VALUES (?, ?, ?, ?, ?, ?, ?)", + taskDatabase.execSQL("INSERT INTO tasks (hash, outputPath, outputFile, downloadSource, mediaAuthor, iconUrl, downloadStage) VALUES (?, ?, ?, ?, ?, ?, ?)", arrayOf( task.metadata.mediaIdentifier, task.metadata.outputPath, task.outputFile, - task.metadata.mediaDisplayType, - task.metadata.mediaDisplaySource, + task.metadata.downloadSource, + task.metadata.mediaAuthor, task.metadata.iconUrl, task.downloadStage.name ) @@ -56,13 +55,13 @@ class DownloadTaskManager { } fun updateTask(task: DownloadObject) { - taskDatabase.execSQL("UPDATE tasks SET hash = ?, outputPath = ?, outputFile = ?, mediaDisplayType = ?, mediaDisplaySource = ?, iconUrl = ?, downloadStage = ? WHERE id = ?", + taskDatabase.execSQL("UPDATE tasks SET hash = ?, outputPath = ?, outputFile = ?, downloadSource = ?, mediaAuthor = ?, iconUrl = ?, downloadStage = ? WHERE id = ?", arrayOf( task.metadata.mediaIdentifier, task.metadata.outputPath, task.outputFile, - task.metadata.mediaDisplayType, - task.metadata.mediaDisplaySource, + task.metadata.downloadSource, + task.metadata.mediaAuthor, task.metadata.iconUrl, task.downloadStage.name, task.downloadId @@ -113,11 +112,11 @@ class DownloadTaskManager { removeTask(task.downloadId) } - fun queryFirstTasks(filter: MediaFilter): Map { - val isPendingFilter = filter == MediaFilter.PENDING + fun queryFirstTasks(filter: MediaDownloadSource): Map { + val isPendingFilter = filter == MediaDownloadSource.PENDING val tasks = mutableMapOf() - tasks.putAll(pendingTasks.filter { isPendingFilter || filter.matches(it.value.metadata.mediaDisplayType) }) + tasks.putAll(pendingTasks.filter { isPendingFilter || filter.matches(it.value.metadata.downloadSource) }) if (isPendingFilter) { return tasks.toSortedMap(reverseOrder()) } @@ -132,16 +131,16 @@ class DownloadTaskManager { } @SuppressLint("Range") - fun queryTasks(from: Int, amount: Int = 30, filter: MediaFilter = MediaFilter.NONE): Map { - if (filter == MediaFilter.PENDING) { + fun queryTasks(from: Int, amount: Int = 30, filter: MediaDownloadSource = MediaDownloadSource.NONE): Map { + if (filter == MediaDownloadSource.PENDING) { return emptyMap() } val cursor = taskDatabase.rawQuery( - "SELECT * FROM tasks WHERE id < ? AND mediaDisplayType LIKE ? ORDER BY id DESC LIMIT ?", + "SELECT * FROM tasks WHERE id < ? AND downloadSource LIKE ? ORDER BY id DESC LIMIT ?", arrayOf( from.toString(), - if (filter.shouldIgnoreFilter) "%" else "%${filter.key}", + if (filter.ignoreFilter) "%" else "%${filter.key}", amount.toString() ) ) @@ -155,12 +154,12 @@ class DownloadTaskManager { metadata = DownloadMetadata( outputPath = cursor.getStringOrNull("outputPath")!!, mediaIdentifier = cursor.getStringOrNull("hash"), - mediaDisplayType = cursor.getStringOrNull("mediaDisplayType"), - mediaDisplaySource = cursor.getStringOrNull("mediaDisplaySource"), + downloadSource = cursor.getStringOrNull("downloadSource") ?: MediaDownloadSource.NONE.key, + mediaAuthor = cursor.getStringOrNull("mediaAuthor"), iconUrl = cursor.getStringOrNull("iconUrl") ) ).apply { - downloadTaskManager = this@DownloadTaskManager + updateTaskCallback = { updateTask(it) } downloadStage = DownloadStage.valueOf(cursor.getStringOrNull("downloadStage")!!) //if downloadStage is not saved, it means the app was killed before the download was finished if (downloadStage != DownloadStage.SAVED) { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/downloads/DownloadsSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/downloads/DownloadsSection.kt index 2cd20aad..2e4cb3d8 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/downloads/DownloadsSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/downloads/DownloadsSection.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -49,27 +50,42 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.rememberAsyncImagePainter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.launch +import me.rhunk.snapenhance.core.download.data.MediaDownloadSource import me.rhunk.snapenhance.data.FileType -import me.rhunk.snapenhance.core.download.data.DownloadObject -import me.rhunk.snapenhance.core.download.data.MediaFilter +import me.rhunk.snapenhance.download.DownloadObject import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.util.BitmojiImage import me.rhunk.snapenhance.ui.util.ImageRequestHelper class DownloadsSection : Section() { private val loadedDownloads = mutableStateOf(mapOf()) - private var currentFilter = mutableStateOf(MediaFilter.NONE) + private var currentFilter = mutableStateOf(MediaDownloadSource.NONE) + private val coroutineScope = CoroutineScope(Dispatchers.IO) override fun onResumed() { super.onResumed() - loadByFilter(currentFilter.value) + coroutineScope.launch { + loadByFilter(currentFilter.value) + } } - private fun loadByFilter(filter: MediaFilter) { + private fun loadByFilter(filter: MediaDownloadSource) { this.currentFilter.value = filter synchronized(loadedDownloads) { - loadedDownloads.value = context.downloadTaskManager.queryFirstTasks(filter) + loadedDownloads.value = context.downloadTaskManager.queryFirstTasks(filter).toMutableMap() + } + } + + private fun removeTask(download: DownloadObject) { + synchronized(loadedDownloads) { + loadedDownloads.value = loadedDownloads.value.toMutableMap().also { + it.remove(download.downloadId) + } + context.downloadTaskManager.removeTask(download) } } @@ -87,7 +103,6 @@ class DownloadsSection : Section() { @Composable private fun FilterList() { - val coroutineScope = rememberCoroutineScope() var showMenu by remember { mutableStateOf(false) } IconButton(onClick = { showMenu = !showMenu}) { Icon( @@ -97,7 +112,7 @@ class DownloadsSection : Section() { } DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { - MediaFilter.values().toList().forEach { filter -> + MediaDownloadSource.values().toList().forEach { filter -> DropdownMenuItem( text = { Row( @@ -110,7 +125,7 @@ class DownloadsSection : Section() { selected = (currentFilter.value == filter), onClick = null ) - Text(filter.name, modifier = Modifier.weight(1f)) + Text(filter.displayName, modifier = Modifier.weight(1f)) } }, onClick = { @@ -144,11 +159,12 @@ class DownloadsSection : Section() { context.androidContext, download.outputFile ), - imageLoader = context.imageLoader + imageLoader = context.imageLoader, + filterQuality = FilterQuality.None, ), modifier = Modifier .matchParentSize() - .blur(12.dp), + .blur(5.dp), contentDescription = null, contentScale = ContentScale.FillWidth ) @@ -156,9 +172,9 @@ class DownloadsSection : Section() { Row( modifier = Modifier .padding(start = 10.dp, end = 10.dp) - .fillMaxWidth() .fillMaxHeight(), - verticalAlignment = Alignment.CenterVertically + + verticalAlignment = Alignment.CenterVertically, ){ //info card Row( @@ -177,13 +193,13 @@ class DownloadsSection : Section() { verticalArrangement = Arrangement.SpaceBetween ) { Text( - text = download.metadata.mediaDisplayType ?: "", + text = MediaDownloadSource.fromKey(download.metadata.downloadSource).displayName, overflow = TextOverflow.Ellipsis, fontSize = 16.sp, fontWeight = FontWeight.Bold ) Text( - text = download.metadata.mediaDisplaySource ?: "", + text = download.metadata.mediaAuthor ?: "", overflow = TextOverflow.Ellipsis, fontSize = 12.sp, fontWeight = FontWeight.Light @@ -191,16 +207,17 @@ class DownloadsSection : Section() { } } - Spacer(modifier = Modifier.weight(1f)) - //action buttons Row( modifier = Modifier - .padding(5.dp), + .padding(5.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically ) { FilledIconButton( onClick = { + removeTask(download) }, colors = IconButtonDefaults.iconButtonColors( containerColor = MaterialTheme.colorScheme.error, @@ -240,6 +257,7 @@ class DownloadsSection : Section() { @Composable override fun Content() { val scrollState = rememberLazyListState() + val scope = rememberCoroutineScope() LazyColumn( state = scrollState, @@ -252,14 +270,19 @@ class DownloadsSection : Section() { item { Spacer(Modifier.height(20.dp)) if (loadedDownloads.value.isEmpty()) { - Text(text = "No downloads", fontSize = 20.sp, modifier = Modifier + Text(text = "(empty)", fontSize = 20.sp, modifier = Modifier .fillMaxWidth() .padding(10.dp), textAlign = TextAlign.Center) } - LaunchedEffect(true) { + LaunchedEffect(Unit) { val lastItemIndex = (loadedDownloads.value.size - 1).takeIf { it >= 0 } ?: return@LaunchedEffect - lazyLoadFromIndex(lastItemIndex) - scrollState.animateScrollToItem(lastItemIndex) + scope.launch(Dispatchers.IO) { + lazyLoadFromIndex(lastItemIndex) + }.asCompletableFuture().thenAccept { + scope.launch { + scrollState.animateScrollToItem(lastItemIndex) + } + } } } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt index 45f531d3..4e7ec276 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/util/ComposeImageHelper.kt @@ -53,6 +53,8 @@ object ImageRequestHelper { fun newDownloadPreviewImageRequest(context: Context, filePath: String?) = ImageRequest.Builder(context) .data(filePath) .cacheKey(filePath) + .memoryCacheKey(filePath) .crossfade(true) + .crossfade(200) .build() } \ No newline at end of file diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json index cb351cc3..cf6f07d2 100644 --- a/core/src/main/assets/lang/en_US.json +++ b/core/src/main/assets/lang/en_US.json @@ -388,11 +388,12 @@ "conversation_info": "\uD83D\uDC64 Conversation Info" }, "path_format": { - "create_user_folder": "Create folder for each user", + "create_author_folder": "Create folder for each author", + "create_source_folder": "Create folder for each media source type", "append_hash": "Add a unique hash to the file name", + "append_source": "Add the media source to the file name", "append_username": "Add the username to the file name", - "append_date_time": "Add the date and time to the file name", - "append_type": "Add the media type to the file name" + "append_date_time": "Add the date and time to the file name" }, "auto_download_sources": { "friend_snaps": "Friend Snaps", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt index 7a741fc9..d6cb698d 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt @@ -14,11 +14,12 @@ class DownloaderConfig : ConfigContainer() { ) val preventSelfAutoDownload = boolean("prevent_self_auto_download") val pathFormat = multiple("path_format", - "create_user_folder", + "create_author_folder", + "create_source_folder", "append_hash", + "append_source", + "append_username", "append_date_time", - "append_type", - "append_username" ).apply { set(mutableListOf("append_hash", "append_date_time", "append_type", "append_username")) } val allowDuplicate = boolean("allow_duplicate") val mergeOverlays = boolean("merge_overlays") { addNotices(FeatureNotice.MAY_CAUSE_CRASHES) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadMetadata.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadMetadata.kt index 8a342bdc..f18d77ad 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadMetadata.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/DownloadMetadata.kt @@ -3,7 +3,7 @@ package me.rhunk.snapenhance.core.download.data data class DownloadMetadata( val mediaIdentifier: String?, val outputPath: String, - val mediaDisplaySource: String?, - val mediaDisplayType: String?, + val mediaAuthor: String?, + val downloadSource: String, val iconUrl: String? ) \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaDownloadSource.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaDownloadSource.kt new file mode 100644 index 00000000..6659c499 --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaDownloadSource.kt @@ -0,0 +1,28 @@ +package me.rhunk.snapenhance.core.download.data + +enum class MediaDownloadSource( + val key: String, + val displayName: String = key, + val pathName: String = key, + val ignoreFilter: Boolean = false +) { + NONE("none", "None", ignoreFilter = true), + PENDING("pending", "Pending", ignoreFilter = true), + CHAT_MEDIA("chat_media", "Chat Media", "chat_media"), + STORY("story", "Story", "story"), + PUBLIC_STORY("public_story", "Public Story", "public_story"), + SPOTLIGHT("spotlight", "Spotlight", "spotlight"), + PROFILE_PICTURE("profile_picture", "Profile Picture", "profile_picture"); + + fun matches(source: String?): Boolean { + if (source == null) return false + return source.contains(key, ignoreCase = true) + } + + companion object { + fun fromKey(key: String?): MediaDownloadSource { + if (key == null) return NONE + return values().find { it.key == key } ?: NONE + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaFilter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaFilter.kt deleted file mode 100644 index edf59178..00000000 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/download/data/MediaFilter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package me.rhunk.snapenhance.core.download.data - -enum class MediaFilter( - val key: String, - val shouldIgnoreFilter: Boolean = false -) { - NONE("none", true), - PENDING("pending", true), - CHAT_MEDIA("chat_media"), - STORY("story"), - SPOTLIGHT("spotlight"), - PROFILE_PICTURE("profile_picture"); - - fun matches(source: String?): Boolean { - if (source == null) return false - return source.contains(key, ignoreCase = true) - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt index 61c00d5b..e2ab736f 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt @@ -12,7 +12,7 @@ import me.rhunk.snapenhance.core.download.DownloadManagerClient import me.rhunk.snapenhance.core.download.data.DownloadMediaType import me.rhunk.snapenhance.core.download.data.DownloadMetadata import me.rhunk.snapenhance.core.download.data.InputMedia -import me.rhunk.snapenhance.core.download.data.MediaFilter +import me.rhunk.snapenhance.core.download.data.MediaDownloadSource import me.rhunk.snapenhance.core.download.data.SplitMediaAssetType import me.rhunk.snapenhance.core.download.data.toKeyPair import me.rhunk.snapenhance.core.messaging.MessagingRuleType @@ -45,16 +45,20 @@ import kotlin.coroutines.suspendCoroutine import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi +private fun String.sanitizeForPath(): String { + return this.replace(" ", "_") + .replace(Regex("\\p{Cntrl}"), "") +} + @OptIn(ExperimentalEncodingApi::class) class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleType.AUTO_DOWNLOAD, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { private var lastSeenMediaInfoMap: MutableMap? = null private var lastSeenMapParams: ParamMap? = null private fun provideDownloadManagerClient( - pathSuffix: String, mediaIdentifier: String, - mediaDisplaySource: String? = null, - mediaDisplayType: String? = null, + mediaAuthor: String, + downloadSource: MediaDownloadSource, friendInfo: FriendInfo? = null ): DownloadManagerClient { val generatedHash = mediaIdentifier.hashCode().toString(16).replaceFirst("-", "") @@ -66,7 +70,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp context.shortToast(context.translation["download_processor.download_started_toast"]) } - val outputPath = createNewFilePath(generatedHash, mediaDisplayType, pathSuffix) + val outputPath = createNewFilePath(generatedHash, downloadSource, mediaAuthor) return DownloadManagerClient( context = context, @@ -74,8 +78,8 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp mediaIdentifier = if (!context.config.downloader.allowDuplicate.get()) { generatedHash } else null, - mediaDisplaySource = mediaDisplaySource, - mediaDisplayType = mediaDisplayType, + mediaAuthor = mediaAuthor, + downloadSource = downloadSource.key, iconUrl = iconUrl, outputPath = outputPath ), @@ -106,13 +110,9 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } - //TODO: implement subfolder argument - private fun createNewFilePath(hexHash: String, mediaDisplayType: String?, pathPrefix: String): String { + private fun createNewFilePath(hexHash: String, downloadSource: MediaDownloadSource, mediaAuthor: String): String { val pathFormat by context.config.downloader.pathFormat - val sanitizedPathPrefix = pathPrefix - .replace(" ", "_") - .replace(Regex("[\\p{Cntrl}]"), "") - .ifEmpty { hexHash } + val sanitizedMediaAuthor = mediaAuthor.sanitizeForPath().ifEmpty { hexHash } val currentDateTime = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH).format(System.currentTimeMillis()) @@ -126,19 +126,20 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } } - if (pathFormat.contains("create_user_folder")) { - finalPath.append(sanitizedPathPrefix).append("/") + if (pathFormat.contains("create_author_folder")) { + finalPath.append(sanitizedMediaAuthor).append("/") + } + if (pathFormat.contains("create_source_folder")) { + finalPath.append(downloadSource.pathName).append("/") } if (pathFormat.contains("append_hash")) { appendFileName(hexHash) } - mediaDisplayType?.let { - if (pathFormat.contains("append_type")) { - appendFileName(it.lowercase().replace(" ", "-")) - } + if (pathFormat.contains("append_source")) { + appendFileName(downloadSource.pathName) } if (pathFormat.contains("append_username")) { - appendFileName(sanitizedPathPrefix) + appendFileName(sanitizedMediaAuthor) } if (pathFormat.contains("append_date_time")) { appendFileName(currentDateTime) @@ -235,10 +236,9 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp val authorUsername = author.usernameForSorting!! downloadOperaMedia(provideDownloadManagerClient( - pathSuffix = authorUsername, mediaIdentifier = "$conversationId$senderId${conversationMessage.serverMessageId}", - mediaDisplaySource = authorUsername, - mediaDisplayType = MediaFilter.CHAT_MEDIA.key, + mediaAuthor = authorUsername, + downloadSource = MediaDownloadSource.CHAT_MEDIA, friendInfo = author ), mediaInfoMap) @@ -278,10 +278,9 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp if (!forceDownload && !canUseRule(author.userId!!)) return downloadOperaMedia(provideDownloadManagerClient( - pathSuffix = authorName, mediaIdentifier = paramMap["MEDIA_ID"].toString(), - mediaDisplaySource = authorName, - mediaDisplayType = MediaFilter.STORY.key, + mediaAuthor = authorName, + downloadSource = MediaDownloadSource.STORY, friendInfo = author ), mediaInfoMap) return @@ -292,15 +291,12 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp //public stories if ((snapSource == "PUBLIC_USER" || snapSource == "SAVED_STORY") && (forceDownload || canAutoDownload("public_stories"))) { - val userDisplayName = (if (paramMap.containsKey("USER_DISPLAY_NAME")) paramMap["USER_DISPLAY_NAME"].toString() else "").replace( - "[\\p{Cntrl}]".toRegex(), - "") + val userDisplayName = (if (paramMap.containsKey("USER_DISPLAY_NAME")) paramMap["USER_DISPLAY_NAME"].toString() else "").sanitizeForPath() downloadOperaMedia(provideDownloadManagerClient( - pathSuffix = "Public-Stories/$userDisplayName", mediaIdentifier = paramMap["SNAP_ID"].toString(), - mediaDisplayType = userDisplayName, - mediaDisplaySource = "Public Story" + mediaAuthor = userDisplayName, + downloadSource = MediaDownloadSource.PUBLIC_STORY, ), mediaInfoMap) return } @@ -308,10 +304,9 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp //spotlight if (snapSource == "SINGLE_SNAP_STORY" && (forceDownload || canAutoDownload("spotlight"))) { downloadOperaMedia(provideDownloadManagerClient( - pathSuffix = "Spotlight", mediaIdentifier = paramMap["SNAP_ID"].toString(), - mediaDisplayType = MediaFilter.SPOTLIGHT.key, - mediaDisplaySource = paramMap["TIME_STAMP"].toString() + downloadSource = MediaDownloadSource.SPOTLIGHT, + mediaAuthor = paramMap["TIME_STAMP"].toString() ), mediaInfoMap) return } @@ -319,9 +314,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp //stories with mpeg dash media //TODO: option to download multiple chapters if (paramMap.containsKey("LONGFORM_VIDEO_PLAYLIST_ITEM") && forceDownload) { - val storyName = paramMap["STORY_NAME"].toString().replace( - "[\\p{Cntrl}]".toRegex(), - "") + val storyName = paramMap["STORY_NAME"].toString().sanitizeForPath() //get the position of the media in the playlist and the duration val snapItem = SnapPlaylistItem(paramMap["SNAP_PLAYLIST_ITEM"]!!) @@ -338,20 +331,19 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp val duration: Long? = nextChapter?.startTimeMs?.minus(snapChapterTimestamp) //get the mpd playlist and append the cdn url to baseurl nodes + context.log.verbose("Downloading dash media ${paramMap["MEDIA_ID"].toString()}", featureKey) 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) - } + val urlIndexes = arrayOf(it.indexOf("https://cf-st.sc-cdn.net"), it.indexOf("https://bolt-gcdn.sc-cdn.net")) + + urlIndexes.firstOrNull { index -> index != -1 }?.let { validIndex -> + it.substring(validIndex) + } ?: "${RemoteMediaResolver.CF_ST_CDN_D}$it" } provideDownloadManagerClient( - pathSuffix = "Pro-Stories/${storyName}", mediaIdentifier = "${paramMap["STORY_ID"]}-${snapItem.snapId}", - mediaDisplaySource = storyName, - mediaDisplayType = "Pro Story" + downloadSource = MediaDownloadSource.PUBLIC_STORY, + mediaAuthor = storyName ).downloadDashMedia( playlistUrl, snapChapterTimestamp, @@ -476,10 +468,9 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp if (!isPreview) { val encryptionKeys = EncryptionHelper.getEncryptionKeys(contentType, messageReader, isArroyo = isArroyoMessage) provideDownloadManagerClient( - pathSuffix = authorName, mediaIdentifier = "${message.clientConversationId}${message.senderId}${message.serverMessageId}", - mediaDisplaySource = authorName, - mediaDisplayType = MediaFilter.CHAT_MEDIA.key, + downloadSource = MediaDownloadSource.CHAT_MEDIA, + mediaAuthor = authorName, friendInfo = friendInfo ).downloadSingleMedia( Base64.UrlSafe.encode(urlProto), @@ -532,10 +523,9 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp fun downloadProfilePicture(url: String, author: String) { provideDownloadManagerClient( - pathSuffix = "Profile Pictures", mediaIdentifier = url.hashCode().toString(16).replaceFirst("-", ""), - mediaDisplaySource = author, - mediaDisplayType = MediaFilter.PROFILE_PICTURE.key + mediaAuthor = author, + downloadSource = MediaDownloadSource.PROFILE_PICTURE ).downloadSingleMedia( url, DownloadMediaType.REMOTE_MEDIA diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt index f13cfd62..80f6af5c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt @@ -12,13 +12,13 @@ import javax.crypto.spec.SecretKeySpec object EncryptionHelper { fun getEncryptionKeys(contentType: ContentType, messageProto: ProtoReader, isArroyo: Boolean): Pair? { - val messageMediaInfo = MediaDownloaderHelper.getMessageMediaInfo(messageProto, contentType, isArroyo) ?: return null - val encryptionProtoIndex = if (messageMediaInfo.contains(Constants.ENCRYPTION_PROTO_INDEX_V2)) { + val mediaEncryptionInfo = MediaDownloaderHelper.getMessageMediaEncryptionInfo(messageProto, contentType, isArroyo) ?: return null + val encryptionProtoIndex = if (mediaEncryptionInfo.contains(Constants.ENCRYPTION_PROTO_INDEX_V2)) { Constants.ENCRYPTION_PROTO_INDEX_V2 } else { Constants.ENCRYPTION_PROTO_INDEX } - val encryptionProto = messageMediaInfo.followPath(encryptionProtoIndex) ?: return null + val encryptionProto = mediaEncryptionInfo.followPath(encryptionProtoIndex) ?: return null var key: ByteArray = encryptionProto.getByteArray(1)!! var iv: ByteArray = encryptionProto.getByteArray(2)!! diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt index df4a7973..bd5b76d9 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt @@ -18,7 +18,7 @@ import java.util.zip.ZipInputStream object MediaDownloaderHelper { - fun getMessageMediaInfo(protoReader: ProtoReader, contentType: ContentType, isArroyo: Boolean): ProtoReader? { + fun getMessageMediaEncryptionInfo(protoReader: ProtoReader, contentType: ContentType, isArroyo: Boolean): ProtoReader? { val messageContainerPath = if (isArroyo) protoReader.followPath(*Constants.ARROYO_MEDIA_CONTAINER_PROTO_PATH)!! else protoReader val mediaContainerPath = if (contentType == ContentType.NOTE) intArrayOf(6, 1, 1) else intArrayOf(5, 1, 1) @@ -27,12 +27,13 @@ object MediaDownloaderHelper { ContentType.SNAP -> messageContainerPath.followPath(*(intArrayOf(11) + mediaContainerPath)) ContentType.EXTERNAL_MEDIA -> { val externalMediaTypes = arrayOf( - intArrayOf(3, 3), //normal external media - intArrayOf(7, 12, 3), //attached story reply - intArrayOf(7, 3) //original story reply + intArrayOf(3, 3, *mediaContainerPath), //normal external media + intArrayOf(7, 15, 1, 1), //attached audio note + intArrayOf(7, 12, 3, *mediaContainerPath), //attached story reply + intArrayOf(7, 3, *mediaContainerPath), //original story reply ) externalMediaTypes.forEach { path -> - messageContainerPath.followPath(*(path + mediaContainerPath))?.also { return it } + messageContainerPath.followPath(*path)?.also { return it } } null }