refactor: upgrade to jellyfin 10.9 (#757)

* refactor: upgrade to jellyfin 10.9

* chore: upgrade to jellyfin sdk 1.5.0-beta.2

* fix: don't show resumable items in next up

* chore: upgrade to jellyfin sdk 1.5.0-beta.3

* fix: sync offline playback progress

* refactor: initialize BrandingApi in JellyfinApi

* refactor: speed up quick connect auth

* perf: load home data on Default dispatcher
This commit is contained in:
Jarne Demeulemeester 2024-06-16 12:29:26 +02:00 committed by GitHub
parent ebfa81e053
commit ba20b2fd37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 97 additions and 83 deletions

View file

@ -325,8 +325,8 @@ class MovieFragment : Fragment() {
it.displayProfiles.firstOrNull()?.apply { it.displayProfiles.firstOrNull()?.apply {
videoProfileChip.text = this.raw videoProfileChip.text = this.raw
videoProfileChip.isVisible = when (this) { videoProfileChip.isVisible = when (this) {
DisplayProfile.HDR,
DisplayProfile.HDR10, DisplayProfile.HDR10,
DisplayProfile.HDR10_PLUS,
DisplayProfile.HLG, DisplayProfile.HLG,
-> { -> {
videoProfileChip.chipStartPadding = .0f videoProfileChip.chipStartPadding = .0f

View file

@ -61,6 +61,7 @@ import dev.jdtech.jellyfin.viewmodels.MovieViewModel
import dev.jdtech.jellyfin.viewmodels.PlayerItemsEvent import dev.jdtech.jellyfin.viewmodels.PlayerItemsEvent
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.PersonKind
import java.util.UUID import java.util.UUID
import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.core.R as CoreR
@ -343,6 +344,7 @@ private fun MovieScreenLayoutPreview() {
director = BaseItemPerson( director = BaseItemPerson(
id = UUID.randomUUID(), id = UUID.randomUUID(),
name = "Robert Rodriguez", name = "Robert Rodriguez",
type = PersonKind.DIRECTOR,
), ),
writers = emptyList(), writers = emptyList(),
videoMetadata = VideoMetadata( videoMetadata = VideoMetadata(

View file

@ -35,8 +35,10 @@ object ApiModule {
val user = serverWithAddressAndUser.user val user = serverWithAddressAndUser.user
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = serverAddress.address api.update(
api.accessToken = user?.accessToken baseUrl = serverAddress.address,
accessToken = user?.accessToken,
)
userId = user?.id userId = user?.id
} }

View file

@ -44,7 +44,7 @@ class SortDialogFragment(
when (sortType) { when (sortType) {
"sortBy" -> { "sortBy" -> {
val sortByOptions = resources.getStringArray(R.array.sort_by_options) val sortByOptions = resources.getStringArray(R.array.sort_by_options)
val sortByValues = SortBy.values() val sortByValues = SortBy.entries
builder builder
.setTitle(getString(R.string.sort_by)) .setTitle(getString(R.string.sort_by))
.setSingleChoiceItems( .setSingleChoiceItems(
@ -64,7 +64,7 @@ class SortDialogFragment(
} }
"sortOrder" -> { "sortOrder" -> {
val sortByOptions = resources.getStringArray(R.array.sort_order_options) val sortByOptions = resources.getStringArray(R.array.sort_order_options)
val sortOrderValues = SortOrder.values() val sortOrderValues = SortOrder.entries
builder builder
.setTitle(getString(R.string.sort_order)) .setTitle(getString(R.string.sort_order))

View file

@ -18,7 +18,7 @@ fun BaseItemDto.toView(): View {
return View( return View(
id = id, id = id,
name = name ?: "", name = name ?: "",
type = CollectionType.fromString(collectionType), type = CollectionType.fromString(collectionType?.serialName),
) )
} }

View file

@ -202,8 +202,10 @@ constructor(
appPreferences.currentServer = server.id appPreferences.currentServer = server.id
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = recommendedServerInfo.address api.update(
api.accessToken = null baseUrl = recommendedServerInfo.address,
accessToken = null,
)
} }
_uiState.emit(UiState.Normal) _uiState.emit(UiState.Normal)

View file

@ -11,6 +11,7 @@ import dev.jdtech.jellyfin.models.HomeSection
import dev.jdtech.jellyfin.models.UiText import dev.jdtech.jellyfin.models.UiText
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.toView import dev.jdtech.jellyfin.utils.toView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -41,13 +42,12 @@ class HomeViewModel @Inject internal constructor(
viewModelScope.launch { viewModelScope.launch {
try { try {
repository.postCapabilities() repository.postCapabilities()
} catch (_: Exception) { } catch (_: Exception) { }
}
} }
} }
fun loadData() { fun loadData() {
viewModelScope.launch { viewModelScope.launch(Dispatchers.Default) {
_uiState.emit(UiState.Loading) _uiState.emit(UiState.Loading)
try { try {
val items = mutableListOf<HomeItem>() val items = mutableListOf<HomeItem>()
@ -93,7 +93,7 @@ class HomeViewModel @Inject internal constructor(
private suspend fun loadViews() = repository private suspend fun loadViews() = repository
.getUserViews() .getUserViews()
.filter { view -> CollectionType.fromString(view.collectionType) in CollectionType.supported } .filter { view -> CollectionType.fromString(view.collectionType?.serialName) in CollectionType.supported }
.map { view -> view to repository.getLatestMedia(view.id) } .map { view -> view to repository.getLatestMedia(view.id) }
.filter { (_, latest) -> latest.isNotEmpty() } .filter { (_, latest) -> latest.isNotEmpty() }
.map { (view, latest) -> view.toView().apply { items = latest } } .map { (view, latest) -> view.toView().apply { items = latest } }

View file

@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.extensions.authenticateWithQuickConnect import org.jellyfin.sdk.api.client.extensions.authenticateWithQuickConnect
import org.jellyfin.sdk.api.client.extensions.brandingApi
import org.jellyfin.sdk.model.api.AuthenticateUserByName import org.jellyfin.sdk.model.api.AuthenticateUserByName
import org.jellyfin.sdk.model.api.AuthenticationResult import org.jellyfin.sdk.model.api.AuthenticationResult
import javax.inject.Inject import javax.inject.Inject
@ -72,7 +71,7 @@ constructor(
private fun loadDisclaimer() { private fun loadDisclaimer() {
viewModelScope.launch { viewModelScope.launch {
loginDisclaimer = jellyfinApi.api.brandingApi.getBrandingOptions().content.loginDisclaimer loginDisclaimer = jellyfinApi.brandingApi.getBrandingOptions().content.loginDisclaimer
_uiState.emit(UiState.Normal(loginDisclaimer)) _uiState.emit(UiState.Normal(loginDisclaimer))
} }
} }
@ -104,7 +103,7 @@ constructor(
private fun loadQuickConnectAvailable() { private fun loadQuickConnectAvailable() {
viewModelScope.launch { viewModelScope.launch {
try { try {
val isEnabled by jellyfinApi.quickConnectApi.getEnabled() val isEnabled by jellyfinApi.quickConnectApi.getQuickConnectEnabled()
if (isEnabled) { if (isEnabled) {
_quickConnectUiState.emit(QuickConnectUiState.Normal) _quickConnectUiState.emit(QuickConnectUiState.Normal)
} }
@ -155,12 +154,12 @@ constructor(
} }
quickConnectJob = viewModelScope.launch { quickConnectJob = viewModelScope.launch {
try { try {
var quickConnectState = jellyfinApi.quickConnectApi.initiate().content var quickConnectState = jellyfinApi.quickConnectApi.initiateQuickConnect().content
_quickConnectUiState.emit(QuickConnectUiState.Waiting(quickConnectState.code)) _quickConnectUiState.emit(QuickConnectUiState.Waiting(quickConnectState.code))
while (!quickConnectState.authenticated) { while (!quickConnectState.authenticated) {
quickConnectState = jellyfinApi.quickConnectApi.connect(quickConnectState.secret).content
delay(5000L) delay(5000L)
quickConnectState = jellyfinApi.quickConnectApi.getQuickConnectState(quickConnectState.secret).content
} }
val authenticationResult by jellyfinApi.userApi.authenticateWithQuickConnect( val authenticationResult by jellyfinApi.userApi.authenticateWithQuickConnect(
secret = quickConnectState.secret, secret = quickConnectState.secret,
@ -189,7 +188,7 @@ constructor(
insertUser(appPreferences.currentServer!!, user) insertUser(appPreferences.currentServer!!, user)
jellyfinApi.apply { jellyfinApi.apply {
api.accessToken = authenticationResult.accessToken api.update(accessToken = authenticationResult.accessToken)
userId = authenticationResult.user?.id userId = authenticationResult.user?.id
} }
} }

View file

@ -30,6 +30,8 @@ import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
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.api.PersonKind
import org.jellyfin.sdk.model.api.VideoRangeType
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -119,7 +121,7 @@ constructor(
private suspend fun getActors(item: FindroidMovie): List<BaseItemPerson> { private suspend fun getActors(item: FindroidMovie): List<BaseItemPerson> {
val actors: List<BaseItemPerson> val actors: List<BaseItemPerson>
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
actors = item.people.filter { it.type == "Actor" } actors = item.people.filter { it.type == PersonKind.ACTOR }
} }
return actors return actors
} }
@ -127,7 +129,7 @@ constructor(
private suspend fun getDirector(item: FindroidMovie): BaseItemPerson? { private suspend fun getDirector(item: FindroidMovie): BaseItemPerson? {
val director: BaseItemPerson? val director: BaseItemPerson?
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
director = item.people.firstOrNull { it.type == "Director" } director = item.people.firstOrNull { it.type == PersonKind.DIRECTOR }
} }
return director return director
} }
@ -135,7 +137,7 @@ constructor(
private suspend fun getWriters(item: FindroidMovie): List<BaseItemPerson> { private suspend fun getWriters(item: FindroidMovie): List<BaseItemPerson> {
val writers: List<BaseItemPerson> val writers: List<BaseItemPerson>
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
writers = item.people.filter { it.type == "Writer" } writers = item.people.filter { it.type == PersonKind.WRITER }
} }
return writers return writers
} }
@ -213,9 +215,9 @@ constructor(
DisplayProfile.DOLBY_VISION DisplayProfile.DOLBY_VISION
} else { } else {
when (videoRangeType) { when (videoRangeType) {
DisplayProfile.HDR.raw -> DisplayProfile.HDR VideoRangeType.HDR10 -> DisplayProfile.HDR10
DisplayProfile.HDR10.raw -> DisplayProfile.HDR10 VideoRangeType.HDR10_PLUS -> DisplayProfile.HDR10_PLUS
DisplayProfile.HLG.raw -> DisplayProfile.HLG VideoRangeType.HLG -> DisplayProfile.HLG
else -> DisplayProfile.SDR else -> DisplayProfile.SDR
} }
}, },

View file

@ -74,7 +74,9 @@ constructor(
server.currentServerAddressId = address.id server.currentServerAddressId = address.id
database.update(server) database.update(server)
jellyfinApi.api.baseUrl = address.address jellyfinApi.api.update(
baseUrl = address.address,
)
eventsChannel.send(ServerAddressesEvent.NavigateToHome) eventsChannel.send(ServerAddressesEvent.NavigateToHome)
} }
@ -84,7 +86,9 @@ constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val jellyfinApi = JellyfinApi(context) val jellyfinApi = JellyfinApi(context)
jellyfinApi.api.baseUrl = address jellyfinApi.api.update(
baseUrl = address,
)
val systemInfo by jellyfinApi.systemApi.getPublicSystemInfo() val systemInfo by jellyfinApi.systemApi.getPublicSystemInfo()
if (systemInfo.id != currentServerId) { if (systemInfo.id != currentServerId) {
return@launch return@launch

View file

@ -101,8 +101,10 @@ constructor(
// If server has no selected user, navigate to login fragment // If server has no selected user, navigate to login fragment
if (user == null) { if (user == null) {
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = serverAddress.address api.update(
api.accessToken = null baseUrl = serverAddress.address,
accessToken = null,
)
userId = null userId = null
} }
appPreferences.currentServer = server.id appPreferences.currentServer = server.id
@ -111,8 +113,10 @@ constructor(
} }
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = serverAddress.address api.update(
api.accessToken = user.accessToken baseUrl = serverAddress.address,
accessToken = user.accessToken,
)
userId = user.id userId = user.id
} }

View file

@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.BaseItemPerson import org.jellyfin.sdk.model.api.BaseItemPerson
import org.jellyfin.sdk.model.api.PersonKind
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -100,7 +101,7 @@ constructor(
private suspend fun getActors(item: FindroidShow): List<BaseItemPerson> { private suspend fun getActors(item: FindroidShow): List<BaseItemPerson> {
val actors: List<BaseItemPerson> val actors: List<BaseItemPerson>
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
actors = item.people.filter { it.type == "Actor" } actors = item.people.filter { it.type == PersonKind.ACTOR }
} }
return actors return actors
} }
@ -108,7 +109,7 @@ constructor(
private suspend fun getDirector(item: FindroidShow): BaseItemPerson? { private suspend fun getDirector(item: FindroidShow): BaseItemPerson? {
val director: BaseItemPerson? val director: BaseItemPerson?
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
director = item.people.firstOrNull { it.type == "Director" } director = item.people.firstOrNull { it.type == PersonKind.DIRECTOR }
} }
return director return director
} }
@ -116,7 +117,7 @@ constructor(
private suspend fun getWriters(item: FindroidShow): List<BaseItemPerson> { private suspend fun getWriters(item: FindroidShow): List<BaseItemPerson> {
val writers: List<BaseItemPerson> val writers: List<BaseItemPerson>
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
writers = item.people.filter { it.type == "Writer" } writers = item.people.filter { it.type == PersonKind.WRITER }
} }
return writers return writers
} }

View file

@ -19,7 +19,7 @@ import javax.inject.Inject
class UserSelectViewModel class UserSelectViewModel
@Inject @Inject
constructor( constructor(
private val appPreferences: AppPreferences, appPreferences: AppPreferences,
private val jellyfinApi: JellyfinApi, private val jellyfinApi: JellyfinApi,
private val database: ServerDatabaseDao, private val database: ServerDatabaseDao,
) : ViewModel() { ) : ViewModel() {
@ -71,7 +71,9 @@ constructor(
database.update(server) database.update(server)
jellyfinApi.apply { jellyfinApi.apply {
api.accessToken = user.accessToken api.update(
accessToken = user.accessToken,
)
userId = user.id userId = user.id
} }

View file

@ -73,7 +73,9 @@ constructor(
database.update(server) database.update(server)
jellyfinApi.apply { jellyfinApi.apply {
api.accessToken = user.accessToken api.update(
accessToken = user.accessToken,
)
userId = user.id userId = user.id
} }

View file

@ -40,8 +40,10 @@ class SyncWorker @AssistedInject constructor(
val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: continue val serverAddress = serverWithAddressesAndUsers.addresses.firstOrNull { it.id == server.currentServerAddressId } ?: continue
for (user in serverWithAddressesAndUsers.users) { for (user in serverWithAddressesAndUsers.users) {
jellyfinApi.apply { jellyfinApi.apply {
api.baseUrl = serverAddress.address api.update(
api.accessToken = user.accessToken baseUrl = serverAddress.address,
accessToken = user.accessToken,
)
userId = user.id userId = user.id
} }
val movies = database.getMoviesByServerId(server.id).map { it.toFindroidMovie(database, user.id) } val movies = database.getMoviesByServerId(server.id).map { it.toFindroidMovie(database, user.id) }
@ -66,17 +68,16 @@ class SyncWorker @AssistedInject constructor(
try { try {
when (userData.played) { when (userData.played) {
true -> jellyfinApi.playStateApi.markPlayedItem(user.id, item.id) true -> jellyfinApi.playStateApi.markPlayedItem(item.id, user.id)
false -> jellyfinApi.playStateApi.markUnplayedItem(user.id, item.id) false -> jellyfinApi.playStateApi.markUnplayedItem(item.id, user.id)
} }
when (userData.favorite) { when (userData.favorite) {
true -> jellyfinApi.userLibraryApi.markFavoriteItem(user.id, item.id) true -> jellyfinApi.userLibraryApi.markFavoriteItem(item.id, user.id)
false -> jellyfinApi.userLibraryApi.unmarkFavoriteItem(user.id, item.id) false -> jellyfinApi.userLibraryApi.unmarkFavoriteItem(item.id, user.id)
} }
jellyfinApi.playStateApi.onPlaybackStopped( jellyfinApi.playStateApi.onPlaybackStopped(
userId = user.id,
itemId = item.id, itemId = item.id,
positionTicks = userData.playbackPositionTicks, positionTicks = userData.playbackPositionTicks,
) )

View file

@ -4,6 +4,7 @@ import android.content.Context
import dev.jdtech.jellyfin.Constants import dev.jdtech.jellyfin.Constants
import dev.jdtech.jellyfin.data.BuildConfig import dev.jdtech.jellyfin.data.BuildConfig
import org.jellyfin.sdk.api.client.HttpClientOptions import org.jellyfin.sdk.api.client.HttpClientOptions
import org.jellyfin.sdk.api.client.extensions.brandingApi
import org.jellyfin.sdk.api.client.extensions.devicesApi import org.jellyfin.sdk.api.client.extensions.devicesApi
import org.jellyfin.sdk.api.client.extensions.itemsApi import org.jellyfin.sdk.api.client.extensions.itemsApi
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
@ -19,6 +20,8 @@ import org.jellyfin.sdk.api.client.extensions.videosApi
import org.jellyfin.sdk.createJellyfin import org.jellyfin.sdk.createJellyfin
import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.ClientInfo
import java.util.UUID import java.util.UUID
import kotlin.time.DurationUnit
import kotlin.time.toDuration
/** /**
* Jellyfin API class using org.jellyfin.sdk:jellyfin-platform-android * Jellyfin API class using org.jellyfin.sdk:jellyfin-platform-android
@ -40,25 +43,26 @@ class JellyfinApi(
} }
val api = jellyfin.createApi( val api = jellyfin.createApi(
httpClientOptions = HttpClientOptions( httpClientOptions = HttpClientOptions(
requestTimeout = requestTimeout, requestTimeout = requestTimeout.toDuration(DurationUnit.MILLISECONDS),
connectTimeout = connectTimeout, connectTimeout = connectTimeout.toDuration(DurationUnit.MILLISECONDS),
socketTimeout = socketTimeout, socketTimeout = socketTimeout.toDuration(DurationUnit.MILLISECONDS),
), ),
) )
var userId: UUID? = null var userId: UUID? = null
val brandingApi = api.brandingApi
val devicesApi = api.devicesApi val devicesApi = api.devicesApi
val systemApi = api.systemApi
val userApi = api.userApi
val viewsApi = api.userViewsApi
val itemsApi = api.itemsApi val itemsApi = api.itemsApi
val userLibraryApi = api.userLibraryApi
val showsApi = api.tvShowsApi
val sessionApi = api.sessionApi
val videosApi = api.videosApi
val mediaInfoApi = api.mediaInfoApi val mediaInfoApi = api.mediaInfoApi
val playStateApi = api.playStateApi val playStateApi = api.playStateApi
val quickConnectApi = api.quickConnectApi val quickConnectApi = api.quickConnectApi
val sessionApi = api.sessionApi
val showsApi = api.tvShowsApi
val systemApi = api.systemApi
val userApi = api.userApi
val userLibraryApi = api.userLibraryApi
val videosApi = api.videosApi
val viewsApi = api.userViewsApi
companion object { companion object {
@Volatile @Volatile

View file

@ -25,7 +25,7 @@ data class FindroidCollection(
fun BaseItemDto.toFindroidCollection( fun BaseItemDto.toFindroidCollection(
jellyfinRepository: JellyfinRepository, jellyfinRepository: JellyfinRepository,
): FindroidCollection? { ): FindroidCollection? {
val type = CollectionType.fromString(collectionType) val type = CollectionType.fromString(collectionType?.serialName)
if (type !in CollectionType.supported) { if (type !in CollectionType.supported) {
return null return null

View file

@ -3,6 +3,7 @@ package dev.jdtech.jellyfin.models
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
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.api.VideoRangeType
data class FindroidMediaStream( data class FindroidMediaStream(
val title: String, val title: String,
@ -13,7 +14,7 @@ data class FindroidMediaStream(
val isExternal: Boolean, val isExternal: Boolean,
val path: String?, val path: String?,
val channelLayout: String?, val channelLayout: String?,
val videoRangeType: String?, val videoRangeType: VideoRangeType?,
val height: Int?, val height: Int?,
val width: Int?, val width: Int?,
val videoDoViTitle: String?, val videoDoViTitle: String?,
@ -46,7 +47,7 @@ fun FindroidMediaStreamDto.toFindroidMediaStream(): FindroidMediaStream {
isExternal = isExternal, isExternal = isExternal,
path = path, path = path,
channelLayout = channelLayout, channelLayout = channelLayout,
videoRangeType = videoRangeType, videoRangeType = VideoRangeType.fromNameOrNull(videoRangeType ?: ""),
height = height, height = height,
width = width, width = width,
videoDoViTitle = videoDoViTitle, videoDoViTitle = videoDoViTitle,

View file

@ -39,7 +39,7 @@ fun FindroidMediaStream.toFindroidMediaStreamDto(id: UUID, sourceId: String, pat
isExternal = isExternal, isExternal = isExternal,
path = path, path = path,
channelLayout = channelLayout, channelLayout = channelLayout,
videoRangeType = videoRangeType, videoRangeType = videoRangeType?.name,
height = height, height = height,
width = width, width = width,
videoDoViTitle = videoDoViTitle, videoDoViTitle = videoDoViTitle,

View file

@ -18,8 +18,8 @@ enum class Resolution(val raw: String) {
enum class DisplayProfile(val raw: String) { enum class DisplayProfile(val raw: String) {
SDR("SDR"), SDR("SDR"),
HDR("HDR"),
HDR10("HDR10"), HDR10("HDR10"),
HDR10_PLUS("HDR10+"),
DOLBY_VISION("Vision"), DOLBY_VISION("Vision"),
HLG("HLG"), HLG("HLG"),
} }

View file

@ -41,6 +41,8 @@ import org.jellyfin.sdk.model.api.DlnaProfileType
import org.jellyfin.sdk.model.api.GeneralCommandType import org.jellyfin.sdk.model.api.GeneralCommandType
import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.ItemFilter import org.jellyfin.sdk.model.api.ItemFilter
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.MediaType
import org.jellyfin.sdk.model.api.PlaybackInfoDto import org.jellyfin.sdk.model.api.PlaybackInfoDto
import org.jellyfin.sdk.model.api.PublicSystemInfo import org.jellyfin.sdk.model.api.PublicSystemInfo
import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.SortOrder
@ -66,38 +68,38 @@ class JellyfinRepositoryImpl(
} }
override suspend fun getItem(itemId: UUID): BaseItemDto = withContext(Dispatchers.IO) { override suspend fun getItem(itemId: UUID): BaseItemDto = withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem(jellyfinApi.userId!!, itemId).content jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content
} }
override suspend fun getEpisode(itemId: UUID): FindroidEpisode = override suspend fun getEpisode(itemId: UUID): FindroidEpisode =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi.getItem(
jellyfinApi.userId!!,
itemId, itemId,
jellyfinApi.userId!!,
).content.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!! ).content.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
} }
override suspend fun getMovie(itemId: UUID): FindroidMovie = override suspend fun getMovie(itemId: UUID): FindroidMovie =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi.getItem(
jellyfinApi.userId!!,
itemId, itemId,
jellyfinApi.userId!!,
).content.toFindroidMovie(this@JellyfinRepositoryImpl, database) ).content.toFindroidMovie(this@JellyfinRepositoryImpl, database)
} }
override suspend fun getShow(itemId: UUID): FindroidShow = override suspend fun getShow(itemId: UUID): FindroidShow =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi.getItem(
jellyfinApi.userId!!,
itemId, itemId,
jellyfinApi.userId!!,
).content.toFindroidShow(this@JellyfinRepositoryImpl) ).content.toFindroidShow(this@JellyfinRepositoryImpl)
} }
override suspend fun getSeason(itemId: UUID): FindroidSeason = override suspend fun getSeason(itemId: UUID): FindroidSeason =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi.getItem(
jellyfinApi.userId!!,
itemId, itemId,
jellyfinApi.userId!!,
).content.toFindroidSeason(this@JellyfinRepositoryImpl) ).content.toFindroidSeason(this@JellyfinRepositoryImpl)
} }
@ -125,7 +127,7 @@ class JellyfinRepositoryImpl(
parentId = parentId, parentId = parentId,
includeItemTypes = includeTypes, includeItemTypes = includeTypes,
recursive = recursive, recursive = recursive,
sortBy = listOf(sortBy.sortString), sortBy = listOf(ItemSortBy.fromName(sortBy.sortString)),
sortOrder = listOf(sortOrder), sortOrder = listOf(sortOrder),
startIndex = startIndex, startIndex = startIndex,
limit = limit, limit = limit,
@ -251,7 +253,8 @@ class JellyfinRepositoryImpl(
jellyfinApi.showsApi.getNextUp( jellyfinApi.showsApi.getNextUp(
jellyfinApi.userId!!, jellyfinApi.userId!!,
limit = 24, limit = 24,
seriesId = seriesId?.toString(), seriesId = seriesId,
enableResumable = false,
).content.items ).content.items
.orEmpty() .orEmpty()
.mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl) } .mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl) }
@ -301,23 +304,10 @@ class JellyfinRepositoryImpl(
DirectPlayProfile(type = DlnaProfileType.AUDIO), DirectPlayProfile(type = DlnaProfileType.AUDIO),
), ),
transcodingProfiles = emptyList(), transcodingProfiles = emptyList(),
responseProfiles = emptyList(),
subtitleProfiles = listOf( subtitleProfiles = listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL), SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL), SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
), ),
xmlRootAttributes = emptyList(),
supportedMediaTypes = "",
enableAlbumArtInDidl = false,
enableMsMediaReceiverRegistrar = false,
enableSingleAlbumArtLimit = false,
enableSingleSubtitleLimit = false,
ignoreTranscodeByteRangeRequests = false,
maxAlbumArtHeight = 1_000_000_000,
maxAlbumArtWidth = 1_000_000_000,
requiresPlainFolders = false,
requiresPlainVideoItems = false,
timelineOffsetSeconds = 0,
), ),
maxStreamingBitrate = 1_000_000_000, maxStreamingBitrate = 1_000_000_000,
), ),
@ -420,7 +410,7 @@ class JellyfinRepositoryImpl(
Timber.d("Sending capabilities") Timber.d("Sending capabilities")
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.sessionApi.postCapabilities( jellyfinApi.sessionApi.postCapabilities(
playableMediaTypes = listOf("Video"), playableMediaTypes = listOf(MediaType.VIDEO),
supportedCommands = listOf( supportedCommands = listOf(
GeneralCommandType.VOLUME_UP, GeneralCommandType.VOLUME_UP,
GeneralCommandType.VOLUME_DOWN, GeneralCommandType.VOLUME_DOWN,
@ -444,7 +434,7 @@ class JellyfinRepositoryImpl(
override suspend fun postPlaybackStart(itemId: UUID) { override suspend fun postPlaybackStart(itemId: UUID) {
Timber.d("Sending start $itemId") Timber.d("Sending start $itemId")
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.playStateApi.onPlaybackStart(jellyfinApi.userId!!, itemId) jellyfinApi.playStateApi.onPlaybackStart(itemId)
} }
} }
@ -471,7 +461,6 @@ class JellyfinRepositoryImpl(
} }
try { try {
jellyfinApi.playStateApi.onPlaybackStopped( jellyfinApi.playStateApi.onPlaybackStopped(
jellyfinApi.userId!!,
itemId, itemId,
positionTicks = positionTicks, positionTicks = positionTicks,
) )
@ -491,7 +480,6 @@ class JellyfinRepositoryImpl(
database.setPlaybackPositionTicks(itemId, jellyfinApi.userId!!, positionTicks) database.setPlaybackPositionTicks(itemId, jellyfinApi.userId!!, positionTicks)
try { try {
jellyfinApi.playStateApi.onPlaybackProgress( jellyfinApi.playStateApi.onPlaybackProgress(
jellyfinApi.userId!!,
itemId, itemId,
positionTicks = positionTicks, positionTicks = positionTicks,
isPaused = isPaused, isPaused = isPaused,

View file

@ -28,7 +28,7 @@ androidx-work = "2.9.0"
coil = "2.6.0" coil = "2.6.0"
hilt = "2.51.1" hilt = "2.51.1"
compose-destinations = "1.10.2" compose-destinations = "1.10.2"
jellyfin = "1.4.7" jellyfin = "1.5.0-beta.3"
junit = "4.13.2" junit = "4.13.2"
kotlin = "2.0.0" kotlin = "2.0.0"
kotlinx-serialization = "1.7.0" kotlinx-serialization = "1.7.0"