mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
refactor: Enhance error handling in API calls with logging
This commit is contained in:
@@ -11,6 +11,7 @@ import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.model.Episode
|
||||
import hu.bbara.purefin.model.Movie
|
||||
import hu.bbara.purefin.model.Series
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -105,7 +106,14 @@ class InMemoryMediaRepository @Inject constructor(
|
||||
|
||||
override fun observeSeriesWithContent(seriesId: UUID): Flow<Series?> {
|
||||
scope.launch {
|
||||
try {
|
||||
ensureSeriesContentLoaded(seriesId)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Exception) {
|
||||
Log.e("InMemoryMediaRepository", "Failed to load content for series $seriesId", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return seriesState.map { it[seriesId] }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import hu.bbara.purefin.data.PlaybackMethod
|
||||
import hu.bbara.purefin.data.PlaybackReportContext
|
||||
import hu.bbara.purefin.data.UserSessionRepository
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -85,26 +86,31 @@ class JellyfinApiClient @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun configureFromSession(): Boolean = withContext(Dispatchers.IO) {
|
||||
logApiFailure("configureFromSession") {
|
||||
ensureConfigured()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun authenticate(
|
||||
url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
): AuthenticationResult? = withContext(Dispatchers.IO) {
|
||||
logApiFailure("authenticate") {
|
||||
val trimmedUrl = url.trim()
|
||||
if (trimmedUrl.isBlank()) {
|
||||
return@withContext null
|
||||
return@logApiFailure null
|
||||
}
|
||||
|
||||
api.update(baseUrl = trimmedUrl)
|
||||
api.userApi.authenticateUserByName(username = username, password = password).content
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLibraries(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getLibraries") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val response = api.userViewsApi.getUserViews(
|
||||
userId = getUserId(),
|
||||
@@ -114,10 +120,12 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getLibraries", response.content.toString())
|
||||
response.content.items
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLibraryContent(libraryId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getLibraryContent($libraryId)") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val getItemsRequest = GetItemsRequest(
|
||||
userId = getUserId(),
|
||||
@@ -132,12 +140,14 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getLibraryContent", response.content.toString())
|
||||
response.content.items
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSuggestions(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getSuggestions") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val userId = getUserId() ?: return@withContext emptyList()
|
||||
val userId = getUserId() ?: return@logApiFailure emptyList()
|
||||
val response = api.suggestionsApi.getSuggestions(
|
||||
userId = userId,
|
||||
mediaType = listOf(MediaType.VIDEO),
|
||||
@@ -148,12 +158,14 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getSuggestions", response.content.toString())
|
||||
response.content.items
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContinueWatching(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getContinueWatching") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val userId = getUserId() ?: return@withContext emptyList()
|
||||
val userId = getUserId() ?: return@logApiFailure emptyList()
|
||||
val getResumeItemsRequest = GetResumeItemsRequest(
|
||||
userId = userId,
|
||||
fields = itemFields,
|
||||
@@ -165,8 +177,10 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getContinueWatching", response.content.toString())
|
||||
response.content.items
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getNextUpEpisodes(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getNextUpEpisodes") {
|
||||
if (!ensureConfigured()) {
|
||||
throw IllegalStateException("Not configured")
|
||||
}
|
||||
@@ -179,10 +193,12 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getNextUpEpisodes", result.content.toString())
|
||||
result.content.items
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLatestFromLibrary(libraryId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getLatestFromLibrary($libraryId)") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val response = api.userLibraryApi.getLatestMedia(
|
||||
userId = getUserId(),
|
||||
@@ -194,19 +210,23 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getLatestFromLibrary", response.content.toString())
|
||||
response.content
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getItemInfo(mediaId: UUID): BaseItemDto? = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getItemInfo($mediaId)") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext null
|
||||
return@logApiFailure null
|
||||
}
|
||||
val result = api.userLibraryApi.getItem(itemId = mediaId, userId = getUserId())
|
||||
Log.d("getItemInfo", result.content.toString())
|
||||
result.content
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSeasons(seriesId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getSeasons($seriesId)") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val result = api.tvShowsApi.getSeasons(
|
||||
userId = getUserId(),
|
||||
@@ -217,10 +237,12 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getSeasons", result.content.toString())
|
||||
result.content.items
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getEpisodesInSeason(seriesId: UUID, seasonId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getEpisodesInSeason(series=$seriesId, season=$seasonId)") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val result = api.tvShowsApi.getEpisodes(
|
||||
userId = getUserId(),
|
||||
@@ -232,13 +254,15 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getEpisodesInSeason", result.content.toString())
|
||||
result.content.items
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getNextEpisodes(episodeId: UUID, count: Int): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getNextEpisodes($episodeId, count=$count)") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val episodeInfo = getItemInfo(episodeId) ?: return@withContext emptyList()
|
||||
val seriesId = episodeInfo.seriesId ?: return@withContext emptyList()
|
||||
val episodeInfo = getItemInfo(episodeId) ?: return@logApiFailure emptyList()
|
||||
val seriesId = episodeInfo.seriesId ?: return@logApiFailure emptyList()
|
||||
val nextUpEpisodesResult = api.tvShowsApi.getEpisodes(
|
||||
userId = getUserId(),
|
||||
seriesId = seriesId,
|
||||
@@ -250,10 +274,12 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getNextEpisodes", nextUpEpisodes.toString())
|
||||
nextUpEpisodes
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGenres(id: UUID? = null) : List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getGenres($id)") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val result = api.genresApi.getGenres(
|
||||
userId = getUserId(),
|
||||
@@ -262,10 +288,12 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getGenres", result.toString())
|
||||
result.content.items
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMediaSources(mediaId: UUID): List<MediaSourceInfo> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getMediaSources($mediaId)") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val result = api.mediaInfoApi.getPostedPlaybackInfo(
|
||||
mediaId,
|
||||
@@ -278,10 +306,12 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getMediaSources", result.toString())
|
||||
result.content.mediaSources
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMediaSegments(mediaId: UUID) : List<MediaSegmentDto> = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getMediaSegments($mediaId)") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
return@logApiFailure emptyList()
|
||||
}
|
||||
val result = api.mediaSegmentsApi.getItemSegments(
|
||||
itemId = mediaId,
|
||||
@@ -290,13 +320,15 @@ class JellyfinApiClient @Inject constructor(
|
||||
Log.d("getMediaSegments", result.toString())
|
||||
result.content.items
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPlaybackInfo(
|
||||
mediaId: UUID,
|
||||
deviceProfile: DeviceProfile,
|
||||
): PlaybackInfoResponse? = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getPlaybackInfo($mediaId)") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext null
|
||||
return@logApiFailure null
|
||||
}
|
||||
api.mediaInfoApi.getPostedPlaybackInfo(
|
||||
mediaId,
|
||||
@@ -312,6 +344,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
),
|
||||
).content
|
||||
}
|
||||
}
|
||||
|
||||
fun getVideoStreamUrl(
|
||||
itemId: UUID,
|
||||
@@ -320,7 +353,8 @@ class JellyfinApiClient @Inject constructor(
|
||||
tag: String? = null,
|
||||
playSessionId: String? = null,
|
||||
liveStreamId: String? = null,
|
||||
): String = api.videosApi.getVideoStreamUrl(
|
||||
): String = try {
|
||||
api.videosApi.getVideoStreamUrl(
|
||||
itemId = itemId,
|
||||
container = container,
|
||||
mediaSourceId = mediaSourceId,
|
||||
@@ -329,20 +363,27 @@ class JellyfinApiClient @Inject constructor(
|
||||
playSessionId = playSessionId,
|
||||
liveStreamId = liveStreamId,
|
||||
)
|
||||
} catch (error: Exception) {
|
||||
Log.e(TAG, "getVideoStreamUrl($itemId) failed", error)
|
||||
throw error
|
||||
}
|
||||
|
||||
suspend fun getPublicSystemInfoVersion(): String? = withContext(Dispatchers.IO) {
|
||||
logApiFailure("getPublicSystemInfoVersion") {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext null
|
||||
return@logApiFailure null
|
||||
}
|
||||
SystemApi(api).getPublicSystemInfo().content.version
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reportPlaybackStart(
|
||||
itemId: UUID,
|
||||
positionTicks: Long = 0L,
|
||||
reportContext: PlaybackReportContext,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) return@withContext
|
||||
logApiFailure("reportPlaybackStart($itemId)") {
|
||||
if (!ensureConfigured()) return@logApiFailure
|
||||
api.playStateApi.reportPlaybackStart(
|
||||
PlaybackStartInfo(
|
||||
itemId = itemId,
|
||||
@@ -361,6 +402,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reportPlaybackProgress(
|
||||
itemId: UUID,
|
||||
@@ -368,7 +410,8 @@ class JellyfinApiClient @Inject constructor(
|
||||
isPaused: Boolean,
|
||||
reportContext: PlaybackReportContext,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) return@withContext
|
||||
logApiFailure("reportPlaybackProgress($itemId)") {
|
||||
if (!ensureConfigured()) return@logApiFailure
|
||||
api.playStateApi.reportPlaybackProgress(
|
||||
PlaybackProgressInfo(
|
||||
itemId = itemId,
|
||||
@@ -387,13 +430,15 @@ class JellyfinApiClient @Inject constructor(
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reportPlaybackStopped(
|
||||
itemId: UUID,
|
||||
positionTicks: Long,
|
||||
reportContext: PlaybackReportContext,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) return@withContext
|
||||
logApiFailure("reportPlaybackStopped($itemId)") {
|
||||
if (!ensureConfigured()) return@logApiFailure
|
||||
api.playStateApi.reportPlaybackStopped(
|
||||
PlaybackStopInfo(
|
||||
itemId = itemId,
|
||||
@@ -405,10 +450,26 @@ class JellyfinApiClient @Inject constructor(
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> logApiFailure(operation: String, block: suspend () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Exception) {
|
||||
Log.e(TAG, "$operation failed", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private fun PlaybackMethod.toJellyfinPlayMethod(): PlayMethod = when (this) {
|
||||
PlaybackMethod.DIRECT_PLAY -> PlayMethod.DIRECT_PLAY
|
||||
PlaybackMethod.DIRECT_STREAM -> PlayMethod.DIRECT_STREAM
|
||||
PlaybackMethod.TRANSCODE -> PlayMethod.TRANSCODE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "JellyfinApiClient"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user