Add strm support (#66)

Co-authored-by: Jarne Demeulemeester <32322857+jarnedemeulemeester@users.noreply.github.com>
This commit is contained in:
lsrom 2021-11-16 19:44:49 +01:00 committed by GitHub
parent d7a47b0a3e
commit 8c5d0bebf0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 134 additions and 84 deletions

View file

@ -2,7 +2,7 @@ package dev.jdtech.jellyfin.models
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.util.* import java.util.UUID
@Parcelize @Parcelize
data class PlayerItem( data class PlayerItem(

View file

@ -28,20 +28,32 @@ fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context:
@Suppress("MagicNumber") @Suppress("MagicNumber")
Timber.d("REQUESTING PERMISSION") Timber.d("REQUESTING PERMISSION")
if (ContextCompat.checkSelfPermission(context.requireActivity(), if (ContextCompat.checkSelfPermission(
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED context.requireActivity(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) { ) {
if (ActivityCompat.shouldShowRequestPermissionRationale(context.requireActivity(), if (ActivityCompat.shouldShowRequestPermissionRationale(
Manifest.permission.WRITE_EXTERNAL_STORAGE)) { context.requireActivity(),
ActivityCompat.requestPermissions(context.requireActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) )
) {
ActivityCompat.requestPermissions(
context.requireActivity(),
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1
)
} else { } else {
ActivityCompat.requestPermissions(context.requireActivity(), ActivityCompat.requestPermissions(
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) 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) { if (!granted) {
context.requireContext().toast(R.string.download_no_storage_permission) 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) val downloadRequest = DownloadManager.Request(uri)
.setTitle(downloadRequestItem.metadata.name) .setTitle(downloadRequestItem.metadata.name)
.setDescription("Downloading") .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) .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()) 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) { 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 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 -> metadataFile.printWriter().use { out ->
out.println(metadata.id) out.println(metadata.id)
out.println(metadata.type.toString()) out.println(metadata.type.toString())
@ -108,10 +131,19 @@ fun loadDownloadedEpisodes(context: Context): List<PlayerItem> {
val defaultStorage = getDownloadLocation(context) val defaultStorage = getDownloadLocation(context)
defaultStorage?.walk()?.forEach { defaultStorage?.walk()?.forEach {
if (it.isFile && it.extension == "") { if (it.isFile && it.extension == "") {
try{ try {
val metadataFile = File(defaultStorage, "${it.name}.metadata").readLines() val metadataFile = File(defaultStorage, "${it.name}.metadata").readLines()
val metadata = parseMetadataFile(metadataFile) 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) { } catch (e: Exception) {
it.delete() it.delete()
Timber.e(e) Timber.e(e)
@ -136,7 +168,7 @@ fun postDownloadPlaybackProgress(uri: String, playbackPosition: Long, playedPerc
try { try {
val metadataFile = File("${uri}.metadata") val metadataFile = File("${uri}.metadata")
val metadataArray = metadataFile.readLines().toMutableList() val metadataArray = metadataFile.readLines().toMutableList()
if(metadataArray[1] == "Episode"){ if (metadataArray[1] == "Episode") {
metadataArray[6] = playbackPosition.toString() metadataArray[6] = playbackPosition.toString()
metadataArray[7] = playedPercentage.toString() metadataArray[7] = playedPercentage.toString()
} else if (metadataArray[1] == "Movie") { } else if (metadataArray[1] == "Movie") {
@ -155,11 +187,17 @@ fun postDownloadPlaybackProgress(uri: String, playbackPosition: Long, playedPerc
} }
} }
fun downloadMetadataToBaseItemDto(metadata: DownloadMetadata) : BaseItemDto { fun downloadMetadataToBaseItemDto(metadata: DownloadMetadata): BaseItemDto {
val userData = UserItemDataDto(playbackPositionTicks = metadata.playbackPosition ?: 0, val userData = UserItemDataDto(
playedPercentage = metadata.playedPercentage, isFavorite = false, playCount = 0, played = false) 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, type = metadata.type,
seriesName = metadata.seriesName, seriesName = metadata.seriesName,
name = metadata.name, name = metadata.name,
@ -170,8 +208,9 @@ fun downloadMetadataToBaseItemDto(metadata: DownloadMetadata) : BaseItemDto {
) )
} }
fun baseItemDtoToDownloadMetadata(item: BaseItemDto) : DownloadMetadata { fun baseItemDtoToDownloadMetadata(item: BaseItemDto): DownloadMetadata {
return DownloadMetadata(id = item.id, return DownloadMetadata(
id = item.id,
type = item.type, type = item.type,
seriesName = item.seriesName, seriesName = item.seriesName,
name = item.name, name = item.name,
@ -183,31 +222,41 @@ fun baseItemDtoToDownloadMetadata(item: BaseItemDto) : DownloadMetadata {
) )
} }
fun parseMetadataFile(metadataFile: List<String>) : DownloadMetadata { fun parseMetadataFile(metadataFile: List<String>): DownloadMetadata {
if (metadataFile[1] == "Episode") { if (metadataFile[1] == "Episode") {
return DownloadMetadata(id = UUID.fromString(metadataFile[0]), return DownloadMetadata(
id = UUID.fromString(metadataFile[0]),
type = metadataFile[1], type = metadataFile[1],
seriesName = metadataFile[2], seriesName = metadataFile[2],
name = metadataFile[3], name = metadataFile[3],
parentIndexNumber = metadataFile[4].toInt(), parentIndexNumber = metadataFile[4].toInt(),
indexNumber = metadataFile[5].toInt(), indexNumber = metadataFile[5].toInt(),
playbackPosition = metadataFile[6].toLong(), 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]) seriesId = UUID.fromString(metadataFile[8])
) )
} else { } else {
return DownloadMetadata(id = UUID.fromString(metadataFile[0]), return DownloadMetadata(
id = UUID.fromString(metadataFile[0]),
type = metadataFile[1], type = metadataFile[1],
name = metadataFile[2], name = metadataFile[2],
playbackPosition = metadataFile[3].toLong(), 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) { suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context: Context) {
val items = loadDownloadedEpisodes(context) val items = loadDownloadedEpisodes(context)
items.forEach{ items.forEach() {
try { try {
val localPlaybackProgress = it.metadata?.playbackPosition val localPlaybackProgress = it.metadata?.playbackPosition
val localPlayedPercentage = it.metadata?.playedPercentage val localPlayedPercentage = it.metadata?.playedPercentage
@ -220,13 +269,13 @@ suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context
var playedPercentage = 0.0 var playedPercentage = 0.0
if (localPlaybackProgress != null) { if (localPlaybackProgress != null) {
if (localPlaybackProgress > playbackProgress){ if (localPlaybackProgress > playbackProgress) {
playbackProgress = localPlaybackProgress playbackProgress = localPlaybackProgress
playedPercentage = localPlayedPercentage!! playedPercentage = localPlayedPercentage!!
} }
} }
if (remotePlaybackProgress != null) { if (remotePlaybackProgress != null) {
if (remotePlaybackProgress > playbackProgress){ if (remotePlaybackProgress > playbackProgress) {
playbackProgress = remotePlaybackProgress playbackProgress = remotePlaybackProgress
playedPercentage = remotePlayedPercentage!! playedPercentage = remotePlayedPercentage!!
} }
@ -234,7 +283,11 @@ suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context
if (playbackProgress != 0.toLong()) { if (playbackProgress != 0.toLong()) {
postDownloadPlaybackProgress(it.mediaSourceUri, playbackProgress, playedPercentage) 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") Timber.d("Percentage: $playedPercentage")
} }
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -103,11 +103,9 @@ constructor(
val mediaItems: MutableList<MediaItem> = mutableListOf() val mediaItems: MutableList<MediaItem> = mutableListOf()
try { try {
for (item in items) { for (item in items) {
playFromDownloads = item.mediaSourceUri.isNotEmpty() val streamUrl = when {
val streamUrl = if(!playFromDownloads){ item.mediaSourceUri.isNotEmpty() -> item.mediaSourceUri
jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) else -> jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId)
}else{
item.mediaSourceUri
} }
Timber.d("Stream url: $streamUrl") Timber.d("Stream url: $streamUrl")

View file

@ -13,6 +13,7 @@ import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.ItemFields import org.jellyfin.sdk.model.api.ItemFields
import org.jellyfin.sdk.model.api.LocationType.VIRTUAL import org.jellyfin.sdk.model.api.LocationType.VIRTUAL
import org.jellyfin.sdk.model.api.MediaProtocol
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -79,14 +80,7 @@ class PlayerViewModel @Inject internal constructor(
return repository return repository
.getIntros(item.id) .getIntros(item.id)
.filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true } .filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true }
.map { intro -> .map { intro -> intro.toPlayerItem(mediaSourceIndex = 0, playbackPosition = 0) }
PlayerItem(
intro.name,
intro.id,
intro.mediaSources?.get(0)?.id!!,
0
)
}
} }
private suspend fun prepareMediaPlayerItems( private suspend fun prepareMediaPlayerItems(
@ -104,14 +98,7 @@ class PlayerViewModel @Inject internal constructor(
item: BaseItemDto, item: BaseItemDto,
playbackPosition: Long, playbackPosition: Long,
mediaSourceIndex: Int mediaSourceIndex: Int
) = listOf( ) = listOf(item.toPlayerItem(mediaSourceIndex, playbackPosition))
PlayerItem(
item.name,
item.id,
item.mediaSources?.get(mediaSourceIndex)?.id!!,
playbackPosition
)
)
private suspend fun seriesToPlayerItems( private suspend fun seriesToPlayerItems(
item: BaseItemDto, item: BaseItemDto,
@ -134,23 +121,15 @@ class PlayerViewModel @Inject internal constructor(
playbackPosition: Long, playbackPosition: Long,
mediaSourceIndex: Int mediaSourceIndex: Int
): List<PlayerItem> { ): List<PlayerItem> {
val episodes = repository.getEpisodes( return repository
.getEpisodes(
seriesId = item.seriesId!!, seriesId = item.seriesId!!,
seasonId = item.id, seasonId = item.id,
fields = listOf(ItemFields.MEDIA_SOURCES) fields = listOf(ItemFields.MEDIA_SOURCES)
) )
return episodes
.filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true } .filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true }
.filter { it.locationType != VIRTUAL } .filter { it.locationType != VIRTUAL }
.map { episode -> .map { episode -> episode.toPlayerItem(mediaSourceIndex, playbackPosition) }
PlayerItem(
episode.name,
episode.id,
episode.mediaSources?.get(mediaSourceIndex)?.id!!,
playbackPosition
)
}
} }
private suspend fun episodeToPlayerItems( private suspend fun episodeToPlayerItems(
@ -158,22 +137,42 @@ class PlayerViewModel @Inject internal constructor(
playbackPosition: Long, playbackPosition: Long,
mediaSourceIndex: Int mediaSourceIndex: Int
): List<PlayerItem> { ): List<PlayerItem> {
val episodes = repository.getEpisodes( return repository
.getEpisodes(
seriesId = item.seriesId!!, seriesId = item.seriesId!!,
seasonId = item.seasonId!!, seasonId = item.seasonId!!,
fields = listOf(ItemFields.MEDIA_SOURCES), fields = listOf(ItemFields.MEDIA_SOURCES),
startItemId = item.id startItemId = item.id
) )
return episodes
.filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true } .filter { it.mediaSources != null && it.mediaSources?.isNotEmpty() == true }
.filter { it.locationType != VIRTUAL } .filter { it.locationType != VIRTUAL }
.map { episode -> .map { episode -> episode.toPlayerItem(mediaSourceIndex, playbackPosition) }
PlayerItem( }
episode.name,
episode.id, private fun BaseItemDto.toPlayerItem(
episode.mediaSources?.get(mediaSourceIndex)?.id!!, mediaSourceIndex: Int,
playbackPosition 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
) )
} }
} }