commit ec05e4f5d4d43a281a673a911195108aec9c72f0
parent 0b626df1ebcef262702a46e8641f5ba9c8466718
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Fri,  1 Sep 2023 20:04:31 +0200

feat(media_downloader): ability to select chapters for dash media

Diffstat:
Mapp/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt | 1+
Mcore/src/main/assets/lang/en_US.json | 4++++
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/DownloaderConfig.kt | 1+
Mcore/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
4 files changed, 81 insertions(+), 18 deletions(-)

diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt @@ -93,6 +93,7 @@ class FFMpegProcessor( this += "-c:a" to (ffmpegOptions.customAudioCodec.get().takeIf { it.isNotEmpty() } ?: "copy") this += "-crf" to ffmpegOptions.constantRateFactor.get().let { "\"$it\"" } this += "-b:v" to ffmpegOptions.videoBitrate.get().toString() + "K" + this += "-b:a" to ffmpegOptions.audioBitrate.get().toString() + "K" } when (args.action) { diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json @@ -143,6 +143,10 @@ "name": "Video Bitrate", "description": "Set the video bitrate (in kbps)" }, + "audio_bitrate": { + "name": "Audio Bitrate", + "description": "Set the audio bitrate (in kbps)" + }, "custom_video_codec": { "name": "Custom Video Codec", "description": "Set a custom video codec (e.g. libx264)" 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 @@ -12,6 +12,7 @@ class DownloaderConfig : ConfigContainer() { } val constantRateFactor = integer("constant_rate_factor", 30) val videoBitrate = integer("video_bitrate", 5000) + val audioBitrate = integer("audio_bitrate", 128) val customVideoCodec = string("custom_video_codec") { addFlags(ConfigFlag.NO_TRANSLATE) } val customAudioCodec = string("custom_audio_codec") { addFlags(ConfigFlag.NO_TRANSLATE) } } 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 @@ -50,6 +50,12 @@ private fun String.sanitizeForPath(): String { .replace(Regex("\\p{Cntrl}"), "") } +class SnapChapterInfo( + val offset: Long, + val duration: Long? +) + + @OptIn(ExperimentalEncodingApi::class) class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleType.AUTO_DOWNLOAD, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null @@ -312,26 +318,25 @@ 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().sanitizeForPath() - //get the position of the media in the playlist and the duration val snapItem = SnapPlaylistItem(paramMap["SNAP_PLAYLIST_ITEM"]!!) val snapChapterList = LongformVideoPlaylistItem(paramMap["LONGFORM_VIDEO_PLAYLIST_ITEM"]!!).chapters + val currentChapterIndex = snapChapterList.indexOfFirst { it.snapId == snapItem.snapId } + if (snapChapterList.isEmpty()) { context.shortToast("No chapters found") return } - val snapChapter = snapChapterList.first { it.snapId == snapItem.snapId } - val nextChapter = snapChapterList.getOrNull(snapChapterList.indexOf(snapChapter) + 1) - //add 100ms to the start time to prevent the video from starting too early - val snapChapterTimestamp = snapChapter.startTimeMs.plus(100) - val duration: Long? = nextChapter?.startTimeMs?.minus(snapChapterTimestamp) + fun prettyPrintTime(time: Long): String { + val seconds = time / 1000 + val minutes = seconds / 60 + val hours = minutes / 60 + return "${hours % 24}:${minutes % 60}:${seconds % 60}" + } - //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 urlIndexes = arrayOf(it.indexOf("https://cf-st.sc-cdn.net"), it.indexOf("https://bolt-gcdn.sc-cdn.net")) @@ -340,15 +345,67 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp } ?: "${RemoteMediaResolver.CF_ST_CDN_D}$it" } - provideDownloadManagerClient( - mediaIdentifier = "${paramMap["STORY_ID"]}-${snapItem.snapId}", - downloadSource = MediaDownloadSource.PUBLIC_STORY, - mediaAuthor = storyName - ).downloadDashMedia( - playlistUrl, - snapChapterTimestamp, - duration - ) + context.runOnUiThread { + val selectedChapters = mutableListOf<Int>() + val chapters = snapChapterList.mapIndexed { index, snapChapter -> + val nextChapter = snapChapterList.getOrNull(index + 1) + val duration = nextChapter?.startTimeMs?.minus(snapChapter.startTimeMs) + SnapChapterInfo(snapChapter.startTimeMs, duration) + } + ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity!!).apply { + setTitle("Download dash media") + setMultiChoiceItems( + chapters.map { "Segment ${prettyPrintTime(it.offset)} - ${prettyPrintTime(it.offset + (it.duration ?: 0))}" }.toTypedArray(), + List(chapters.size) { index -> currentChapterIndex == index }.toBooleanArray() + ) { _, which, isChecked -> + if (isChecked) { + selectedChapters.add(which) + } else if (selectedChapters.contains(which)) { + selectedChapters.remove(which) + } + } + setPositiveButton("Download") { dialog, which -> + val groups = mutableListOf<MutableList<SnapChapterInfo>>() + var currentGroup = mutableListOf<SnapChapterInfo>() + var lastChapterIndex = -1 + + //check for consecutive chapters + chapters.filterIndexed { index, _ -> selectedChapters.contains(index) } + .forEachIndexed { index, pair -> + if (lastChapterIndex != -1 && index != lastChapterIndex + 1) { + groups.add(currentGroup) + currentGroup = mutableListOf() + } + currentGroup.add(pair) + lastChapterIndex = index + } + + if (currentGroup.isNotEmpty()) { + groups.add(currentGroup) + } + + groups.forEach { group -> + val firstChapter = group.first() + val lastChapter = group.last() + val duration = if (firstChapter == lastChapter) { + firstChapter.duration + } else { + lastChapter.duration?.let { lastChapter.offset - firstChapter.offset + it } + } + + provideDownloadManagerClient( + mediaIdentifier = "${paramMap["STORY_ID"]}-${firstChapter.offset}-${lastChapter.offset}", + downloadSource = MediaDownloadSource.PUBLIC_STORY, + mediaAuthor = storyName + ).downloadDashMedia( + playlistUrl, + firstChapter.offset.plus(100), + duration + ) + } + } + }.show() + } } }