mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Support playing multiple file versions (#30)
Adds support for playing different versions of a media item. Additionally, you can pick the audio or subtitle tracks for the chosen version. The file with the highest resolution is the default, but you can switch versions or tracks in the "More" menu. These choices are persisted across playbacks, just as in #26.
This commit is contained in:
parent
e2db5ab4f3
commit
f57fd25f8e
15 changed files with 771 additions and 196 deletions
|
|
@ -1,8 +1,13 @@
|
|||
package com.github.damontecres.wholphin.data
|
||||
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.MediaStream
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
|
@ -24,38 +29,93 @@ class ItemPlaybackRepository
|
|||
val itemPlayback = itemPlaybackDao.getItem(user = user, itemId = itemId)
|
||||
if (itemPlayback != null) {
|
||||
Timber.v("Got itemPlayback for %s", itemId)
|
||||
val source =
|
||||
item.data.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == itemPlayback.sourceId }
|
||||
if (source != null) {
|
||||
val audioStream =
|
||||
if (itemPlayback.audioIndexEnabled) {
|
||||
source.mediaStreams?.firstOrNull { it.index == itemPlayback.audioIndex }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val subtitleStream =
|
||||
if (itemPlayback.subtitleIndexEnabled) {
|
||||
source.mediaStreams?.firstOrNull { it.index == itemPlayback.subtitleIndex }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
ChosenStreams(
|
||||
itemId,
|
||||
audioStream,
|
||||
subtitleStream,
|
||||
itemPlayback.subtitleIndex == TrackIndex.DISABLED,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
getChosenItemFromPlayback(item, itemPlayback)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getChosenItemFromPlayback(
|
||||
item: BaseItem,
|
||||
itemPlayback: ItemPlayback,
|
||||
): ChosenStreams? {
|
||||
val source =
|
||||
item.data.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == itemPlayback.sourceId }
|
||||
if (source != null) {
|
||||
val audioStream =
|
||||
if (itemPlayback.audioIndexEnabled) {
|
||||
source.mediaStreams?.firstOrNull { it.index == itemPlayback.audioIndex }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val subtitleStream =
|
||||
if (itemPlayback.subtitleIndexEnabled) {
|
||||
source.mediaStreams?.firstOrNull { it.index == itemPlayback.subtitleIndex }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return ChosenStreams(
|
||||
itemPlayback,
|
||||
item.id,
|
||||
source.id?.toUUIDOrNull(),
|
||||
audioStream,
|
||||
subtitleStream,
|
||||
itemPlayback.subtitleIndex == TrackIndex.DISABLED,
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun savePlayVersion(
|
||||
itemId: UUID,
|
||||
sourceId: UUID,
|
||||
): ItemPlayback? =
|
||||
withContext(Dispatchers.IO) {
|
||||
serverRepository.currentUser?.let { user ->
|
||||
val itemPlayback =
|
||||
ItemPlayback(
|
||||
userId = user.rowId,
|
||||
itemId = itemId,
|
||||
sourceId = sourceId,
|
||||
)
|
||||
Timber.v("Saving play version %s", itemPlayback)
|
||||
saveItemPlayback(itemPlayback)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveTrackSelection(
|
||||
item: BaseItem,
|
||||
itemPlayback: ItemPlayback?,
|
||||
trackIndex: Int,
|
||||
type: MediaStreamType,
|
||||
) = serverRepository.currentUser?.let { user ->
|
||||
var toSave =
|
||||
itemPlayback ?: ItemPlayback(
|
||||
userId = user.rowId,
|
||||
itemId = item.id,
|
||||
sourceId = chooseSource(item.data, null)?.id?.toUUIDOrNull(),
|
||||
)
|
||||
toSave =
|
||||
when (type) {
|
||||
MediaStreamType.AUDIO -> toSave.copy(audioIndex = trackIndex)
|
||||
MediaStreamType.SUBTITLE -> toSave.copy(subtitleIndex = trackIndex)
|
||||
else -> toSave
|
||||
}
|
||||
Timber.v("Saving track selection %s", toSave)
|
||||
saveItemPlayback(toSave)
|
||||
}
|
||||
|
||||
suspend fun saveItemPlayback(itemPlayback: ItemPlayback): ItemPlayback {
|
||||
val rowId = itemPlaybackDao.saveItem(itemPlayback)
|
||||
return itemPlayback.copy(rowId = rowId)
|
||||
}
|
||||
}
|
||||
|
||||
data class ChosenStreams(
|
||||
val itemPlayback: ItemPlayback,
|
||||
val itemId: UUID,
|
||||
val sourceId: UUID?,
|
||||
val audioStream: MediaStream?,
|
||||
val subtitleStream: MediaStream?,
|
||||
val subtitlesDisabled: Boolean,
|
||||
|
|
|
|||
|
|
@ -7,9 +7,18 @@ import androidx.room.ForeignKey
|
|||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.damontecres.wholphin.data.JellyfinUser
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.util.UuidSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaStream
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(
|
||||
|
|
@ -39,6 +48,119 @@ data class ItemPlayback(
|
|||
@Transient val subtitleIndexEnabled = subtitleIndex >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [MediaSourceInfo] with the highest video resolution
|
||||
*/
|
||||
fun chooseSource(sources: List<MediaSourceInfo>?) =
|
||||
sources?.letNotEmpty { sources ->
|
||||
val result =
|
||||
sources.maxByOrNull { s ->
|
||||
s.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO }?.let { video ->
|
||||
(video.width ?: 0) * (video.height ?: 0)
|
||||
} ?: 0
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [MediaSourceInfo] that matched the [ItemPlayback] or else the one with the highest resolution
|
||||
*/
|
||||
fun chooseSource(
|
||||
dto: BaseItemDto,
|
||||
itemPlayback: ItemPlayback?,
|
||||
) = itemPlayback?.sourceId?.let { dto.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == itemPlayback.sourceId } }
|
||||
?: chooseSource(dto.mediaSources) // dto.mediaSources?.firstOrNull()
|
||||
|
||||
fun chooseStream(
|
||||
dto: BaseItemDto,
|
||||
itemPlayback: ItemPlayback?,
|
||||
type: MediaStreamType,
|
||||
prefs: UserPreferences,
|
||||
): MediaStream? {
|
||||
val source = chooseSource(dto, itemPlayback)
|
||||
return source?.mediaStreams?.letNotEmpty { streams ->
|
||||
val candidates = streams.filter { it.type == type }
|
||||
when (type) {
|
||||
MediaStreamType.AUDIO -> chooseAudioStream(candidates, itemPlayback, prefs)
|
||||
MediaStreamType.SUBTITLE -> chooseSubtitleStream(candidates, itemPlayback, prefs)
|
||||
else -> candidates.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun chooseAudioStream(
|
||||
candidates: List<MediaStream>,
|
||||
itemPlayback: ItemPlayback?,
|
||||
prefs: UserPreferences,
|
||||
): MediaStream? =
|
||||
if (itemPlayback?.audioIndexEnabled == true) {
|
||||
candidates.firstOrNull { it.index == itemPlayback.audioIndex }
|
||||
} else {
|
||||
// TODO audio selection based on channel layout or preferences or default
|
||||
val audioLanguage = prefs.userConfig.audioLanguagePreference
|
||||
if (audioLanguage.isNotNullOrBlank()) {
|
||||
val sorted =
|
||||
candidates.sortedWith(compareBy<MediaStream> { it.language }.thenByDescending { it.channels })
|
||||
sorted.firstOrNull { it.language == audioLanguage && it.isDefault }
|
||||
?: sorted.firstOrNull { it.language == audioLanguage }
|
||||
?: sorted.firstOrNull { it.isDefault }
|
||||
?: sorted.firstOrNull()
|
||||
} else {
|
||||
candidates.firstOrNull { it.isDefault }
|
||||
?: candidates.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun chooseSubtitleStream(
|
||||
candidates: List<MediaStream>,
|
||||
itemPlayback: ItemPlayback?,
|
||||
prefs: UserPreferences,
|
||||
): MediaStream? {
|
||||
if (itemPlayback?.subtitleIndex == TrackIndex.DISABLED) {
|
||||
return null
|
||||
} else if (itemPlayback?.subtitleIndexEnabled == true) {
|
||||
return candidates.firstOrNull { it.index == itemPlayback.subtitleIndex }
|
||||
} else {
|
||||
val subtitleLanguage = prefs.userConfig.subtitleLanguagePreference
|
||||
return when (prefs.userConfig.subtitleMode) {
|
||||
SubtitlePlaybackMode.ALWAYS -> {
|
||||
if (subtitleLanguage != null) {
|
||||
candidates.firstOrNull { it.language == subtitleLanguage }
|
||||
} else {
|
||||
candidates.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.ONLY_FORCED ->
|
||||
if (subtitleLanguage != null) {
|
||||
candidates.firstOrNull { it.language == subtitleLanguage && it.isForced }
|
||||
} else {
|
||||
candidates.firstOrNull { it.isForced }
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.SMART -> {
|
||||
val audioLanguage = prefs.userConfig.audioLanguagePreference
|
||||
if (audioLanguage != null && subtitleLanguage != null && audioLanguage != subtitleLanguage) {
|
||||
candidates.firstOrNull { it.language == subtitleLanguage }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.DEFAULT -> {
|
||||
// TODO check for language?
|
||||
(
|
||||
candidates.firstOrNull { it.isDefault && it.isForced }
|
||||
?: candidates.firstOrNull { it.isDefault }
|
||||
?: candidates.firstOrNull { it.isForced }
|
||||
)
|
||||
}
|
||||
|
||||
SubtitlePlaybackMode.NONE -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object TrackIndex {
|
||||
/**
|
||||
* The user has not explicitly specified a track to use
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue