mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Refactor JellyfinApiClient into thin API wrapper
This commit is contained in:
@@ -7,12 +7,13 @@ import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import hu.bbara.purefin.core.data.PlayableMediaRepository
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.data.jellyfin.client.PlaybackDecision
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import hu.bbara.purefin.data.jellyfin.client.playbackCustomCacheKey
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.data.jellyfin.playback.JellyfinPlaybackResolver
|
||||
import hu.bbara.purefin.data.jellyfin.playback.PlaybackDecision
|
||||
import hu.bbara.purefin.data.jellyfin.playback.playbackCustomCacheKey
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -26,11 +27,12 @@ import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
@Singleton
|
||||
class DefaultPlayableMediaRepository @Inject constructor(
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val jellyfinPlaybackResolver: JellyfinPlaybackResolver,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
) : PlayableMediaRepository {
|
||||
|
||||
override suspend fun getMediaItem(mediaId: UUID): Pair<MediaItem, Long?>? = withContext(Dispatchers.IO) {
|
||||
val playbackDecision = jellyfinApiClient.getPlaybackDecision(mediaId) ?: return@withContext null
|
||||
val playbackDecision = jellyfinPlaybackResolver.getPlaybackDecision(mediaId) ?: return@withContext null
|
||||
val baseItem = jellyfinApiClient.getItemInfo(mediaId)
|
||||
|
||||
val resumePositionMs = calculateResumePosition(baseItem, playbackDecision.mediaSource)
|
||||
@@ -64,7 +66,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
if (existingIds.contains(stringId)) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val playbackDecision = jellyfinApiClient.getPlaybackDecision(id) ?: return@mapNotNull null
|
||||
val playbackDecision = jellyfinPlaybackResolver.getPlaybackDecision(id) ?: return@mapNotNull null
|
||||
val artworkUrl = ImageUrlBuilder.toImageUrl(serverUrl, id, ArtworkKind.PRIMARY)
|
||||
createMediaItem(
|
||||
mediaId = stringId,
|
||||
|
||||
@@ -10,7 +10,9 @@ import hu.bbara.purefin.core.data.NetworkMonitor
|
||||
import hu.bbara.purefin.core.data.PlayableMediaRepository
|
||||
import hu.bbara.purefin.core.data.PlaybackProgressReporter
|
||||
import hu.bbara.purefin.core.data.SessionBootstrapper
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.data.jellyfin.download.JellyfinDownloadMediaSourceResolver
|
||||
import hu.bbara.purefin.data.jellyfin.session.JellyfinAuthenticationRepository
|
||||
import hu.bbara.purefin.data.jellyfin.session.JellyfinSessionBootstrapper
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -19,15 +21,15 @@ abstract class JellyfinBindingsModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindSessionBootstrapper(impl: JellyfinApiClient): SessionBootstrapper
|
||||
abstract fun bindSessionBootstrapper(impl: JellyfinSessionBootstrapper): SessionBootstrapper
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindAuthenticationRepository(impl: JellyfinApiClient): AuthenticationRepository
|
||||
abstract fun bindAuthenticationRepository(impl: JellyfinAuthenticationRepository): AuthenticationRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindDownloadMediaSourceResolver(impl: JellyfinApiClient): DownloadMediaSourceResolver
|
||||
abstract fun bindDownloadMediaSourceResolver(impl: JellyfinDownloadMediaSourceResolver): DownloadMediaSourceResolver
|
||||
|
||||
@Binds
|
||||
abstract fun bindPlayableMediaRepository(impl: DefaultPlayableMediaRepository): PlayableMediaRepository
|
||||
|
||||
@@ -3,20 +3,9 @@ package hu.bbara.purefin.data.jellyfin.client
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import hu.bbara.purefin.core.data.AuthenticationRepository
|
||||
import hu.bbara.purefin.core.data.DownloadMediaSourceResolver
|
||||
import hu.bbara.purefin.core.data.EpisodeDownloadSource
|
||||
import hu.bbara.purefin.core.data.MovieDownloadSource
|
||||
import hu.bbara.purefin.core.data.PlaybackMethod
|
||||
import hu.bbara.purefin.core.data.PlaybackProfileFamily
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import hu.bbara.purefin.core.data.SessionBootstrapper
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -34,27 +23,27 @@ import org.jellyfin.sdk.api.client.extensions.videosApi
|
||||
import org.jellyfin.sdk.api.operations.SystemApi
|
||||
import org.jellyfin.sdk.createJellyfin
|
||||
import org.jellyfin.sdk.model.ClientInfo
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
import org.jellyfin.sdk.model.api.AuthenticationResult
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.api.PlayMethod
|
||||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||
import org.jellyfin.sdk.model.api.PlaybackInfoResponse
|
||||
import org.jellyfin.sdk.model.api.PlaybackOrder
|
||||
import org.jellyfin.sdk.model.api.PlaybackProgressInfo
|
||||
import org.jellyfin.sdk.model.api.PlaybackStartInfo
|
||||
import org.jellyfin.sdk.model.api.PlaybackStopInfo
|
||||
import org.jellyfin.sdk.model.api.PlayMethod
|
||||
import org.jellyfin.sdk.model.api.RepeatMode
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -62,15 +51,22 @@ import javax.inject.Singleton
|
||||
class JellyfinApiClient @Inject constructor(
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
private val playbackProfilePolicy: PlaybackProfilePolicy,
|
||||
) : SessionBootstrapper, AuthenticationRepository, DownloadMediaSourceResolver {
|
||||
) {
|
||||
private val jellyfin = createJellyfin {
|
||||
context = applicationContext
|
||||
clientInfo = ClientInfo(name = "Purefin", version = "0.0.1")
|
||||
}
|
||||
|
||||
private val api = jellyfin.createApi()
|
||||
private val serverVersionCache = ConcurrentHashMap<String, ServerVersion>()
|
||||
|
||||
private val itemFields =
|
||||
listOf(
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.PARENT_ID,
|
||||
ItemFields.DATE_LAST_REFRESHED,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.SEASON_USER_DATA,
|
||||
)
|
||||
|
||||
private suspend fun getUserId(): UUID? = userSessionRepository.userId.first()
|
||||
|
||||
@@ -85,36 +81,23 @@ class JellyfinApiClient @Inject constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun login(url: String, username: String, password: String): Boolean = withContext(Dispatchers.IO) {
|
||||
val trimmedUrl = url.trim()
|
||||
if (trimmedUrl.isBlank()) {
|
||||
return@withContext false
|
||||
}
|
||||
api.update(baseUrl = trimmedUrl)
|
||||
try {
|
||||
val response = api.userApi.authenticateUserByName(username = username, password = password)
|
||||
val authResult = response.content
|
||||
|
||||
val token = authResult.accessToken ?: return@withContext false
|
||||
val userId = authResult.user?.id ?: return@withContext false
|
||||
userSessionRepository.setAccessToken(accessToken = token)
|
||||
userSessionRepository.setUserId(userId)
|
||||
userSessionRepository.setLoggedIn(true)
|
||||
api.update(accessToken = token)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e("JellyfinApiClient", "Login failed", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun initialize() {
|
||||
withContext(Dispatchers.IO) {
|
||||
suspend fun configureFromSession(): Boolean = withContext(Dispatchers.IO) {
|
||||
ensureConfigured()
|
||||
}
|
||||
|
||||
suspend fun authenticate(
|
||||
url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
): AuthenticationResult? = withContext(Dispatchers.IO) {
|
||||
val trimmedUrl = url.trim()
|
||||
if (trimmedUrl.isBlank()) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
suspend fun updateApiClient() = initialize()
|
||||
api.update(baseUrl = trimmedUrl)
|
||||
api.userApi.authenticateUserByName(username = username, password = password).content
|
||||
}
|
||||
|
||||
suspend fun getLibraries(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) {
|
||||
@@ -129,15 +112,6 @@ class JellyfinApiClient @Inject constructor(
|
||||
response.content.items
|
||||
}
|
||||
|
||||
private val itemFields =
|
||||
listOf(
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.PARENT_ID,
|
||||
ItemFields.DATE_LAST_REFRESHED,
|
||||
ItemFields.OVERVIEW,
|
||||
ItemFields.SEASON_USER_DATA,
|
||||
)
|
||||
|
||||
suspend fun getLibraryContent(libraryId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
@@ -160,16 +134,13 @@ class JellyfinApiClient @Inject constructor(
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
val userId = getUserId()
|
||||
if (userId == null) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
val userId = getUserId() ?: return@withContext emptyList()
|
||||
val response = api.suggestionsApi.getSuggestions(
|
||||
userId = userId,
|
||||
mediaType = listOf(MediaType.VIDEO),
|
||||
type = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||
limit = 8,
|
||||
enableTotalRecordCount = true
|
||||
enableTotalRecordCount = true,
|
||||
)
|
||||
Log.d("getSuggestions", response.content.toString())
|
||||
response.content.items
|
||||
@@ -179,10 +150,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
val userId = getUserId()
|
||||
if (userId == null) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
val userId = getUserId() ?: return@withContext emptyList()
|
||||
val getResumeItemsRequest = GetResumeItemsRequest(
|
||||
userId = userId,
|
||||
fields = itemFields,
|
||||
@@ -209,12 +177,6 @@ class JellyfinApiClient @Inject constructor(
|
||||
result.content.items
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the latest media items from a specified library including Movie, Episode, Season.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
suspend fun getLatestFromLibrary(libraryId: UUID): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
@@ -224,7 +186,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
parentId = libraryId,
|
||||
fields = itemFields,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE, BaseItemKind.SEASON),
|
||||
limit = 10
|
||||
limit = 10,
|
||||
)
|
||||
Log.d("getLatestFromLibrary", response.content.toString())
|
||||
response.content
|
||||
@@ -234,10 +196,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext null
|
||||
}
|
||||
val result = api.userLibraryApi.getItem(
|
||||
itemId = mediaId,
|
||||
userId = getUserId()
|
||||
)
|
||||
val result = api.userLibraryApi.getItem(itemId = mediaId, userId = getUserId())
|
||||
Log.d("getItemInfo", result.content.toString())
|
||||
result.content
|
||||
}
|
||||
@@ -250,7 +209,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
userId = getUserId(),
|
||||
seriesId = seriesId,
|
||||
fields = itemFields,
|
||||
enableUserData = true
|
||||
enableUserData = true,
|
||||
)
|
||||
Log.d("getSeasons", result.content.toString())
|
||||
result.content.items
|
||||
@@ -265,7 +224,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
seriesId = seriesId,
|
||||
seasonId = seasonId,
|
||||
fields = itemFields,
|
||||
enableUserData = true
|
||||
enableUserData = true,
|
||||
)
|
||||
Log.d("getEpisodesInSeason", result.content.toString())
|
||||
result.content.items
|
||||
@@ -275,7 +234,6 @@ class JellyfinApiClient @Inject constructor(
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
// TODO pass complete Episode object not only an id
|
||||
val episodeInfo = getItemInfo(episodeId) ?: return@withContext emptyList()
|
||||
val seriesId = episodeInfo.seriesId ?: return@withContext emptyList()
|
||||
val nextUpEpisodesResult = api.tvShowsApi.getEpisodes(
|
||||
@@ -283,9 +241,8 @@ class JellyfinApiClient @Inject constructor(
|
||||
seriesId = seriesId,
|
||||
enableUserData = true,
|
||||
startItemId = episodeId,
|
||||
limit = count + 1
|
||||
limit = count + 1,
|
||||
)
|
||||
//Remove first element as we need only the next episodes
|
||||
val nextUpEpisodes = nextUpEpisodesResult.content.items.drop(1)
|
||||
Log.d("getNextEpisodes", nextUpEpisodes.toString())
|
||||
nextUpEpisodes
|
||||
@@ -295,8 +252,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
val result = api.mediaInfoApi
|
||||
.getPostedPlaybackInfo(
|
||||
val result = api.mediaInfoApi.getPostedPlaybackInfo(
|
||||
mediaId,
|
||||
PlaybackInfoDto(
|
||||
userId = getUserId(),
|
||||
@@ -308,16 +264,14 @@ class JellyfinApiClient @Inject constructor(
|
||||
result.content.mediaSources
|
||||
}
|
||||
|
||||
suspend fun getPlaybackDecision(mediaId: UUID): PlaybackDecision? = withContext(Dispatchers.IO) {
|
||||
suspend fun getPlaybackInfo(
|
||||
mediaId: UUID,
|
||||
deviceProfile: DeviceProfile,
|
||||
): PlaybackInfoResponse? = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
val serverVersion = getServerVersion(serverUrl)
|
||||
val deviceProfile = playbackProfilePolicy.create(serverVersion)
|
||||
|
||||
val response = api.mediaInfoApi.getPostedPlaybackInfo(
|
||||
api.mediaInfoApi.getPostedPlaybackInfo(
|
||||
mediaId,
|
||||
PlaybackInfoDto(
|
||||
userId = getUserId(),
|
||||
@@ -329,118 +283,31 @@ class JellyfinApiClient @Inject constructor(
|
||||
allowAudioStreamCopy = true,
|
||||
autoOpenLiveStream = false,
|
||||
),
|
||||
)
|
||||
|
||||
val playbackInfo = response.content
|
||||
if (playbackInfo.errorCode != null) {
|
||||
Log.w(TAG, "Playback info failed for $mediaId with ${playbackInfo.errorCode}")
|
||||
return@withContext null
|
||||
).content
|
||||
}
|
||||
|
||||
val decision = PlaybackDecisionResolver.resolve(
|
||||
mediaSources = playbackInfo.mediaSources,
|
||||
playSessionId = playbackInfo.playSessionId,
|
||||
serverUrl = serverUrl,
|
||||
directPlayUrl = { mediaSource ->
|
||||
api.videosApi.getVideoStreamUrl(
|
||||
itemId = mediaId,
|
||||
container = mediaSource.container,
|
||||
mediaSourceId = mediaSource.id,
|
||||
fun getVideoStreamUrl(
|
||||
itemId: UUID,
|
||||
mediaSourceId: String?,
|
||||
container: String? = null,
|
||||
tag: String? = null,
|
||||
playSessionId: String? = null,
|
||||
liveStreamId: String? = null,
|
||||
): String = api.videosApi.getVideoStreamUrl(
|
||||
itemId = itemId,
|
||||
container = container,
|
||||
mediaSourceId = mediaSourceId,
|
||||
static = true,
|
||||
tag = mediaSource.eTag,
|
||||
playSessionId = playbackInfo.playSessionId,
|
||||
liveStreamId = mediaSource.liveStreamId,
|
||||
)
|
||||
},
|
||||
tag = tag,
|
||||
playSessionId = playSessionId,
|
||||
liveStreamId = liveStreamId,
|
||||
)
|
||||
|
||||
if (decision == null) {
|
||||
Log.w(TAG, "No compatible playback path for $mediaId")
|
||||
} else {
|
||||
Log.d(TAG, "Playback decision for $mediaId resolved as ${decision.reportContext.playMethod}")
|
||||
}
|
||||
decision
|
||||
}
|
||||
|
||||
suspend fun getMediaPlaybackUrl(mediaId: UUID, mediaSource: MediaSourceInfo): String? = withContext(Dispatchers.IO) {
|
||||
suspend fun getPublicSystemInfoVersion(): String? = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val shouldTranscode = mediaSource.supportsTranscoding == true &&
|
||||
(mediaSource.supportsDirectPlay == false || mediaSource.transcodingUrl != null)
|
||||
|
||||
val url = if (shouldTranscode && !mediaSource.transcodingUrl.isNullOrBlank()) {
|
||||
val baseUrl = userSessionRepository.serverUrl.first().trim().trimEnd('/')
|
||||
"$baseUrl${mediaSource.transcodingUrl}"
|
||||
} else {
|
||||
api.videosApi.getVideoStreamUrl(
|
||||
itemId = mediaId,
|
||||
static = true,
|
||||
mediaSourceId = mediaSource.id,
|
||||
)
|
||||
}
|
||||
|
||||
Log.d("getMediaPlaybackUrl", "Direct play: ${!shouldTranscode}, URL: $url")
|
||||
url
|
||||
}
|
||||
|
||||
override suspend fun resolveMovieDownload(movieId: UUID): MovieDownloadSource? = withContext(Dispatchers.IO) {
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
val source = getMediaSources(movieId).firstOrNull() ?: return@withContext null
|
||||
val playbackUrl = getMediaPlaybackUrl(movieId, source) ?: return@withContext null
|
||||
val itemInfo = getItemInfo(movieId) ?: return@withContext null
|
||||
val movie = itemInfo.toMovie(serverUrl)
|
||||
|
||||
MovieDownloadSource(
|
||||
movie = movie,
|
||||
playbackUrl = playbackUrl,
|
||||
customCacheKey = source.customCacheKey(movieId, playbackUrl),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun resolveEpisodeDownload(episodeId: UUID): EpisodeDownloadSource? = withContext(Dispatchers.IO) {
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
val source = getMediaSources(episodeId).firstOrNull() ?: return@withContext null
|
||||
val playbackUrl = getMediaPlaybackUrl(episodeId, source) ?: return@withContext null
|
||||
val episodeDto = getItemInfo(episodeId) ?: return@withContext null
|
||||
val episode = episodeDto.toEpisode(serverUrl)
|
||||
val series = getItemInfo(episode.seriesId)?.toSeries(serverUrl) ?: return@withContext null
|
||||
val season = getItemInfo(episode.seasonId)?.toSeason(series.id) ?: return@withContext null
|
||||
|
||||
EpisodeDownloadSource(
|
||||
episode = episode,
|
||||
series = series,
|
||||
season = season,
|
||||
playbackUrl = playbackUrl,
|
||||
customCacheKey = source.customCacheKey(episodeId, playbackUrl),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun isEpisodeWatched(episodeId: UUID): Boolean {
|
||||
return getItemInfo(episodeId)?.userData?.played == true
|
||||
}
|
||||
|
||||
override suspend fun getUnwatchedEpisodeIds(
|
||||
seriesId: UUID,
|
||||
excludedEpisodeIds: Set<UUID>,
|
||||
limit: Int,
|
||||
): List<UUID> = withContext(Dispatchers.IO) {
|
||||
if (limit <= 0) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
val seasons = getSeasons(seriesId)
|
||||
val episodes = buildList {
|
||||
seasons.forEach { season ->
|
||||
addAll(getEpisodesInSeason(seriesId, season.id))
|
||||
}
|
||||
}
|
||||
episodes
|
||||
.filter { episode ->
|
||||
episode.userData?.played != true && episode.id !in excludedEpisodeIds
|
||||
}
|
||||
.take(limit)
|
||||
.map { it.id }
|
||||
SystemApi(api).getPublicSystemInfo().content.version
|
||||
}
|
||||
|
||||
suspend fun reportPlaybackStart(
|
||||
@@ -464,7 +331,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
playMethod = reportContext.playMethod.toJellyfinPlayMethod(),
|
||||
repeatMode = RepeatMode.REPEAT_NONE,
|
||||
playbackOrder = PlaybackOrder.DEFAULT,
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -490,7 +357,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
playMethod = reportContext.playMethod.toJellyfinPlayMethod(),
|
||||
repeatMode = RepeatMode.REPEAT_NONE,
|
||||
playbackOrder = PlaybackOrder.DEFAULT,
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -508,109 +375,8 @@ class JellyfinApiClient @Inject constructor(
|
||||
liveStreamId = reportContext.liveStreamId,
|
||||
playSessionId = reportContext.playSessionId,
|
||||
failed = false,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getServerVersion(serverUrl: String): ServerVersion {
|
||||
serverVersionCache[serverUrl]?.let { return it }
|
||||
|
||||
val parsedVersion = runCatching {
|
||||
val versionString = SystemApi(api).getPublicSystemInfo().content.version
|
||||
versionString?.let(ServerVersion::fromString)
|
||||
}.onFailure { error ->
|
||||
Log.w(TAG, "Unable to fetch server version for $serverUrl", error)
|
||||
}.getOrNull()
|
||||
|
||||
val resolvedVersion = parsedVersion ?: PlaybackProfileDefaults.fallbackServerVersion
|
||||
serverVersionCache[serverUrl] = resolvedVersion
|
||||
return resolvedVersion
|
||||
}
|
||||
|
||||
private fun BaseItemDto.toMovie(serverUrl: String): Movie {
|
||||
return Movie(
|
||||
id = id,
|
||||
libraryId = parentId ?: UUID.randomUUID(),
|
||||
title = name ?: "Unknown title",
|
||||
progress = userData?.playedPercentage,
|
||||
watched = userData?.played ?: false,
|
||||
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
|
||||
rating = officialRating ?: "NR",
|
||||
runtime = formatRuntime(runTimeTicks),
|
||||
format = container?.uppercase() ?: "VIDEO",
|
||||
synopsis = overview ?: "No synopsis available",
|
||||
imageUrlPrefix = ImageUrlBuilder.toPrefixImageUrl(serverUrl, id),
|
||||
audioTrack = "ENG",
|
||||
subtitles = "ENG",
|
||||
cast = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
||||
return Episode(
|
||||
id = id,
|
||||
seriesId = seriesId ?: UUID.randomUUID(),
|
||||
seasonId = parentId ?: UUID.randomUUID(),
|
||||
title = name ?: "Unknown title",
|
||||
index = indexNumber ?: 0,
|
||||
synopsis = overview ?: "No synopsis available.",
|
||||
releaseDate = productionYear?.toString() ?: "—",
|
||||
rating = officialRating ?: "NR",
|
||||
runtime = formatRuntime(runTimeTicks),
|
||||
progress = userData?.playedPercentage,
|
||||
watched = userData?.played ?: false,
|
||||
format = container?.uppercase() ?: "VIDEO",
|
||||
imageUrlPrefix = ImageUrlBuilder.toPrefixImageUrl(serverUrl, id),
|
||||
cast = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BaseItemDto.toSeries(serverUrl: String): Series {
|
||||
return Series(
|
||||
id = id,
|
||||
libraryId = parentId ?: UUID.randomUUID(),
|
||||
name = name ?: "Unknown",
|
||||
synopsis = overview ?: "No synopsis available",
|
||||
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
|
||||
imageUrlPrefix = ImageUrlBuilder.toPrefixImageUrl(serverUrl, id),
|
||||
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
|
||||
seasonCount = childCount ?: 0,
|
||||
seasons = emptyList(),
|
||||
cast = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BaseItemDto.toSeason(seriesId: UUID): Season {
|
||||
return Season(
|
||||
id = id,
|
||||
seriesId = this.seriesId ?: seriesId,
|
||||
name = name ?: "Unknown",
|
||||
index = indexNumber ?: 0,
|
||||
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
|
||||
episodeCount = childCount ?: 0,
|
||||
episodes = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MediaSourceInfo.customCacheKey(mediaId: UUID, playbackUrl: String): String? {
|
||||
val shouldTranscode = supportsTranscoding == true &&
|
||||
(supportsDirectPlay == false || transcodingUrl != null)
|
||||
if (shouldTranscode) {
|
||||
return null
|
||||
}
|
||||
return playbackCustomCacheKey(
|
||||
mediaId = mediaId.toString(),
|
||||
playbackUrl = playbackUrl,
|
||||
playMethod = PlaybackMethod.DIRECT_PLAY,
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatRuntime(ticks: Long?): String {
|
||||
if (ticks == null || ticks <= 0) return "—"
|
||||
val totalSeconds = ticks / 10_000_000
|
||||
val hours = TimeUnit.SECONDS.toHours(totalSeconds)
|
||||
val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
|
||||
return if (hours > 0) "${hours}h ${minutes}m" else "${minutes}m"
|
||||
}
|
||||
|
||||
private fun PlaybackMethod.toJellyfinPlayMethod(): PlayMethod = when (this) {
|
||||
@@ -618,8 +384,4 @@ class JellyfinApiClient @Inject constructor(
|
||||
PlaybackMethod.DIRECT_STREAM -> PlayMethod.DIRECT_STREAM
|
||||
PlaybackMethod.TRANSCODE -> PlayMethod.TRANSCODE
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "JellyfinApiClient"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package hu.bbara.purefin.data.jellyfin.download
|
||||
|
||||
import hu.bbara.purefin.core.image.ImageUrlBuilder
|
||||
import hu.bbara.purefin.core.model.Episode
|
||||
import hu.bbara.purefin.core.model.Movie
|
||||
import hu.bbara.purefin.core.model.Season
|
||||
import hu.bbara.purefin.core.model.Series
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
internal fun BaseItemDto.toMovie(serverUrl: String): Movie {
|
||||
return Movie(
|
||||
id = id,
|
||||
libraryId = parentId ?: UUID.randomUUID(),
|
||||
title = name ?: "Unknown title",
|
||||
progress = userData?.playedPercentage,
|
||||
watched = userData?.played ?: false,
|
||||
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
|
||||
rating = officialRating ?: "NR",
|
||||
runtime = formatRuntime(runTimeTicks),
|
||||
format = container?.uppercase() ?: "VIDEO",
|
||||
synopsis = overview ?: "No synopsis available",
|
||||
imageUrlPrefix = ImageUrlBuilder.toPrefixImageUrl(serverUrl, id),
|
||||
audioTrack = "ENG",
|
||||
subtitles = "ENG",
|
||||
cast = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
||||
return Episode(
|
||||
id = id,
|
||||
seriesId = seriesId ?: UUID.randomUUID(),
|
||||
seasonId = parentId ?: UUID.randomUUID(),
|
||||
title = name ?: "Unknown title",
|
||||
index = indexNumber ?: 0,
|
||||
synopsis = overview ?: "No synopsis available.",
|
||||
releaseDate = productionYear?.toString() ?: "—",
|
||||
rating = officialRating ?: "NR",
|
||||
runtime = formatRuntime(runTimeTicks),
|
||||
progress = userData?.playedPercentage,
|
||||
watched = userData?.played ?: false,
|
||||
format = container?.uppercase() ?: "VIDEO",
|
||||
imageUrlPrefix = ImageUrlBuilder.toPrefixImageUrl(serverUrl, id),
|
||||
cast = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun BaseItemDto.toSeries(serverUrl: String): Series {
|
||||
return Series(
|
||||
id = id,
|
||||
libraryId = parentId ?: UUID.randomUUID(),
|
||||
name = name ?: "Unknown",
|
||||
synopsis = overview ?: "No synopsis available",
|
||||
year = productionYear?.toString() ?: premiereDate?.year?.toString().orEmpty(),
|
||||
imageUrlPrefix = ImageUrlBuilder.toPrefixImageUrl(serverUrl, id),
|
||||
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
|
||||
seasonCount = childCount ?: 0,
|
||||
seasons = emptyList(),
|
||||
cast = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun BaseItemDto.toSeason(seriesId: UUID): Season {
|
||||
return Season(
|
||||
id = id,
|
||||
seriesId = this.seriesId ?: seriesId,
|
||||
name = name ?: "Unknown",
|
||||
index = indexNumber ?: 0,
|
||||
unwatchedEpisodeCount = userData?.unplayedItemCount ?: 0,
|
||||
episodeCount = childCount ?: 0,
|
||||
episodes = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatRuntime(ticks: Long?): String {
|
||||
if (ticks == null || ticks <= 0) return "—"
|
||||
val totalSeconds = ticks / 10_000_000
|
||||
val hours = TimeUnit.SECONDS.toHours(totalSeconds)
|
||||
val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
|
||||
return if (hours > 0) "${hours}h ${minutes}m" else "${minutes}m"
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package hu.bbara.purefin.data.jellyfin.download
|
||||
|
||||
import hu.bbara.purefin.core.data.DownloadMediaSourceResolver
|
||||
import hu.bbara.purefin.core.data.EpisodeDownloadSource
|
||||
import hu.bbara.purefin.core.data.MovieDownloadSource
|
||||
import hu.bbara.purefin.core.data.PlaybackMethod
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.data.jellyfin.playback.PlaybackDecisionResolver
|
||||
import hu.bbara.purefin.data.jellyfin.playback.playbackCustomCacheKey
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
|
||||
@Singleton
|
||||
class JellyfinDownloadMediaSourceResolver @Inject constructor(
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
) : DownloadMediaSourceResolver {
|
||||
override suspend fun resolveMovieDownload(movieId: UUID): MovieDownloadSource? = withContext(Dispatchers.IO) {
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
if (serverUrl.isBlank()) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val mediaSource = jellyfinApiClient.getMediaSources(movieId).firstOrNull() ?: return@withContext null
|
||||
val playbackUrl = resolvePlaybackUrl(movieId, mediaSource, serverUrl) ?: return@withContext null
|
||||
val itemInfo = jellyfinApiClient.getItemInfo(movieId) ?: return@withContext null
|
||||
|
||||
MovieDownloadSource(
|
||||
movie = itemInfo.toMovie(serverUrl),
|
||||
playbackUrl = playbackUrl,
|
||||
customCacheKey = mediaSource.downloadCustomCacheKey(movieId, playbackUrl),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun resolveEpisodeDownload(episodeId: UUID): EpisodeDownloadSource? = withContext(Dispatchers.IO) {
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
if (serverUrl.isBlank()) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val mediaSource = jellyfinApiClient.getMediaSources(episodeId).firstOrNull() ?: return@withContext null
|
||||
val playbackUrl = resolvePlaybackUrl(episodeId, mediaSource, serverUrl) ?: return@withContext null
|
||||
val episodeDto = jellyfinApiClient.getItemInfo(episodeId) ?: return@withContext null
|
||||
val episode = episodeDto.toEpisode(serverUrl)
|
||||
val series = jellyfinApiClient.getItemInfo(episode.seriesId)?.toSeries(serverUrl) ?: return@withContext null
|
||||
val season = jellyfinApiClient.getItemInfo(episode.seasonId)?.toSeason(series.id) ?: return@withContext null
|
||||
|
||||
EpisodeDownloadSource(
|
||||
episode = episode,
|
||||
series = series,
|
||||
season = season,
|
||||
playbackUrl = playbackUrl,
|
||||
customCacheKey = mediaSource.downloadCustomCacheKey(episodeId, playbackUrl),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun isEpisodeWatched(episodeId: UUID): Boolean {
|
||||
return jellyfinApiClient.getItemInfo(episodeId)?.userData?.played == true
|
||||
}
|
||||
|
||||
override suspend fun getUnwatchedEpisodeIds(
|
||||
seriesId: UUID,
|
||||
excludedEpisodeIds: Set<UUID>,
|
||||
limit: Int,
|
||||
): List<UUID> = withContext(Dispatchers.IO) {
|
||||
if (limit <= 0) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
|
||||
val seasons = jellyfinApiClient.getSeasons(seriesId)
|
||||
val episodes = buildList {
|
||||
seasons.forEach { season ->
|
||||
addAll(jellyfinApiClient.getEpisodesInSeason(seriesId, season.id))
|
||||
}
|
||||
}
|
||||
|
||||
episodes
|
||||
.filter { episode ->
|
||||
episode.userData?.played != true && episode.id !in excludedEpisodeIds
|
||||
}
|
||||
.take(limit)
|
||||
.map { it.id }
|
||||
}
|
||||
|
||||
private fun resolvePlaybackUrl(
|
||||
mediaId: UUID,
|
||||
mediaSource: MediaSourceInfo,
|
||||
serverUrl: String,
|
||||
): String? {
|
||||
val shouldTranscode = mediaSource.supportsTranscoding == true &&
|
||||
(mediaSource.supportsDirectPlay == false || mediaSource.transcodingUrl != null)
|
||||
|
||||
return if (shouldTranscode && !mediaSource.transcodingUrl.isNullOrBlank()) {
|
||||
PlaybackDecisionResolver.absolutePlaybackUrl(serverUrl, requireNotNull(mediaSource.transcodingUrl))
|
||||
} else {
|
||||
jellyfinApiClient.getVideoStreamUrl(
|
||||
itemId = mediaId,
|
||||
mediaSourceId = mediaSource.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediaSourceInfo.downloadCustomCacheKey(
|
||||
mediaId: UUID,
|
||||
playbackUrl: String,
|
||||
): String? {
|
||||
val shouldTranscode = supportsTranscoding == true &&
|
||||
(supportsDirectPlay == false || transcodingUrl != null)
|
||||
if (shouldTranscode) {
|
||||
return null
|
||||
}
|
||||
return playbackCustomCacheKey(
|
||||
mediaId = mediaId.toString(),
|
||||
playbackUrl = playbackUrl,
|
||||
playMethod = PlaybackMethod.DIRECT_PLAY,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import android.media.MediaCodecInfo
|
||||
import android.media.MediaCodecInfo.CodecProfileLevel
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import android.media.MediaCodecInfo.CodecProfileLevel
|
||||
import android.media.MediaCodecList
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import android.media.MediaCodecList
|
||||
import android.util.Log
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import org.jellyfin.sdk.model.api.CodecProfile
|
||||
import org.jellyfin.sdk.model.api.CodecType
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import androidx.media3.common.MimeTypes
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
@@ -0,0 +1,80 @@
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import android.util.Log
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
|
||||
@Singleton
|
||||
class JellyfinPlaybackResolver @Inject constructor(
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
private val playbackProfilePolicy: PlaybackProfilePolicy,
|
||||
) {
|
||||
private val serverVersionCache = ConcurrentHashMap<String, ServerVersion>()
|
||||
|
||||
suspend fun getPlaybackDecision(mediaId: UUID): PlaybackDecision? = withContext(Dispatchers.IO) {
|
||||
val serverUrl = userSessionRepository.serverUrl.first().trim()
|
||||
if (serverUrl.isBlank()) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val serverVersion = getServerVersion(serverUrl)
|
||||
val playbackInfo = jellyfinApiClient.getPlaybackInfo(
|
||||
mediaId = mediaId,
|
||||
deviceProfile = playbackProfilePolicy.create(serverVersion),
|
||||
) ?: return@withContext null
|
||||
|
||||
if (playbackInfo.errorCode != null) {
|
||||
Log.w(TAG, "Playback info failed for $mediaId with ${playbackInfo.errorCode}")
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val decision = PlaybackDecisionResolver.resolve(
|
||||
mediaSources = playbackInfo.mediaSources,
|
||||
playSessionId = playbackInfo.playSessionId,
|
||||
serverUrl = serverUrl,
|
||||
directPlayUrl = { mediaSource ->
|
||||
jellyfinApiClient.getVideoStreamUrl(
|
||||
itemId = mediaId,
|
||||
container = mediaSource.container,
|
||||
mediaSourceId = mediaSource.id,
|
||||
tag = mediaSource.eTag,
|
||||
playSessionId = playbackInfo.playSessionId,
|
||||
liveStreamId = mediaSource.liveStreamId,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
if (decision == null) {
|
||||
Log.w(TAG, "No compatible playback path for $mediaId")
|
||||
} else {
|
||||
Log.d(TAG, "Playback decision for $mediaId resolved as ${decision.reportContext.playMethod}")
|
||||
}
|
||||
decision
|
||||
}
|
||||
|
||||
private suspend fun getServerVersion(serverUrl: String): ServerVersion {
|
||||
serverVersionCache[serverUrl]?.let { return it }
|
||||
|
||||
val resolvedVersion = runCatching {
|
||||
jellyfinApiClient.getPublicSystemInfoVersion()?.let(ServerVersion::fromString)
|
||||
}.onFailure { error ->
|
||||
Log.w(TAG, "Unable to fetch server version for $serverUrl", error)
|
||||
}.getOrNull() ?: PlaybackProfileDefaults.fallbackServerVersion
|
||||
|
||||
serverVersionCache[serverUrl] = resolvedVersion
|
||||
return resolvedVersion
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "PlaybackResolver"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackMethod
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackMethod
|
||||
import hu.bbara.purefin.core.data.PlaybackReportContext
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -1,6 +1,5 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackProfileFamily
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package hu.bbara.purefin.data.jellyfin.session
|
||||
|
||||
import android.util.Log
|
||||
import hu.bbara.purefin.core.data.AuthenticationRepository
|
||||
import hu.bbara.purefin.core.data.session.UserSessionRepository
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class JellyfinAuthenticationRepository @Inject constructor(
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
) : AuthenticationRepository {
|
||||
override suspend fun login(url: String, username: String, password: String): Boolean {
|
||||
return try {
|
||||
val authResult = jellyfinApiClient.authenticate(url = url, username = username, password = password)
|
||||
?: return false
|
||||
val token = authResult.accessToken ?: return false
|
||||
val userId = authResult.user?.id ?: return false
|
||||
|
||||
userSessionRepository.setAccessToken(accessToken = token)
|
||||
userSessionRepository.setUserId(userId)
|
||||
userSessionRepository.setLoggedIn(true)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Login failed", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "JellyfinAuthRepo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package hu.bbara.purefin.data.jellyfin.session
|
||||
|
||||
import hu.bbara.purefin.core.data.SessionBootstrapper
|
||||
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class JellyfinSessionBootstrapper @Inject constructor(
|
||||
private val jellyfinApiClient: JellyfinApiClient,
|
||||
) : SessionBootstrapper {
|
||||
override suspend fun initialize() {
|
||||
jellyfinApiClient.configureFromSession()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import androidx.media3.common.MimeTypes
|
||||
import org.jellyfin.sdk.model.ServerVersion
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackMethod
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -1,4 +1,4 @@
|
||||
package hu.bbara.purefin.data.jellyfin.client
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import hu.bbara.purefin.core.data.PlaybackMethod
|
||||
import org.jellyfin.sdk.model.api.MediaProtocol
|
||||
Reference in New Issue
Block a user