mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Persist audio & subtitle track changes for series if in different language (#405)
This only applies if the user has set a preferred audio and/or subtitle language on the web UI! If the user chooses an audio/subtitle track that is a different language than their preferred, it will be remembered for that series. Playback of future episodes of the series will pick a matching language. The audio & subtitle selections are independent. You can make the choice before playback or during playback. Closes #378
This commit is contained in:
parent
033bdccaa2
commit
0f4254c6ec
21 changed files with 990 additions and 324 deletions
|
|
@ -0,0 +1,383 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 11,
|
||||
"identityHash": "26bbe361923e3aac064c08c52e65264e",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "servers",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "users",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "rowId",
|
||||
"columnName": "rowId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "serverId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pin",
|
||||
"columnName": "pin",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"rowId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_users_id_serverId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"serverId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_users_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_users_serverId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"serverId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "servers",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serverId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "ItemPlayback",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "rowId",
|
||||
"columnName": "rowId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "sourceId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "audioIndex",
|
||||
"columnName": "audioIndex",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subtitleIndex",
|
||||
"columnName": "subtitleIndex",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"rowId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_ItemPlayback_userId_itemId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "NavDrawerPinnedItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
]
|
||||
},
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LibraryDisplayInfo",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sort",
|
||||
"columnName": "sort",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "direction",
|
||||
"columnName": "direction",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filter",
|
||||
"columnName": "filter",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true,
|
||||
"defaultValue": "'{}'"
|
||||
},
|
||||
{
|
||||
"fieldPath": "viewOptions",
|
||||
"columnName": "viewOptions",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_LibraryDisplayInfo_userId_itemId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"itemId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "PlaybackLanguageChoice",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "userId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "seriesId",
|
||||
"columnName": "seriesId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "itemId",
|
||||
"columnName": "itemId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "audioLanguage",
|
||||
"columnName": "audioLanguage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "subtitleLanguage",
|
||||
"columnName": "subtitleLanguage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "subtitlesDisabled",
|
||||
"columnName": "subtitlesDisabled",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"userId",
|
||||
"seriesId"
|
||||
]
|
||||
},
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "users",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"userId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"rowId"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '26bbe361923e3aac064c08c52e65264e')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.data.model.JellyfinServer
|
|||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
||||
import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice
|
||||
import com.github.damontecres.wholphin.ui.components.ViewOptions
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
|
@ -22,8 +23,15 @@ import timber.log.Timber
|
|||
import java.util.UUID
|
||||
|
||||
@Database(
|
||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
|
||||
version = 10,
|
||||
entities = [
|
||||
JellyfinServer::class,
|
||||
JellyfinUser::class,
|
||||
ItemPlayback::class,
|
||||
NavDrawerPinnedItem::class,
|
||||
LibraryDisplayInfo::class,
|
||||
PlaybackLanguageChoice::class,
|
||||
],
|
||||
version = 11,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(3, 4),
|
||||
|
|
@ -33,6 +41,7 @@ import java.util.UUID
|
|||
AutoMigration(7, 8),
|
||||
AutoMigration(8, 9),
|
||||
AutoMigration(9, 10),
|
||||
AutoMigration(10, 11),
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
@ -44,6 +53,8 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
abstract fun serverPreferencesDao(): ServerPreferencesDao
|
||||
|
||||
abstract fun libraryDisplayInfoDao(): LibraryDisplayInfoDao
|
||||
|
||||
abstract fun playbackLanguageChoiceDao(): PlaybackLanguageChoiceDao
|
||||
}
|
||||
|
||||
class Converters {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,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.PlaybackLanguageChoice
|
||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
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.serializer.toUUIDOrNull
|
||||
|
|
@ -20,47 +23,59 @@ class ItemPlaybackRepository
|
|||
constructor(
|
||||
val serverRepository: ServerRepository,
|
||||
val itemPlaybackDao: ItemPlaybackDao,
|
||||
private val streamChoiceService: StreamChoiceService,
|
||||
) {
|
||||
fun getSelectedTracks(
|
||||
suspend fun getSelectedTracks(
|
||||
itemId: UUID,
|
||||
item: BaseItem,
|
||||
prefs: UserPreferences,
|
||||
): ChosenStreams? =
|
||||
serverRepository.currentUser.value?.let { user ->
|
||||
val itemPlayback = itemPlaybackDao.getItem(user = user, itemId = itemId)
|
||||
if (itemPlayback != null) {
|
||||
Timber.v("Got itemPlayback for %s", itemId)
|
||||
getChosenItemFromPlayback(item, itemPlayback)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||
Timber.v("For ${item.id}: itemPlayback=${itemPlayback != null}, plc=${plc != null}")
|
||||
return getChosenItemFromPlayback(item, itemPlayback, plc, prefs)
|
||||
}
|
||||
|
||||
fun getChosenItemFromPlayback(
|
||||
item: BaseItem,
|
||||
itemPlayback: ItemPlayback,
|
||||
itemPlayback: ItemPlayback?,
|
||||
plc: PlaybackLanguageChoice?,
|
||||
prefs: UserPreferences,
|
||||
): ChosenStreams? {
|
||||
val source =
|
||||
item.data.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == itemPlayback.sourceId }
|
||||
item.data.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == itemPlayback?.sourceId }
|
||||
?: streamChoiceService.chooseSource(item.data.mediaSources.orEmpty())
|
||||
if (source != null) {
|
||||
val audioStream =
|
||||
if (itemPlayback.audioIndexEnabled) {
|
||||
source.mediaStreams?.firstOrNull { it.index == itemPlayback.audioIndex }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
streamChoiceService.chooseAudioStream(
|
||||
candidates =
|
||||
source.mediaStreams
|
||||
?.filter { it.type == MediaStreamType.AUDIO }
|
||||
.orEmpty(),
|
||||
itemPlayback = itemPlayback,
|
||||
playbackLanguageChoice = plc,
|
||||
prefs = prefs,
|
||||
)
|
||||
val subtitleStream =
|
||||
if (itemPlayback.subtitleIndexEnabled) {
|
||||
source.mediaStreams?.firstOrNull { it.index == itemPlayback.subtitleIndex }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
streamChoiceService.chooseSubtitleStream(
|
||||
candidates =
|
||||
source.mediaStreams
|
||||
?.filter { it.type == MediaStreamType.SUBTITLE }
|
||||
.orEmpty(),
|
||||
itemPlayback = itemPlayback,
|
||||
playbackLanguageChoice = plc,
|
||||
prefs = prefs,
|
||||
)
|
||||
return ChosenStreams(
|
||||
itemPlayback,
|
||||
item.id,
|
||||
source.id?.toUUIDOrNull(),
|
||||
audioStream,
|
||||
subtitleStream,
|
||||
itemPlayback.subtitleIndex == TrackIndex.DISABLED,
|
||||
itemPlayback = itemPlayback,
|
||||
plc = plc,
|
||||
itemId = item.id,
|
||||
source = source,
|
||||
videoStream = source.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO },
|
||||
audioStream = audioStream,
|
||||
subtitleStream = subtitleStream,
|
||||
subtitlesDisabled = itemPlayback?.subtitleIndex == TrackIndex.DISABLED,
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
|
|
@ -89,22 +104,61 @@ class ItemPlaybackRepository
|
|||
itemPlayback: ItemPlayback?,
|
||||
trackIndex: Int,
|
||||
type: MediaStreamType,
|
||||
) = serverRepository.currentUser.value?.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
|
||||
): ItemPlayback =
|
||||
serverRepository.current.value!!.let { current ->
|
||||
val source =
|
||||
itemPlayback?.sourceId?.let { sourceId ->
|
||||
item.data.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == sourceId }
|
||||
} ?: streamChoiceService.chooseSource(item.data, null)
|
||||
if (source == null) {
|
||||
Timber.w("Could not find media source for ${item.id}")
|
||||
throw IllegalArgumentException("Could not find media source for ${item.id}")
|
||||
}
|
||||
Timber.v("Saving track selection %s", toSave)
|
||||
saveItemPlayback(toSave)
|
||||
}
|
||||
var toSave =
|
||||
itemPlayback ?: ItemPlayback(
|
||||
userId = current.user.rowId,
|
||||
itemId = item.id,
|
||||
sourceId = source.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)
|
||||
toSave = saveItemPlayback(toSave)
|
||||
val seriesId = item.data.seriesId
|
||||
if (seriesId != null && trackIndex != TrackIndex.UNSPECIFIED) {
|
||||
if (type == MediaStreamType.AUDIO) {
|
||||
val audioLang = current.userDto.configuration?.audioLanguagePreference
|
||||
val stream = source.mediaStreams?.first { it.index == trackIndex }
|
||||
if (stream?.language != null && stream.language != audioLang) {
|
||||
streamChoiceService.updateAudio(item.data, stream.language!!)
|
||||
}
|
||||
} else if (type == MediaStreamType.SUBTITLE) {
|
||||
if (trackIndex == TrackIndex.DISABLED) {
|
||||
streamChoiceService.updateSubtitles(
|
||||
item.data,
|
||||
subtitleLang = null,
|
||||
subtitlesDisabled = true,
|
||||
)
|
||||
} else {
|
||||
val subtitleLang =
|
||||
current.userDto.configuration?.subtitleLanguagePreference
|
||||
val stream = source.mediaStreams?.first { it.index == trackIndex }
|
||||
if (stream?.language != null && stream.language != subtitleLang) {
|
||||
streamChoiceService.updateSubtitles(
|
||||
item.data,
|
||||
stream.language!!,
|
||||
subtitlesDisabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
toSave
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the [ItemPlayback] into the database, returning the same object with the rowId updated if needed
|
||||
|
|
@ -127,9 +181,11 @@ class ItemPlaybackRepository
|
|||
}
|
||||
|
||||
data class ChosenStreams(
|
||||
val itemPlayback: ItemPlayback,
|
||||
val itemPlayback: ItemPlayback?,
|
||||
val plc: PlaybackLanguageChoice?,
|
||||
val itemId: UUID,
|
||||
val sourceId: UUID?,
|
||||
val source: MediaSourceInfo,
|
||||
val videoStream: MediaStream?,
|
||||
val audioStream: MediaStream?,
|
||||
val subtitleStream: MediaStream?,
|
||||
val subtitlesDisabled: Boolean,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.github.damontecres.wholphin.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice
|
||||
import java.util.UUID
|
||||
|
||||
@Dao
|
||||
interface PlaybackLanguageChoiceDao {
|
||||
@Query("SELECT * FROM PlaybackLanguageChoice WHERE userId=:userId AND seriesId=:seriesId")
|
||||
suspend fun get(
|
||||
userId: Int,
|
||||
seriesId: UUID,
|
||||
): PlaybackLanguageChoice?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(plc: PlaybackLanguageChoice): Long
|
||||
}
|
||||
|
|
@ -7,19 +7,10 @@ import androidx.room.ForeignKey
|
|||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
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.UUIDSerializer
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(
|
||||
|
|
@ -53,122 +44,6 @@ data class ItemPlayback(
|
|||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
@file:UseSerializers(UUIDSerializer::class)
|
||||
|
||||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = JellyfinUser::class,
|
||||
parentColumns = arrayOf("rowId"),
|
||||
childColumns = arrayOf("userId"),
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
primaryKeys = ["userId", "seriesId"],
|
||||
)
|
||||
@Serializable
|
||||
data class PlaybackLanguageChoice(
|
||||
val userId: Int,
|
||||
val seriesId: UUID,
|
||||
val itemId: UUID? = null,
|
||||
val audioLanguage: String? = null,
|
||||
val subtitleLanguage: String? = null,
|
||||
val subtitlesDisabled: Boolean? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.PlaybackLanguageChoice
|
||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
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 timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class StreamChoiceService
|
||||
@Inject
|
||||
constructor(
|
||||
private val serverRepository: ServerRepository,
|
||||
private val playbackLanguageChoiceDao: PlaybackLanguageChoiceDao,
|
||||
) {
|
||||
suspend fun updateAudio(
|
||||
dto: BaseItemDto,
|
||||
audioLang: String,
|
||||
) = update(dto) {
|
||||
it.copy(
|
||||
audioLanguage = audioLang,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun updateSubtitles(
|
||||
dto: BaseItemDto,
|
||||
subtitleLang: String?,
|
||||
subtitlesDisabled: Boolean,
|
||||
) = update(dto) {
|
||||
it.copy(
|
||||
subtitleLanguage = if (subtitlesDisabled) null else subtitleLang,
|
||||
subtitlesDisabled = subtitlesDisabled,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun update(
|
||||
dto: BaseItemDto,
|
||||
update: (PlaybackLanguageChoice) -> PlaybackLanguageChoice,
|
||||
) {
|
||||
val seriesId = dto.seriesId
|
||||
if (seriesId != null) {
|
||||
val userId = serverRepository.currentUser.value!!.rowId
|
||||
val currentPlc =
|
||||
playbackLanguageChoiceDao.get(userId, seriesId)
|
||||
?: PlaybackLanguageChoice(userId, seriesId, dto.id)
|
||||
val newPlc = update.invoke(currentPlc)
|
||||
Timber.v("Saving series PLC: %s", newPlc)
|
||||
playbackLanguageChoiceDao.save(newPlc)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPlaybackLanguageChoice(dto: BaseItemDto) =
|
||||
dto.seriesId?.let {
|
||||
playbackLanguageChoiceDao.get(serverRepository.currentUser.value!!.rowId, it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [MediaSourceInfo] that matched the [ItemPlayback] or else the one with the highest resolution
|
||||
*/
|
||||
fun chooseSource(
|
||||
dto: BaseItemDto,
|
||||
itemPlayback: ItemPlayback?,
|
||||
): MediaSourceInfo? =
|
||||
itemPlayback?.sourceId?.let { dto.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == itemPlayback.sourceId } }
|
||||
?: chooseSource(dto.mediaSources) // dto.mediaSources?.firstOrNull()
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
suspend fun chooseStream(
|
||||
source: MediaSourceInfo,
|
||||
seriesId: UUID?,
|
||||
itemPlayback: ItemPlayback?,
|
||||
plc: PlaybackLanguageChoice?,
|
||||
type: MediaStreamType,
|
||||
prefs: UserPreferences,
|
||||
): MediaStream? {
|
||||
val plc = plc ?: seriesId?.let { playbackLanguageChoiceDao.get(serverRepository.currentUser.value!!.rowId, it) }
|
||||
return source.mediaStreams?.letNotEmpty { streams ->
|
||||
val candidates = streams.filter { it.type == type }
|
||||
when (type) {
|
||||
MediaStreamType.AUDIO -> chooseAudioStream(candidates, itemPlayback, plc, prefs)
|
||||
MediaStreamType.SUBTITLE -> chooseSubtitleStream(candidates, itemPlayback, plc, prefs)
|
||||
else -> candidates.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun chooseAudioStream(
|
||||
candidates: List<MediaStream>,
|
||||
itemPlayback: ItemPlayback?,
|
||||
playbackLanguageChoice: PlaybackLanguageChoice?,
|
||||
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 =
|
||||
if (prefs.userConfig.audioLanguagePreference.isNotNullOrBlank()) {
|
||||
// If the user has chosen a preferred language, but changed tracks on the series, use that
|
||||
// Otherwise, use their preferred language
|
||||
playbackLanguageChoice?.audioLanguage
|
||||
?: prefs.userConfig.audioLanguagePreference
|
||||
} else {
|
||||
null
|
||||
}
|
||||
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?,
|
||||
playbackLanguageChoice: PlaybackLanguageChoice?,
|
||||
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 =
|
||||
if (prefs.userConfig.subtitleLanguagePreference.isNotNullOrBlank()) {
|
||||
// If the user has chosen a preferred language, but changed tracks on the series, use that
|
||||
// Otherwise, use their preferred language
|
||||
playbackLanguageChoice?.subtitleLanguage
|
||||
?: prefs.userConfig.subtitleLanguagePreference
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val subtitleMode =
|
||||
when {
|
||||
playbackLanguageChoice?.subtitlesDisabled == false &&
|
||||
playbackLanguageChoice.subtitleLanguage != null &&
|
||||
subtitleLanguage.isNotNullOrBlank() -> {
|
||||
// User has a subtitle language preference, but has chosen a different language for the series
|
||||
// So override their normal playback mode to always display subtitles
|
||||
SubtitlePlaybackMode.ALWAYS
|
||||
}
|
||||
|
||||
playbackLanguageChoice?.subtitlesDisabled == true -> {
|
||||
// Series level settings disables subtitles
|
||||
SubtitlePlaybackMode.NONE
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Fallback to the user's preference
|
||||
prefs.userConfig.subtitleMode
|
||||
}
|
||||
}
|
||||
return when (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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class UserPreferencesService
|
||||
@Inject
|
||||
constructor(
|
||||
private val serverRepository: ServerRepository,
|
||||
private val preferencesDataStore: DataStore<AppPreferences>,
|
||||
) {
|
||||
suspend fun getCurrent(): UserPreferences =
|
||||
serverRepository.currentUserDto.value!!.configuration.let { userConfig ->
|
||||
val appPrefs = preferencesDataStore.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
||||
UserPreferences(
|
||||
appPrefs,
|
||||
userConfig ?: DefaultUserConfiguration,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.data.ItemPlaybackDao
|
|||
import com.github.damontecres.wholphin.data.JellyfinServerDao
|
||||
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
||||
import com.github.damontecres.wholphin.data.Migrations
|
||||
import com.github.damontecres.wholphin.data.PlaybackLanguageChoiceDao
|
||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
||||
|
|
@ -56,6 +57,10 @@ object DatabaseModule {
|
|||
@Singleton
|
||||
fun libraryDisplayInfoDao(db: AppDatabase): LibraryDisplayInfoDao = db.libraryDisplayInfoDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun playbackLanguageChoiceDao(db: AppDatabase): PlaybackLanguageChoiceDao = db.playbackLanguageChoiceDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun userPreferencesDataStore(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -26,12 +27,8 @@ import androidx.tv.material3.MaterialTheme
|
|||
import androidx.tv.material3.ProvideTextStyle
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
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 com.github.damontecres.wholphin.data.model.chooseStream
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.preferences.AppThemeColors
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.playback.audioStreamCount
|
||||
|
|
@ -40,16 +37,30 @@ import com.github.damontecres.wholphin.ui.playback.externalSubtitlesCount
|
|||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
import com.github.damontecres.wholphin.util.languageName
|
||||
import com.github.damontecres.wholphin.util.profile.Codec
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaStream
|
||||
import org.jellyfin.sdk.model.api.VideoRange
|
||||
import org.jellyfin.sdk.model.api.VideoRangeType
|
||||
|
||||
@Composable
|
||||
@NonRestartableComposable
|
||||
fun VideoStreamDetails(
|
||||
preferences: UserPreferences,
|
||||
dto: BaseItemDto,
|
||||
itemPlayback: ItemPlayback?,
|
||||
chosenStreams: ChosenStreams?,
|
||||
modifier: Modifier = Modifier,
|
||||
) = VideoStreamDetails(
|
||||
chosenStreams?.source,
|
||||
chosenStreams?.videoStream,
|
||||
chosenStreams?.audioStream,
|
||||
chosenStreams?.subtitleStream,
|
||||
modifier,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun VideoStreamDetails(
|
||||
source: MediaSourceInfo?,
|
||||
videoStream: MediaStream?,
|
||||
audioStream: MediaStream?,
|
||||
subtitleStream: MediaStream?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -57,17 +68,6 @@ fun VideoStreamDetails(
|
|||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
val source = remember(dto, itemPlayback) { chooseSource(dto, itemPlayback) }
|
||||
|
||||
val videoStream =
|
||||
remember(dto, itemPlayback) {
|
||||
chooseStream(
|
||||
dto,
|
||||
itemPlayback,
|
||||
MediaStreamType.VIDEO,
|
||||
preferences,
|
||||
)
|
||||
}
|
||||
val video =
|
||||
remember(videoStream) {
|
||||
videoStream
|
||||
|
|
@ -95,15 +95,6 @@ fun VideoStreamDetails(
|
|||
StreamLabel(it)
|
||||
}
|
||||
|
||||
val audioStream =
|
||||
remember(dto, itemPlayback) {
|
||||
chooseStream(
|
||||
dto,
|
||||
itemPlayback,
|
||||
MediaStreamType.AUDIO,
|
||||
preferences,
|
||||
)
|
||||
}
|
||||
val audioCount = remember(source) { source?.audioStreamCount ?: 0 }
|
||||
val audio =
|
||||
remember(audioCount, audioStream) {
|
||||
|
|
@ -124,23 +115,14 @@ fun VideoStreamDetails(
|
|||
modifier = Modifier.widthIn(max = 200.dp),
|
||||
)
|
||||
|
||||
val subtitleStream =
|
||||
remember(dto, itemPlayback) {
|
||||
chooseStream(
|
||||
dto,
|
||||
itemPlayback,
|
||||
MediaStreamType.SUBTITLE,
|
||||
preferences,
|
||||
)
|
||||
}
|
||||
val subtitleCount =
|
||||
remember(source) {
|
||||
(source?.embeddedSubtitleCount ?: 0) + (source?.externalSubtitlesCount ?: 0)
|
||||
}
|
||||
var disabled by remember { mutableStateOf(false) }
|
||||
val subtitle =
|
||||
remember(subtitleCount, subtitleStream, itemPlayback) {
|
||||
if (itemPlayback?.subtitleIndex == TrackIndex.DISABLED) {
|
||||
remember(subtitleCount, subtitleStream) {
|
||||
if (subtitleCount > 0 && subtitleStream == null) {
|
||||
disabled = true
|
||||
context.getString(R.string.disabled)
|
||||
} else if (subtitleCount == 0 || subtitleStream == null) {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import androidx.lifecycle.compose.LifecycleStartEffect
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
|
|
@ -57,6 +56,7 @@ import kotlinx.coroutines.launch
|
|||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ fun EpisodeDetails(
|
|||
watched = ep.data.userData?.played ?: false,
|
||||
favorite = ep.data.userData?.isFavorite ?: false,
|
||||
seriesId = ep.data.seriesId,
|
||||
sourceId = chosenStreams?.sourceId,
|
||||
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||
actions = moreActions,
|
||||
onChooseVersion = {
|
||||
chooseVersion =
|
||||
|
|
@ -174,25 +174,26 @@ fun EpisodeDetails(
|
|||
moreDialog = null
|
||||
},
|
||||
onChooseTracks = { type ->
|
||||
chooseSource(
|
||||
ep.data,
|
||||
chosenStreams?.itemPlayback,
|
||||
)?.let { source ->
|
||||
chooseVersion =
|
||||
chooseStream(
|
||||
context = context,
|
||||
streams = source.mediaStreams.orEmpty(),
|
||||
type = type,
|
||||
onClick = { trackIndex ->
|
||||
viewModel.saveTrackSelection(
|
||||
ep,
|
||||
chosenStreams?.itemPlayback,
|
||||
trackIndex,
|
||||
type,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
viewModel.streamChoiceService
|
||||
.chooseSource(
|
||||
ep.data,
|
||||
chosenStreams?.itemPlayback,
|
||||
)?.let { source ->
|
||||
chooseVersion =
|
||||
chooseStream(
|
||||
context = context,
|
||||
streams = source.mediaStreams.orEmpty(),
|
||||
type = type,
|
||||
onClick = { trackIndex ->
|
||||
viewModel.saveTrackSelection(
|
||||
ep,
|
||||
chosenStreams?.itemPlayback,
|
||||
trackIndex,
|
||||
type,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -58,9 +58,7 @@ fun EpisodeDetailsHeader(
|
|||
EpisodeQuickDetails(dto)
|
||||
|
||||
VideoStreamDetails(
|
||||
preferences = preferences,
|
||||
dto = dto,
|
||||
itemPlayback = chosenStreams?.itemPlayback,
|
||||
chosenStreams = chosenStreams,
|
||||
modifier = Modifier.padding(bottom = padding),
|
||||
)
|
||||
dto.taglines?.firstOrNull()?.let { tagline ->
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import com.github.damontecres.wholphin.data.model.ItemPlayback
|
|||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||
import com.github.damontecres.wholphin.services.ThemeSongPlayer
|
||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
|
|
@ -44,8 +46,10 @@ class EpisodeViewModel
|
|||
private val navigationManager: NavigationManager,
|
||||
val serverRepository: ServerRepository,
|
||||
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||
val streamChoiceService: StreamChoiceService,
|
||||
private val themeSongPlayer: ThemeSongPlayer,
|
||||
private val favoriteWatchManager: FavoriteWatchManager,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
@Assisted val itemId: UUID,
|
||||
) : ViewModel() {
|
||||
@AssistedFactory
|
||||
|
|
@ -85,8 +89,9 @@ class EpisodeViewModel
|
|||
"Error fetching movie",
|
||||
),
|
||||
) {
|
||||
val prefs = userPreferencesService.getCurrent()
|
||||
val item = fetchAndSetItem().await()
|
||||
val result = itemPlaybackRepository.getSelectedTracks(item.id, item)
|
||||
val result = itemPlaybackRepository.getSelectedTracks(item.id, item, prefs)
|
||||
withContext(Dispatchers.Main) {
|
||||
this@EpisodeViewModel.item.value = item
|
||||
chosenStreams.value = result
|
||||
|
|
@ -115,10 +120,12 @@ class EpisodeViewModel
|
|||
sourceId: UUID,
|
||||
) {
|
||||
viewModelScope.launchIO {
|
||||
val prefs = userPreferencesService.getCurrent()
|
||||
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
|
||||
val chosen =
|
||||
result?.let {
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chosenStreams.value = chosen
|
||||
|
|
@ -133,6 +140,8 @@ class EpisodeViewModel
|
|||
type: MediaStreamType,
|
||||
) {
|
||||
viewModelScope.launchIO {
|
||||
val prefs = userPreferencesService.getCurrent()
|
||||
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||
val result =
|
||||
itemPlaybackRepository.saveTrackSelection(
|
||||
item = item,
|
||||
|
|
@ -142,7 +151,7 @@ class EpisodeViewModel
|
|||
)
|
||||
val chosen =
|
||||
result?.let {
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chosenStreams.value = chosen
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ import com.github.damontecres.wholphin.data.model.Person
|
|||
import com.github.damontecres.wholphin.data.model.RemoteTrailer
|
||||
import com.github.damontecres.wholphin.data.model.Trailer
|
||||
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.TrailerService
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
|
|
@ -83,6 +82,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
|||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration
|
||||
|
||||
|
|
@ -204,7 +204,7 @@ fun MovieDetails(
|
|||
watched = movie.data.userData?.played ?: false,
|
||||
favorite = movie.data.userData?.isFavorite ?: false,
|
||||
seriesId = null,
|
||||
sourceId = chosenStreams?.sourceId,
|
||||
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||
actions = moreActions,
|
||||
onChooseVersion = {
|
||||
chooseVersion =
|
||||
|
|
@ -221,25 +221,26 @@ fun MovieDetails(
|
|||
moreDialog = null
|
||||
},
|
||||
onChooseTracks = { type ->
|
||||
chooseSource(
|
||||
movie.data,
|
||||
chosenStreams?.itemPlayback,
|
||||
)?.let { source ->
|
||||
chooseVersion =
|
||||
chooseStream(
|
||||
context = context,
|
||||
streams = source.mediaStreams.orEmpty(),
|
||||
type = type,
|
||||
onClick = { trackIndex ->
|
||||
viewModel.saveTrackSelection(
|
||||
movie,
|
||||
chosenStreams?.itemPlayback,
|
||||
trackIndex,
|
||||
type,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
viewModel.streamChoiceService
|
||||
.chooseSource(
|
||||
movie.data,
|
||||
chosenStreams?.itemPlayback,
|
||||
)?.let { source ->
|
||||
chooseVersion =
|
||||
chooseStream(
|
||||
context = context,
|
||||
streams = source.mediaStreams.orEmpty(),
|
||||
type = type,
|
||||
onClick = { trackIndex ->
|
||||
viewModel.saveTrackSelection(
|
||||
movie,
|
||||
chosenStreams?.itemPlayback,
|
||||
trackIndex,
|
||||
type,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -72,9 +72,7 @@ fun MovieDetailsHeader(
|
|||
}
|
||||
|
||||
VideoStreamDetails(
|
||||
preferences = preferences,
|
||||
dto = dto,
|
||||
itemPlayback = chosenStreams?.itemPlayback,
|
||||
chosenStreams = chosenStreams,
|
||||
modifier = Modifier.padding(bottom = padding),
|
||||
)
|
||||
dto.taglines?.firstOrNull()?.let { tagline ->
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ import com.github.damontecres.wholphin.services.ExtrasService
|
|||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.PeopleFavorites
|
||||
import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||
import com.github.damontecres.wholphin.services.ThemeSongPlayer
|
||||
import com.github.damontecres.wholphin.services.TrailerService
|
||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
|
|
@ -54,11 +56,13 @@ class MovieViewModel
|
|||
private val navigationManager: NavigationManager,
|
||||
val serverRepository: ServerRepository,
|
||||
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||
val streamChoiceService: StreamChoiceService,
|
||||
private val themeSongPlayer: ThemeSongPlayer,
|
||||
private val favoriteWatchManager: FavoriteWatchManager,
|
||||
private val peopleFavorites: PeopleFavorites,
|
||||
private val trailerService: TrailerService,
|
||||
private val extrasService: ExtrasService,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
@Assisted val itemId: UUID,
|
||||
) : ViewModel() {
|
||||
@AssistedFactory
|
||||
|
|
@ -104,7 +108,12 @@ class MovieViewModel
|
|||
),
|
||||
) {
|
||||
val item = fetchAndSetItem().await()
|
||||
val result = itemPlaybackRepository.getSelectedTracks(item.id, item)
|
||||
val result =
|
||||
itemPlaybackRepository.getSelectedTracks(
|
||||
item.id,
|
||||
item,
|
||||
userPreferencesService.getCurrent(),
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
this@MovieViewModel.item.value = item
|
||||
chosenStreams.value = result
|
||||
|
|
@ -172,10 +181,12 @@ class MovieViewModel
|
|||
sourceId: UUID,
|
||||
) {
|
||||
viewModelScope.launchIO {
|
||||
val prefs = userPreferencesService.getCurrent()
|
||||
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
|
||||
val chosen =
|
||||
result?.let {
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chosenStreams.value = chosen
|
||||
|
|
@ -190,6 +201,8 @@ class MovieViewModel
|
|||
type: MediaStreamType,
|
||||
) {
|
||||
viewModelScope.launchIO {
|
||||
val prefs = userPreferencesService.getCurrent()
|
||||
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||
val result =
|
||||
itemPlaybackRepository.saveTrackSelection(
|
||||
item = item,
|
||||
|
|
@ -199,7 +212,7 @@ class MovieViewModel
|
|||
)
|
||||
val chosen =
|
||||
result?.let {
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chosenStreams.value = chosen
|
||||
|
|
|
|||
|
|
@ -37,9 +37,7 @@ fun FocusedEpisodeHeader(
|
|||
|
||||
if (dto != null) {
|
||||
VideoStreamDetails(
|
||||
preferences = preferences,
|
||||
dto = dto,
|
||||
itemPlayback = chosenStreams?.itemPlayback,
|
||||
chosenStreams = chosenStreams,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import androidx.lifecycle.compose.LifecycleStartEffect
|
|||
import androidx.lifecycle.map
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
|
|
@ -53,6 +52,7 @@ import org.jellyfin.sdk.model.api.PersonKind
|
|||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration
|
||||
|
|
@ -219,7 +219,7 @@ fun SeriesOverview(
|
|||
watched = ep.data.userData?.played ?: false,
|
||||
favorite = ep.data.userData?.isFavorite ?: false,
|
||||
seriesId = series.id,
|
||||
sourceId = chosenStreams?.sourceId,
|
||||
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||
actions =
|
||||
MoreDialogActions(
|
||||
navigateTo = viewModel::navigateTo,
|
||||
|
|
@ -257,25 +257,26 @@ fun SeriesOverview(
|
|||
moreDialog = null
|
||||
},
|
||||
onChooseTracks = { type ->
|
||||
chooseSource(
|
||||
ep.data,
|
||||
chosenStreams?.itemPlayback,
|
||||
)?.let { source ->
|
||||
chooseVersion =
|
||||
chooseStream(
|
||||
context = context,
|
||||
streams = source.mediaStreams.orEmpty(),
|
||||
type = type,
|
||||
onClick = { trackIndex ->
|
||||
viewModel.saveTrackSelection(
|
||||
ep,
|
||||
chosenStreams?.itemPlayback,
|
||||
trackIndex,
|
||||
type,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
viewModel.streamChoiceService
|
||||
.chooseSource(
|
||||
ep.data,
|
||||
chosenStreams?.itemPlayback,
|
||||
)?.let { source ->
|
||||
chooseVersion =
|
||||
chooseStream(
|
||||
context = context,
|
||||
streams = source.mediaStreams.orEmpty(),
|
||||
type = type,
|
||||
onClick = { trackIndex ->
|
||||
viewModel.saveTrackSelection(
|
||||
ep,
|
||||
chosenStreams?.itemPlayback,
|
||||
trackIndex,
|
||||
type,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ import com.github.damontecres.wholphin.services.ExtrasService
|
|||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.PeopleFavorites
|
||||
import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||
import com.github.damontecres.wholphin.services.ThemeSongPlayer
|
||||
import com.github.damontecres.wholphin.services.TrailerService
|
||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
||||
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||
|
|
@ -70,6 +72,8 @@ class SeriesViewModel
|
|||
private val peopleFavorites: PeopleFavorites,
|
||||
private val trailerService: TrailerService,
|
||||
private val extrasService: ExtrasService,
|
||||
val streamChoiceService: StreamChoiceService,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
) : ItemViewModel(api) {
|
||||
private lateinit var seriesId: UUID
|
||||
private lateinit var prefs: UserPreferences
|
||||
|
|
@ -372,7 +376,12 @@ class SeriesViewModel
|
|||
chosenStreamsJob?.cancel()
|
||||
chosenStreamsJob =
|
||||
viewModelScope.launchIO {
|
||||
val result = itemPlaybackRepository.getSelectedTracks(itemId, item)
|
||||
val result =
|
||||
itemPlaybackRepository.getSelectedTracks(
|
||||
itemId,
|
||||
item,
|
||||
userPreferencesService.getCurrent(),
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
chosenStreams.value = result
|
||||
}
|
||||
|
|
@ -384,10 +393,12 @@ class SeriesViewModel
|
|||
sourceId: UUID,
|
||||
) {
|
||||
viewModelScope.launchIO {
|
||||
val prefs = userPreferencesService.getCurrent()
|
||||
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
|
||||
val chosen =
|
||||
result?.let {
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chosenStreams.value = chosen
|
||||
|
|
@ -402,6 +413,8 @@ class SeriesViewModel
|
|||
type: MediaStreamType,
|
||||
) {
|
||||
viewModelScope.launchIO {
|
||||
val prefs = userPreferencesService.getCurrent()
|
||||
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||
val result =
|
||||
itemPlaybackRepository.saveTrackSelection(
|
||||
item = item,
|
||||
|
|
@ -411,7 +424,7 @@ class SeriesViewModel
|
|||
)
|
||||
val chosen =
|
||||
result?.let {
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chosenStreams.value = chosen
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@ import com.github.damontecres.wholphin.data.model.Chapter
|
|||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.Playlist
|
||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||
import com.github.damontecres.wholphin.data.model.chooseStream
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
|
||||
|
|
@ -43,6 +41,7 @@ import com.github.damontecres.wholphin.services.PlayerFactory
|
|||
import com.github.damontecres.wholphin.services.PlaylistCreationResult
|
||||
import com.github.damontecres.wholphin.services.PlaylistCreator
|
||||
import com.github.damontecres.wholphin.services.RefreshRateService
|
||||
import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.onMain
|
||||
|
|
@ -123,6 +122,7 @@ class PlaybackViewModel
|
|||
private val deviceInfo: DeviceInfo,
|
||||
private val deviceProfileService: DeviceProfileService,
|
||||
private val refreshRateService: RefreshRateService,
|
||||
val streamChoiceService: StreamChoiceService,
|
||||
) : ViewModel(),
|
||||
Player.Listener,
|
||||
AnalyticsListener {
|
||||
|
|
@ -338,7 +338,8 @@ class PlaybackViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
val mediaSource = chooseSource(base, playbackConfig)
|
||||
val mediaSource = streamChoiceService.chooseSource(base, playbackConfig)
|
||||
val plc = streamChoiceService.getPlaybackLanguageChoice(base)
|
||||
|
||||
if (mediaSource == null) {
|
||||
showToast(
|
||||
|
|
@ -362,12 +363,26 @@ class PlaybackViewModel
|
|||
.orEmpty()
|
||||
|
||||
val audioIndex =
|
||||
chooseStream(base, playbackConfig, MediaStreamType.AUDIO, preferences)
|
||||
?.index
|
||||
streamChoiceService
|
||||
.chooseStream(
|
||||
mediaSource,
|
||||
base.seriesId,
|
||||
playbackConfig,
|
||||
plc,
|
||||
MediaStreamType.AUDIO,
|
||||
preferences,
|
||||
)?.index
|
||||
|
||||
val subtitleIndex =
|
||||
chooseStream(base, playbackConfig, MediaStreamType.SUBTITLE, preferences)
|
||||
?.index
|
||||
streamChoiceService
|
||||
.chooseStream(
|
||||
mediaSource,
|
||||
base.seriesId,
|
||||
playbackConfig,
|
||||
plc,
|
||||
MediaStreamType.SUBTITLE,
|
||||
preferences,
|
||||
)?.index
|
||||
|
||||
Timber.d("Selected mediaSource=${mediaSource.id}, audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
||||
|
||||
|
|
@ -563,7 +578,7 @@ class PlaybackViewModel
|
|||
|
||||
else -> throw Exception("No supported playback method")
|
||||
}
|
||||
Timber.v("Playback decision: $transcodeType")
|
||||
Timber.i("Playback decision for $itemId: $transcodeType")
|
||||
|
||||
val externalSubtitleCount = source.externalSubtitlesCount
|
||||
|
||||
|
|
@ -605,21 +620,6 @@ class PlaybackViewModel
|
|||
liveStreamId = source.liveStreamId,
|
||||
mediaSourceInfo = source,
|
||||
)
|
||||
val itemPlayback =
|
||||
currentItemPlayback.copy(
|
||||
sourceId = source.id?.toUUIDOrNull(),
|
||||
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
|
||||
subtitleIndex = subtitleIndex ?: TrackIndex.DISABLED,
|
||||
)
|
||||
if (userInitiated) {
|
||||
viewModelScope.launchIO {
|
||||
Timber.v("Saving user initiated item playback: %s", itemPlayback)
|
||||
val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback)
|
||||
withContext(Dispatchers.Main) {
|
||||
this@PlaybackViewModel.currentItemPlayback.value = updated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preferences.appPreferences.playbackPreferences.refreshRateSwitching) {
|
||||
source.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO }?.let {
|
||||
|
|
@ -638,14 +638,13 @@ class PlaybackViewModel
|
|||
api = api,
|
||||
player = player,
|
||||
playback = playback,
|
||||
itemPlayback = itemPlayback,
|
||||
itemPlayback = currentItemPlayback,
|
||||
)
|
||||
player.addListener(activityListener)
|
||||
this@PlaybackViewModel.activityListener = activityListener
|
||||
|
||||
loading.value = LoadingState.Success
|
||||
this@PlaybackViewModel.currentPlayback.update { playback }
|
||||
this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
|
||||
player.setMediaItem(
|
||||
mediaItem,
|
||||
positionMs,
|
||||
|
|
@ -679,23 +678,41 @@ class PlaybackViewModel
|
|||
|
||||
fun changeAudioStream(index: Int) {
|
||||
viewModelScope.launchIO {
|
||||
Timber.d("Changing audio track to %s", index)
|
||||
val itemPlayback =
|
||||
itemPlaybackRepository.saveTrackSelection(
|
||||
item = item,
|
||||
itemPlayback = currentItemPlayback.value!!,
|
||||
trackIndex = index,
|
||||
type = MediaStreamType.AUDIO,
|
||||
)
|
||||
this@PlaybackViewModel.currentItemPlayback.setValueOnMain(itemPlayback)
|
||||
changeStreams(
|
||||
item,
|
||||
currentItemPlayback.value!!,
|
||||
itemPlayback,
|
||||
index,
|
||||
currentItemPlayback.value?.subtitleIndex,
|
||||
itemPlayback.subtitleIndex,
|
||||
onMain { player.currentPosition },
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeSubtitleStream(index: Int?): Job =
|
||||
fun changeSubtitleStream(index: Int): Job =
|
||||
viewModelScope.launchIO {
|
||||
Timber.d("Changing subtitle track to %s", index)
|
||||
val itemPlayback =
|
||||
itemPlaybackRepository.saveTrackSelection(
|
||||
item = item,
|
||||
itemPlayback = currentItemPlayback.value!!,
|
||||
trackIndex = index,
|
||||
type = MediaStreamType.SUBTITLE,
|
||||
)
|
||||
this@PlaybackViewModel.currentItemPlayback.setValueOnMain(itemPlayback)
|
||||
changeStreams(
|
||||
item,
|
||||
currentItemPlayback.value!!,
|
||||
currentItemPlayback.value?.audioIndex,
|
||||
itemPlayback,
|
||||
itemPlayback.audioIndex,
|
||||
index,
|
||||
onMain { player.currentPosition },
|
||||
true,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||
import com.github.damontecres.wholphin.data.model.chooseSource
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.onMain
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
|
|
@ -101,7 +100,7 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles(
|
|||
api.userLibraryApi.getItem(itemId = it.itemId).content,
|
||||
api,
|
||||
)
|
||||
val mediaSource = chooseSource(item.data, it)
|
||||
val mediaSource = streamChoiceService.chooseSource(item.data, it)
|
||||
if (mediaSource == null) {
|
||||
// This shouldn't happen, but just in case
|
||||
showToast(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue