mirror of
https://github.com/bbara04/Purefin.git
synced 2026-04-01 01:30:08 +02:00
fix: move all network I/O off the main thread to prevent UI freezes
Wrap all JellyfinApiClient suspend functions with withContext(Dispatchers.IO) so callers (ViewModels on Main dispatcher) no longer block the UI thread. Replace runBlocking in JellyfinAuthInterceptor with a reactive cached token to avoid blocking OkHttp threads. Add IO dispatching to player MediaRepository for DataStore reads.
This commit is contained in:
@@ -4,7 +4,9 @@ import android.content.Context
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import hu.bbara.purefin.session.UserSessionRepository
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.Response
|
import org.jellyfin.sdk.api.client.Response
|
||||||
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
||||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||||
@@ -65,18 +67,18 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun login(url: String, username: String, password: String): Boolean {
|
suspend fun login(url: String, username: String, password: String): Boolean = withContext(Dispatchers.IO) {
|
||||||
val trimmedUrl = url.trim()
|
val trimmedUrl = url.trim()
|
||||||
if (trimmedUrl.isBlank()) {
|
if (trimmedUrl.isBlank()) {
|
||||||
return false
|
return@withContext false
|
||||||
}
|
}
|
||||||
api.update(baseUrl = trimmedUrl)
|
api.update(baseUrl = trimmedUrl)
|
||||||
return try {
|
try {
|
||||||
val response = api.userApi.authenticateUserByName(username = username, password = password)
|
val response = api.userApi.authenticateUserByName(username = username, password = password)
|
||||||
val authResult = response.content
|
val authResult = response.content
|
||||||
|
|
||||||
val token = authResult.accessToken ?: return false
|
val token = authResult.accessToken ?: return@withContext false
|
||||||
val userId = authResult.user?.id ?: return false
|
val userId = authResult.user?.id ?: return@withContext false
|
||||||
userSessionRepository.setAccessToken(accessToken = token)
|
userSessionRepository.setAccessToken(accessToken = token)
|
||||||
userSessionRepository.setUserId(userId)
|
userSessionRepository.setUserId(userId)
|
||||||
userSessionRepository.setLoggedIn(true)
|
userSessionRepository.setLoggedIn(true)
|
||||||
@@ -88,13 +90,13 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateApiClient() {
|
suspend fun updateApiClient() = withContext(Dispatchers.IO) {
|
||||||
ensureConfigured()
|
ensureConfigured()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getLibraries(): List<BaseItemDto> {
|
suspend fun getLibraries(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return emptyList()
|
return@withContext emptyList()
|
||||||
}
|
}
|
||||||
val response = api.userViewsApi.getUserViews(
|
val response = api.userViewsApi.getUserViews(
|
||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
@@ -102,8 +104,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
includeHidden = false,
|
includeHidden = false,
|
||||||
)
|
)
|
||||||
Log.d("getLibraries", response.content.toString())
|
Log.d("getLibraries", response.content.toString())
|
||||||
val libraries = response.content.items
|
response.content.items
|
||||||
return libraries
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val itemFields =
|
private val itemFields =
|
||||||
@@ -115,9 +116,9 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
ItemFields.SEASON_USER_DATA
|
ItemFields.SEASON_USER_DATA
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun getLibraryContent(libraryId: UUID): List<BaseItemDto> {
|
suspend fun getLibraryContent(libraryId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return emptyList()
|
return@withContext emptyList()
|
||||||
}
|
}
|
||||||
val getItemsRequest = GetItemsRequest(
|
val getItemsRequest = GetItemsRequest(
|
||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
@@ -130,16 +131,16 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
)
|
)
|
||||||
val response = api.itemsApi.getItems(getItemsRequest)
|
val response = api.itemsApi.getItems(getItemsRequest)
|
||||||
Log.d("getLibraryContent", response.content.toString())
|
Log.d("getLibraryContent", response.content.toString())
|
||||||
return response.content.items
|
response.content.items
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getContinueWatching(): List<BaseItemDto> {
|
suspend fun getContinueWatching(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return emptyList()
|
return@withContext emptyList()
|
||||||
}
|
}
|
||||||
val userId = getUserId()
|
val userId = getUserId()
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
return emptyList()
|
return@withContext emptyList()
|
||||||
}
|
}
|
||||||
val getResumeItemsRequest = GetResumeItemsRequest(
|
val getResumeItemsRequest = GetResumeItemsRequest(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
@@ -150,10 +151,10 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
)
|
)
|
||||||
val response: Response<BaseItemDtoQueryResult> = api.itemsApi.getResumeItems(getResumeItemsRequest)
|
val response: Response<BaseItemDtoQueryResult> = api.itemsApi.getResumeItems(getResumeItemsRequest)
|
||||||
Log.d("getContinueWatching", response.content.toString())
|
Log.d("getContinueWatching", response.content.toString())
|
||||||
return response.content.items
|
response.content.items
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getNextUpEpisodes(mediaId: UUID): List<BaseItemDto> {
|
suspend fun getNextUpEpisodes(mediaId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
throw IllegalStateException("Not configured")
|
throw IllegalStateException("Not configured")
|
||||||
}
|
}
|
||||||
@@ -165,7 +166,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
)
|
)
|
||||||
val result = api.tvShowsApi.getNextUp(getNextUpRequest)
|
val result = api.tvShowsApi.getNextUp(getNextUpRequest)
|
||||||
Log.d("getNextUpEpisodes", result.content.toString())
|
Log.d("getNextUpEpisodes", result.content.toString())
|
||||||
return result.content.items
|
result.content.items
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,9 +175,9 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
* @param libraryId The UUID of the library to fetch from
|
* @param libraryId The UUID of the library to fetch from
|
||||||
* @return A list of [BaseItemDto] representing the latest media items that includes Movie, Episode, Season, or an empty list if not configured
|
* @return A list of [BaseItemDto] representing the latest media items that includes Movie, Episode, Season, or an empty list if not configured
|
||||||
*/
|
*/
|
||||||
suspend fun getLatestFromLibrary(libraryId: UUID): List<BaseItemDto> {
|
suspend fun getLatestFromLibrary(libraryId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return emptyList()
|
return@withContext emptyList()
|
||||||
}
|
}
|
||||||
val response = api.userLibraryApi.getLatestMedia(
|
val response = api.userLibraryApi.getLatestMedia(
|
||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
@@ -186,24 +187,24 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
limit = 10
|
limit = 10
|
||||||
)
|
)
|
||||||
Log.d("getLatestFromLibrary", response.content.toString())
|
Log.d("getLatestFromLibrary", response.content.toString())
|
||||||
return response.content
|
response.content
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getItemInfo(mediaId: UUID): BaseItemDto? {
|
suspend fun getItemInfo(mediaId: UUID): BaseItemDto? = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return null
|
return@withContext null
|
||||||
}
|
}
|
||||||
val result = api.userLibraryApi.getItem(
|
val result = api.userLibraryApi.getItem(
|
||||||
itemId = mediaId,
|
itemId = mediaId,
|
||||||
userId = getUserId()
|
userId = getUserId()
|
||||||
)
|
)
|
||||||
Log.d("getItemInfo", result.content.toString())
|
Log.d("getItemInfo", result.content.toString())
|
||||||
return result.content
|
result.content
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSeasons(seriesId: UUID): List<BaseItemDto> {
|
suspend fun getSeasons(seriesId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return emptyList()
|
return@withContext emptyList()
|
||||||
}
|
}
|
||||||
val result = api.tvShowsApi.getSeasons(
|
val result = api.tvShowsApi.getSeasons(
|
||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
@@ -212,12 +213,12 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
enableUserData = true
|
enableUserData = true
|
||||||
)
|
)
|
||||||
Log.d("getSeasons", result.content.toString())
|
Log.d("getSeasons", result.content.toString())
|
||||||
return result.content.items
|
result.content.items
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getEpisodesInSeason(seriesId: UUID, seasonId: UUID): List<BaseItemDto> {
|
suspend fun getEpisodesInSeason(seriesId: UUID, seasonId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return emptyList()
|
return@withContext emptyList()
|
||||||
}
|
}
|
||||||
val result = api.tvShowsApi.getEpisodes(
|
val result = api.tvShowsApi.getEpisodes(
|
||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
@@ -227,16 +228,16 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
enableUserData = true
|
enableUserData = true
|
||||||
)
|
)
|
||||||
Log.d("getEpisodesInSeason", result.content.toString())
|
Log.d("getEpisodesInSeason", result.content.toString())
|
||||||
return result.content.items
|
result.content.items
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getNextEpisodes(episodeId: UUID, count: Int = 10): List<BaseItemDto> {
|
suspend fun getNextEpisodes(episodeId: UUID, count: Int = 10): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return emptyList()
|
return@withContext emptyList()
|
||||||
}
|
}
|
||||||
// TODO pass complete Episode object not only an id
|
// TODO pass complete Episode object not only an id
|
||||||
val episodeInfo = getItemInfo(episodeId) ?: return emptyList()
|
val episodeInfo = getItemInfo(episodeId) ?: return@withContext emptyList()
|
||||||
val seriesId = episodeInfo.seriesId ?: return emptyList()
|
val seriesId = episodeInfo.seriesId ?: return@withContext emptyList()
|
||||||
val nextUpEpisodesResult = api.tvShowsApi.getEpisodes(
|
val nextUpEpisodesResult = api.tvShowsApi.getEpisodes(
|
||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
@@ -247,10 +248,10 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
//Remove first element as we need only the next episodes
|
//Remove first element as we need only the next episodes
|
||||||
val nextUpEpisodes = nextUpEpisodesResult.content.items.drop(1)
|
val nextUpEpisodes = nextUpEpisodesResult.content.items.drop(1)
|
||||||
Log.d("getNextEpisodes", nextUpEpisodes.toString())
|
Log.d("getNextEpisodes", nextUpEpisodes.toString())
|
||||||
return nextUpEpisodes
|
nextUpEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMediaSources(mediaId: UUID): List<MediaSourceInfo> {
|
suspend fun getMediaSources(mediaId: UUID): List<MediaSourceInfo> = withContext(Dispatchers.IO) {
|
||||||
val result = api.mediaInfoApi
|
val result = api.mediaInfoApi
|
||||||
.getPostedPlaybackInfo(
|
.getPostedPlaybackInfo(
|
||||||
mediaId,
|
mediaId,
|
||||||
@@ -276,12 +277,12 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
Log.d("getMediaSources", result.toString())
|
Log.d("getMediaSources", result.toString())
|
||||||
return result.content.mediaSources
|
result.content.mediaSources
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMediaPlaybackUrl(mediaId: UUID, mediaSourceId: String? = null): String? {
|
suspend fun getMediaPlaybackUrl(mediaId: UUID, mediaSourceId: String? = null): String? = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) {
|
if (!ensureConfigured()) {
|
||||||
return null
|
return@withContext null
|
||||||
}
|
}
|
||||||
val response = api.videosApi.getVideoStreamUrl(
|
val response = api.videosApi.getVideoStreamUrl(
|
||||||
itemId = mediaId,
|
itemId = mediaId,
|
||||||
@@ -289,11 +290,11 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
mediaSourceId = mediaSourceId,
|
mediaSourceId = mediaSourceId,
|
||||||
)
|
)
|
||||||
Log.d("getMediaPlaybackUrl", response)
|
Log.d("getMediaPlaybackUrl", response)
|
||||||
return response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reportPlaybackStart(itemId: UUID, positionTicks: Long = 0L) {
|
suspend fun reportPlaybackStart(itemId: UUID, positionTicks: Long = 0L) = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) return
|
if (!ensureConfigured()) return@withContext
|
||||||
api.playStateApi.reportPlaybackStart(
|
api.playStateApi.reportPlaybackStart(
|
||||||
PlaybackStartInfo(
|
PlaybackStartInfo(
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
@@ -308,8 +309,8 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reportPlaybackProgress(itemId: UUID, positionTicks: Long, isPaused: Boolean) {
|
suspend fun reportPlaybackProgress(itemId: UUID, positionTicks: Long, isPaused: Boolean) = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) return
|
if (!ensureConfigured()) return@withContext
|
||||||
api.playStateApi.reportPlaybackProgress(
|
api.playStateApi.reportPlaybackProgress(
|
||||||
PlaybackProgressInfo(
|
PlaybackProgressInfo(
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
@@ -324,8 +325,8 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reportPlaybackStopped(itemId: UUID, positionTicks: Long) {
|
suspend fun reportPlaybackStopped(itemId: UUID, positionTicks: Long) = withContext(Dispatchers.IO) {
|
||||||
if (!ensureConfigured()) return
|
if (!ensureConfigured()) return@withContext
|
||||||
api.playStateApi.reportPlaybackStopped(
|
api.playStateApi.reportPlaybackStopped(
|
||||||
PlaybackStopInfo(
|
PlaybackStopInfo(
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
|
|||||||
@@ -1,21 +1,34 @@
|
|||||||
package hu.bbara.purefin.client
|
package hu.bbara.purefin.client
|
||||||
|
|
||||||
import hu.bbara.purefin.session.UserSessionRepository
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class JellyfinAuthInterceptor @Inject constructor (
|
@Singleton
|
||||||
private val userSessionRepository: UserSessionRepository
|
class JellyfinAuthInterceptor @Inject constructor(
|
||||||
|
userSessionRepository: UserSessionRepository
|
||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var cachedToken: String = ""
|
||||||
|
|
||||||
|
init {
|
||||||
|
userSessionRepository.accessToken
|
||||||
|
.onEach { cachedToken = it }
|
||||||
|
.launchIn(CoroutineScope(SupervisorJob() + Dispatchers.IO))
|
||||||
|
}
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val token = runBlocking { userSessionRepository.accessToken.first() }
|
val token = cachedToken
|
||||||
val request = chain.request().newBuilder()
|
val request = chain.request().newBuilder()
|
||||||
.addHeader("X-Emby-Token", token)
|
.addHeader("X-Emby-Token", token)
|
||||||
// Some Jellyfin versions prefer the Authorization header:
|
|
||||||
// .addHeader("Authorization", "MediaBrowser Client=\"YourAppName\", Device=\"YourDevice\", DeviceId=\"123\", Version=\"1.0.0\", Token=\"$token\"")
|
|
||||||
.build()
|
.build()
|
||||||
return chain.proceed(request)
|
return chain.proceed(request)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import javax.inject.Inject
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import hu.bbara.purefin.image.JellyfinImageHelper
|
import hu.bbara.purefin.image.JellyfinImageHelper
|
||||||
import hu.bbara.purefin.session.UserSessionRepository
|
import hu.bbara.purefin.session.UserSessionRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
|
||||||
@ViewModelScoped
|
@ViewModelScoped
|
||||||
@@ -20,13 +22,13 @@ class MediaRepository @Inject constructor(
|
|||||||
private val userSessionRepository: UserSessionRepository
|
private val userSessionRepository: UserSessionRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? {
|
suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? = withContext(Dispatchers.IO) {
|
||||||
val mediaSources = jellyfinApiClient.getMediaSources(mediaId)
|
val mediaSources = jellyfinApiClient.getMediaSources(mediaId)
|
||||||
val selectedMediaSource = mediaSources.firstOrNull() ?: return null
|
val selectedMediaSource = mediaSources.firstOrNull() ?: return@withContext null
|
||||||
val playbackUrl = jellyfinApiClient.getMediaPlaybackUrl(
|
val playbackUrl = jellyfinApiClient.getMediaPlaybackUrl(
|
||||||
mediaId = mediaId,
|
mediaId = mediaId,
|
||||||
mediaSourceId = selectedMediaSource.id
|
mediaSourceId = selectedMediaSource.id
|
||||||
) ?: return null
|
) ?: return@withContext null
|
||||||
val baseItem = jellyfinApiClient.getItemInfo(mediaId)
|
val baseItem = jellyfinApiClient.getItemInfo(mediaId)
|
||||||
|
|
||||||
// Calculate resume position
|
// Calculate resume position
|
||||||
@@ -43,7 +45,7 @@ class MediaRepository @Inject constructor(
|
|||||||
artworkUrl = artworkUrl
|
artworkUrl = artworkUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
return Pair(mediaItem, resumePositionMs)
|
Pair(mediaItem, resumePositionMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateResumePosition(
|
private fun calculateResumePosition(
|
||||||
@@ -70,10 +72,10 @@ class MediaRepository @Inject constructor(
|
|||||||
return if (percentage in 5.0..95.0) positionMs else null
|
return if (percentage in 5.0..95.0) positionMs else null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getNextUpMediaItems(episodeId: UUID, existingIds: Set<String>, count: Int = 5): List<MediaItem> {
|
suspend fun getNextUpMediaItems(episodeId: UUID, existingIds: Set<String>, count: Int = 5): List<MediaItem> = withContext(Dispatchers.IO) {
|
||||||
val serverUrl = userSessionRepository.serverUrl.first()
|
val serverUrl = userSessionRepository.serverUrl.first()
|
||||||
val episodes = jellyfinApiClient.getNextEpisodes(episodeId = episodeId, count = count)
|
val episodes = jellyfinApiClient.getNextEpisodes(episodeId = episodeId, count = count)
|
||||||
return episodes.mapNotNull { episode ->
|
episodes.mapNotNull { episode ->
|
||||||
val id = episode.id ?: return@mapNotNull null
|
val id = episode.id ?: return@mapNotNull null
|
||||||
val stringId = id.toString()
|
val stringId = id.toString()
|
||||||
if (existingIds.contains(stringId)) {
|
if (existingIds.contains(stringId)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user