mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +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.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||||
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
|
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 com.github.damontecres.wholphin.ui.components.ViewOptions
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
|
@ -22,8 +23,15 @@ import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class, NavDrawerPinnedItem::class, LibraryDisplayInfo::class],
|
entities = [
|
||||||
version = 10,
|
JellyfinServer::class,
|
||||||
|
JellyfinUser::class,
|
||||||
|
ItemPlayback::class,
|
||||||
|
NavDrawerPinnedItem::class,
|
||||||
|
LibraryDisplayInfo::class,
|
||||||
|
PlaybackLanguageChoice::class,
|
||||||
|
],
|
||||||
|
version = 11,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(3, 4),
|
AutoMigration(3, 4),
|
||||||
|
|
@ -33,6 +41,7 @@ import java.util.UUID
|
||||||
AutoMigration(7, 8),
|
AutoMigration(7, 8),
|
||||||
AutoMigration(8, 9),
|
AutoMigration(8, 9),
|
||||||
AutoMigration(9, 10),
|
AutoMigration(9, 10),
|
||||||
|
AutoMigration(10, 11),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
@ -44,6 +53,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun serverPreferencesDao(): ServerPreferencesDao
|
abstract fun serverPreferencesDao(): ServerPreferencesDao
|
||||||
|
|
||||||
abstract fun libraryDisplayInfoDao(): LibraryDisplayInfoDao
|
abstract fun libraryDisplayInfoDao(): LibraryDisplayInfoDao
|
||||||
|
|
||||||
|
abstract fun playbackLanguageChoiceDao(): PlaybackLanguageChoiceDao
|
||||||
}
|
}
|
||||||
|
|
||||||
class Converters {
|
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.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
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.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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
import org.jellyfin.sdk.model.api.MediaStream
|
import org.jellyfin.sdk.model.api.MediaStream
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
|
|
@ -20,47 +23,59 @@ class ItemPlaybackRepository
|
||||||
constructor(
|
constructor(
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val itemPlaybackDao: ItemPlaybackDao,
|
val itemPlaybackDao: ItemPlaybackDao,
|
||||||
|
private val streamChoiceService: StreamChoiceService,
|
||||||
) {
|
) {
|
||||||
fun getSelectedTracks(
|
suspend fun getSelectedTracks(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
|
prefs: UserPreferences,
|
||||||
): ChosenStreams? =
|
): ChosenStreams? =
|
||||||
serverRepository.currentUser.value?.let { user ->
|
serverRepository.currentUser.value?.let { user ->
|
||||||
val itemPlayback = itemPlaybackDao.getItem(user = user, itemId = itemId)
|
val itemPlayback = itemPlaybackDao.getItem(user = user, itemId = itemId)
|
||||||
if (itemPlayback != null) {
|
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||||
Timber.v("Got itemPlayback for %s", itemId)
|
Timber.v("For ${item.id}: itemPlayback=${itemPlayback != null}, plc=${plc != null}")
|
||||||
getChosenItemFromPlayback(item, itemPlayback)
|
return getChosenItemFromPlayback(item, itemPlayback, plc, prefs)
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChosenItemFromPlayback(
|
fun getChosenItemFromPlayback(
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
itemPlayback: ItemPlayback,
|
itemPlayback: ItemPlayback?,
|
||||||
|
plc: PlaybackLanguageChoice?,
|
||||||
|
prefs: UserPreferences,
|
||||||
): ChosenStreams? {
|
): ChosenStreams? {
|
||||||
val source =
|
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) {
|
if (source != null) {
|
||||||
val audioStream =
|
val audioStream =
|
||||||
if (itemPlayback.audioIndexEnabled) {
|
streamChoiceService.chooseAudioStream(
|
||||||
source.mediaStreams?.firstOrNull { it.index == itemPlayback.audioIndex }
|
candidates =
|
||||||
} else {
|
source.mediaStreams
|
||||||
null
|
?.filter { it.type == MediaStreamType.AUDIO }
|
||||||
}
|
.orEmpty(),
|
||||||
|
itemPlayback = itemPlayback,
|
||||||
|
playbackLanguageChoice = plc,
|
||||||
|
prefs = prefs,
|
||||||
|
)
|
||||||
val subtitleStream =
|
val subtitleStream =
|
||||||
if (itemPlayback.subtitleIndexEnabled) {
|
streamChoiceService.chooseSubtitleStream(
|
||||||
source.mediaStreams?.firstOrNull { it.index == itemPlayback.subtitleIndex }
|
candidates =
|
||||||
} else {
|
source.mediaStreams
|
||||||
null
|
?.filter { it.type == MediaStreamType.SUBTITLE }
|
||||||
}
|
.orEmpty(),
|
||||||
|
itemPlayback = itemPlayback,
|
||||||
|
playbackLanguageChoice = plc,
|
||||||
|
prefs = prefs,
|
||||||
|
)
|
||||||
return ChosenStreams(
|
return ChosenStreams(
|
||||||
itemPlayback,
|
itemPlayback = itemPlayback,
|
||||||
item.id,
|
plc = plc,
|
||||||
source.id?.toUUIDOrNull(),
|
itemId = item.id,
|
||||||
audioStream,
|
source = source,
|
||||||
subtitleStream,
|
videoStream = source.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO },
|
||||||
itemPlayback.subtitleIndex == TrackIndex.DISABLED,
|
audioStream = audioStream,
|
||||||
|
subtitleStream = subtitleStream,
|
||||||
|
subtitlesDisabled = itemPlayback?.subtitleIndex == TrackIndex.DISABLED,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
|
|
@ -89,12 +104,21 @@ class ItemPlaybackRepository
|
||||||
itemPlayback: ItemPlayback?,
|
itemPlayback: ItemPlayback?,
|
||||||
trackIndex: Int,
|
trackIndex: Int,
|
||||||
type: MediaStreamType,
|
type: MediaStreamType,
|
||||||
) = serverRepository.currentUser.value?.let { user ->
|
): 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}")
|
||||||
|
}
|
||||||
var toSave =
|
var toSave =
|
||||||
itemPlayback ?: ItemPlayback(
|
itemPlayback ?: ItemPlayback(
|
||||||
userId = user.rowId,
|
userId = current.user.rowId,
|
||||||
itemId = item.id,
|
itemId = item.id,
|
||||||
sourceId = chooseSource(item.data, null)?.id?.toUUIDOrNull(),
|
sourceId = source.id?.toUUIDOrNull(),
|
||||||
)
|
)
|
||||||
toSave =
|
toSave =
|
||||||
when (type) {
|
when (type) {
|
||||||
|
|
@ -103,7 +127,37 @@ class ItemPlaybackRepository
|
||||||
else -> toSave
|
else -> toSave
|
||||||
}
|
}
|
||||||
Timber.v("Saving track selection %s", toSave)
|
Timber.v("Saving track selection %s", toSave)
|
||||||
saveItemPlayback(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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -127,9 +181,11 @@ class ItemPlaybackRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ChosenStreams(
|
data class ChosenStreams(
|
||||||
val itemPlayback: ItemPlayback,
|
val itemPlayback: ItemPlayback?,
|
||||||
|
val plc: PlaybackLanguageChoice?,
|
||||||
val itemId: UUID,
|
val itemId: UUID,
|
||||||
val sourceId: UUID?,
|
val source: MediaSourceInfo,
|
||||||
|
val videoStream: MediaStream?,
|
||||||
val audioStream: MediaStream?,
|
val audioStream: MediaStream?,
|
||||||
val subtitleStream: MediaStream?,
|
val subtitleStream: MediaStream?,
|
||||||
val subtitlesDisabled: Boolean,
|
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.Ignore
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
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.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import kotlinx.serialization.UseSerializers
|
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.UUIDSerializer
|
||||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
|
|
@ -53,122 +44,6 @@ data class ItemPlayback(
|
||||||
val subtitleIndexEnabled = subtitleIndex >= 0
|
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 {
|
object TrackIndex {
|
||||||
/**
|
/**
|
||||||
* The user has not explicitly specified a track to use
|
* 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.JellyfinServerDao
|
||||||
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
import com.github.damontecres.wholphin.data.LibraryDisplayInfoDao
|
||||||
import com.github.damontecres.wholphin.data.Migrations
|
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.data.ServerPreferencesDao
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
import com.github.damontecres.wholphin.preferences.AppPreferencesSerializer
|
||||||
|
|
@ -56,6 +57,10 @@ object DatabaseModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun libraryDisplayInfoDao(db: AppDatabase): LibraryDisplayInfoDao = db.libraryDisplayInfoDao()
|
fun libraryDisplayInfoDao(db: AppDatabase): LibraryDisplayInfoDao = db.libraryDisplayInfoDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun playbackLanguageChoiceDao(db: AppDatabase): PlaybackLanguageChoiceDao = db.playbackLanguageChoiceDao()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun userPreferencesDataStore(
|
fun userPreferencesDataStore(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.NonRestartableComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -26,12 +27,8 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.ProvideTextStyle
|
import androidx.tv.material3.ProvideTextStyle
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
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.AppThemeColors
|
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.FontAwesome
|
||||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||||
import com.github.damontecres.wholphin.ui.playback.audioStreamCount
|
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.ui.theme.WholphinTheme
|
||||||
import com.github.damontecres.wholphin.util.languageName
|
import com.github.damontecres.wholphin.util.languageName
|
||||||
import com.github.damontecres.wholphin.util.profile.Codec
|
import com.github.damontecres.wholphin.util.profile.Codec
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
import org.jellyfin.sdk.model.api.MediaStream
|
||||||
import org.jellyfin.sdk.model.api.VideoRange
|
import org.jellyfin.sdk.model.api.VideoRange
|
||||||
import org.jellyfin.sdk.model.api.VideoRangeType
|
import org.jellyfin.sdk.model.api.VideoRangeType
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
fun VideoStreamDetails(
|
fun VideoStreamDetails(
|
||||||
preferences: UserPreferences,
|
chosenStreams: ChosenStreams?,
|
||||||
dto: BaseItemDto,
|
modifier: Modifier = Modifier,
|
||||||
itemPlayback: ItemPlayback?,
|
) = VideoStreamDetails(
|
||||||
|
chosenStreams?.source,
|
||||||
|
chosenStreams?.videoStream,
|
||||||
|
chosenStreams?.audioStream,
|
||||||
|
chosenStreams?.subtitleStream,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoStreamDetails(
|
||||||
|
source: MediaSourceInfo?,
|
||||||
|
videoStream: MediaStream?,
|
||||||
|
audioStream: MediaStream?,
|
||||||
|
subtitleStream: MediaStream?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
@ -57,17 +68,6 @@ fun VideoStreamDetails(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
val source = remember(dto, itemPlayback) { chooseSource(dto, itemPlayback) }
|
|
||||||
|
|
||||||
val videoStream =
|
|
||||||
remember(dto, itemPlayback) {
|
|
||||||
chooseStream(
|
|
||||||
dto,
|
|
||||||
itemPlayback,
|
|
||||||
MediaStreamType.VIDEO,
|
|
||||||
preferences,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val video =
|
val video =
|
||||||
remember(videoStream) {
|
remember(videoStream) {
|
||||||
videoStream
|
videoStream
|
||||||
|
|
@ -95,15 +95,6 @@ fun VideoStreamDetails(
|
||||||
StreamLabel(it)
|
StreamLabel(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val audioStream =
|
|
||||||
remember(dto, itemPlayback) {
|
|
||||||
chooseStream(
|
|
||||||
dto,
|
|
||||||
itemPlayback,
|
|
||||||
MediaStreamType.AUDIO,
|
|
||||||
preferences,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val audioCount = remember(source) { source?.audioStreamCount ?: 0 }
|
val audioCount = remember(source) { source?.audioStreamCount ?: 0 }
|
||||||
val audio =
|
val audio =
|
||||||
remember(audioCount, audioStream) {
|
remember(audioCount, audioStream) {
|
||||||
|
|
@ -124,23 +115,14 @@ fun VideoStreamDetails(
|
||||||
modifier = Modifier.widthIn(max = 200.dp),
|
modifier = Modifier.widthIn(max = 200.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
val subtitleStream =
|
|
||||||
remember(dto, itemPlayback) {
|
|
||||||
chooseStream(
|
|
||||||
dto,
|
|
||||||
itemPlayback,
|
|
||||||
MediaStreamType.SUBTITLE,
|
|
||||||
preferences,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val subtitleCount =
|
val subtitleCount =
|
||||||
remember(source) {
|
remember(source) {
|
||||||
(source?.embeddedSubtitleCount ?: 0) + (source?.externalSubtitlesCount ?: 0)
|
(source?.embeddedSubtitleCount ?: 0) + (source?.externalSubtitlesCount ?: 0)
|
||||||
}
|
}
|
||||||
var disabled by remember { mutableStateOf(false) }
|
var disabled by remember { mutableStateOf(false) }
|
||||||
val subtitle =
|
val subtitle =
|
||||||
remember(subtitleCount, subtitleStream, itemPlayback) {
|
remember(subtitleCount, subtitleStream) {
|
||||||
if (itemPlayback?.subtitleIndex == TrackIndex.DISABLED) {
|
if (subtitleCount > 0 && subtitleStream == null) {
|
||||||
disabled = true
|
disabled = true
|
||||||
context.getString(R.string.disabled)
|
context.getString(R.string.disabled)
|
||||||
} else if (subtitleCount == 0 || subtitleStream == null) {
|
} 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.R
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
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.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
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.api.MediaType
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
|
@ -157,7 +157,7 @@ fun EpisodeDetails(
|
||||||
watched = ep.data.userData?.played ?: false,
|
watched = ep.data.userData?.played ?: false,
|
||||||
favorite = ep.data.userData?.isFavorite ?: false,
|
favorite = ep.data.userData?.isFavorite ?: false,
|
||||||
seriesId = ep.data.seriesId,
|
seriesId = ep.data.seriesId,
|
||||||
sourceId = chosenStreams?.sourceId,
|
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||||
actions = moreActions,
|
actions = moreActions,
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
|
|
@ -174,7 +174,8 @@ fun EpisodeDetails(
|
||||||
moreDialog = null
|
moreDialog = null
|
||||||
},
|
},
|
||||||
onChooseTracks = { type ->
|
onChooseTracks = { type ->
|
||||||
chooseSource(
|
viewModel.streamChoiceService
|
||||||
|
.chooseSource(
|
||||||
ep.data,
|
ep.data,
|
||||||
chosenStreams?.itemPlayback,
|
chosenStreams?.itemPlayback,
|
||||||
)?.let { source ->
|
)?.let { source ->
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,7 @@ fun EpisodeDetailsHeader(
|
||||||
EpisodeQuickDetails(dto)
|
EpisodeQuickDetails(dto)
|
||||||
|
|
||||||
VideoStreamDetails(
|
VideoStreamDetails(
|
||||||
preferences = preferences,
|
chosenStreams = chosenStreams,
|
||||||
dto = dto,
|
|
||||||
itemPlayback = chosenStreams?.itemPlayback,
|
|
||||||
modifier = Modifier.padding(bottom = padding),
|
modifier = Modifier.padding(bottom = padding),
|
||||||
)
|
)
|
||||||
dto.taglines?.firstOrNull()?.let { tagline ->
|
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.preferences.ThemeSongVolume
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
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.ThemeSongPlayer
|
||||||
|
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
|
|
@ -44,8 +46,10 @@ class EpisodeViewModel
|
||||||
private val navigationManager: NavigationManager,
|
private val navigationManager: NavigationManager,
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val itemPlaybackRepository: ItemPlaybackRepository,
|
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||||
|
val streamChoiceService: StreamChoiceService,
|
||||||
private val themeSongPlayer: ThemeSongPlayer,
|
private val themeSongPlayer: ThemeSongPlayer,
|
||||||
private val favoriteWatchManager: FavoriteWatchManager,
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
|
private val userPreferencesService: UserPreferencesService,
|
||||||
@Assisted val itemId: UUID,
|
@Assisted val itemId: UUID,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -85,8 +89,9 @@ class EpisodeViewModel
|
||||||
"Error fetching movie",
|
"Error fetching movie",
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
val prefs = userPreferencesService.getCurrent()
|
||||||
val item = fetchAndSetItem().await()
|
val item = fetchAndSetItem().await()
|
||||||
val result = itemPlaybackRepository.getSelectedTracks(item.id, item)
|
val result = itemPlaybackRepository.getSelectedTracks(item.id, item, prefs)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@EpisodeViewModel.item.value = item
|
this@EpisodeViewModel.item.value = item
|
||||||
chosenStreams.value = result
|
chosenStreams.value = result
|
||||||
|
|
@ -115,10 +120,12 @@ class EpisodeViewModel
|
||||||
sourceId: UUID,
|
sourceId: UUID,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
val prefs = userPreferencesService.getCurrent()
|
||||||
|
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||||
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
|
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
|
||||||
val chosen =
|
val chosen =
|
||||||
result?.let {
|
result?.let {
|
||||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
chosenStreams.value = chosen
|
chosenStreams.value = chosen
|
||||||
|
|
@ -133,6 +140,8 @@ class EpisodeViewModel
|
||||||
type: MediaStreamType,
|
type: MediaStreamType,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
val prefs = userPreferencesService.getCurrent()
|
||||||
|
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||||
val result =
|
val result =
|
||||||
itemPlaybackRepository.saveTrackSelection(
|
itemPlaybackRepository.saveTrackSelection(
|
||||||
item = item,
|
item = item,
|
||||||
|
|
@ -142,7 +151,7 @@ class EpisodeViewModel
|
||||||
)
|
)
|
||||||
val chosen =
|
val chosen =
|
||||||
result?.let {
|
result?.let {
|
||||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
chosenStreams.value = chosen
|
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.RemoteTrailer
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
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.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.services.TrailerService
|
import com.github.damontecres.wholphin.services.TrailerService
|
||||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
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.api.MediaType
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
|
@ -204,7 +204,7 @@ fun MovieDetails(
|
||||||
watched = movie.data.userData?.played ?: false,
|
watched = movie.data.userData?.played ?: false,
|
||||||
favorite = movie.data.userData?.isFavorite ?: false,
|
favorite = movie.data.userData?.isFavorite ?: false,
|
||||||
seriesId = null,
|
seriesId = null,
|
||||||
sourceId = chosenStreams?.sourceId,
|
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||||
actions = moreActions,
|
actions = moreActions,
|
||||||
onChooseVersion = {
|
onChooseVersion = {
|
||||||
chooseVersion =
|
chooseVersion =
|
||||||
|
|
@ -221,7 +221,8 @@ fun MovieDetails(
|
||||||
moreDialog = null
|
moreDialog = null
|
||||||
},
|
},
|
||||||
onChooseTracks = { type ->
|
onChooseTracks = { type ->
|
||||||
chooseSource(
|
viewModel.streamChoiceService
|
||||||
|
.chooseSource(
|
||||||
movie.data,
|
movie.data,
|
||||||
chosenStreams?.itemPlayback,
|
chosenStreams?.itemPlayback,
|
||||||
)?.let { source ->
|
)?.let { source ->
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,7 @@ fun MovieDetailsHeader(
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoStreamDetails(
|
VideoStreamDetails(
|
||||||
preferences = preferences,
|
chosenStreams = chosenStreams,
|
||||||
dto = dto,
|
|
||||||
itemPlayback = chosenStreams?.itemPlayback,
|
|
||||||
modifier = Modifier.padding(bottom = padding),
|
modifier = Modifier.padding(bottom = padding),
|
||||||
)
|
)
|
||||||
dto.taglines?.firstOrNull()?.let { tagline ->
|
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.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.PeopleFavorites
|
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.ThemeSongPlayer
|
||||||
import com.github.damontecres.wholphin.services.TrailerService
|
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.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
|
@ -54,11 +56,13 @@ class MovieViewModel
|
||||||
private val navigationManager: NavigationManager,
|
private val navigationManager: NavigationManager,
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val itemPlaybackRepository: ItemPlaybackRepository,
|
val itemPlaybackRepository: ItemPlaybackRepository,
|
||||||
|
val streamChoiceService: StreamChoiceService,
|
||||||
private val themeSongPlayer: ThemeSongPlayer,
|
private val themeSongPlayer: ThemeSongPlayer,
|
||||||
private val favoriteWatchManager: FavoriteWatchManager,
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
private val peopleFavorites: PeopleFavorites,
|
private val peopleFavorites: PeopleFavorites,
|
||||||
private val trailerService: TrailerService,
|
private val trailerService: TrailerService,
|
||||||
private val extrasService: ExtrasService,
|
private val extrasService: ExtrasService,
|
||||||
|
private val userPreferencesService: UserPreferencesService,
|
||||||
@Assisted val itemId: UUID,
|
@Assisted val itemId: UUID,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -104,7 +108,12 @@ class MovieViewModel
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val item = fetchAndSetItem().await()
|
val item = fetchAndSetItem().await()
|
||||||
val result = itemPlaybackRepository.getSelectedTracks(item.id, item)
|
val result =
|
||||||
|
itemPlaybackRepository.getSelectedTracks(
|
||||||
|
item.id,
|
||||||
|
item,
|
||||||
|
userPreferencesService.getCurrent(),
|
||||||
|
)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@MovieViewModel.item.value = item
|
this@MovieViewModel.item.value = item
|
||||||
chosenStreams.value = result
|
chosenStreams.value = result
|
||||||
|
|
@ -172,10 +181,12 @@ class MovieViewModel
|
||||||
sourceId: UUID,
|
sourceId: UUID,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
val prefs = userPreferencesService.getCurrent()
|
||||||
|
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||||
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
|
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
|
||||||
val chosen =
|
val chosen =
|
||||||
result?.let {
|
result?.let {
|
||||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
chosenStreams.value = chosen
|
chosenStreams.value = chosen
|
||||||
|
|
@ -190,6 +201,8 @@ class MovieViewModel
|
||||||
type: MediaStreamType,
|
type: MediaStreamType,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
val prefs = userPreferencesService.getCurrent()
|
||||||
|
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||||
val result =
|
val result =
|
||||||
itemPlaybackRepository.saveTrackSelection(
|
itemPlaybackRepository.saveTrackSelection(
|
||||||
item = item,
|
item = item,
|
||||||
|
|
@ -199,7 +212,7 @@ class MovieViewModel
|
||||||
)
|
)
|
||||||
val chosen =
|
val chosen =
|
||||||
result?.let {
|
result?.let {
|
||||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
chosenStreams.value = chosen
|
chosenStreams.value = chosen
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,7 @@ fun FocusedEpisodeHeader(
|
||||||
|
|
||||||
if (dto != null) {
|
if (dto != null) {
|
||||||
VideoStreamDetails(
|
VideoStreamDetails(
|
||||||
preferences = preferences,
|
chosenStreams = chosenStreams,
|
||||||
dto = dto,
|
|
||||||
itemPlayback = chosenStreams?.itemPlayback,
|
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import androidx.lifecycle.compose.LifecycleStartEffect
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
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.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
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.extensions.ticks
|
||||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
|
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
@ -219,7 +219,7 @@ fun SeriesOverview(
|
||||||
watched = ep.data.userData?.played ?: false,
|
watched = ep.data.userData?.played ?: false,
|
||||||
favorite = ep.data.userData?.isFavorite ?: false,
|
favorite = ep.data.userData?.isFavorite ?: false,
|
||||||
seriesId = series.id,
|
seriesId = series.id,
|
||||||
sourceId = chosenStreams?.sourceId,
|
sourceId = chosenStreams?.source?.id?.toUUIDOrNull(),
|
||||||
actions =
|
actions =
|
||||||
MoreDialogActions(
|
MoreDialogActions(
|
||||||
navigateTo = viewModel::navigateTo,
|
navigateTo = viewModel::navigateTo,
|
||||||
|
|
@ -257,7 +257,8 @@ fun SeriesOverview(
|
||||||
moreDialog = null
|
moreDialog = null
|
||||||
},
|
},
|
||||||
onChooseTracks = { type ->
|
onChooseTracks = { type ->
|
||||||
chooseSource(
|
viewModel.streamChoiceService
|
||||||
|
.chooseSource(
|
||||||
ep.data,
|
ep.data,
|
||||||
chosenStreams?.itemPlayback,
|
chosenStreams?.itemPlayback,
|
||||||
)?.let { source ->
|
)?.let { source ->
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,10 @@ import com.github.damontecres.wholphin.services.ExtrasService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.PeopleFavorites
|
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.ThemeSongPlayer
|
||||||
import com.github.damontecres.wholphin.services.TrailerService
|
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.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
||||||
import com.github.damontecres.wholphin.ui.equalsNotNull
|
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||||
|
|
@ -70,6 +72,8 @@ class SeriesViewModel
|
||||||
private val peopleFavorites: PeopleFavorites,
|
private val peopleFavorites: PeopleFavorites,
|
||||||
private val trailerService: TrailerService,
|
private val trailerService: TrailerService,
|
||||||
private val extrasService: ExtrasService,
|
private val extrasService: ExtrasService,
|
||||||
|
val streamChoiceService: StreamChoiceService,
|
||||||
|
private val userPreferencesService: UserPreferencesService,
|
||||||
) : ItemViewModel(api) {
|
) : ItemViewModel(api) {
|
||||||
private lateinit var seriesId: UUID
|
private lateinit var seriesId: UUID
|
||||||
private lateinit var prefs: UserPreferences
|
private lateinit var prefs: UserPreferences
|
||||||
|
|
@ -372,7 +376,12 @@ class SeriesViewModel
|
||||||
chosenStreamsJob?.cancel()
|
chosenStreamsJob?.cancel()
|
||||||
chosenStreamsJob =
|
chosenStreamsJob =
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val result = itemPlaybackRepository.getSelectedTracks(itemId, item)
|
val result =
|
||||||
|
itemPlaybackRepository.getSelectedTracks(
|
||||||
|
itemId,
|
||||||
|
item,
|
||||||
|
userPreferencesService.getCurrent(),
|
||||||
|
)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
chosenStreams.value = result
|
chosenStreams.value = result
|
||||||
}
|
}
|
||||||
|
|
@ -384,10 +393,12 @@ class SeriesViewModel
|
||||||
sourceId: UUID,
|
sourceId: UUID,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
val prefs = userPreferencesService.getCurrent()
|
||||||
|
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||||
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
|
val result = itemPlaybackRepository.savePlayVersion(item.id, sourceId)
|
||||||
val chosen =
|
val chosen =
|
||||||
result?.let {
|
result?.let {
|
||||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
chosenStreams.value = chosen
|
chosenStreams.value = chosen
|
||||||
|
|
@ -402,6 +413,8 @@ class SeriesViewModel
|
||||||
type: MediaStreamType,
|
type: MediaStreamType,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
|
val prefs = userPreferencesService.getCurrent()
|
||||||
|
val plc = streamChoiceService.getPlaybackLanguageChoice(item.data)
|
||||||
val result =
|
val result =
|
||||||
itemPlaybackRepository.saveTrackSelection(
|
itemPlaybackRepository.saveTrackSelection(
|
||||||
item = item,
|
item = item,
|
||||||
|
|
@ -411,7 +424,7 @@ class SeriesViewModel
|
||||||
)
|
)
|
||||||
val chosen =
|
val chosen =
|
||||||
result?.let {
|
result?.let {
|
||||||
itemPlaybackRepository.getChosenItemFromPlayback(item, result)
|
itemPlaybackRepository.getChosenItemFromPlayback(item, result, plc, prefs)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
chosenStreams.value = chosen
|
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.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.data.model.Playlist
|
import com.github.damontecres.wholphin.data.model.Playlist
|
||||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
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.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
|
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.PlaylistCreationResult
|
||||||
import com.github.damontecres.wholphin.services.PlaylistCreator
|
import com.github.damontecres.wholphin.services.PlaylistCreator
|
||||||
import com.github.damontecres.wholphin.services.RefreshRateService
|
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.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.onMain
|
import com.github.damontecres.wholphin.ui.onMain
|
||||||
|
|
@ -123,6 +122,7 @@ class PlaybackViewModel
|
||||||
private val deviceInfo: DeviceInfo,
|
private val deviceInfo: DeviceInfo,
|
||||||
private val deviceProfileService: DeviceProfileService,
|
private val deviceProfileService: DeviceProfileService,
|
||||||
private val refreshRateService: RefreshRateService,
|
private val refreshRateService: RefreshRateService,
|
||||||
|
val streamChoiceService: StreamChoiceService,
|
||||||
) : ViewModel(),
|
) : ViewModel(),
|
||||||
Player.Listener,
|
Player.Listener,
|
||||||
AnalyticsListener {
|
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) {
|
if (mediaSource == null) {
|
||||||
showToast(
|
showToast(
|
||||||
|
|
@ -362,12 +363,26 @@ class PlaybackViewModel
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|
||||||
val audioIndex =
|
val audioIndex =
|
||||||
chooseStream(base, playbackConfig, MediaStreamType.AUDIO, preferences)
|
streamChoiceService
|
||||||
?.index
|
.chooseStream(
|
||||||
|
mediaSource,
|
||||||
|
base.seriesId,
|
||||||
|
playbackConfig,
|
||||||
|
plc,
|
||||||
|
MediaStreamType.AUDIO,
|
||||||
|
preferences,
|
||||||
|
)?.index
|
||||||
|
|
||||||
val subtitleIndex =
|
val subtitleIndex =
|
||||||
chooseStream(base, playbackConfig, MediaStreamType.SUBTITLE, preferences)
|
streamChoiceService
|
||||||
?.index
|
.chooseStream(
|
||||||
|
mediaSource,
|
||||||
|
base.seriesId,
|
||||||
|
playbackConfig,
|
||||||
|
plc,
|
||||||
|
MediaStreamType.SUBTITLE,
|
||||||
|
preferences,
|
||||||
|
)?.index
|
||||||
|
|
||||||
Timber.d("Selected mediaSource=${mediaSource.id}, audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
Timber.d("Selected mediaSource=${mediaSource.id}, audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
||||||
|
|
||||||
|
|
@ -563,7 +578,7 @@ class PlaybackViewModel
|
||||||
|
|
||||||
else -> throw Exception("No supported playback method")
|
else -> throw Exception("No supported playback method")
|
||||||
}
|
}
|
||||||
Timber.v("Playback decision: $transcodeType")
|
Timber.i("Playback decision for $itemId: $transcodeType")
|
||||||
|
|
||||||
val externalSubtitleCount = source.externalSubtitlesCount
|
val externalSubtitleCount = source.externalSubtitlesCount
|
||||||
|
|
||||||
|
|
@ -605,21 +620,6 @@ class PlaybackViewModel
|
||||||
liveStreamId = source.liveStreamId,
|
liveStreamId = source.liveStreamId,
|
||||||
mediaSourceInfo = source,
|
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) {
|
if (preferences.appPreferences.playbackPreferences.refreshRateSwitching) {
|
||||||
source.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO }?.let {
|
source.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO }?.let {
|
||||||
|
|
@ -638,14 +638,13 @@ class PlaybackViewModel
|
||||||
api = api,
|
api = api,
|
||||||
player = player,
|
player = player,
|
||||||
playback = playback,
|
playback = playback,
|
||||||
itemPlayback = itemPlayback,
|
itemPlayback = currentItemPlayback,
|
||||||
)
|
)
|
||||||
player.addListener(activityListener)
|
player.addListener(activityListener)
|
||||||
this@PlaybackViewModel.activityListener = activityListener
|
this@PlaybackViewModel.activityListener = activityListener
|
||||||
|
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
this@PlaybackViewModel.currentPlayback.update { playback }
|
this@PlaybackViewModel.currentPlayback.update { playback }
|
||||||
this@PlaybackViewModel.currentItemPlayback.value = itemPlayback
|
|
||||||
player.setMediaItem(
|
player.setMediaItem(
|
||||||
mediaItem,
|
mediaItem,
|
||||||
positionMs,
|
positionMs,
|
||||||
|
|
@ -679,23 +678,41 @@ class PlaybackViewModel
|
||||||
|
|
||||||
fun changeAudioStream(index: Int) {
|
fun changeAudioStream(index: Int) {
|
||||||
viewModelScope.launchIO {
|
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(
|
changeStreams(
|
||||||
item,
|
item,
|
||||||
currentItemPlayback.value!!,
|
itemPlayback,
|
||||||
index,
|
index,
|
||||||
currentItemPlayback.value?.subtitleIndex,
|
itemPlayback.subtitleIndex,
|
||||||
onMain { player.currentPosition },
|
onMain { player.currentPosition },
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeSubtitleStream(index: Int?): Job =
|
fun changeSubtitleStream(index: Int): Job =
|
||||||
viewModelScope.launchIO {
|
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(
|
changeStreams(
|
||||||
item,
|
item,
|
||||||
currentItemPlayback.value!!,
|
itemPlayback,
|
||||||
currentItemPlayback.value?.audioIndex,
|
itemPlayback.audioIndex,
|
||||||
index,
|
index,
|
||||||
onMain { player.currentPosition },
|
onMain { player.currentPosition },
|
||||||
true,
|
true,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
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.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.onMain
|
import com.github.damontecres.wholphin.ui.onMain
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
|
|
@ -101,7 +100,7 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles(
|
||||||
api.userLibraryApi.getItem(itemId = it.itemId).content,
|
api.userLibraryApi.getItem(itemId = it.itemId).content,
|
||||||
api,
|
api,
|
||||||
)
|
)
|
||||||
val mediaSource = chooseSource(item.data, it)
|
val mediaSource = streamChoiceService.chooseSource(item.data, it)
|
||||||
if (mediaSource == null) {
|
if (mediaSource == null) {
|
||||||
// This shouldn't happen, but just in case
|
// This shouldn't happen, but just in case
|
||||||
showToast(
|
showToast(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue