fix(media_downloader): story voice note reply

- refactor media author and download source
- optimize download section
This commit is contained in:
rhunk 2023-09-01 11:50:42 +02:00
parent ea6260463c
commit 5776d44111
15 changed files with 170 additions and 139 deletions

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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<Int, DownloadObject> {
val isPendingFilter = filter == MediaFilter.PENDING
fun queryFirstTasks(filter: MediaDownloadSource): Map<Int, DownloadObject> {
val isPendingFilter = filter == MediaDownloadSource.PENDING
val tasks = mutableMapOf<Int, DownloadObject>()
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<Int, DownloadObject> {
if (filter == MediaFilter.PENDING) {
fun queryTasks(from: Int, amount: Int = 30, filter: MediaDownloadSource = MediaDownloadSource.NONE): Map<Int, DownloadObject> {
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) {

View File

@ -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<Int, DownloadObject>())
private var currentFilter = mutableStateOf(MediaFilter.NONE)
private var currentFilter = mutableStateOf(MediaDownloadSource.NONE)
private val coroutineScope = CoroutineScope(Dispatchers.IO)
override fun onResumed() {
super.onResumed()
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,16 +270,21 @@ 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
scope.launch(Dispatchers.IO) {
lazyLoadFromIndex(lastItemIndex)
}.asCompletableFuture().thenAccept {
scope.launch {
scrollState.animateScrollToItem(lastItemIndex)
}
}
}
}
}
}
}

View File

@ -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()
}

View File

@ -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",

View File

@ -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) }

View File

@ -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?
)

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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<SplitMediaAssetType, MediaInfo>? = 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

View File

@ -12,13 +12,13 @@ import javax.crypto.spec.SecretKeySpec
object EncryptionHelper {
fun getEncryptionKeys(contentType: ContentType, messageProto: ProtoReader, isArroyo: Boolean): Pair<ByteArray, ByteArray>? {
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)!!

View File

@ -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
}