Add strm support (#66)
Co-authored-by: Jarne Demeulemeester <32322857+jarnedemeulemeester@users.noreply.github.com>
This commit is contained in:
parent
d7a47b0a3e
commit
8c5d0bebf0
4 changed files with 134 additions and 84 deletions
|
@ -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(
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -111,7 +134,16 @@ fun loadDownloadedEpisodes(context: Context): List<PlayerItem> {
|
||||||
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)
|
||||||
|
@ -156,10 +188,16 @@ 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,
|
||||||
|
@ -171,7 +209,8 @@ 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,
|
||||||
|
@ -185,29 +224,39 @@ 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
|
||||||
|
@ -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) {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue