Skip to content

Commit 67600d5

Browse files
feat: support batch download
1 parent 8ad426e commit 67600d5

11 files changed

Lines changed: 166 additions & 5 deletions

File tree

android/src/main/kotlin/project/pipepipe/app/MainActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import project.pipepipe.app.helper.SponsorBlockHelper
3939
import project.pipepipe.app.helper.ToastManager
4040
import project.pipepipe.app.platform.AndroidActions
4141
import project.pipepipe.app.platform.AndroidMediaController
42+
import project.pipepipe.app.platform.AndroidMenuItems
4243
import project.pipepipe.app.platform.AndroidRouteHandler
4344
import project.pipepipe.app.service.FeedUpdateManager
4445
import project.pipepipe.app.ui.component.*
@@ -89,6 +90,7 @@ class MainActivity : ComponentActivity() {
8990
onResetFeedState = { FeedUpdateManager.resetState() },
9091
)
9192
SharedContext.platformRouteHandler = AndroidRouteHandler()
93+
SharedContext.platformMenuItems = AndroidMenuItems()
9294

9395

9496
composeView.setContent {

android/src/main/kotlin/project/pipepipe/app/download/DownloadManager.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,10 @@ class DownloadManager(private val context: Context) {
6868
formatId = formatId
6969
)
7070

71-
// Start download service to keep app alive
72-
DownloadService.start(context)
73-
7471
// Check if we can start this download immediately
7572
if (activeWorkers.size < maxConcurrent) {
73+
// Start download service to keep app alive
74+
DownloadService.start(context)
7675
startWorker(downloadId)
7776
} else {
7877
Log.d(TAG, "Download queued (${activeWorkers.size}/$maxConcurrent active)")

android/src/main/kotlin/project/pipepipe/app/download/DownloadWorker.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,11 @@ class DownloadWorker(
142142
addOption("--write-auto-sub")
143143
addOption("--sub-lang", download.format_id)
144144
} else {
145-
addOption("-f", download.format_id)
145+
if (download.format_id.startsWith("res")) {
146+
addOption("-S", download.format_id)
147+
} else {
148+
addOption("-f", download.format_id)
149+
}
146150
addOption("--embed-thumbnail")
147151
if (download.download_type == "AUDIO") {
148152
addOption("--remux-video", "webm>opus")

android/src/main/kotlin/project/pipepipe/app/download/YtDlpFormatHelper.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,4 +303,27 @@ object YtDlpFormatHelper {
303303

304304
return Triple(sortedVideos, sortedAudios, subtitleIds)
305305
}
306+
private const val DEFAULT_RESOLUTION_KEY = "default_resolution_key"
307+
308+
fun getFormatIdForVideo(): String {
309+
val defaultResolution = SharedContext.settingsManager.getString(DEFAULT_RESOLUTION_KEY, "auto")
310+
311+
return when (defaultResolution) {
312+
"best" -> "bv+ba" // Best video + best audio
313+
"worst", "lowest" -> "wv+ba" // Worst video + best audio
314+
"auto" -> "res:720" // 720p as default
315+
else -> {
316+
// Parse resolution like "1080p", "720p" etc.
317+
if (defaultResolution.endsWith("p")) {
318+
"res:${defaultResolution.replace("p","")}"
319+
} else {
320+
"res:720" // Fallback to 720p
321+
}
322+
}
323+
}
324+
}
325+
326+
fun getFormatIdForAudio(): String {
327+
return "ba[ext=m4a]/ba"
328+
}
306329
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package project.pipepipe.app.platform
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.material.icons.Icons
8+
import androidx.compose.material.icons.filled.Download
9+
import androidx.compose.material3.*
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.mutableStateOf
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.unit.dp
14+
import dev.icerock.moko.resources.compose.stringResource
15+
import kotlinx.coroutines.Dispatchers
16+
import kotlinx.coroutines.launch
17+
import project.pipepipe.app.MR
18+
import project.pipepipe.app.database.DatabaseOperations
19+
import project.pipepipe.app.download.DownloadManagerHolder
20+
import project.pipepipe.app.download.YtDlpFormatHelper
21+
import project.pipepipe.app.uistate.DownloadType
22+
23+
24+
class AndroidMenuItems : PlatformMenuItems {
25+
private val _showDownloadDialog = mutableStateOf(false)
26+
27+
@Composable
28+
override fun localPlaylistMenuItems() {
29+
DropdownMenuItem(
30+
text = { Text(stringResource(MR.strings.download)) },
31+
onClick = {
32+
showDownloadDialog()
33+
},
34+
leadingIcon = { Icon(Icons.Default.Download, contentDescription = null) }
35+
)
36+
}
37+
38+
@Composable
39+
override fun localPlaylistDialogs(playlistId: Long?) {
40+
if (_showDownloadDialog.value && playlistId != null) {
41+
DownloadTypeSelectionDialog(
42+
onDismiss = { hideDownloadDialog() },
43+
onVideoSelected = { startBatchDownload(DownloadType.VIDEO, playlistId) },
44+
onAudioSelected = { startBatchDownload(DownloadType.AUDIO, playlistId) }
45+
)
46+
}
47+
}
48+
49+
fun showDownloadDialog() {
50+
_showDownloadDialog.value = true
51+
}
52+
53+
fun hideDownloadDialog() {
54+
_showDownloadDialog.value = false
55+
}
56+
57+
private fun startBatchDownload(type: DownloadType, playlistId: Long) {
58+
val formatId = if (type == DownloadType.VIDEO) {
59+
YtDlpFormatHelper.getFormatIdForVideo()
60+
} else {
61+
YtDlpFormatHelper.getFormatIdForAudio()
62+
}
63+
64+
// Placeholder logic - will be modified later with playlistId
65+
// For now, this demonstrates the structure
66+
kotlinx.coroutines.GlobalScope.launch(Dispatchers.IO) {
67+
DatabaseOperations.loadPlaylistsItemsFromDatabase(playlistId).forEach { streamInfo ->
68+
DownloadManagerHolder.instance.addDownload(
69+
url = streamInfo.url,
70+
title = streamInfo.name ?: "Unknown",
71+
imageUrl = streamInfo.thumbnailUrl,
72+
duration = if (type == DownloadType.SUBTITLE) 0 else streamInfo.duration?.toInt() ?: 0,
73+
downloadType = type,
74+
quality = "auto",
75+
codec = "auto",
76+
formatId = formatId
77+
)
78+
}
79+
}
80+
hideDownloadDialog()
81+
}
82+
83+
@Composable
84+
private fun DownloadTypeSelectionDialog(
85+
onDismiss: () -> Unit,
86+
onVideoSelected: () -> Unit,
87+
onAudioSelected: () -> Unit
88+
) {
89+
AlertDialog(
90+
onDismissRequest = onDismiss,
91+
title = { Text(stringResource(MR.strings.download)) },
92+
text = {
93+
Column(modifier = Modifier.fillMaxWidth()) {
94+
Text(
95+
text = stringResource(MR.strings.download_dialog_message),
96+
style = MaterialTheme.typography.bodyMedium
97+
)
98+
}
99+
},
100+
confirmButton = {
101+
TextButton(onClick = onVideoSelected) {
102+
Text(stringResource(MR.strings.download_video_quality))
103+
}
104+
},
105+
dismissButton = {
106+
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
107+
TextButton(onClick = onDismiss) {
108+
Text(stringResource(MR.strings.cancel))
109+
}
110+
TextButton(onClick = onAudioSelected) {
111+
Text(stringResource(MR.strings.download_audio_format))
112+
}
113+
}
114+
}
115+
)
116+
}
117+
}

android/src/main/kotlin/project/pipepipe/app/ui/item/DownloadItem.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ private fun TagsRow(state: DownloadItemState) {
308308
horizontalArrangement = Arrangement.spacedBy(6.dp)
309309
) {
310310
TagBadge(
311-
text = state.quality,
311+
text = if (state.quality == "auto") stringResource(MR.strings.auto)else state.quality,
312312
color = MaterialTheme.colorScheme.secondaryContainer,
313313
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
314314
)

library/src/commonMain/kotlin/project/pipepipe/app/SharedContext.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import project.pipepipe.app.platform.PlatformActions
2727
import project.pipepipe.app.platform.PlatformMediaController
2828
import project.pipepipe.app.platform.PlatformRouteHandler
2929
import project.pipepipe.app.platform.PlatformMediaItem
30+
import project.pipepipe.app.platform.PlatformMenuItems
3031
import project.pipepipe.shared.infoitem.SupportedServiceInfo
3132
import project.pipepipe.shared.job.SupportedJobType
3233
import project.pipepipe.shared.state.SessionManager
@@ -52,6 +53,7 @@ object SharedContext {
5253
lateinit var platformDatabaseActions: PlatformDatabaseActions
5354
var platformMediaController: PlatformMediaController? = null
5455
lateinit var platformRouteHandler: PlatformRouteHandler
56+
lateinit var platformMenuItems: PlatformMenuItems
5557
// Safe in single-activity architecture where Activity lifecycle matches application lifecycle
5658
lateinit var navController: NavHostController
5759

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package project.pipepipe.app.platform
2+
3+
import androidx.compose.runtime.Composable
4+
5+
interface PlatformMenuItems {
6+
@Composable
7+
fun localPlaylistMenuItems()
8+
@Composable
9+
fun localPlaylistDialogs(playlistId: Long?)
10+
}

library/src/commonMain/kotlin/project/pipepipe/app/ui/screens/playlistdetail/PlaylistDetailScreen.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ fun PlaylistDetailScreen(
273273
)
274274
}
275275

276+
SharedContext.platformMenuItems.localPlaylistDialogs(uiState.playlistInfo?.url?.substringAfterLast("/")?.toLongOrNull())
277+
276278
if (isSearchActive) {
277279
BackHandler {
278280
focusManager.clearFocus()

library/src/commonMain/kotlin/project/pipepipe/app/ui/screens/playlistdetail/PlaylistMenus.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ fun PlaylistMoreMenu(
174174
},
175175
leadingIcon = { Icon(Icons.Default.Delete, contentDescription = null) }
176176
)
177+
SharedContext.platformMenuItems.localPlaylistMenuItems()
177178
}
178179

179180
PlaylistType.FEED -> {

0 commit comments

Comments
 (0)