mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Merge branch 'main' into fea/music
This commit is contained in:
commit
cd06c0380e
122 changed files with 8844 additions and 1446 deletions
|
|
@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -47,7 +48,12 @@ class BackdropService
|
|||
|
||||
suspend fun submit(item: BaseItem) =
|
||||
withContext(Dispatchers.IO) {
|
||||
val imageUrl = imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)!!
|
||||
val imageUrl =
|
||||
if (item.type == BaseItemKind.GENRE) {
|
||||
item.imageUrlOverride
|
||||
} else {
|
||||
imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)!!
|
||||
}
|
||||
submit(item.id.toString(), imageUrl)
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -30,6 +30,9 @@ class ImageUrlService
|
|||
useSeriesForPrimary: Boolean,
|
||||
imageTags: Map<ImageType, String?>,
|
||||
imageType: ImageType,
|
||||
parentThumbId: UUID? = null,
|
||||
parentBackdropId: UUID? = null,
|
||||
backdropTags: List<String> = emptyList(),
|
||||
fillWidth: Int? = null,
|
||||
fillHeight: Int? = null,
|
||||
): String? =
|
||||
|
|
@ -54,8 +57,65 @@ class ImageUrlService
|
|||
}
|
||||
}
|
||||
|
||||
ImageType.THUMB -> {
|
||||
if (useSeriesForPrimary && parentThumbId != null &&
|
||||
(itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON)
|
||||
) {
|
||||
// Use parent's thumb
|
||||
getItemImageUrl(
|
||||
itemId = parentThumbId,
|
||||
imageType = imageType,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
} else if (useSeriesForPrimary && parentBackdropId != null &&
|
||||
(itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON)
|
||||
) {
|
||||
// No parent thumb, so use backdrop instead
|
||||
getItemImageUrl(
|
||||
itemId = parentBackdropId,
|
||||
imageType = ImageType.BACKDROP,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
} else if (parentThumbId != null && itemType == BaseItemKind.SEASON && imageType !in imageTags) {
|
||||
getItemImageUrl(
|
||||
itemId = parentThumbId,
|
||||
imageType = imageType,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
} else if (useSeriesForPrimary &&
|
||||
parentThumbId == null &&
|
||||
itemType == BaseItemKind.EPISODE &&
|
||||
imageType !in imageTags
|
||||
) {
|
||||
// Workaround to fall back to episode image if no parent thumb
|
||||
getItemImageUrl(
|
||||
itemId = itemId,
|
||||
imageType = ImageType.PRIMARY,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
} else if (imageType !in imageTags && backdropTags.isNotEmpty()) {
|
||||
// If no thumb, use backdrop if available
|
||||
getItemImageUrl(
|
||||
itemId = itemId,
|
||||
imageType = ImageType.BACKDROP,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
} else {
|
||||
getItemImageUrl(
|
||||
itemId = itemId,
|
||||
imageType = imageType,
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ImageType.PRIMARY,
|
||||
ImageType.THUMB,
|
||||
ImageType.BANNER,
|
||||
-> {
|
||||
if (useSeriesForPrimary && seriesId != null &&
|
||||
|
|
@ -99,15 +159,19 @@ class ImageUrlService
|
|||
imageType: ImageType,
|
||||
fillWidth: Int? = null,
|
||||
fillHeight: Int? = null,
|
||||
useSeriesForPrimary: Boolean? = null,
|
||||
): String? =
|
||||
if (item != null) {
|
||||
getItemImageUrl(
|
||||
itemId = item.id,
|
||||
itemType = item.type,
|
||||
seriesId = item.data.seriesId,
|
||||
useSeriesForPrimary = item.useSeriesForPrimary,
|
||||
useSeriesForPrimary = useSeriesForPrimary ?: item.useSeriesForPrimary,
|
||||
imageTags = item.data.imageTags.orEmpty(),
|
||||
imageType = imageType,
|
||||
parentThumbId = item.data.parentThumbItemId,
|
||||
parentBackdropId = item.data.parentBackdropItemId,
|
||||
backdropTags = item.data.backdropImageTags.orEmpty(),
|
||||
fillWidth = fillWidth,
|
||||
fillHeight = fillHeight,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import android.content.Context
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.main.LatestData
|
||||
import com.github.damontecres.wholphin.ui.main.supportedLatestCollectionTypes
|
||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||
import com.github.damontecres.wholphin.util.supportItemKinds
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
|
@ -21,6 +19,8 @@ import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
|||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.UserDto
|
||||
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||
|
|
@ -44,6 +44,7 @@ class LatestNextUpService
|
|||
userId: UUID,
|
||||
limit: Int,
|
||||
includeEpisodes: Boolean,
|
||||
useSeriesForPrimary: Boolean = true,
|
||||
): List<BaseItem> {
|
||||
val request =
|
||||
GetResumeItemsRequest(
|
||||
|
|
@ -60,13 +61,19 @@ class LatestNextUpService
|
|||
remove(BaseItemKind.EPISODE)
|
||||
}
|
||||
},
|
||||
enableImageTypes =
|
||||
listOf(
|
||||
ImageType.PRIMARY,
|
||||
ImageType.THUMB,
|
||||
ImageType.BACKDROP,
|
||||
),
|
||||
)
|
||||
val items =
|
||||
api.itemsApi
|
||||
.getResumeItems(request)
|
||||
.content
|
||||
.items
|
||||
.map { BaseItem.from(it, api, true) }
|
||||
.map { BaseItem.from(it, api, useSeriesForPrimary) }
|
||||
return items
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +83,7 @@ class LatestNextUpService
|
|||
enableRewatching: Boolean,
|
||||
enableResumable: Boolean,
|
||||
maxDays: Int,
|
||||
useSeriesForPrimary: Boolean = true,
|
||||
): List<BaseItem> {
|
||||
val nextUpDateCutoff =
|
||||
maxDays.takeIf { it > 0 }?.let { LocalDateTime.now().minusDays(it.toLong()) }
|
||||
|
|
@ -96,7 +104,7 @@ class LatestNextUpService
|
|||
.getNextUp(request)
|
||||
.content
|
||||
.items
|
||||
.map { BaseItem.from(it, api, true) }
|
||||
.map { BaseItem.from(it, api, useSeriesForPrimary) }
|
||||
return nextUp
|
||||
}
|
||||
|
||||
|
|
@ -192,3 +200,17 @@ class LatestNextUpService
|
|||
return@withContext result
|
||||
}
|
||||
}
|
||||
|
||||
val supportedLatestCollectionTypes =
|
||||
setOf(
|
||||
CollectionType.MOVIES,
|
||||
CollectionType.TVSHOWS,
|
||||
CollectionType.HOMEVIDEOS,
|
||||
// Exclude Live TV because a recording folder view will be used instead
|
||||
null, // Recordings & mixed collection types
|
||||
)
|
||||
|
||||
data class LatestData(
|
||||
val title: String,
|
||||
val request: GetLatestMediaRequest,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
package com.github.damontecres.wholphin.services
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.asFlow
|
||||
import com.github.damontecres.wholphin.data.ServerPreferencesDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.NavPinType
|
||||
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
|
||||
import com.github.damontecres.wholphin.ui.main.settings.Library
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
||||
import com.github.damontecres.wholphin.util.supportedCollectionTypes
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.UserDto
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NavDrawerService
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
@param:DefaultCoroutineScope private val coroutineScope: CoroutineScope,
|
||||
private val api: ApiClient,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val serverPreferencesDao: ServerPreferencesDao,
|
||||
private val seerrServerRepository: SeerrServerRepository,
|
||||
) {
|
||||
private val _state = MutableStateFlow(NavDrawerItemState.EMPTY)
|
||||
val state: StateFlow<NavDrawerItemState> = _state
|
||||
|
||||
init {
|
||||
serverRepository.currentUser
|
||||
.asFlow()
|
||||
.combine(serverRepository.currentUserDto.asFlow()) { user, userDto ->
|
||||
Pair(user, userDto)
|
||||
}.onEach { (user, userDto) ->
|
||||
Timber.d("User updated: user=%s, userDto=%s", user?.id, userDto?.id)
|
||||
_state.update {
|
||||
it.copy(
|
||||
items = emptyList(),
|
||||
moreItems = emptyList(),
|
||||
)
|
||||
}
|
||||
if (user != null && userDto != null && user.id == userDto.id) {
|
||||
updateNavDrawer(user, userDto)
|
||||
}
|
||||
}.launchIn(coroutineScope)
|
||||
seerrServerRepository.active
|
||||
.onEach { discoverActive ->
|
||||
_state.update { it.copy(discoverEnabled = discoverActive) }
|
||||
}.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
suspend fun getAllUserLibraries(
|
||||
userId: UUID,
|
||||
tvAccess: Boolean,
|
||||
): List<Library> {
|
||||
val userViews =
|
||||
api.userViewsApi
|
||||
.getUserViews(userId = userId)
|
||||
.content.items
|
||||
val recordingFolders =
|
||||
if (tvAccess) {
|
||||
api.liveTvApi
|
||||
.getRecordingFolders(userId = userId)
|
||||
.content.items
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
} else {
|
||||
setOf()
|
||||
}
|
||||
val libraries =
|
||||
userViews
|
||||
.filter { it.collectionType in supportedCollectionTypes || it.id in recordingFolders }
|
||||
.map {
|
||||
Library(
|
||||
itemId = it.id,
|
||||
name = it.name ?: "",
|
||||
type = it.type,
|
||||
collectionType = it.collectionType ?: CollectionType.UNKNOWN,
|
||||
isRecordingFolder = it.id in recordingFolders,
|
||||
)
|
||||
}
|
||||
return libraries
|
||||
}
|
||||
|
||||
suspend fun getFilteredUserLibraries(
|
||||
user: JellyfinUser,
|
||||
tvAccess: Boolean,
|
||||
): List<Library> {
|
||||
val pins =
|
||||
serverPreferencesDao
|
||||
.getNavDrawerPinnedItems(user)
|
||||
.associateBy { it.itemId }
|
||||
val libraries =
|
||||
getAllUserLibraries(user.id, tvAccess)
|
||||
.filterNot { pins[ServerNavDrawerItem.getId(it.itemId)]?.type == NavPinType.UNPINNED }
|
||||
return libraries
|
||||
}
|
||||
|
||||
suspend fun updateNavDrawer(
|
||||
user: JellyfinUser,
|
||||
userDto: UserDto,
|
||||
) {
|
||||
val builtins = listOf(NavDrawerItem.Favorites, NavDrawerItem.Discover)
|
||||
val allLibraries = getAllUserLibraries(user.id, userDto.tvAccess)
|
||||
val libraries =
|
||||
allLibraries
|
||||
.map {
|
||||
val destination =
|
||||
if (it.isRecordingFolder) {
|
||||
Destination.Recordings(it.itemId)
|
||||
} else {
|
||||
Destination.MediaItem(
|
||||
it.itemId,
|
||||
it.type,
|
||||
it.collectionType,
|
||||
)
|
||||
}
|
||||
ServerNavDrawerItem(
|
||||
itemId = it.itemId,
|
||||
name = it.name,
|
||||
destination = destination,
|
||||
type = it.collectionType,
|
||||
)
|
||||
}
|
||||
val allItems = builtins + libraries
|
||||
|
||||
val navDrawerPins =
|
||||
serverPreferencesDao.getNavDrawerPinnedItems(user).associateBy { it.itemId }
|
||||
|
||||
val items = mutableListOf<NavDrawerItem>()
|
||||
val moreItems = mutableListOf<NavDrawerItem>()
|
||||
allItems
|
||||
// Sort by order if non-default, existing items before customize will have -1 value
|
||||
// New items from the server will get Int.MAX_VALUE
|
||||
// Items the user doesn't have access to anymore will be skipped
|
||||
.sortedBy { navDrawerPins[it.id]?.order?.takeIf { it >= 0 } ?: Int.MAX_VALUE }
|
||||
.forEach {
|
||||
// Assume pinned if unknown
|
||||
val pinned = navDrawerPins[it.id]?.type ?: NavPinType.PINNED
|
||||
if (pinned == NavPinType.PINNED) {
|
||||
items.add(it)
|
||||
} else {
|
||||
moreItems.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
items = items,
|
||||
moreItems = moreItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NavDrawerItemState(
|
||||
val items: List<NavDrawerItem>,
|
||||
val moreItems: List<NavDrawerItem>,
|
||||
val discoverEnabled: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY = NavDrawerItemState(emptyList(), emptyList(), false)
|
||||
}
|
||||
}
|
||||
|
||||
val UserDto.tvAccess: Boolean get() = policy?.enableLiveTvAccess == true
|
||||
|
|
@ -28,21 +28,6 @@ class RefreshRateService
|
|||
constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
private val display get() = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
|
||||
|
||||
val supportedDisplayModes get() = display.supportedModes.orEmpty()
|
||||
|
||||
private val displayModes: List<DisplayMode> by lazy {
|
||||
display.supportedModes
|
||||
.orEmpty()
|
||||
.map { DisplayMode(it) }
|
||||
.sortedWith(
|
||||
compareByDescending<DisplayMode>({ it.physicalWidth * it.physicalHeight })
|
||||
.thenBy { it.refreshRateRounded },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the best display mode for the given stream and signal to change to it
|
||||
*/
|
||||
|
|
@ -55,6 +40,18 @@ class RefreshRateService
|
|||
Timber.v("Not switching either refresh rate nor resolution")
|
||||
return@withContext
|
||||
}
|
||||
val displayManager =
|
||||
MainActivity.instance.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
|
||||
val displayModes =
|
||||
display.supportedModes
|
||||
.orEmpty()
|
||||
.map { DisplayMode(it) }
|
||||
.sortedWith(
|
||||
compareByDescending<DisplayMode>({ it.physicalWidth * it.physicalHeight })
|
||||
.thenBy { it.refreshRateRounded },
|
||||
)
|
||||
|
||||
val currentDisplayMode = display.mode
|
||||
require(stream.type == MediaStreamType.VIDEO) { "Stream is not video" }
|
||||
val width = stream.width
|
||||
|
|
|
|||
|
|
@ -44,18 +44,33 @@ class SeerrServerRepository
|
|||
private val serverRepository: ServerRepository,
|
||||
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
|
||||
) {
|
||||
private val _current = MutableStateFlow<CurrentSeerr?>(null)
|
||||
val current: StateFlow<CurrentSeerr?> = _current
|
||||
val currentServer: Flow<SeerrServer?> = current.map { it?.server }
|
||||
val currentUser: Flow<SeerrUser?> = current.map { it?.user }
|
||||
private val _connection =
|
||||
MutableStateFlow<SeerrConnectionStatus>(SeerrConnectionStatus.NotConfigured)
|
||||
val connection: StateFlow<SeerrConnectionStatus> = _connection
|
||||
|
||||
val current: Flow<CurrentSeerr?> =
|
||||
_connection.map { (it as? SeerrConnectionStatus.Success)?.current }
|
||||
val currentServer: Flow<SeerrServer?> =
|
||||
connection.map { (it as? SeerrConnectionStatus.Success)?.current?.server }
|
||||
val currentUser: Flow<SeerrUser?> =
|
||||
connection.map { (it as? SeerrConnectionStatus.Success)?.current?.user }
|
||||
|
||||
/**
|
||||
* Whether Seerr integration is currently active of not
|
||||
*/
|
||||
val active: Flow<Boolean> = current.map { it != null && seerrApi.active }
|
||||
val active: Flow<Boolean> =
|
||||
connection.map { it is SeerrConnectionStatus.Success && seerrApi.active }
|
||||
|
||||
fun clear() {
|
||||
_current.update { null }
|
||||
_connection.update { SeerrConnectionStatus.NotConfigured }
|
||||
seerrApi.update("", null)
|
||||
}
|
||||
|
||||
fun error(
|
||||
serverUrl: String,
|
||||
exception: Exception,
|
||||
) {
|
||||
_connection.update { SeerrConnectionStatus.Error(serverUrl, exception) }
|
||||
seerrApi.update("", null)
|
||||
}
|
||||
|
||||
|
|
@ -65,8 +80,10 @@ class SeerrServerRepository
|
|||
userConfig: SeerrUserConfig,
|
||||
) {
|
||||
val publicSettings = seerrApi.api.settingsApi.settingsPublicGet()
|
||||
_current.update {
|
||||
CurrentSeerr(server, user, userConfig, publicSettings)
|
||||
_connection.update {
|
||||
SeerrConnectionStatus.Success(
|
||||
CurrentSeerr(server, user, userConfig, publicSettings),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +171,7 @@ class SeerrServerRepository
|
|||
}
|
||||
|
||||
suspend fun removeServer() {
|
||||
val current = _current.value ?: return
|
||||
val current = (_connection.value as? SeerrConnectionStatus.Success)?.current ?: return
|
||||
seerrServerDao.deleteUser(current.server.id, current.user.jellyfinUserRowId)
|
||||
clear()
|
||||
}
|
||||
|
|
@ -165,6 +182,19 @@ class SeerrServerRepository
|
|||
*/
|
||||
typealias SeerrUserConfig = User
|
||||
|
||||
sealed interface SeerrConnectionStatus {
|
||||
data object NotConfigured : SeerrConnectionStatus
|
||||
|
||||
data class Error(
|
||||
val serverUrl: String,
|
||||
val ex: Exception,
|
||||
) : SeerrConnectionStatus
|
||||
|
||||
data class Success(
|
||||
val current: CurrentSeerr,
|
||||
) : SeerrConnectionStatus
|
||||
}
|
||||
|
||||
data class CurrentSeerr(
|
||||
val server: SeerrServer,
|
||||
val user: SeerrUser,
|
||||
|
|
@ -226,6 +256,7 @@ class UserSwitchListener
|
|||
private val seerrServerRepository: SeerrServerRepository,
|
||||
private val seerrServerDao: SeerrServerDao,
|
||||
private val seerrApi: SeerrApi,
|
||||
private val homeSettingsService: HomeSettingsService,
|
||||
) {
|
||||
init {
|
||||
context as AppCompatActivity
|
||||
|
|
@ -233,41 +264,47 @@ class UserSwitchListener
|
|||
serverRepository.currentUser.asFlow().collect { user ->
|
||||
Timber.d("New user")
|
||||
seerrServerRepository.clear()
|
||||
homeSettingsService.currentSettings.update { HomePageResolvedSettings.EMPTY }
|
||||
if (user != null) {
|
||||
seerrServerDao
|
||||
.getUsersByJellyfinUser(user.rowId)
|
||||
.firstOrNull()
|
||||
?.let { seerrUser ->
|
||||
val server = seerrServerDao.getServer(seerrUser.serverId)?.server
|
||||
if (server != null) {
|
||||
Timber.i("Found a seerr user & server")
|
||||
seerrApi.update(server.url, seerrUser.credential)
|
||||
val userConfig =
|
||||
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
|
||||
try {
|
||||
login(
|
||||
seerrApi.api,
|
||||
seerrUser.authMethod,
|
||||
seerrUser.username,
|
||||
seerrUser.password,
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Error logging into %s", server.url)
|
||||
seerrServerRepository.clear()
|
||||
return@let
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
seerrApi.api.usersApi.authMeGet()
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Error logging into %s", server.url)
|
||||
seerrServerRepository.clear()
|
||||
return@let
|
||||
}
|
||||
// Check for home settings
|
||||
launchIO {
|
||||
homeSettingsService.loadCurrentSettings(user.id)
|
||||
}
|
||||
// Check for seerr server
|
||||
launchIO {
|
||||
seerrServerDao
|
||||
.getUsersByJellyfinUser(user.rowId)
|
||||
.lastOrNull()
|
||||
?.let { seerrUser ->
|
||||
val server =
|
||||
seerrServerDao.getServer(seerrUser.serverId)?.server
|
||||
if (server != null) {
|
||||
Timber.i("Found a seerr user & server")
|
||||
try {
|
||||
seerrApi.update(server.url, seerrUser.credential)
|
||||
val userConfig =
|
||||
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
|
||||
login(
|
||||
seerrApi.api,
|
||||
seerrUser.authMethod,
|
||||
seerrUser.username,
|
||||
seerrUser.password,
|
||||
)
|
||||
} else {
|
||||
seerrApi.api.usersApi.authMeGet()
|
||||
}
|
||||
seerrServerRepository.set(server, seerrUser, userConfig)
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(
|
||||
ex,
|
||||
"Error logging into %s",
|
||||
server.url,
|
||||
)
|
||||
seerrServerRepository.error(server.url, ex)
|
||||
}
|
||||
seerrServerRepository.set(server, seerrUser, userConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.work.WorkManager
|
|||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
|
|
@ -49,21 +50,8 @@ class SuggestionService
|
|||
.asFlow()
|
||||
.flatMapLatest { user ->
|
||||
val userId = user?.id ?: return@flatMapLatest flowOf(SuggestionsResource.Empty)
|
||||
val cachedIds = cache.get(userId, parentId, itemKind)?.ids.orEmpty()
|
||||
if (cachedIds.isNotEmpty()) {
|
||||
flow {
|
||||
try {
|
||||
emit(
|
||||
SuggestionsResource.Success(
|
||||
fetchItemsByIds(cachedIds, itemKind),
|
||||
),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to fetch items")
|
||||
emit(SuggestionsResource.Empty)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val cachedSuggestions = cache.get(userId, parentId, itemKind)
|
||||
if (cachedSuggestions == null) {
|
||||
workManager
|
||||
.getWorkInfosForUniqueWorkFlow(SuggestionsWorker.WORK_NAME)
|
||||
.map { workInfos ->
|
||||
|
|
@ -73,6 +61,23 @@ class SuggestionService
|
|||
}
|
||||
if (isActive) SuggestionsResource.Loading else SuggestionsResource.Empty
|
||||
}
|
||||
} else if (cachedSuggestions.ids.isEmpty()) {
|
||||
flowOf(SuggestionsResource.Empty)
|
||||
} else {
|
||||
flow {
|
||||
try {
|
||||
emit(
|
||||
SuggestionsResource.Success(
|
||||
fetchItemsByIds(cachedSuggestions.ids, itemKind),
|
||||
),
|
||||
)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to fetch items")
|
||||
emit(SuggestionsResource.Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,15 +60,20 @@ class SuggestionsCache
|
|||
): CachedSuggestions? {
|
||||
val key = cacheKey(userId, libraryId, itemKind)
|
||||
return memoryCache.getOrPut(key) {
|
||||
try {
|
||||
mutex.withLock {
|
||||
File(cacheDir, "$key.json").inputStream().use {
|
||||
mutex.withLock {
|
||||
try {
|
||||
val cacheFile = File(cacheDir, "$key.json")
|
||||
if (!cacheFile.exists()) {
|
||||
return@withLock null
|
||||
}
|
||||
|
||||
cacheFile.inputStream().use {
|
||||
json.decodeFromStream<CachedSuggestions>(it)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception reading from disk cache")
|
||||
null
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception reading from disk cache")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,11 +85,8 @@ class SuggestionsWorker
|
|||
views
|
||||
.mapNotNull { view ->
|
||||
val itemKind =
|
||||
when (view.collectionType) {
|
||||
CollectionType.MOVIES -> BaseItemKind.MOVIE
|
||||
CollectionType.TVSHOWS -> BaseItemKind.SERIES
|
||||
else -> return@mapNotNull null
|
||||
}
|
||||
getTypeForCollection(view.collectionType)
|
||||
?: return@mapNotNull null
|
||||
async(Dispatchers.IO) {
|
||||
runCatching {
|
||||
Timber.v("Fetching suggestions for view %s", view.id)
|
||||
|
|
@ -267,5 +264,12 @@ class SuggestionsWorker
|
|||
const val WORK_NAME = "com.github.damontecres.wholphin.services.SuggestionsWorker"
|
||||
const val PARAM_USER_ID = "userId"
|
||||
const val PARAM_SERVER_ID = "serverId"
|
||||
|
||||
fun getTypeForCollection(collectionType: CollectionType?): BaseItemKind? =
|
||||
when (collectionType) {
|
||||
CollectionType.MOVIES -> BaseItemKind.MOVIE
|
||||
CollectionType.TVSHOWS -> BaseItemKind.SERIES
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ annotation class StandardOkHttpClient
|
|||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class IoCoroutineScope
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class DefaultCoroutineScope
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppModule {
|
||||
|
|
@ -177,6 +181,11 @@ object AppModule {
|
|||
@IoCoroutineScope
|
||||
fun ioCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@DefaultCoroutineScope
|
||||
fun defaultCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun workManager(
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ class TvProviderWorker
|
|||
getPotentialItems(
|
||||
userId,
|
||||
prefs.homePagePreferences.enableRewatchingNextUp,
|
||||
prefs.homePagePreferences.combineContinueNext,
|
||||
prefs.homePagePreferences.maxDaysNextUp,
|
||||
)
|
||||
val potentialItemsToAddIds = potentialItemsToAdd.map { it.id.toString() }
|
||||
|
|
@ -145,7 +144,6 @@ class TvProviderWorker
|
|||
private suspend fun getPotentialItems(
|
||||
userId: UUID,
|
||||
enableRewatching: Boolean,
|
||||
combineContinueNext: Boolean,
|
||||
maxDaysNextUp: Int,
|
||||
): List<BaseItem> {
|
||||
val resumeItems = latestNextUpService.getResume(userId, 10, true)
|
||||
|
|
@ -154,11 +152,7 @@ class TvProviderWorker
|
|||
latestNextUpService
|
||||
.getNextUp(userId, 10, enableRewatching, false, maxDaysNextUp)
|
||||
.filter { it.data.seriesId != null && it.data.seriesId !in seriesIds }
|
||||
return if (combineContinueNext) {
|
||||
latestNextUpService.buildCombined(resumeItems, nextUpItems)
|
||||
} else {
|
||||
resumeItems + nextUpItems
|
||||
}
|
||||
return latestNextUpService.buildCombined(resumeItems, nextUpItems)
|
||||
}
|
||||
|
||||
private suspend fun getCurrentTvChannelNextUp(): List<WatchNextProgram> =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue