Various fixes (#33)

Fixes a few issues to prepare for a release.
This commit is contained in:
damontecres 2025-10-18 18:45:08 -04:00 committed by GitHub
parent 405e170750
commit dc3193e2dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 127 additions and 61 deletions

View file

@ -106,9 +106,21 @@ class ItemPlaybackRepository
saveItemPlayback(toSave)
}
/**
* Saves the [ItemPlayback] into the database, returning the same object with the rowId updated if needed
*/
suspend fun saveItemPlayback(itemPlayback: ItemPlayback): ItemPlayback {
val rowId = itemPlaybackDao.saveItem(itemPlayback)
return itemPlayback.copy(rowId = rowId)
val toSave =
if (itemPlayback.userId < 0) {
val userRowId =
serverRepository.currentUser?.rowId?.takeIf { it >= 0 }
?: throw IllegalStateException("Trying to save an ItemPlayback without a user, but there is no current user")
itemPlayback.copy(userId = userRowId)
} else {
itemPlayback
}
val id = itemPlaybackDao.saveItem(toSave)
return toSave.copy(rowId = id)
}
}

View file

@ -84,10 +84,23 @@ interface JellyfinServerDao {
fun addOrUpdateUser(user: JellyfinUser) {
val result = addUser(user)
if (result == -1L) {
updateUser(user)
val toSave =
if (user.rowId <= 0) {
val temp = getUser(user.serverId, user.id)!!
user.copy(rowId = temp.rowId)
} else {
user
}
updateUser(toSave)
}
}
@Query("SELECT * FROM users WHERE serverId = :serverId AND id = :userId")
fun getUser(
serverId: UUID,
userId: UUID,
): JellyfinUser?
@Query("DELETE FROM servers WHERE id = :serverId")
fun deleteServer(serverId: UUID)

View file

@ -87,6 +87,8 @@ class ServerRepository
id = userDto.id,
name = userDto.name,
)
if (updatedUser.rowId <= 0) {
}
serverDao.addOrUpdateServer(updatedServer)
serverDao.addOrUpdateUser(updatedUser)
userPreferencesDataStore.updateData {
@ -119,7 +121,6 @@ class ServerRepository
serverDao.getServer(serverId)
}
if (serverAndUsers != null) {
_currentServer = serverAndUsers.server
val user = serverAndUsers.users.firstOrNull { it.id == userId }
if (user != null) {
changeUser(serverAndUsers.server, user)

View file

@ -39,6 +39,19 @@ val DefaultItemFields =
ItemFields.TRICKPLAY,
ItemFields.SORT_NAME,
ItemFields.CHAPTERS,
ItemFields.MEDIA_SOURCES,
)
/**
* [ItemFields] for higher level displays such as grids or rows
*/
val SlimItemFields =
listOf(
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
ItemFields.SEASON_USER_DATA,
ItemFields.CHILD_COUNT,
ItemFields.OVERVIEW,
ItemFields.SORT_NAME,
)
val DefaultButtonPadding =

View file

@ -29,8 +29,8 @@ import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
import com.github.damontecres.wholphin.ui.data.SortAndDirection
@ -135,7 +135,7 @@ class CollectionFolderViewModel
SortOrder.ASCENDING,
SortOrder.ASCENDING,
),
fields = DefaultItemFields,
fields = SlimItemFields,
),
)
val newPager =

View file

@ -16,8 +16,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.cards.GridCard
import com.github.damontecres.wholphin.ui.detail.CardGrid
import com.github.damontecres.wholphin.ui.nav.Destination
@ -54,7 +54,7 @@ class GenreViewModel
val request =
GetGenresRequest(
parentId = itemId,
fields = DefaultItemFields,
fields = SlimItemFields,
)
val pager = ApiRequestPager(api, request, GetGenresRequestHandler, viewModelScope).init()
withContext(Dispatchers.Main) {

View file

@ -10,8 +10,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.main.HomeRow
import com.github.damontecres.wholphin.ui.main.HomeSection
@ -52,7 +52,7 @@ class RecommendedMovieViewModel
val resumeItemsRequest =
GetResumeItemsRequest(
parentId = parentId,
fields = DefaultItemFields,
fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.MOVIE),
enableUserData = true,
)
@ -62,7 +62,7 @@ class RecommendedMovieViewModel
val recentlyReleasedRequest =
GetItemsRequest(
parentId = parentId,
fields = DefaultItemFields,
fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.MOVIE),
recursive = true,
enableUserData = true,
@ -75,7 +75,7 @@ class RecommendedMovieViewModel
val recentlyAddedRequest =
GetItemsRequest(
parentId = parentId,
fields = DefaultItemFields,
fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.MOVIE),
recursive = true,
enableUserData = true,
@ -94,7 +94,7 @@ class RecommendedMovieViewModel
val unwatchedTopRatedRequest =
GetItemsRequest(
parentId = parentId,
fields = DefaultItemFields,
fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.MOVIE),
recursive = true,
enableUserData = true,

View file

@ -10,8 +10,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.main.HomeRow
import com.github.damontecres.wholphin.ui.main.HomeSection
@ -54,7 +54,7 @@ class RecommendedTvShowViewModel
val resumeItemsRequest =
GetResumeItemsRequest(
parentId = parentId,
fields = DefaultItemFields,
fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.EPISODE),
enableUserData = true,
)
@ -64,7 +64,7 @@ class RecommendedTvShowViewModel
val nextUpRequest =
GetNextUpRequest(
parentId = parentId,
fields = DefaultItemFields,
fields = SlimItemFields,
enableUserData = true,
)
val nextUpItems = ApiRequestPager(api, nextUpRequest, GetNextUpRequestHandler, viewModelScope, useSeriesForPrimary = true)
@ -72,7 +72,7 @@ class RecommendedTvShowViewModel
val recentlyReleasedRequest =
GetItemsRequest(
parentId = parentId,
fields = DefaultItemFields,
fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.EPISODE),
recursive = true,
enableUserData = true,
@ -85,7 +85,7 @@ class RecommendedTvShowViewModel
val recentlyAddedRequest =
GetItemsRequest(
parentId = parentId,
fields = DefaultItemFields,
fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.EPISODE),
recursive = true,
enableUserData = true,
@ -104,7 +104,7 @@ class RecommendedTvShowViewModel
val unwatchedTopRatedRequest =
GetItemsRequest(
parentId = parentId,
fields = DefaultItemFields,
fields = SlimItemFields,
includeItemTypes = listOf(BaseItemKind.SERIES),
recursive = true,
enableUserData = true,

View file

@ -2,9 +2,7 @@ package com.github.damontecres.wholphin.ui.detail
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.ui.graphics.Color
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
@ -87,8 +85,7 @@ fun buildMoreDialogItems(
add(
DialogItem(
"Choose Version",
Icons.Default.ArrowForward,
iconColor = Color.Green.copy(alpha = .8f),
R.string.fa_file_video,
) {
onChooseVersion.invoke()
},
@ -104,7 +101,7 @@ fun buildMoreDialogItems(
add(
DialogItem(
"Choose audio",
Icons.Default.Settings,
R.string.fa_volume_low,
) {
onChooseTracks.invoke(MediaStreamType.AUDIO)
},
@ -114,7 +111,7 @@ fun buildMoreDialogItems(
add(
DialogItem(
"Choose subtitles",
Icons.Default.Settings,
R.string.fa_closed_captioning,
) {
onChooseTracks.invoke(MediaStreamType.SUBTITLE)
},

View file

@ -30,6 +30,7 @@ import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.profile.Codec
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@ -135,7 +136,13 @@ class SeriesViewModel
val url =
api.universalAudioApi.getUniversalAudioStreamUrl(
theme.id,
container = listOf("opus", "mp3", "aaa", "flac"),
container =
listOf(
Codec.Audio.OPUS,
Codec.Audio.MP3,
Codec.Audio.AAC,
Codec.Audio.FLAC,
),
)
Timber.Forest.v("Found theme song for series $seriesId")
withContext(Dispatchers.Main) {

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState
@ -119,7 +119,7 @@ class HomeViewModel
val request =
GetResumeItemsRequest(
userId = userId,
fields = DefaultItemFields,
fields = SlimItemFields,
limit = limit,
includeItemTypes = supportItemKinds,
)
@ -140,7 +140,7 @@ class HomeViewModel
val request =
GetNextUpRequest(
userId = userId,
fields = DefaultItemFields,
fields = SlimItemFields,
imageTypeLimit = 1,
parentId = null,
limit = limit,
@ -176,7 +176,7 @@ class HomeViewModel
view.name?.let { "Recently Added in $it" }
val request =
GetLatestMediaRequest(
fields = DefaultItemFields,
fields = SlimItemFields,
imageTypeLimit = 1,
parentId = view.id,
groupItems = true,

View file

@ -26,7 +26,7 @@ import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.cards.EpisodeCard
import com.github.damontecres.wholphin.ui.cards.ItemRow
import com.github.damontecres.wholphin.ui.cards.SeasonCard
@ -78,7 +78,7 @@ class SearchViewModel
searchTerm = query,
recursive = true,
includeItemTypes = listOf(BaseItemKind.MOVIE),
fields = DefaultItemFields,
fields = SlimItemFields,
limit = 25,
)
val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
@ -93,7 +93,7 @@ class SearchViewModel
searchTerm = query,
recursive = true,
includeItemTypes = listOf(BaseItemKind.SERIES),
fields = DefaultItemFields,
fields = SlimItemFields,
limit = 25,
)
val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
@ -108,7 +108,7 @@ class SearchViewModel
searchTerm = query,
recursive = true,
includeItemTypes = listOf(BaseItemKind.EPISODE),
fields = DefaultItemFields,
fields = SlimItemFields,
limit = 25,
)
val pager = ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)

View file

@ -17,6 +17,7 @@ import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.ItemPlaybackRepository
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.Chapter
@ -100,6 +101,7 @@ class PlaybackViewModel
val navigationManager: NavigationManager,
val itemPlaybackDao: ItemPlaybackDao,
val serverRepository: ServerRepository,
val itemPlaybackRepository: ItemPlaybackRepository,
) : ViewModel(),
Player.Listener {
val player: ExoPlayer =
@ -461,10 +463,9 @@ class PlaybackViewModel
if (userInitiated) {
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
Timber.v("Saving user initiated item playback: %s", itemPlayback)
val rowId = itemPlaybackDao.saveItem(itemPlayback)
val updated = itemPlaybackRepository.saveItemPlayback(itemPlayback)
withContext(Dispatchers.Main) {
this@PlaybackViewModel.currentItemPlayback.value =
itemPlayback.copy(rowId = rowId)
this@PlaybackViewModel.currentItemPlayback.value = updated
}
}
}

View file

@ -125,9 +125,7 @@ fun SwitchUserContent(
LaunchedEffect(Unit) {
viewModel.clearSwitchUserState()
if (useQuickConnect) {
viewModel.initiateQuickConnect(server) {
viewModel.navigationManager.goToHome()
}
viewModel.initiateQuickConnect(server)
}
}
BasicDialog(
@ -180,10 +178,18 @@ fun SwitchUserContent(
)
when (val s = userState) {
is LoadingState.Error -> {
Text(
text = s.message ?: s.exception?.localizedMessage ?: "Error",
color = MaterialTheme.colorScheme.error,
)
s.message?.let {
Text(
text = it,
color = MaterialTheme.colorScheme.error,
)
}
s.exception?.localizedMessage?.let {
Text(
text = it,
color = MaterialTheme.colorScheme.error,
)
}
}
else -> {}
@ -208,6 +214,7 @@ fun SwitchUserContent(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Text,
),
isInputValid = { userState !is LoadingState.Error },
modifier = Modifier.focusRequester(focusRequester),
)
}
@ -235,6 +242,7 @@ fun SwitchUserContent(
KeyboardActions(
onDone = { onSubmit.invoke() },
),
isInputValid = { userState !is LoadingState.Error },
modifier = Modifier,
)
}

View file

@ -154,7 +154,7 @@ class SwitchUserViewModel
}
} catch (ex: Exception) {
Timber.e(ex, "Error switching user")
setError(ex)
setError("Error switching user", ex)
}
}
}
@ -178,15 +178,19 @@ class SwitchUserViewModel
}
} catch (ex: Exception) {
Timber.e(ex, "Error logging in user")
setError(ex)
if (ex is InvalidStatusException && ex.status == 401) {
withContext(Dispatchers.Main) {
switchUserState.value =
LoadingState.Error("Invalid username or password")
}
} else {
setError("Error during login", ex)
}
}
}
}
fun initiateQuickConnect(
server: JellyfinServer,
onAuthenticated: () -> Unit,
) {
fun initiateQuickConnect(server: JellyfinServer) {
quickConnectJob?.cancel()
viewModelScope.launchIO {
try {
@ -218,19 +222,21 @@ class SwitchUserViewModel
)
serverRepository.changeUser(server.url, authenticationResult)
withContext(Dispatchers.Main) {
onAuthenticated.invoke()
navigationManager.goToHome()
}
}
} catch (ex: Exception) {
Timber.e(ex, "Error during quick connect")
if (ex is InvalidStatusException && ex.status == 401) {
quickConnectState.value = null
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(server.id, false)
}
withContext(Dispatchers.Main) {
quickConnectState.value = null
serverQuickConnect.value =
serverQuickConnect.value!!.toMutableMap().apply {
put(server.id, false)
}
}
}
setError(ex)
setError("Error with Quick Connect", ex)
}
}
}
@ -329,8 +335,10 @@ class SwitchUserViewModel
}
}
private suspend fun setError(ex: Exception) =
withContext(Dispatchers.Main) {
switchUserState.value = LoadingState.Error(ex)
}
private suspend fun setError(
msg: String? = null,
ex: Exception? = null,
) = withContext(Dispatchers.Main) {
switchUserState.value = LoadingState.Error(msg, ex)
}
}

View file

@ -34,4 +34,6 @@
<string name="fa_shuffle" translatable="false">&#xf074;</string>
<string name="fa_open_folder" translatable="false">&#xf07c;</string>
<string name="fa_list_ul" translatable="false">&#xf0ca;</string>
<string name="fa_file_video" translatable="false">&#xf1c8;</string>
<string name="fa_volume_low" translatable="false">&#xf027;</string>
</resources>