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:
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()
+ }
}
}