lint: klint standard

This commit is contained in:
nomadics9 2024-07-19 05:01:09 +03:00
parent 062781a43d
commit 633ee6b8c4
3 changed files with 636 additions and 438 deletions

View file

@ -30,7 +30,6 @@ import org.jellyfin.sdk.model.api.EncodingContext
import org.jellyfin.sdk.model.api.MediaStreamType import org.jellyfin.sdk.model.api.MediaStreamType
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import kotlin.Exception
import kotlin.math.ceil import kotlin.math.ceil
import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.core.R as CoreR
@ -48,13 +47,15 @@ class DownloaderImpl(
storageIndex: Int, storageIndex: Int,
): Pair<Long, UiText?> { ): Pair<Long, UiText?> {
try { try {
val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId } val source =
jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
val intro = jellyfinRepository.getIntroTimestamps(item.id) val intro = jellyfinRepository.getIntroTimestamps(item.id)
val trickplayInfo = if (item is FindroidSources) { val trickplayInfo =
item.trickplayInfo?.get(sourceId) if (item is FindroidSources) {
} else { item.trickplayInfo?.get(sourceId)
null } else {
} null
}
val storageLocation = context.getExternalFilesDirs(null)[storageIndex] val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
if (storageLocation == null || Environment.getExternalStorageState(storageLocation) != Environment.MEDIA_MOUNTED) { if (storageLocation == null || Environment.getExternalStorageState(storageLocation) != Environment.MEDIA_MOUNTED) {
return Pair(-1, UiText.StringResource(CoreR.string.storage_unavailable)) return Pair(-1, UiText.StringResource(CoreR.string.storage_unavailable))
@ -85,24 +86,29 @@ class DownloaderImpl(
database.insertIntro(intro.toIntroDto(item.id)) database.insertIntro(intro.toIntroDto(item.id))
} }
if (appPreferences.downloadQuality != "Original") { if (appPreferences.downloadQuality != "Original") {
downloadEmbeddedMediaStreams(item, source,storageIndex) downloadEmbeddedMediaStreams(item, source, storageIndex)
val transcodingUrl =getTranscodedUrl(item.id,appPreferences.downloadQuality!!) val transcodingUrl =
val request = DownloadManager.Request(transcodingUrl) getTranscodedUrl(item.id, appPreferences.downloadQuality!!)
.setTitle(item.name) val request =
.setAllowedOverMetered(appPreferences.downloadOverMobileData) DownloadManager
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .Request(transcodingUrl)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) .setTitle(item.name)
.setDestinationUri(path) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request) val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId) database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null) return Pair(downloadId, null)
}else { } else {
val request = DownloadManager.Request(source.path.toUri()) val request =
.setTitle(item.name) DownloadManager
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .Request(source.path.toUri())
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .setTitle(item.name)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setDestinationUri(path) .setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request) val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId) database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null) return Pair(downloadId, null)
@ -111,7 +117,8 @@ class DownloaderImpl(
is FindroidEpisode -> { is FindroidEpisode -> {
database.insertShow( database.insertShow(
jellyfinRepository.getShow(item.seriesId) jellyfinRepository
.getShow(item.seriesId)
.toFindroidShowDto(appPreferences.currentServer!!), .toFindroidShowDto(appPreferences.currentServer!!),
) )
database.insertSeason( database.insertSeason(
@ -128,24 +135,29 @@ class DownloaderImpl(
database.insertIntro(intro.toIntroDto(item.id)) database.insertIntro(intro.toIntroDto(item.id))
} }
if (appPreferences.downloadQuality != "Original") { if (appPreferences.downloadQuality != "Original") {
downloadEmbeddedMediaStreams(item, source,storageIndex) downloadEmbeddedMediaStreams(item, source, storageIndex)
val transcodingUrl = getTranscodedUrl(item.id, appPreferences.downloadQuality!!) val transcodingUrl =
val request = DownloadManager.Request(transcodingUrl) getTranscodedUrl(item.id, appPreferences.downloadQuality!!)
.setTitle(item.name) val request =
.setAllowedOverMetered(appPreferences.downloadOverMobileData) DownloadManager
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .Request(transcodingUrl)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) .setTitle(item.name)
.setDestinationUri(path) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request) val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId) database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null) return Pair(downloadId, null)
}else { } else {
val request = DownloadManager.Request(source.path.toUri()) val request =
.setTitle(item.name) DownloadManager
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .Request(source.path.toUri())
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .setTitle(item.name)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setDestinationUri(path) .setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationUri(path)
val downloadId = downloadManager.enqueue(request) val downloadId = downloadManager.enqueue(request)
database.setSourceDownloadId(source.id, downloadId) database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null) return Pair(downloadId, null)
@ -157,24 +169,41 @@ class DownloaderImpl(
try { try {
val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId } val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId }
deleteItem(item, source) deleteItem(item, source)
} catch (_: Exception) {} } catch (_: Exception) {
}
return Pair(-1, if (e.message != null) UiText.DynamicString(e.message!!) else UiText.StringResource(CoreR.string.unknown_error)) return Pair(
-1,
if (e.message != null) {
UiText.DynamicString(e.message!!)
} else {
UiText.StringResource(
CoreR.string.unknown_error,
)
},
)
} }
} }
override suspend fun cancelDownload(item: FindroidItem, source: FindroidSource) { override suspend fun cancelDownload(
item: FindroidItem,
source: FindroidSource,
) {
if (source.downloadId != null) { if (source.downloadId != null) {
downloadManager.remove(source.downloadId!!) downloadManager.remove(source.downloadId!!)
} }
deleteItem(item, source) deleteItem(item, source)
} }
override suspend fun deleteItem(item: FindroidItem, source: FindroidSource) { override suspend fun deleteItem(
item: FindroidItem,
source: FindroidSource,
) {
when (item) { when (item) {
is FindroidMovie -> { is FindroidMovie -> {
database.deleteMovie(item.id) database.deleteMovie(item.id)
} }
is FindroidEpisode -> { is FindroidEpisode -> {
database.deleteEpisode(item.id) database.deleteEpisode(item.id)
val remainingEpisodes = database.getEpisodesBySeasonId(item.seasonId) val remainingEpisodes = database.getEpisodesBySeasonId(item.seasonId)
@ -212,23 +241,29 @@ class DownloaderImpl(
if (downloadId == null) { if (downloadId == null) {
return Pair(downloadStatus, progress) return Pair(downloadStatus, progress)
} }
val query = DownloadManager.Query() val query =
.setFilterById(downloadId) DownloadManager
.Query()
.setFilterById(downloadId)
val cursor = downloadManager.query(query) val cursor = downloadManager.query(query)
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
downloadStatus = cursor.getInt( downloadStatus =
cursor.getColumnIndexOrThrow( cursor.getInt(
DownloadManager.COLUMN_STATUS, cursor.getColumnIndexOrThrow(
), DownloadManager.COLUMN_STATUS,
) ),
)
when (downloadStatus) { when (downloadStatus) {
DownloadManager.STATUS_RUNNING -> { DownloadManager.STATUS_RUNNING -> {
val totalBytes = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) val totalBytes =
cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
if (totalBytes > 0) { if (totalBytes > 0) {
val downloadedBytes = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) val downloadedBytes =
cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
progress = downloadedBytes.times(100).div(totalBytes).toInt() progress = downloadedBytes.times(100).div(totalBytes).toInt()
} }
} }
DownloadManager.STATUS_SUCCESSFUL -> { DownloadManager.STATUS_SUCCESSFUL -> {
progress = 100 progress = 100
} }
@ -247,14 +282,28 @@ class DownloaderImpl(
val storageLocation = context.getExternalFilesDirs(null)[storageIndex] val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
for (mediaStream in source.mediaStreams.filter { it.isExternal }) { for (mediaStream in source.mediaStreams.filter { it.isExternal }) {
val id = UUID.randomUUID() val id = UUID.randomUUID()
val streamPath = Uri.fromFile(File(storageLocation, "downloads/${item.id}.${source.id}.$id.download")) val streamPath =
database.insertMediaStream(mediaStream.toFindroidMediaStreamDto(id, source.id, streamPath.path.orEmpty())) Uri.fromFile(
val request = DownloadManager.Request(Uri.parse(mediaStream.path)) File(
.setTitle(mediaStream.title) storageLocation,
.setAllowedOverMetered(appPreferences.downloadOverMobileData) "downloads/${item.id}.${source.id}.$id.download",
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) ),
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) )
.setDestinationUri(streamPath) database.insertMediaStream(
mediaStream.toFindroidMediaStreamDto(
id,
source.id,
streamPath.path.orEmpty(),
),
)
val request =
DownloadManager
.Request(Uri.parse(mediaStream.path))
.setTitle(mediaStream.title)
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
.setDestinationUri(streamPath)
val downloadId = downloadManager.enqueue(request) val downloadId = downloadManager.enqueue(request)
database.setMediaStreamDownloadId(id, downloadId) database.setMediaStreamDownloadId(id, downloadId)
} }
@ -263,57 +312,66 @@ class DownloaderImpl(
private fun downloadEmbeddedMediaStreams( private fun downloadEmbeddedMediaStreams(
item: FindroidItem, item: FindroidItem,
source: FindroidSource, source: FindroidSource,
storageIndex: Int = 0 storageIndex: Int = 0,
) { ) {
val storageLocation = context.getExternalFilesDirs(null)[storageIndex] val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
val subtitleStreams = source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null } val subtitleStreams =
source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null }
for (mediaStream in subtitleStreams) { for (mediaStream in subtitleStreams) {
var deliveryUrl = mediaStream.path!! var deliveryUrl = mediaStream.path!!
if (mediaStream.codec == "webvtt") { if (mediaStream.codec == "webvtt") {
deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt") deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt")
} }
val id = UUID.randomUUID() val id = UUID.randomUUID()
val streamPath = Uri.fromFile( val streamPath =
File( Uri.fromFile(
storageLocation, File(
"downloads/${item.id}.${source.id}.$id.download" storageLocation,
"downloads/${item.id}.${source.id}.$id.download",
),
) )
)
database.insertMediaStream( database.insertMediaStream(
mediaStream.toFindroidMediaStreamDto( mediaStream.toFindroidMediaStreamDto(
id, id,
source.id, source.id,
streamPath.path.orEmpty() streamPath.path.orEmpty(),
) ),
) )
val request = DownloadManager.Request(Uri.parse(deliveryUrl)) val request =
.setTitle(mediaStream.title) DownloadManager
.setAllowedOverMetered(appPreferences.downloadOverMobileData) .Request(Uri.parse(deliveryUrl))
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming) .setTitle(mediaStream.title)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) .setAllowedOverMetered(appPreferences.downloadOverMobileData)
.setDestinationUri(streamPath) .setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
.setDestinationUri(streamPath)
val downloadId = downloadManager.enqueue(request) val downloadId = downloadManager.enqueue(request)
database.setMediaStreamDownloadId(id, downloadId) database.setMediaStreamDownloadId(id, downloadId)
} }
} }
private suspend fun downloadTrickplayData( private suspend fun downloadTrickplayData(
itemId: UUID, itemId: UUID,
sourceId: String, sourceId: String,
trickplayInfo: FindroidTrickplayInfo, trickplayInfo: FindroidTrickplayInfo,
) { ) {
val maxIndex = ceil(trickplayInfo.thumbnailCount.toDouble().div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)).toInt() val maxIndex =
ceil(
trickplayInfo.thumbnailCount
.toDouble()
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight),
).toInt()
val byteArrays = mutableListOf<ByteArray>() val byteArrays = mutableListOf<ByteArray>()
for (i in 0..maxIndex) { for (i in 0..maxIndex) {
jellyfinRepository.getTrickplayData( jellyfinRepository
itemId, .getTrickplayData(
trickplayInfo.width, itemId,
i, trickplayInfo.width,
)?.let { byteArray -> i,
byteArrays.add(byteArray) )?.let { byteArray ->
} byteArrays.add(byteArray)
}
} }
saveTrickplayData(itemId, sourceId, trickplayInfo, byteArrays) saveTrickplayData(itemId, sourceId, trickplayInfo, byteArrays)
} }
@ -333,22 +391,38 @@ class DownloaderImpl(
} }
} }
private suspend fun getTranscodedUrl(itemId: UUID, quality: String): Uri? { private suspend fun getTranscodedUrl(
val maxBitrate = when (quality) { itemId: UUID,
"720p" -> 2000000 // 2 Mbps quality: String,
"480p" -> 1000000 // 1 Mbps ): Uri? {
"360p" -> 800000 // 800Kbps val maxBitrate =
else -> 2000000 when (quality) {
} "720p" -> 2000000 // 2 Mbps
"480p" -> 1000000 // 1 Mbps
"360p" -> 800000 // 800Kbps
else -> 2000000
}
return try { return try {
val deviceProfile =
val deviceProfile = jellyfinRepository.buildDeviceProfile(maxBitrate,"mkv", EncodingContext.STATIC) jellyfinRepository.buildDeviceProfile(maxBitrate, "mkv", EncodingContext.STATIC)
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(itemId,false,deviceProfile,maxBitrate) val playbackInfo =
val mediaSourceId = playbackInfo.content.mediaSources.firstOrNull()?.id!! jellyfinRepository.getPostedPlaybackInfo(itemId, false, deviceProfile, maxBitrate)
val mediaSourceId =
playbackInfo.content.mediaSources
.firstOrNull()
?.id!!
val playSessionId = playbackInfo.content.playSessionId!! val playSessionId = playbackInfo.content.playSessionId!!
val deviceId = jellyfinRepository.getDeviceId() val deviceId = jellyfinRepository.getDeviceId()
val downloadUrl = jellyfinRepository.getVideoStreambyContainerUrl(itemId, deviceId, mediaSourceId, playSessionId, maxBitrate, "ts") val downloadUrl =
jellyfinRepository.getVideoStreambyContainerUrl(
itemId,
deviceId,
mediaSourceId,
playSessionId,
maxBitrate,
"ts",
)
val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality) val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality)
transcodeUri transcodeUri
@ -361,15 +435,18 @@ class DownloaderImpl(
private fun buildTranscodeUri( private fun buildTranscodeUri(
transcodingUrl: String, transcodingUrl: String,
maxBitrate: Int, maxBitrate: Int,
quality: String quality: String,
): Uri { ): Uri {
val resolution = when (quality) { val resolution =
"720p" -> "720" when (quality) {
"480p" -> "480" "720p" -> "720"
"360p" -> "360" "480p" -> "480"
else -> "720" "360p" -> "360"
} else -> "720"
return Uri.parse(transcodingUrl).buildUpon() }
return Uri
.parse(transcodingUrl)
.buildUpon()
.appendQueryParameter("MaxVideoHeight", resolution) .appendQueryParameter("MaxVideoHeight", resolution)
.appendQueryParameter("MaxVideoBitRate", maxBitrate.toString()) .appendQueryParameter("MaxVideoBitRate", maxBitrate.toString())
.appendQueryParameter("subtitleMethod", "External") .appendQueryParameter("subtitleMethod", "External")

View file

@ -68,55 +68,70 @@ class JellyfinRepositoryImpl(
private val database: ServerDatabaseDao, private val database: ServerDatabaseDao,
private val appPreferences: AppPreferences, private val appPreferences: AppPreferences,
) : JellyfinRepository { ) : JellyfinRepository {
override suspend fun getPublicSystemInfo(): PublicSystemInfo = withContext(Dispatchers.IO) { override suspend fun getPublicSystemInfo(): PublicSystemInfo =
jellyfinApi.systemApi.getPublicSystemInfo().content withContext(Dispatchers.IO) {
} jellyfinApi.systemApi.getPublicSystemInfo().content
}
override suspend fun getUserViews(): List<BaseItemDto> = withContext(Dispatchers.IO) { override suspend fun getUserViews(): List<BaseItemDto> =
jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items.orEmpty() withContext(Dispatchers.IO) {
} jellyfinApi.viewsApi
.getUserViews(jellyfinApi.userId!!)
.content.items
.orEmpty()
}
override suspend fun getItem(itemId: UUID): BaseItemDto = withContext(Dispatchers.IO) { override suspend fun getItem(itemId: UUID): BaseItemDto =
jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content withContext(Dispatchers.IO) {
} jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content
}
override suspend fun getEpisode(itemId: UUID): FindroidEpisode = override suspend fun getEpisode(itemId: UUID): FindroidEpisode =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi
itemId, .getItem(
jellyfinApi.userId!!, itemId,
).content.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!! jellyfinApi.userId!!,
).content
.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
} }
override suspend fun getMovie(itemId: UUID): FindroidMovie = override suspend fun getMovie(itemId: UUID): FindroidMovie =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi
itemId, .getItem(
jellyfinApi.userId!!, itemId,
).content.toFindroidMovie(this@JellyfinRepositoryImpl, database) jellyfinApi.userId!!,
).content
.toFindroidMovie(this@JellyfinRepositoryImpl, database)
} }
override suspend fun getShow(itemId: UUID): FindroidShow = override suspend fun getShow(itemId: UUID): FindroidShow =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi
itemId, .getItem(
jellyfinApi.userId!!, itemId,
).content.toFindroidShow(this@JellyfinRepositoryImpl) jellyfinApi.userId!!,
).content
.toFindroidShow(this@JellyfinRepositoryImpl)
} }
override suspend fun getSeason(itemId: UUID): FindroidSeason = override suspend fun getSeason(itemId: UUID): FindroidSeason =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.userLibraryApi.getItem( jellyfinApi.userLibraryApi
itemId, .getItem(
jellyfinApi.userId!!, itemId,
).content.toFindroidSeason(this@JellyfinRepositoryImpl) jellyfinApi.userId!!,
).content
.toFindroidSeason(this@JellyfinRepositoryImpl)
} }
override suspend fun getLibraries(): List<FindroidCollection> = override suspend fun getLibraries(): List<FindroidCollection> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems( jellyfinApi.itemsApi
jellyfinApi.userId!!, .getItems(
).content.items jellyfinApi.userId!!,
).content.items
.orEmpty() .orEmpty()
.mapNotNull { it.toFindroidCollection(this@JellyfinRepositoryImpl) } .mapNotNull { it.toFindroidCollection(this@JellyfinRepositoryImpl) }
} }
@ -131,16 +146,17 @@ class JellyfinRepositoryImpl(
limit: Int?, limit: Int?,
): List<FindroidItem> = ): List<FindroidItem> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems( jellyfinApi.itemsApi
jellyfinApi.userId!!, .getItems(
parentId = parentId, jellyfinApi.userId!!,
includeItemTypes = includeTypes, parentId = parentId,
recursive = recursive, includeItemTypes = includeTypes,
sortBy = listOf(ItemSortBy.fromName(sortBy.sortString)), recursive = recursive,
sortOrder = listOf(sortOrder), sortBy = listOf(ItemSortBy.fromName(sortBy.sortString)),
startIndex = startIndex, sortOrder = listOf(sortOrder),
limit = limit, startIndex = startIndex,
).content.items limit = limit,
).content.items
.orEmpty() .orEmpty()
.mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) } .mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) }
} }
@ -151,13 +167,14 @@ class JellyfinRepositoryImpl(
recursive: Boolean, recursive: Boolean,
sortBy: SortBy, sortBy: SortBy,
sortOrder: SortOrder, sortOrder: SortOrder,
): Flow<PagingData<FindroidItem>> { ): Flow<PagingData<FindroidItem>> =
return Pager( Pager(
config = PagingConfig( config =
pageSize = 10, PagingConfig(
maxSize = 100, pageSize = 10,
enablePlaceholders = false, maxSize = 100,
), enablePlaceholders = false,
),
pagingSourceFactory = { pagingSourceFactory = {
ItemsPagingSource( ItemsPagingSource(
this, this,
@ -169,87 +186,102 @@ class JellyfinRepositoryImpl(
) )
}, },
).flow ).flow
}
override suspend fun getPersonItems( override suspend fun getPersonItems(
personIds: List<UUID>, personIds: List<UUID>,
includeTypes: List<BaseItemKind>?, includeTypes: List<BaseItemKind>?,
recursive: Boolean, recursive: Boolean,
): List<FindroidItem> = withContext(Dispatchers.IO) { ): List<FindroidItem> =
jellyfinApi.itemsApi.getItems( withContext(Dispatchers.IO) {
jellyfinApi.userId!!, jellyfinApi.itemsApi
personIds = personIds, .getItems(
includeItemTypes = includeTypes, jellyfinApi.userId!!,
recursive = recursive, personIds = personIds,
).content.items includeItemTypes = includeTypes,
.orEmpty() recursive = recursive,
.mapNotNull { ).content.items
it.toFindroidItem(this@JellyfinRepositoryImpl, database) .orEmpty()
} .mapNotNull {
} it.toFindroidItem(this@JellyfinRepositoryImpl, database)
}
}
override suspend fun getFavoriteItems(): List<FindroidItem> = override suspend fun getFavoriteItems(): List<FindroidItem> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems( jellyfinApi.itemsApi
jellyfinApi.userId!!, .getItems(
filters = listOf(ItemFilter.IS_FAVORITE), jellyfinApi.userId!!,
includeItemTypes = listOf( filters = listOf(ItemFilter.IS_FAVORITE),
BaseItemKind.MOVIE, includeItemTypes =
BaseItemKind.SERIES, listOf(
BaseItemKind.EPISODE, BaseItemKind.MOVIE,
), BaseItemKind.SERIES,
recursive = true, BaseItemKind.EPISODE,
).content.items ),
recursive = true,
).content.items
.orEmpty() .orEmpty()
.mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) } .mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) }
} }
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> = override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.itemsApi.getItems( jellyfinApi.itemsApi
jellyfinApi.userId!!, .getItems(
searchTerm = searchQuery, jellyfinApi.userId!!,
includeItemTypes = listOf( searchTerm = searchQuery,
BaseItemKind.MOVIE, includeItemTypes =
BaseItemKind.SERIES, listOf(
BaseItemKind.EPISODE, BaseItemKind.MOVIE,
), BaseItemKind.SERIES,
recursive = true, BaseItemKind.EPISODE,
).content.items ),
recursive = true,
).content.items
.orEmpty() .orEmpty()
.mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) } .mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) }
} }
override suspend fun getResumeItems(): List<FindroidItem> { override suspend fun getResumeItems(): List<FindroidItem> {
val items = withContext(Dispatchers.IO) { val items =
jellyfinApi.itemsApi.getResumeItems( withContext(Dispatchers.IO) {
jellyfinApi.userId!!, jellyfinApi.itemsApi
limit = 12, .getResumeItems(
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE), jellyfinApi.userId!!,
).content.items.orEmpty() limit = 12,
} includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
).content.items
.orEmpty()
}
return items.mapNotNull { return items.mapNotNull {
it.toFindroidItem(this, database) it.toFindroidItem(this, database)
} }
} }
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> { override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> {
val items = withContext(Dispatchers.IO) { val items =
jellyfinApi.userLibraryApi.getLatestMedia( withContext(Dispatchers.IO) {
jellyfinApi.userId!!, jellyfinApi.userLibraryApi
parentId = parentId, .getLatestMedia(
limit = 16, jellyfinApi.userId!!,
).content parentId = parentId,
} limit = 16,
).content
}
return items.mapNotNull { return items.mapNotNull {
it.toFindroidItem(this, database) it.toFindroidItem(this, database)
} }
} }
override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List<FindroidSeason> = override suspend fun getSeasons(
seriesId: UUID,
offline: Boolean,
): List<FindroidSeason> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!offline) { if (!offline) {
jellyfinApi.showsApi.getSeasons(seriesId, jellyfinApi.userId!!).content.items jellyfinApi.showsApi
.getSeasons(seriesId, jellyfinApi.userId!!)
.content.items
.orEmpty() .orEmpty()
.map { it.toFindroidSeason(this@JellyfinRepositoryImpl) } .map { it.toFindroidSeason(this@JellyfinRepositoryImpl) }
} else { } else {
@ -259,12 +291,13 @@ class JellyfinRepositoryImpl(
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> = override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.showsApi.getNextUp( jellyfinApi.showsApi
jellyfinApi.userId!!, .getNextUp(
limit = 24, jellyfinApi.userId!!,
seriesId = seriesId, limit = 24,
enableResumable = false, seriesId = seriesId,
).content.items enableResumable = false,
).content.items
.orEmpty() .orEmpty()
.mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl) } .mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl) }
} }
@ -279,14 +312,15 @@ class JellyfinRepositoryImpl(
): List<FindroidEpisode> = ): List<FindroidEpisode> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!offline) { if (!offline) {
jellyfinApi.showsApi.getEpisodes( jellyfinApi.showsApi
seriesId, .getEpisodes(
jellyfinApi.userId!!, seriesId,
seasonId = seasonId, jellyfinApi.userId!!,
fields = fields, seasonId = seasonId,
startItemId = startItemId, fields = fields,
limit = limit, startItemId = startItemId,
).content.items limit = limit,
).content.items
.orEmpty() .orEmpty()
.mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl, database) } .mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl, database) }
} else { } else {
@ -294,39 +328,47 @@ class JellyfinRepositoryImpl(
} }
} }
override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List<FindroidSource> = override suspend fun getMediaSources(
itemId: UUID,
includePath: Boolean,
): List<FindroidSource> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val sources = mutableListOf<FindroidSource>() val sources = mutableListOf<FindroidSource>()
sources.addAll( sources.addAll(
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo( jellyfinApi.mediaInfoApi
itemId, .getPostedPlaybackInfo(
PlaybackInfoDto(
userId = jellyfinApi.userId!!,
deviceProfile = DeviceProfile(
name = "Direct play all",
maxStaticBitrate = 1_000_000_000,
maxStreamingBitrate = 1_000_000_000,
codecProfiles = emptyList(),
containerProfiles = emptyList(),
directPlayProfiles = listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
),
transcodingProfiles = emptyList(),
subtitleProfiles = listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
),
),
maxStreamingBitrate = 1_000_000_000,
),
).content.mediaSources.map {
it.toFindroidSource(
this@JellyfinRepositoryImpl,
itemId, itemId,
includePath, PlaybackInfoDto(
) userId = jellyfinApi.userId!!,
}, deviceProfile =
DeviceProfile(
name = "Direct play all",
maxStaticBitrate = 1_000_000_000,
maxStreamingBitrate = 1_000_000_000,
codecProfiles = emptyList(),
containerProfiles = emptyList(),
directPlayProfiles =
listOf(
DirectPlayProfile(type = DlnaProfileType.VIDEO),
DirectPlayProfile(type = DlnaProfileType.AUDIO),
),
transcodingProfiles = emptyList(),
subtitleProfiles =
listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
),
),
maxStreamingBitrate = 1_000_000_000,
),
).content.mediaSources
.map {
it.toFindroidSource(
this@JellyfinRepositoryImpl,
itemId,
includePath,
)
},
) )
sources.addAll( sources.addAll(
database.getSources(itemId).map { it.toFindroidSource(database) }, database.getSources(itemId).map { it.toFindroidSource(database) },
@ -334,14 +376,18 @@ class JellyfinRepositoryImpl(
sources sources
} }
override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String = override suspend fun getStreamUrl(
itemId: UUID,
mediaSourceId: String,
playSessionId: String?,
): String =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
jellyfinApi.videosApi.getVideoStreamUrl( jellyfinApi.videosApi.getVideoStreamUrl(
itemId, itemId,
static = true, static = true,
mediaSourceId = mediaSourceId, mediaSourceId = mediaSourceId,
playSessionId = playSessionId playSessionId = playSessionId,
) )
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
@ -362,16 +408,21 @@ class JellyfinRepositoryImpl(
pathParameters["itemId"] = itemId pathParameters["itemId"] = itemId
try { try {
return@withContext jellyfinApi.api.get<Intro>( return@withContext jellyfinApi.api
"/Episode/{itemId}/IntroTimestamps/v1", .get<Intro>(
pathParameters, "/Episode/{itemId}/IntroTimestamps/v1",
).content pathParameters,
).content
} catch (e: Exception) { } catch (e: Exception) {
return@withContext null return@withContext null
} }
} }
override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? = override suspend fun getTrickplayData(
itemId: UUID,
width: Int,
index: Int,
): ByteArray? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
try { try {
@ -379,9 +430,13 @@ class JellyfinRepositoryImpl(
if (sources != null) { if (sources != null) {
return@withContext File(sources.first(), index.toString()).readBytes() return@withContext File(sources.first(), index.toString()).readBytes()
} }
} catch (_: Exception) { } } catch (_: Exception) {
}
return@withContext jellyfinApi.trickplayApi.getTrickplayTileImage(itemId, width, index).content.toByteArray() return@withContext jellyfinApi.trickplayApi
.getTrickplayTileImage(itemId, width, index)
.content
.toByteArray()
} catch (e: Exception) { } catch (e: Exception) {
return@withContext null return@withContext null
} }
@ -392,21 +447,22 @@ class JellyfinRepositoryImpl(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
jellyfinApi.sessionApi.postCapabilities( jellyfinApi.sessionApi.postCapabilities(
playableMediaTypes = listOf(MediaType.VIDEO), playableMediaTypes = listOf(MediaType.VIDEO),
supportedCommands = listOf( supportedCommands =
GeneralCommandType.VOLUME_UP, listOf(
GeneralCommandType.VOLUME_DOWN, GeneralCommandType.VOLUME_UP,
GeneralCommandType.TOGGLE_MUTE, GeneralCommandType.VOLUME_DOWN,
GeneralCommandType.SET_AUDIO_STREAM_INDEX, GeneralCommandType.TOGGLE_MUTE,
GeneralCommandType.SET_SUBTITLE_STREAM_INDEX, GeneralCommandType.SET_AUDIO_STREAM_INDEX,
GeneralCommandType.MUTE, GeneralCommandType.SET_SUBTITLE_STREAM_INDEX,
GeneralCommandType.UNMUTE, GeneralCommandType.MUTE,
GeneralCommandType.SET_VOLUME, GeneralCommandType.UNMUTE,
GeneralCommandType.DISPLAY_MESSAGE, GeneralCommandType.SET_VOLUME,
GeneralCommandType.PLAY, GeneralCommandType.DISPLAY_MESSAGE,
GeneralCommandType.PLAY_STATE, GeneralCommandType.PLAY,
GeneralCommandType.PLAY_NEXT, GeneralCommandType.PLAY_STATE,
GeneralCommandType.PLAY_MEDIA_SOURCE, GeneralCommandType.PLAY_NEXT,
), GeneralCommandType.PLAY_MEDIA_SOURCE,
),
supportsMediaControl = true, supportsMediaControl = true,
) )
} }
@ -528,186 +584,215 @@ class JellyfinRepositoryImpl(
} }
} }
override suspend fun getUserConfiguration(): UserConfiguration = withContext(Dispatchers.IO) { override suspend fun getUserConfiguration(): UserConfiguration =
jellyfinApi.userApi.getCurrentUser().content.configuration!! withContext(Dispatchers.IO) {
} jellyfinApi.userApi
.getCurrentUser()
.content.configuration!!
}
override suspend fun getDownloads(): List<FindroidItem> = override suspend fun getDownloads(): List<FindroidItem> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val items = mutableListOf<FindroidItem>() val items = mutableListOf<FindroidItem>()
items.addAll( items.addAll(
database.getMoviesByServerId(appPreferences.currentServer!!) database
.getMoviesByServerId(appPreferences.currentServer!!)
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) }, .map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
) )
items.addAll( items.addAll(
database.getShowsByServerId(appPreferences.currentServer!!) database
.getShowsByServerId(appPreferences.currentServer!!)
.map { it.toFindroidShow(database, jellyfinApi.userId!!) }, .map { it.toFindroidShow(database, jellyfinApi.userId!!) },
) )
items items
} }
override fun getUserId(): UUID { override fun getUserId(): UUID = jellyfinApi.userId!!
return jellyfinApi.userId!!
}
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> =
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> { when (transcodeResolution) {
return when (transcodeResolution) {
1080 -> 8000000 to 384000 // Adjusted for personal can be other values 1080 -> 8000000 to 384000 // Adjusted for personal can be other values
720 -> 2000000 to 384000 // 720p 720 -> 2000000 to 384000 // 720p
480 -> 1000000 to 384000 // 480p 480 -> 1000000 to 384000 // 480p
360 -> 800000 to 128000 // 360p 360 -> 800000 to 128000 // 360p
else -> 12000000 to 384000 // its adaptive but setting max here else -> 12000000 to 384000 // its adaptive but setting max here
} }
}
override suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile { override suspend fun buildDeviceProfile(
val deviceProfile = ClientCapabilitiesDto( maxBitrate: Int,
supportedCommands = emptyList(), container: String,
playableMediaTypes = emptyList(), context: EncodingContext,
supportsMediaControl = true, ): DeviceProfile {
supportsPersistentIdentifier = true, val deviceProfile =
deviceProfile = DeviceProfile( ClientCapabilitiesDto(
name = "AnanasUser", supportedCommands = emptyList(),
id = getUserId().toString(), playableMediaTypes = emptyList(),
maxStaticBitrate = maxBitrate, supportsMediaControl = true,
maxStreamingBitrate = maxBitrate, supportsPersistentIdentifier = true,
codecProfiles = emptyList(), deviceProfile =
containerProfiles = listOf(), DeviceProfile(
directPlayProfiles = listOf( name = "AnanasUser",
DirectPlayProfile(type = DlnaProfileType.VIDEO), id = getUserId().toString(),
DirectPlayProfile(type = DlnaProfileType.AUDIO), maxStaticBitrate = maxBitrate,
), maxStreamingBitrate = maxBitrate,
transcodingProfiles = listOf( codecProfiles = emptyList(),
TranscodingProfile( containerProfiles = listOf(),
container = container, directPlayProfiles =
context = context, listOf(
protocol = MediaStreamProtocol.HLS, DirectPlayProfile(type = DlnaProfileType.VIDEO),
audioCodec = "aac,ac3,eac3", DirectPlayProfile(type = DlnaProfileType.AUDIO),
videoCodec = "hevc,h264", ),
type = DlnaProfileType.VIDEO, transcodingProfiles =
conditions = listOf( listOf(
ProfileCondition( TranscodingProfile(
condition = ProfileConditionType.LESS_THAN_EQUAL, container = container,
property = ProfileConditionValue.VIDEO_BITRATE, context = context,
value = "8000000", protocol = MediaStreamProtocol.HLS,
isRequired = true, audioCodec = "aac,ac3,eac3",
) videoCodec = "hevc,h264",
), type = DlnaProfileType.VIDEO,
copyTimestamps = true, conditions =
enableSubtitlesInManifest = true, listOf(
transcodeSeekInfo = TranscodeSeekInfo.AUTO, ProfileCondition(
condition = ProfileConditionType.LESS_THAN_EQUAL,
property = ProfileConditionValue.VIDEO_BITRATE,
value = "8000000",
isRequired = true,
),
),
copyTimestamps = true,
enableSubtitlesInManifest = true,
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
),
),
subtitleProfiles =
listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("vtt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL),
),
), ),
),
subtitleProfiles = listOf(
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("vtt", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL)
),
) )
)
return deviceProfile.deviceProfile!! return deviceProfile.deviceProfile!!
} }
override suspend fun getPostedPlaybackInfo(
override suspend fun getPostedPlaybackInfo(itemId: UUID ,enableDirectStream: Boolean ,deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse> { itemId: UUID,
val playbackInfo = jellyfinApi.mediaInfoApi.getPostedPlaybackInfo( enableDirectStream: Boolean,
itemId = itemId, deviceProfile: DeviceProfile,
PlaybackInfoDto( maxBitrate: Int,
userId = jellyfinApi.userId!!, ): Response<PlaybackInfoResponse> {
enableTranscoding = true, val playbackInfo =
enableDirectPlay = false, jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
enableDirectStream = enableDirectStream, itemId = itemId,
autoOpenLiveStream = true, PlaybackInfoDto(
deviceProfile = deviceProfile, userId = jellyfinApi.userId!!,
allowAudioStreamCopy = true, enableTranscoding = true,
allowVideoStreamCopy = true, enableDirectPlay = false,
maxStreamingBitrate = maxBitrate, enableDirectStream = enableDirectStream,
autoOpenLiveStream = true,
deviceProfile = deviceProfile,
allowAudioStreamCopy = true,
allowVideoStreamCopy = true,
maxStreamingBitrate = maxBitrate,
),
) )
)
return playbackInfo return playbackInfo
} }
override suspend fun getVideoStreambyContainerUrl(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String { override suspend fun getVideoStreambyContainerUrl(
val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl( itemId: UUID,
itemId, deviceId: String,
static = false, mediaSourceId: String,
deviceId = deviceId, playSessionId: String,
mediaSourceId = mediaSourceId, videoBitrate: Int,
playSessionId = playSessionId, container: String,
videoBitRate = videoBitrate, ): String {
audioBitRate = 384000, val url =
videoCodec = "hevc", jellyfinApi.videosApi.getVideoStreamByContainerUrl(
audioCodec = "aac,ac3,eac3",
container = container,
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL
)
return url
}
override suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String {
val isAuto = videoBitrate == 12000000
val url = if (!isAuto) {
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
itemId, itemId,
static = false, static = false,
deviceId = deviceId, deviceId = deviceId,
mediaSourceId = mediaSourceId, mediaSourceId = mediaSourceId,
playSessionId = playSessionId, playSessionId = playSessionId,
videoBitRate = videoBitrate, videoBitRate = videoBitrate,
enableAdaptiveBitrateStreaming = false, audioBitRate = 384000,
audioBitRate = 384000, //could also be passed with audioBitrate but i preferred not as its not much data anyways
videoCodec = "hevc,h264",
audioCodec = "aac,ac3,eac3",
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
context = EncodingContext.STREAMING,
segmentContainer = "ts",
transcodeReasons = "ContainerBitrateExceedsLimit",
)
} else {
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
itemId,
static = false,
deviceId = deviceId,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
enableAdaptiveBitrateStreaming = true,
videoCodec = "hevc", videoCodec = "hevc",
audioCodec = "aac,ac3,eac3", audioCodec = "aac,ac3,eac3",
container = container,
startTimeTicks = 0, startTimeTicks = 0,
copyTimestamps = true, copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
context = EncodingContext.STREAMING,
segmentContainer = "ts",
transcodeReasons = "ContainerBitrateExceedsLimit",
) )
}
return url return url
} }
override suspend fun getTranscodedVideoStream(
itemId: UUID,
deviceId: String,
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
): String {
val isAuto = videoBitrate == 12000000
val url =
if (!isAuto) {
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
itemId,
static = false,
deviceId = deviceId,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
videoBitRate = videoBitrate,
enableAdaptiveBitrateStreaming = false,
audioBitRate = 384000, // could also be passed with audioBitrate but i preferred not as its not much data anyways
videoCodec = "hevc,h264",
audioCodec = "aac,ac3,eac3",
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
context = EncodingContext.STREAMING,
segmentContainer = "ts",
transcodeReasons = "ContainerBitrateExceedsLimit",
)
} else {
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
itemId,
static = false,
deviceId = deviceId,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
enableAdaptiveBitrateStreaming = true,
videoCodec = "hevc",
audioCodec = "aac,ac3,eac3",
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
context = EncodingContext.STREAMING,
segmentContainer = "ts",
transcodeReasons = "ContainerBitrateExceedsLimit",
)
}
return url
}
override suspend fun getDeviceId(): String { override suspend fun getDeviceId(): String {
val devices = jellyfinApi.devicesApi.getDevices(getUserId()) val devices = jellyfinApi.devicesApi.getDevices(getUserId())
return devices.content.items?.firstOrNull()?.id!! return devices.content.items
?.firstOrNull()
?.id!!
} }
override suspend fun stopEncodingProcess(playSessionId: String) { override suspend fun stopEncodingProcess(playSessionId: String) {
val deviceId = getDeviceId() val deviceId = getDeviceId()
jellyfinApi.api.hlsSegmentApi.stopEncodingProcess( jellyfinApi.api.hlsSegmentApi.stopEncodingProcess(
deviceId = deviceId, deviceId = deviceId,
playSessionId = playSessionId playSessionId = playSessionId,
) )
} }
} }

View file

@ -42,14 +42,9 @@ class JellyfinRepositoryOfflineImpl(
private val database: ServerDatabaseDao, private val database: ServerDatabaseDao,
private val appPreferences: AppPreferences, private val appPreferences: AppPreferences,
) : JellyfinRepository { ) : JellyfinRepository {
override suspend fun getPublicSystemInfo(): PublicSystemInfo = throw Exception("System info not available in offline mode")
override suspend fun getPublicSystemInfo(): PublicSystemInfo { override suspend fun getUserViews(): List<BaseItemDto> = emptyList()
throw Exception("System info not available in offline mode")
}
override suspend fun getUserViews(): List<BaseItemDto> {
return emptyList()
}
override suspend fun getItem(itemId: UUID): BaseItemDto { override suspend fun getItem(itemId: UUID): BaseItemDto {
TODO("Not yet implemented") TODO("Not yet implemented")
@ -113,38 +108,69 @@ class JellyfinRepositoryOfflineImpl(
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> { override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
return withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val movies = database.searchMovies(appPreferences.currentServer!!, searchQuery).map { it.toFindroidMovie(database, jellyfinApi.userId!!) } val movies =
val shows = database.searchShows(appPreferences.currentServer!!, searchQuery).map { it.toFindroidShow(database, jellyfinApi.userId!!) } database.searchMovies(appPreferences.currentServer!!, searchQuery).map {
val episodes = database.searchEpisodes(appPreferences.currentServer!!, searchQuery).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) } it.toFindroidMovie(
database,
@Suppress("ktlint:standard:max-line-length")
jellyfinApi.userId!!,
)
}
val shows =
database
.searchShows(
appPreferences.currentServer!!,
searchQuery,
).map { it.toFindroidShow(database, jellyfinApi.userId!!) }
val episodes =
database.searchEpisodes(appPreferences.currentServer!!, searchQuery).map {
it.toFindroidEpisode(database, jellyfinApi.userId!!)
}
movies + shows + episodes movies + shows + episodes
} }
}
override suspend fun getResumeItems(): List<FindroidItem> { override suspend fun getResumeItems(): List<FindroidItem> =
return withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val movies = database.getMoviesByServerId(appPreferences.currentServer!!).map { it.toFindroidMovie(database, jellyfinApi.userId!!) }.filter { it.playbackPositionTicks > 0 } val movies =
val episodes = database.getEpisodesByServerId(appPreferences.currentServer!!).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }.filter { it.playbackPositionTicks > 0 } database
.getMoviesByServerId(
appPreferences.currentServer!!,
).map { it.toFindroidMovie(database, jellyfinApi.userId!!) }
.filter {
it.playbackPositionTicks >
0
}
val episodes =
database
.getEpisodesByServerId(
appPreferences.currentServer!!,
).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }
.filter {
it.playbackPositionTicks >
0
}
movies + episodes movies + episodes
} }
}
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> { override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> = emptyList()
return emptyList()
}
override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List<FindroidSeason> = override suspend fun getSeasons(
seriesId: UUID,
offline: Boolean,
): List<FindroidSeason> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
database.getSeasonsByShowId(seriesId).map { it.toFindroidSeason(database, jellyfinApi.userId!!) } database.getSeasonsByShowId(seriesId).map { it.toFindroidSeason(database, jellyfinApi.userId!!) }
} }
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> { override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
return withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val result = mutableListOf<FindroidEpisode>() val result = mutableListOf<FindroidEpisode>()
val shows = database.getShowsByServerId(appPreferences.currentServer!!).filter { val shows =
if (seriesId != null) it.id == seriesId else true database.getShowsByServerId(appPreferences.currentServer!!).filter {
} if (seriesId != null) it.id == seriesId else true
}
for (show in shows) { for (show in shows) {
val episodes = database.getEpisodesByShowId(show.id).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) } val episodes = database.getEpisodesByShowId(show.id).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }
val indexOfLastPlayed = episodes.indexOfLast { it.played } val indexOfLastPlayed = episodes.indexOfLast { it.played }
@ -156,7 +182,6 @@ class JellyfinRepositoryOfflineImpl(
} }
result.filter { it.playbackPositionTicks == 0L } result.filter { it.playbackPositionTicks == 0L }
} }
}
override suspend fun getEpisodes( override suspend fun getEpisodes(
seriesId: UUID, seriesId: UUID,
@ -172,12 +197,19 @@ class JellyfinRepositoryOfflineImpl(
items items
} }
override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List<FindroidSource> = override suspend fun getMediaSources(
itemId: UUID,
includePath: Boolean,
): List<FindroidSource> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
database.getSources(itemId).map { it.toFindroidSource(database) } database.getSources(itemId).map { it.toFindroidSource(database) }
} }
override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String { override suspend fun getStreamUrl(
itemId: UUID,
mediaSourceId: String,
playSessionId: String?,
): String {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -186,7 +218,11 @@ class JellyfinRepositoryOfflineImpl(
database.getIntro(itemId)?.toIntro() database.getIntro(itemId)?.toIntro()
} }
override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? = override suspend fun getTrickplayData(
itemId: UUID,
width: Int,
index: Int,
): ByteArray? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
val sources = File(context.filesDir, "trickplay/$itemId").listFiles() ?: return@withContext null val sources = File(context.filesDir, "trickplay/$itemId").listFiles() ?: return@withContext null
@ -200,7 +236,11 @@ class JellyfinRepositoryOfflineImpl(
override suspend fun postPlaybackStart(itemId: UUID) {} override suspend fun postPlaybackStart(itemId: UUID) {}
override suspend fun postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int) { override suspend fun postPlaybackStop(
itemId: UUID,
positionTicks: Long,
playedPercentage: Int,
) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
when { when {
playedPercentage < 10 -> { playedPercentage < 10 -> {
@ -260,35 +300,31 @@ class JellyfinRepositoryOfflineImpl(
} }
} }
override fun getBaseUrl(): String { override fun getBaseUrl(): String = ""
return ""
}
override suspend fun updateDeviceName(name: String) { override suspend fun updateDeviceName(name: String) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun getUserConfiguration(): UserConfiguration? { override suspend fun getUserConfiguration(): UserConfiguration? = null
return null
}
override suspend fun getDownloads(): List<FindroidItem> = override suspend fun getDownloads(): List<FindroidItem> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val items = mutableListOf<FindroidItem>() val items = mutableListOf<FindroidItem>()
items.addAll( items.addAll(
database.getMoviesByServerId(appPreferences.currentServer!!) database
.getMoviesByServerId(appPreferences.currentServer!!)
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) }, .map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
) )
items.addAll( items.addAll(
database.getShowsByServerId(appPreferences.currentServer!!) database
.getShowsByServerId(appPreferences.currentServer!!)
.map { it.toFindroidShow(database, jellyfinApi.userId!!) }, .map { it.toFindroidShow(database, jellyfinApi.userId!!) },
) )
items items
} }
override fun getUserId(): UUID { override fun getUserId(): UUID = jellyfinApi.userId!!
return jellyfinApi.userId!!
}
override suspend fun getDeviceId(): String { override suspend fun getDeviceId(): String {
TODO("Not yet implemented") TODO("Not yet implemented")
@ -301,7 +337,7 @@ class JellyfinRepositoryOfflineImpl(
override suspend fun buildDeviceProfile( override suspend fun buildDeviceProfile(
maxBitrate: Int, maxBitrate: Int,
container: String, container: String,
context: EncodingContext context: EncodingContext,
): DeviceProfile { ): DeviceProfile {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -312,7 +348,7 @@ class JellyfinRepositoryOfflineImpl(
mediaSourceId: String, mediaSourceId: String,
playSessionId: String, playSessionId: String,
videoBitrate: Int, videoBitrate: Int,
container: String container: String,
): String { ): String {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -322,7 +358,7 @@ class JellyfinRepositoryOfflineImpl(
deviceId: String, deviceId: String,
mediaSourceId: String, mediaSourceId: String,
playSessionId: String, playSessionId: String,
videoBitrate: Int videoBitrate: Int,
): String { ): String {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@ -331,7 +367,7 @@ class JellyfinRepositoryOfflineImpl(
itemId: UUID, itemId: UUID,
enableDirectStream: Boolean, enableDirectStream: Boolean,
deviceProfile: DeviceProfile, deviceProfile: DeviceProfile,
maxBitrate: Int maxBitrate: Int,
): Response<PlaybackInfoResponse> { ): Response<PlaybackInfoResponse> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }