mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Remember playback choices & fix some track selection issues (#26)
Remembers changes in playback tracks across playbacks. So, if you change the audio track, quit the app, and return to the same id, the app will play the audio track previously chosen. Additionally, this PR fixes a UI inconsistency when there are multiple versions of a item. Previously sometimes the audio/subtitle tracks for wrong version would be shown during playback.
This commit is contained in:
parent
f220aeee44
commit
6194eba8c5
31 changed files with 1152 additions and 154 deletions
|
|
@ -1,13 +1,66 @@
|
|||
package com.github.damontecres.wholphin.data
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import java.util.UUID
|
||||
|
||||
@Database(
|
||||
entities = [JellyfinServer::class, JellyfinUser::class],
|
||||
version = 2,
|
||||
exportSchema = false,
|
||||
entities = [JellyfinServer::class, JellyfinUser::class, ItemPlayback::class],
|
||||
version = 4,
|
||||
exportSchema = true,
|
||||
autoMigrations = [AutoMigration(3, 4)],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun serverDao(): JellyfinServerDao
|
||||
|
||||
abstract fun itemPlaybackDao(): ItemPlaybackDao
|
||||
}
|
||||
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
fun convertToString(id: UUID): String = id.toString().replace("-", "")
|
||||
|
||||
@TypeConverter
|
||||
fun convertToUUID(str: String): UUID = str.toUUID()
|
||||
}
|
||||
|
||||
object Migrations {
|
||||
val Migrate2to3 =
|
||||
object : Migration(2, 3) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE users RENAME TO users_old")
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE "users" (
|
||||
rowId integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
id text NOT NULL,
|
||||
name text,
|
||||
serverId text NOT NULL,
|
||||
accessToken text,
|
||||
FOREIGN KEY (serverId) REFERENCES "servers" (id) ON DELETE CASCADE ON UPDATE NO ACTION
|
||||
)
|
||||
""".trimIndent(),
|
||||
)
|
||||
db.execSQL("UPDATE servers SET id = REPLACE(id, '-', '')")
|
||||
db.execSQL(
|
||||
"""
|
||||
INSERT INTO users (id, name, serverId, accessToken)
|
||||
SELECT REPLACE(id, '-', ''), name, REPLACE(serverId, '-', ''), accessToken
|
||||
FROM users_old
|
||||
""".trimIndent(),
|
||||
)
|
||||
db.execSQL("DROP TABLE users_old")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS index_users_serverId ON users (serverId)")
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS index_users_id ON users (id)")
|
||||
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_users_id_serverId ON users (id, serverId)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
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.ItemPlayback
|
||||
import java.util.UUID
|
||||
|
||||
@Dao
|
||||
interface ItemPlaybackDao {
|
||||
fun getItem(
|
||||
user: JellyfinUser,
|
||||
itemId: UUID,
|
||||
): ItemPlayback? = getItem(user.rowId, itemId)
|
||||
|
||||
@Query("SELECT * from ItemPlayback WHERE userId=:userId AND itemId=:itemId")
|
||||
fun getItem(
|
||||
userId: Int,
|
||||
itemId: UUID,
|
||||
): ItemPlayback?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun saveItem(item: ItemPlayback): Long
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.github.damontecres.wholphin.data
|
||||
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
||||
import org.jellyfin.sdk.model.api.MediaStream
|
||||
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 ItemPlaybackRepository
|
||||
@Inject
|
||||
constructor(
|
||||
val serverRepository: ServerRepository,
|
||||
val itemPlaybackDao: ItemPlaybackDao,
|
||||
) {
|
||||
fun getSelectedTracks(
|
||||
itemId: UUID,
|
||||
item: BaseItem,
|
||||
): ChosenStreams? =
|
||||
serverRepository.currentUser?.let { user ->
|
||||
val itemPlayback = itemPlaybackDao.getItem(user = user, itemId = itemId)
|
||||
if (itemPlayback != null) {
|
||||
Timber.v("Got itemPlayback for %s", itemId)
|
||||
val source =
|
||||
item.data.mediaSources?.firstOrNull { it.id?.toUUIDOrNull() == itemPlayback.sourceId }
|
||||
if (source != null) {
|
||||
val audioStream =
|
||||
if (itemPlayback.audioIndexEnabled) {
|
||||
source.mediaStreams?.firstOrNull { it.index == itemPlayback.audioIndex }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val subtitleStream =
|
||||
if (itemPlayback.subtitleIndexEnabled) {
|
||||
source.mediaStreams?.firstOrNull { it.index == itemPlayback.subtitleIndex }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
ChosenStreams(
|
||||
itemId,
|
||||
audioStream,
|
||||
subtitleStream,
|
||||
itemPlayback.subtitleIndex == TrackIndex.DISABLED,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ChosenStreams(
|
||||
val itemId: UUID,
|
||||
val audioStream: MediaStream?,
|
||||
val subtitleStream: MediaStream?,
|
||||
val subtitlesDisabled: Boolean,
|
||||
)
|
||||
|
|
@ -5,6 +5,7 @@ import androidx.room.Dao
|
|||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
|
|
@ -12,17 +13,17 @@ import androidx.room.Query
|
|||
import androidx.room.Relation
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(tableName = "servers")
|
||||
data class JellyfinServer(
|
||||
@PrimaryKey val id: String,
|
||||
@PrimaryKey val id: UUID,
|
||||
val name: String?,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
@Entity(
|
||||
tableName = "users",
|
||||
primaryKeys = ["id", "serverId"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = JellyfinServer::class,
|
||||
|
|
@ -31,13 +32,16 @@ data class JellyfinServer(
|
|||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
indices = [Index("id", "serverId", unique = true)],
|
||||
)
|
||||
data class JellyfinUser(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val rowId: Int = 0,
|
||||
@ColumnInfo(index = true)
|
||||
val id: String,
|
||||
val id: UUID,
|
||||
val name: String?,
|
||||
@ColumnInfo(index = true)
|
||||
val serverId: String,
|
||||
val serverId: UUID,
|
||||
val accessToken: String?,
|
||||
)
|
||||
|
||||
|
|
@ -66,16 +70,27 @@ interface JellyfinServerDao {
|
|||
}
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun addUser(user: JellyfinUser)
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun addUser(user: JellyfinUser): Long
|
||||
|
||||
@Update
|
||||
fun updateUser(user: JellyfinUser): Int
|
||||
|
||||
@Transaction
|
||||
fun addOrUpdateUser(user: JellyfinUser) {
|
||||
val result = addUser(user)
|
||||
if (result == -1L) {
|
||||
updateUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("DELETE FROM servers WHERE id = :serverId")
|
||||
fun deleteServer(serverId: String)
|
||||
fun deleteServer(serverId: UUID)
|
||||
|
||||
@Query("DELETE FROM users WHERE serverId = :serverId AND id = :userId")
|
||||
fun deleteUser(
|
||||
serverId: String,
|
||||
userId: String,
|
||||
serverId: UUID,
|
||||
userId: UUID,
|
||||
)
|
||||
|
||||
@Transaction
|
||||
|
|
@ -84,5 +99,5 @@ interface JellyfinServerDao {
|
|||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM servers WHERE id = :serverId")
|
||||
fun getServer(serverId: String): JellyfinServerUsers?
|
||||
fun getServer(serverId: UUID): JellyfinServerUsers?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.Jellyfin
|
||||
|
|
@ -13,7 +14,9 @@ import org.jellyfin.sdk.api.client.extensions.systemApi
|
|||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.model.api.AuthenticationResult
|
||||
import org.jellyfin.sdk.model.api.UserDto
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -81,17 +84,17 @@ class ServerRepository
|
|||
val updatedServer = server.copy(name = sysInfo.serverName)
|
||||
val updatedUser =
|
||||
user.copy(
|
||||
id = userDto.id.toString(),
|
||||
id = userDto.id,
|
||||
name = userDto.name,
|
||||
)
|
||||
serverDao.addOrUpdateServer(updatedServer)
|
||||
serverDao.addUser(updatedUser)
|
||||
serverDao.addOrUpdateUser(updatedUser)
|
||||
userPreferencesDataStore.updateData {
|
||||
it
|
||||
.toBuilder()
|
||||
.apply {
|
||||
currentServerId = updatedServer.id
|
||||
currentUserId = updatedUser.id
|
||||
currentServerId = updatedServer.id.toServerString()
|
||||
currentUserId = updatedUser.id.toServerString()
|
||||
}.build()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
@ -105,9 +108,12 @@ class ServerRepository
|
|||
* Restores a session for the given server & user such as when the app reopens
|
||||
*/
|
||||
suspend fun restoreSession(
|
||||
serverId: String,
|
||||
userId: String,
|
||||
serverId: UUID?,
|
||||
userId: UUID?,
|
||||
): Boolean {
|
||||
if (serverId == null || userId == null) {
|
||||
return false
|
||||
}
|
||||
val serverAndUsers =
|
||||
withContext(Dispatchers.IO) {
|
||||
serverDao.getServer(serverId)
|
||||
|
|
@ -134,7 +140,7 @@ class ServerRepository
|
|||
if (accessToken != null) {
|
||||
val authedUser = authenticationResult.user
|
||||
val server =
|
||||
authenticationResult.serverId?.let {
|
||||
authenticationResult.serverId?.toUUIDOrNull()?.let {
|
||||
JellyfinServer(
|
||||
id = it,
|
||||
name = authedUser?.serverName,
|
||||
|
|
@ -145,7 +151,7 @@ class ServerRepository
|
|||
val user =
|
||||
authedUser?.let {
|
||||
JellyfinUser(
|
||||
id = it.id.toString(),
|
||||
id = it.id,
|
||||
name = it.name,
|
||||
serverId = server.id,
|
||||
accessToken = accessToken,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
@file:UseSerializers(UuidSerializer::class)
|
||||
|
||||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.github.damontecres.wholphin.data.JellyfinUser
|
||||
import com.github.damontecres.wholphin.util.UuidSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = JellyfinUser::class,
|
||||
parentColumns = arrayOf("rowId"),
|
||||
childColumns = arrayOf("userId"),
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
indices = [Index("userId", "itemId", unique = true)],
|
||||
)
|
||||
@Serializable
|
||||
data class ItemPlayback(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val rowId: Long = 0,
|
||||
val userId: Int,
|
||||
val itemId: UUID,
|
||||
val sourceId: UUID? = null,
|
||||
val audioIndex: Int = TrackIndex.UNSPECIFIED,
|
||||
val subtitleIndex: Int = TrackIndex.UNSPECIFIED,
|
||||
) {
|
||||
@Transient val audioIndexEnabled = audioIndex >= 0
|
||||
|
||||
@Transient val subtitleIndexEnabled = subtitleIndex >= 0
|
||||
}
|
||||
|
||||
object TrackIndex {
|
||||
/**
|
||||
* The user has not explicitly specified a track to use
|
||||
*/
|
||||
const val UNSPECIFIED = -1
|
||||
|
||||
/**
|
||||
* The user has explicitly disabled the tracks (eg turned off subtitles)
|
||||
*/
|
||||
const val DISABLED = -2
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue