Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration

/**
* Wrapper for [ItemPlaybackDao] to save [ItemPlayback] objects and also convert them into [ChosenStreams]
*/
@Singleton
class ItemPlaybackRepository
@Inject
Expand All @@ -28,6 +31,13 @@ class ItemPlaybackRepository
private val playbackLanguageChoiceDao: PlaybackLanguageChoiceDao,
private val streamChoiceService: StreamChoiceService,
) {
/**
* Get the chosen source & stream for the given item
*
* This will be either the explicit choice from the user ([ItemPlayback]) or the default choices
*
* @see getChosenItemFromPlayback
*/
suspend fun getSelectedTracks(
itemId: UUID,
item: BaseItem,
Expand All @@ -40,6 +50,11 @@ class ItemPlaybackRepository
return getChosenItemFromPlayback(item, itemPlayback, plc, prefs)
}

/**
* Get the chosen source & stream for the given item
*
* This will be either the explicit choice from the provided [ItemPlayback] & [PlaybackLanguageChoice] or the default choices if they are null
*/
fun getChosenItemFromPlayback(
item: BaseItem,
itemPlayback: ItemPlayback?,
Expand Down Expand Up @@ -86,6 +101,9 @@ class ItemPlaybackRepository
}
}

/**
* Save the choice for a specific media source for an item
*/
suspend fun savePlayVersion(
itemId: UUID,
sourceId: UUID,
Expand All @@ -103,6 +121,11 @@ class ItemPlaybackRepository
}
}

/**
* Save the chosen track (index & type) for an item
*
* Additionally, if the item is part of a TV Series, store the language as the preferred one for this series
*/
suspend fun saveTrackSelection(
item: BaseItem,
itemPlayback: ItemPlayback?,
Expand Down Expand Up @@ -180,6 +203,9 @@ class ItemPlaybackRepository
return toSave.copy(rowId = id)
}

/**
* Wrapper for [ItemPlaybackDao.getTrackModifications] to get the [ItemTrackModification] for an item, if any
*/
suspend fun getTrackModifications(
itemId: UUID,
trackIndex: Int,
Expand All @@ -188,6 +214,9 @@ class ItemPlaybackRepository
itemPlaybackDao.getTrackModifications(userId, itemId, trackIndex)
}

/**
* Create and save a [ItemTrackModification] via [ItemPlaybackDao.saveItem]
*/
suspend fun saveTrackModifications(
itemId: UUID,
trackIndex: Int,
Expand All @@ -206,6 +235,9 @@ class ItemPlaybackRepository
}
}

/**
* Clear the user's source/stream choices for an item
*/
suspend fun deleteChosenStreams(chosenStreams: ChosenStreams?) {
Timber.d("deleteChosenStreams: %s", chosenStreams)
chosenStreams?.plc?.let {
Expand All @@ -219,6 +251,9 @@ class ItemPlaybackRepository
}
}

/**
* Info about which media source and streams the user/app has chosen for playback
*/
data class ChosenStreams(
val itemPlayback: ItemPlayback?,
val plc: PlaybackLanguageChoice?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.github.damontecres.wholphin.services

import com.github.damontecres.wholphin.data.model.BaseItem
import kotlinx.coroutines.delay
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.TimeoutException
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.PlaybackInfoDto
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

/**
* Resolves .strm files to populate the tracks & metadata
*/
@Singleton
class StrmFileHandler
@Inject
constructor(
private val api: ApiClient,
) {
/**
* Attempts to resolve if the strm file is not yet resolved.
*
* If the item is not a strm, file, nothing happens
*
* @return the resolved item or null if could not be resolved
*/
suspend fun resolveStrm(item: BaseItem): BaseItem? {
if (shouldResolveStrm(item)) {
Timber.d("Resolved strm %s", item.id)
resolve(item.id, null)
var count = 5
while (count > 0) {
Timber.v("Checking for streams in %s", item.id)
val fetchedItem =
api.userLibraryApi
.getItem(item.id)
.content
.let { BaseItem(it) }
if (!shouldResolveStrm(fetchedItem)) {
Timber.d("Got updated streams for %s", item.id)
return fetchedItem
}
delay(2000)
count--
}
Timber.w("Resole timed out for %s", item.id)
throw TimeoutException("Resole timed out for " + item.id)
}
return null
}

private suspend fun resolve(
itemId: UUID,
mediaSourceId: String?,
) {
val response by
api.mediaInfoApi
.getPostedPlaybackInfo(
itemId,
PlaybackInfoDto(
deviceProfile = null,
mediaSourceId = mediaSourceId,
autoOpenLiveStream = false,
),
)
if (response.errorCode != null) {
throw IllegalStateException("Error: " + response.errorCode)
}
}

companion object {
fun isStrmFile(item: BaseItem): Boolean =
item.data.path?.endsWith(".strm") == true &&
item.data.mediaSources
?.firstOrNull()
?.isRemote == true

fun shouldResolveStrm(item: BaseItem): Boolean {
if (!isStrmFile(item)) {
return false
}
val streams =
item.data.mediaSources
?.firstOrNull()
?.mediaStreams
return streams.isNullOrEmpty()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ fun ContextMenu(
onDismissRequest = { chooseVersion = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
elevation = 3.dp,
)
}
}
Expand All @@ -285,6 +286,7 @@ fun ContextMenu(
actions.onDeleteItem.invoke(item)
onDismissRequest.invoke()
},
elevation = 3.dp,
)
}
if (showPlayWithDialog) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.TrackIndex
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.formatBitrate
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.playback.SimpleMediaStream
Expand Down Expand Up @@ -630,7 +631,14 @@ fun chooseVersionParams(
SelectedLeadingContent(uuid != null && uuid == chosenSourceId)
},
supportingContent = {
videoStream?.displayTitle?.let { Text(text = it) }
val text =
remember {
buildList {
videoStream?.displayTitle?.let(::add)
source.bitrate?.let(::formatBitrate)?.let(::add)
}.joinToString(", ")
}
Text(text)
},
onClick = { onClick.invoke(index) },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
Expand All @@ -56,13 +57,15 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.data.ChooseVersionParams
import com.github.damontecres.wholphin.ui.data.SortAndDirection
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

Expand All @@ -83,9 +86,11 @@ fun ExpandablePlayButtons(
moreOnClick: () -> Unit,
trailerOnClick: (Trailer) -> Unit,
onConfirmDelete: () -> Unit,
chooseVersionParams: ChooseVersionParams?,
buttonOnFocusChanged: (FocusState) -> Unit,
modifier: Modifier = Modifier,
) {
val resources = LocalResources.current
val firstFocus = remember { FocusRequester() }
val restartInteractionSource = remember { MutableInteractionSource() }
val restartButtonFocused by restartInteractionSource.collectIsFocusedAsState()
Expand All @@ -95,6 +100,15 @@ fun ExpandablePlayButtons(
firstFocus.tryRequestFocus()
}
}
val hasStreams =
remember(chooseVersionParams) {
chooseVersionParams?.chosenStreams?.source.let {
it == null || !it.mediaStreams.isNullOrEmpty()
}
}

var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }

LazyRow(
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(8.dp),
Expand All @@ -109,6 +123,7 @@ fun ExpandablePlayButtons(
resume = resumePosition,
icon = Icons.Default.PlayArrow,
onClick = playOnClick,
enabled = hasStreams,
modifier =
Modifier
.onFocusChanged(buttonOnFocusChanged)
Expand All @@ -122,13 +137,38 @@ fun ExpandablePlayButtons(
resume = Duration.ZERO,
icon = Icons.Default.Refresh,
onClick = playOnClick,
enabled = hasStreams,
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
mirrorIcon = true,
interactionSource = restartInteractionSource,
)
}
}

if (chooseVersionParams != null && chooseVersionParams.mediaSources.size > 1) {
item {
ExpandableFaButton(
title = R.string.version,
iconStringRes = R.string.fa_file_video,
onClick = {
chooseVersion =
chooseVersionParams(
resources,
chooseVersionParams.mediaSources.orEmpty(),
chooseVersionParams.chosenStreams
?.source
?.id
?.toUUIDOrNull(),
) { idx ->
val source = chooseVersionParams.mediaSources[idx]
chooseVersionParams.onChooseVersion.invoke(source)
}
},
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
)
}
}

// Watched button
item("watched") {
ExpandableFaButton(
Expand Down Expand Up @@ -182,6 +222,16 @@ fun ExpandablePlayButtons(
)
}
}
chooseVersion?.let { params ->
DialogPopup(
showDialog = true,
title = params.title,
dialogItems = params.items,
onDismissRequest = { chooseVersion = null },
dismissOnClick = true,
waitToLoad = params.fromLongClick,
)
}
}

val MinButtonSize = 40.dp
Expand Down Expand Up @@ -451,6 +501,7 @@ private fun ExpandablePlayButtonsPreview() {
trailerOnClick = {},
canDelete = true,
onConfirmDelete = {},
chooseVersionParams = null,
modifier = Modifier,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.damontecres.wholphin.ui.data

import com.github.damontecres.wholphin.data.ChosenStreams
import org.jellyfin.sdk.model.api.MediaSourceInfo

/**
* Necessary details & action for choosing a version of some content
*/
data class ChooseVersionParams(
val chosenStreams: ChosenStreams?,
val mediaSources: List<MediaSourceInfo>,
val onChooseVersion: (MediaSourceInfo) -> Unit,
)
Loading
Loading