diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt b/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt index aad7a3c8..7f48644d 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt @@ -2,7 +2,7 @@ package dev.jdtech.jellyfin.models import android.os.Parcelable import kotlinx.parcelize.Parcelize -import java.util.* +import java.util.UUID @Parcelize data class PlayerItem( diff --git a/app/src/main/java/dev/jdtech/jellyfin/utils/DownloadUtilities.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/DownloadUtilities.kt index b76a8f53..60047c8e 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/DownloadUtilities.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/utils/DownloadUtilities.kt @@ -28,20 +28,32 @@ fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context: @Suppress("MagicNumber") Timber.d("REQUESTING PERMISSION") - if (ContextCompat.checkSelfPermission(context.requireActivity(), - Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED + if (ContextCompat.checkSelfPermission( + context.requireActivity(), + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED ) { - if (ActivityCompat.shouldShowRequestPermissionRationale(context.requireActivity(), - Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - ActivityCompat.requestPermissions(context.requireActivity(), - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) + if (ActivityCompat.shouldShowRequestPermissionRationale( + context.requireActivity(), + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + ) { + ActivityCompat.requestPermissions( + context.requireActivity(), + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1 + ) } else { - ActivityCompat.requestPermissions(context.requireActivity(), - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) + ActivityCompat.requestPermissions( + context.requireActivity(), + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1 + ) } } - val granted = ContextCompat.checkSelfPermission(context.requireActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED + val granted = ContextCompat.checkSelfPermission( + context.requireActivity(), + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED if (!granted) { context.requireContext().toast(R.string.download_no_storage_permission) @@ -53,11 +65,22 @@ fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context: val downloadRequest = DownloadManager.Request(uri) .setTitle(downloadRequestItem.metadata.name) .setDescription("Downloading") - .setDestinationUri(Uri.fromFile(File(defaultStorage, downloadRequestItem.itemId.toString()))) + .setDestinationUri( + Uri.fromFile( + File( + defaultStorage, + downloadRequestItem.itemId.toString() + ) + ) + ) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - if(!File(defaultStorage, downloadRequestItem.itemId.toString()).exists()) + if (!File(defaultStorage, downloadRequestItem.itemId.toString()).exists()) downloadFile(downloadRequest, 1, context.requireContext()) - createMetadataFile(downloadRequestItem.metadata, downloadRequestItem.itemId, context.requireContext()) + createMetadataFile( + downloadRequestItem.metadata, + downloadRequestItem.itemId, + context.requireContext() + ) } private fun createMetadataFile(metadata: DownloadMetadata, itemId: UUID, context: Context) { @@ -66,7 +89,7 @@ private fun createMetadataFile(metadata: DownloadMetadata, itemId: UUID, context metadataFile.writeText("") //This might be necessary to make sure that the metadata file is empty - if(metadata.type == "Episode") { + if (metadata.type == "Episode") { metadataFile.printWriter().use { out -> out.println(metadata.id) out.println(metadata.type.toString()) @@ -108,10 +131,19 @@ fun loadDownloadedEpisodes(context: Context): List { val defaultStorage = getDownloadLocation(context) defaultStorage?.walk()?.forEach { if (it.isFile && it.extension == "") { - try{ + try { val metadataFile = File(defaultStorage, "${it.name}.metadata").readLines() val metadata = parseMetadataFile(metadataFile) - items.add(PlayerItem(metadata.name, UUID.fromString(it.name), "", metadata.playbackPosition!!, it.absolutePath, metadata)) + items.add( + PlayerItem( + name = metadata.name, + itemId = UUID.fromString(it.name), + mediaSourceId = "", + playbackPosition = metadata.playbackPosition!!, + mediaSourceUri = it.absolutePath, + metadata = metadata + ) + ) } catch (e: Exception) { it.delete() Timber.e(e) @@ -136,7 +168,7 @@ fun postDownloadPlaybackProgress(uri: String, playbackPosition: Long, playedPerc try { val metadataFile = File("${uri}.metadata") val metadataArray = metadataFile.readLines().toMutableList() - if(metadataArray[1] == "Episode"){ + if (metadataArray[1] == "Episode") { metadataArray[6] = playbackPosition.toString() metadataArray[7] = playedPercentage.toString() } else if (metadataArray[1] == "Movie") { @@ -155,11 +187,17 @@ fun postDownloadPlaybackProgress(uri: String, playbackPosition: Long, playedPerc } } -fun downloadMetadataToBaseItemDto(metadata: DownloadMetadata) : BaseItemDto { - val userData = UserItemDataDto(playbackPositionTicks = metadata.playbackPosition ?: 0, - playedPercentage = metadata.playedPercentage, isFavorite = false, playCount = 0, played = false) +fun downloadMetadataToBaseItemDto(metadata: DownloadMetadata): BaseItemDto { + val userData = UserItemDataDto( + playbackPositionTicks = metadata.playbackPosition ?: 0, + playedPercentage = metadata.playedPercentage, + isFavorite = false, + playCount = 0, + played = false + ) - return BaseItemDto(id = metadata.id, + return BaseItemDto( + id = metadata.id, type = metadata.type, seriesName = metadata.seriesName, name = metadata.name, @@ -170,8 +208,9 @@ fun downloadMetadataToBaseItemDto(metadata: DownloadMetadata) : BaseItemDto { ) } -fun baseItemDtoToDownloadMetadata(item: BaseItemDto) : DownloadMetadata { - return DownloadMetadata(id = item.id, +fun baseItemDtoToDownloadMetadata(item: BaseItemDto): DownloadMetadata { + return DownloadMetadata( + id = item.id, type = item.type, seriesName = item.seriesName, name = item.name, @@ -183,31 +222,41 @@ fun baseItemDtoToDownloadMetadata(item: BaseItemDto) : DownloadMetadata { ) } -fun parseMetadataFile(metadataFile: List) : DownloadMetadata { +fun parseMetadataFile(metadataFile: List): DownloadMetadata { if (metadataFile[1] == "Episode") { - return DownloadMetadata(id = UUID.fromString(metadataFile[0]), + return DownloadMetadata( + id = UUID.fromString(metadataFile[0]), type = metadataFile[1], seriesName = metadataFile[2], name = metadataFile[3], parentIndexNumber = metadataFile[4].toInt(), indexNumber = metadataFile[5].toInt(), playbackPosition = metadataFile[6].toLong(), - playedPercentage = if(metadataFile[7] == "null") {null} else {metadataFile[7].toDouble()}, + playedPercentage = if (metadataFile[7] == "null") { + null + } else { + metadataFile[7].toDouble() + }, seriesId = UUID.fromString(metadataFile[8]) ) } else { - return DownloadMetadata(id = UUID.fromString(metadataFile[0]), + return DownloadMetadata( + id = UUID.fromString(metadataFile[0]), type = metadataFile[1], name = metadataFile[2], playbackPosition = metadataFile[3].toLong(), - playedPercentage = if(metadataFile[4] == "null") {null} else {metadataFile[4].toDouble()}, + playedPercentage = if (metadataFile[4] == "null") { + null + } else { + metadataFile[4].toDouble() + }, ) } } suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context: Context) { val items = loadDownloadedEpisodes(context) - items.forEach{ + items.forEach() { try { val localPlaybackProgress = it.metadata?.playbackPosition val localPlayedPercentage = it.metadata?.playedPercentage @@ -220,13 +269,13 @@ suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context var playedPercentage = 0.0 if (localPlaybackProgress != null) { - if (localPlaybackProgress > playbackProgress){ + if (localPlaybackProgress > playbackProgress) { playbackProgress = localPlaybackProgress playedPercentage = localPlayedPercentage!! } } if (remotePlaybackProgress != null) { - if (remotePlaybackProgress > playbackProgress){ + if (remotePlaybackProgress > playbackProgress) { playbackProgress = remotePlaybackProgress playedPercentage = remotePlayedPercentage!! } @@ -234,7 +283,11 @@ suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context if (playbackProgress != 0.toLong()) { postDownloadPlaybackProgress(it.mediaSourceUri, playbackProgress, playedPercentage) - jellyfinRepository.postPlaybackProgress(it.itemId, playbackProgress.times(10000), true) + jellyfinRepository.postPlaybackProgress( + it.itemId, + playbackProgress.times(10000), + true + ) Timber.d("Percentage: $playedPercentage") } } catch (e: Exception) { diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index ff1e4d5d..ed982f9e 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -103,11 +103,9 @@ constructor( val mediaItems: MutableList = mutableListOf() try { for (item in items) { - playFromDownloads = item.mediaSourceUri.isNotEmpty() - val streamUrl = if(!playFromDownloads){ - jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) - }else{ - item.mediaSourceUri + val streamUrl = when { + item.mediaSourceUri.isNotEmpty() -> item.mediaSourceUri + else -> jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) } Timber.d("Stream url: $streamUrl") diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index a4e819d5..824d7f7c 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.LocationType.VIRTUAL +import org.jellyfin.sdk.model.api.MediaProtocol import timber.log.Timber import javax.inject.Inject @@ -79,14 +80,7 @@ class PlayerViewModel @Inject internal constructor( return repository .getIntros(item.id) .filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true } - .map { intro -> - PlayerItem( - intro.name, - intro.id, - intro.mediaSources?.get(0)?.id!!, - 0 - ) - } + .map { intro -> intro.toPlayerItem(mediaSourceIndex = 0, playbackPosition = 0) } } private suspend fun prepareMediaPlayerItems( @@ -104,14 +98,7 @@ class PlayerViewModel @Inject internal constructor( item: BaseItemDto, playbackPosition: Long, mediaSourceIndex: Int - ) = listOf( - PlayerItem( - item.name, - item.id, - item.mediaSources?.get(mediaSourceIndex)?.id!!, - playbackPosition - ) - ) + ) = listOf(item.toPlayerItem(mediaSourceIndex, playbackPosition)) private suspend fun seriesToPlayerItems( item: BaseItemDto, @@ -134,23 +121,15 @@ class PlayerViewModel @Inject internal constructor( playbackPosition: Long, mediaSourceIndex: Int ): List { - val episodes = repository.getEpisodes( - seriesId = item.seriesId!!, - seasonId = item.id, - fields = listOf(ItemFields.MEDIA_SOURCES) - ) - - return episodes + return repository + .getEpisodes( + seriesId = item.seriesId!!, + seasonId = item.id, + fields = listOf(ItemFields.MEDIA_SOURCES) + ) .filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true } .filter { it.locationType != VIRTUAL } - .map { episode -> - PlayerItem( - episode.name, - episode.id, - episode.mediaSources?.get(mediaSourceIndex)?.id!!, - playbackPosition - ) - } + .map { episode -> episode.toPlayerItem(mediaSourceIndex, playbackPosition) } } private suspend fun episodeToPlayerItems( @@ -158,24 +137,44 @@ class PlayerViewModel @Inject internal constructor( playbackPosition: Long, mediaSourceIndex: Int ): List { - val episodes = repository.getEpisodes( - seriesId = item.seriesId!!, - seasonId = item.seasonId!!, - fields = listOf(ItemFields.MEDIA_SOURCES), - startItemId = item.id - ) - - return episodes + return repository + .getEpisodes( + seriesId = item.seriesId!!, + seasonId = item.seasonId!!, + fields = listOf(ItemFields.MEDIA_SOURCES), + startItemId = item.id + ) .filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true } .filter { it.locationType != VIRTUAL } - .map { episode -> - PlayerItem( - episode.name, - episode.id, - episode.mediaSources?.get(mediaSourceIndex)?.id!!, - playbackPosition - ) - } + .map { episode -> episode.toPlayerItem(mediaSourceIndex, playbackPosition) } + } + + private fun BaseItemDto.toPlayerItem( + mediaSourceIndex: Int, + playbackPosition: Long + ): PlayerItem { + val mediaSource = mediaSources!![mediaSourceIndex] + return when (mediaSource.protocol) { + MediaProtocol.FILE -> PlayerItem( + name = name, + itemId = id, + mediaSourceId = mediaSource.id!!, + playbackPosition = playbackPosition + ) + MediaProtocol.HTTP -> PlayerItem( + name = name, + itemId = id, + mediaSourceId = mediaSource.id!!, + mediaSourceUri = mediaSource.path!!, + playbackPosition = playbackPosition + ) + else -> PlayerItem( + name = name, + itemId = id, + mediaSourceId = mediaSource.id!!, + playbackPosition = playbackPosition + ) + } } sealed class PlayerItemState