Improve offline playback (#68)

* Fix download playback tracking bug

* Remove unused permission

* Add overview text to downloadmetadata

* Add visual indicator of whether item is downloaded

* Use downloaded item when available

* Fix "null" overview text in download metadata

* Fix crash when playing downloaded file with mpv

* Clean up

Co-authored-by: jarnedemeulemeester <jarnedemeulemeester@gmail.com>
This commit is contained in:
Jcuhfehl 2021-11-27 12:18:41 +01:00 committed by GitHub
parent 8c5d0bebf0
commit 598c11f299
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 141 additions and 92 deletions

View file

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M21,15v4a2,2 0,0 1,-2 2H5a2,2 0,0 1,-2 -2v-4"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/red"
android:strokeLineCap="round"/>
<path
android:pathData="M7,10l5,5l5,-5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/red"
android:strokeLineCap="round"/>
<path
android:pathData="M12,15L12,3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@color/red"
android:strokeLineCap="round"/>
</vector>

View file

@ -3,8 +3,6 @@
package="dev.jdtech.jellyfin"> package="dev.jdtech.jellyfin">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-feature android:name="android.software.leanback" android:required="false" /> <uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" /> <uses-feature android:name="android.hardware.touchscreen" android:required="false" />

View file

@ -13,6 +13,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.databinding.ActivityMainAppBinding import dev.jdtech.jellyfin.databinding.ActivityMainAppBinding
import dev.jdtech.jellyfin.fragments.HomeFragmentDirections import dev.jdtech.jellyfin.fragments.HomeFragmentDirections
import dev.jdtech.jellyfin.utils.loadDownloadLocation
import dev.jdtech.jellyfin.viewmodels.MainViewModel import dev.jdtech.jellyfin.viewmodels.MainViewModel
@AndroidEntryPoint @AndroidEntryPoint
@ -57,6 +58,8 @@ class MainActivity : AppCompatActivity() {
if (destination.id == R.id.about_libraries_dest) binding.mainToolbar.title = getString(R.string.app_info) if (destination.id == R.id.about_libraries_dest) binding.mainToolbar.title = getString(R.string.app_info)
} }
loadDownloadLocation(applicationContext)
viewModel.navigateToAddServer.observe(this, { viewModel.navigateToAddServer.observe(this, {
if (it) { if (it) {
navController.navigate(HomeFragmentDirections.actionHomeFragmentToAddServerFragment()) navController.navigate(HomeFragmentDirections.actionHomeFragmentToAddServerFragment())

View file

@ -94,6 +94,15 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
binding.favoriteButton.setImageResource(drawable) binding.favoriteButton.setImageResource(drawable)
}) })
viewModel.downloaded.observe(viewLifecycleOwner, {
val drawable = when (it) {
true -> R.drawable.ic_download_filled
false -> R.drawable.ic_download
}
binding.downloadButton.setImageResource(drawable)
})
viewModel.downloadEpisode.observe(viewLifecycleOwner, { viewModel.downloadEpisode.observe(viewLifecycleOwner, {
if (it) { if (it) {
requestDownload(Uri.parse(viewModel.downloadRequestItem.uri), viewModel.downloadRequestItem, this) requestDownload(Uri.parse(viewModel.downloadRequestItem.uri), viewModel.downloadRequestItem, this)

View file

@ -129,6 +129,15 @@ class MediaInfoFragment : Fragment() {
binding.favoriteButton.setImageResource(drawable) binding.favoriteButton.setImageResource(drawable)
}) })
viewModel.downloaded.observe(viewLifecycleOwner, {
val drawable = when (it) {
true -> R.drawable.ic_download_filled
false -> R.drawable.ic_download
}
binding.downloadButton.setImageResource(drawable)
})
binding.trailerButton.setOnClickListener { binding.trailerButton.setOnClickListener {
if (viewModel.item.value?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener if (viewModel.item.value?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener
val intent = Intent( val intent = Intent(

View file

@ -14,5 +14,7 @@ data class DownloadMetadata(
val indexNumber: Int? = null, val indexNumber: Int? = null,
val playbackPosition: Long? = null, val playbackPosition: Long? = null,
val playedPercentage: Double? = null, val playedPercentage: Double? = null,
val seriesId: UUID? = null val seriesId: UUID? = null,
val played: Boolean? = null,
val overview: String? = null
) : Parcelable ) : Parcelable

View file

@ -1,17 +1,11 @@
package dev.jdtech.jellyfin.utils package dev.jdtech.jellyfin.utils
import android.Manifest
import android.app.DownloadManager import android.app.DownloadManager
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.models.DownloadMetadata import dev.jdtech.jellyfin.models.DownloadMetadata
import dev.jdtech.jellyfin.models.DownloadRequestItem import dev.jdtech.jellyfin.models.DownloadRequestItem
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
@ -22,46 +16,9 @@ import timber.log.Timber
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
var defaultStorage: File? = null
fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context: Fragment) { fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context: Fragment) {
// Storage permission for downloads isn't necessary from Android 10 onwards
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
@Suppress("MagicNumber")
Timber.d("REQUESTING PERMISSION")
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
)
} else {
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
if (!granted) {
context.requireContext().toast(R.string.download_no_storage_permission)
return
}
}
val defaultStorage = getDownloadLocation(context.requireContext())
Timber.d(defaultStorage.toString())
val downloadRequest = DownloadManager.Request(uri) val downloadRequest = DownloadManager.Request(uri)
.setTitle(downloadRequestItem.metadata.name) .setTitle(downloadRequestItem.metadata.name)
.setDescription("Downloading") .setDescription("Downloading")
@ -75,16 +32,13 @@ fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context:
) )
.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, context.requireContext())
createMetadataFile( createMetadataFile(
downloadRequestItem.metadata, downloadRequestItem.metadata,
downloadRequestItem.itemId, downloadRequestItem.itemId)
context.requireContext()
)
} }
private fun createMetadataFile(metadata: DownloadMetadata, itemId: UUID, context: Context) { private fun createMetadataFile(metadata: DownloadMetadata, itemId: UUID) {
val defaultStorage = getDownloadLocation(context)
val metadataFile = File(defaultStorage, "${itemId}.metadata") val metadataFile = File(defaultStorage, "${itemId}.metadata")
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
@ -100,6 +54,8 @@ private fun createMetadataFile(metadata: DownloadMetadata, itemId: UUID, context
out.println(metadata.playbackPosition.toString()) out.println(metadata.playbackPosition.toString())
out.println(metadata.playedPercentage.toString()) out.println(metadata.playedPercentage.toString())
out.println(metadata.seriesId.toString()) out.println(metadata.seriesId.toString())
out.println(metadata.played.toString())
out.println(if (metadata.overview != null) metadata.overview.replace("\n", "\\n") else "")
} }
} else if (metadata.type == "Movie") { } else if (metadata.type == "Movie") {
metadataFile.printWriter().use { out -> metadataFile.printWriter().use { out ->
@ -108,13 +64,14 @@ private fun createMetadataFile(metadata: DownloadMetadata, itemId: UUID, context
out.println(metadata.name.toString()) out.println(metadata.name.toString())
out.println(metadata.playbackPosition.toString()) out.println(metadata.playbackPosition.toString())
out.println(metadata.playedPercentage.toString()) out.println(metadata.playedPercentage.toString())
out.println(metadata.played.toString())
out.println(if (metadata.overview != null) metadata.overview.replace("\n", "\\n") else "")
} }
} }
} }
private fun downloadFile(request: DownloadManager.Request, downloadMethod: Int, context: Context) { private fun downloadFile(request: DownloadManager.Request, context: Context) {
require(downloadMethod >= 0) { "Download method hasn't been set" }
request.apply { request.apply {
setAllowedOverMetered(false) setAllowedOverMetered(false)
setAllowedOverRoaming(false) setAllowedOverRoaming(false)
@ -122,13 +79,12 @@ private fun downloadFile(request: DownloadManager.Request, downloadMethod: Int,
context.getSystemService<DownloadManager>()?.enqueue(request) context.getSystemService<DownloadManager>()?.enqueue(request)
} }
private fun getDownloadLocation(context: Context): File? { fun loadDownloadLocation(context: Context) {
return context.getExternalFilesDir(Environment.DIRECTORY_MOVIES) defaultStorage = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES)
} }
fun loadDownloadedEpisodes(context: Context): List<PlayerItem> { fun loadDownloadedEpisodes(): List<PlayerItem> {
val items = mutableListOf<PlayerItem>() val items = mutableListOf<PlayerItem>()
val defaultStorage = getDownloadLocation(context)
defaultStorage?.walk()?.forEach { defaultStorage?.walk()?.forEach {
if (it.isFile && it.extension == "") { if (it.isFile && it.extension == "") {
try { try {
@ -154,6 +110,29 @@ fun loadDownloadedEpisodes(context: Context): List<PlayerItem> {
return items.toList() return items.toList()
} }
fun itemIsDownloaded(itemId: UUID): Boolean {
val file = File(defaultStorage!!, itemId.toString())
if (file.isFile && file.extension == "") {
if (File(defaultStorage, "${itemId}.metadata").exists()){
return true
}
}
return false
}
fun getDownloadPlayerItem(itemId: UUID): PlayerItem? {
val file = File(defaultStorage!!, itemId.toString())
try{
val metadataFile = File(defaultStorage, "${file.name}.metadata").readLines()
val metadata = parseMetadataFile(metadataFile)
return PlayerItem(metadata.name, UUID.fromString(file.name), "", metadata.playbackPosition!!, file.absolutePath, metadata)
} catch (e: Exception) {
file.delete()
Timber.e(e)
}
return null
}
fun deleteDownloadedEpisode(uri: String) { fun deleteDownloadedEpisode(uri: String) {
try { try {
File(uri).delete() File(uri).delete()
@ -175,7 +154,7 @@ fun postDownloadPlaybackProgress(uri: String, playbackPosition: Long, playedPerc
metadataArray[3] = playbackPosition.toString() metadataArray[3] = playbackPosition.toString()
metadataArray[4] = playedPercentage.toString() metadataArray[4] = playedPercentage.toString()
} }
Timber.d("PLAYEDPERCENTAGE $playedPercentage")
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
metadataFile.printWriter().use { out -> metadataFile.printWriter().use { out ->
metadataArray.forEach { metadataArray.forEach {
@ -204,7 +183,8 @@ fun downloadMetadataToBaseItemDto(metadata: DownloadMetadata): BaseItemDto {
parentIndexNumber = metadata.parentIndexNumber, parentIndexNumber = metadata.parentIndexNumber,
indexNumber = metadata.indexNumber, indexNumber = metadata.indexNumber,
userData = userData, userData = userData,
seriesId = metadata.seriesId seriesId = metadata.seriesId,
overview = metadata.overview
) )
} }
@ -218,7 +198,9 @@ fun baseItemDtoToDownloadMetadata(item: BaseItemDto): DownloadMetadata {
indexNumber = item.indexNumber, indexNumber = item.indexNumber,
playbackPosition = item.userData?.playbackPositionTicks ?: 0, playbackPosition = item.userData?.playbackPositionTicks ?: 0,
playedPercentage = item.userData?.playedPercentage, playedPercentage = item.userData?.playedPercentage,
seriesId = item.seriesId seriesId = item.seriesId,
played = item.userData?.played,
overview = item.overview
) )
} }
@ -237,7 +219,9 @@ fun parseMetadataFile(metadataFile: List<String>): DownloadMetadata {
} else { } else {
metadataFile[7].toDouble() metadataFile[7].toDouble()
}, },
seriesId = UUID.fromString(metadataFile[8]) seriesId = UUID.fromString(metadataFile[8]),
played = metadataFile[9].toBoolean(),
overview = metadataFile[10].replace("\\n", "\n")
) )
} else { } else {
return DownloadMetadata( return DownloadMetadata(
@ -250,12 +234,14 @@ fun parseMetadataFile(metadataFile: List<String>): DownloadMetadata {
} else { } else {
metadataFile[4].toDouble() metadataFile[4].toDouble()
}, },
played = metadataFile[5].toBoolean(),
overview = metadataFile[6].replace("\\n", "\n")
) )
} }
} }
suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context: Context) { suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository) {
val items = loadDownloadedEpisodes(context) val items = loadDownloadedEpisodes()
items.forEach() { items.forEach() {
try { try {
val localPlaybackProgress = it.metadata?.playbackPosition val localPlaybackProgress = it.metadata?.playbackPosition
@ -268,6 +254,10 @@ suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository, context
var playbackProgress: Long = 0 var playbackProgress: Long = 0
var playedPercentage = 0.0 var playedPercentage = 0.0
if (it.metadata?.played == true || item.userData?.played == true){
return@forEach
}
if (localPlaybackProgress != null) { if (localPlaybackProgress != null) {
if (localPlaybackProgress > playbackProgress) { if (localPlaybackProgress > playbackProgress) {
playbackProgress = localPlaybackProgress playbackProgress = localPlaybackProgress

View file

@ -1,28 +1,18 @@
package dev.jdtech.jellyfin.viewmodels package dev.jdtech.jellyfin.viewmodels
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.models.DownloadSection import dev.jdtech.jellyfin.models.DownloadSection
import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.loadDownloadedEpisodes import dev.jdtech.jellyfin.utils.loadDownloadedEpisodes
import dev.jdtech.jellyfin.utils.postDownloadPlaybackProgress
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject
@HiltViewModel class DownloadViewModel : ViewModel() {
class DownloadViewModel
@Inject
constructor(
private val application: Application,
) : ViewModel() {
private val _downloadSections = MutableLiveData<List<DownloadSection>>() private val _downloadSections = MutableLiveData<List<DownloadSection>>()
val downloadSections: LiveData<List<DownloadSection>> = _downloadSections val downloadSections: LiveData<List<DownloadSection>> = _downloadSections
@ -42,7 +32,7 @@ constructor(
_finishedLoading.value = false _finishedLoading.value = false
viewModelScope.launch { viewModelScope.launch {
try { try {
val items = loadDownloadedEpisodes(application) val items = loadDownloadedEpisodes()
if (items.isEmpty()) { if (items.isEmpty()) {
_downloadSections.value = listOf() _downloadSections.value = listOf()
_finishedLoading.value = true _finishedLoading.value = true

View file

@ -13,6 +13,7 @@ import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.baseItemDtoToDownloadMetadata import dev.jdtech.jellyfin.utils.baseItemDtoToDownloadMetadata
import dev.jdtech.jellyfin.utils.deleteDownloadedEpisode import dev.jdtech.jellyfin.utils.deleteDownloadedEpisode
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
import dev.jdtech.jellyfin.utils.itemIsDownloaded
import kotlinx.coroutines.launch 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
@ -46,6 +47,9 @@ constructor(
private val _favorite = MutableLiveData<Boolean>() private val _favorite = MutableLiveData<Boolean>()
val favorite: LiveData<Boolean> = _favorite val favorite: LiveData<Boolean> = _favorite
private val _downloaded = MutableLiveData<Boolean>()
val downloaded: LiveData<Boolean> = _downloaded
private val _downloadEpisode = MutableLiveData<Boolean>() private val _downloadEpisode = MutableLiveData<Boolean>()
val downloadEpisode: LiveData<Boolean> = _downloadEpisode val downloadEpisode: LiveData<Boolean> = _downloadEpisode
@ -56,6 +60,7 @@ constructor(
fun loadEpisode(episodeId: UUID) { fun loadEpisode(episodeId: UUID) {
viewModelScope.launch { viewModelScope.launch {
try { try {
_downloaded.value = itemIsDownloaded(episodeId)
val item = jellyfinRepository.getItem(episodeId) val item = jellyfinRepository.getItem(episodeId)
_item.value = item _item.value = item
_runTime.value = "${item.runTimeTicks?.div(600000000)} min" _runTime.value = "${item.runTimeTicks?.div(600000000)} min"
@ -129,5 +134,6 @@ constructor(
fun doneDownloadEpisode() { fun doneDownloadEpisode() {
_downloadEpisode.value = false _downloadEpisode.value = false
_downloaded.value = true
} }
} }

View file

@ -27,7 +27,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject internal constructor( class HomeViewModel @Inject internal constructor(
private val application: Application, application: Application,
private val repository: JellyfinRepository private val repository: JellyfinRepository
) : ViewModel() { ) : ViewModel() {
@ -67,7 +67,7 @@ class HomeViewModel @Inject internal constructor(
views.postValue(updated) views.postValue(updated)
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
syncPlaybackProgress(repository, application) syncPlaybackProgress(repository)
} }
state.tryEmit(Loading(inProgress = false)) state.tryEmit(Loading(inProgress = false))
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -12,6 +12,7 @@ import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.baseItemDtoToDownloadMetadata import dev.jdtech.jellyfin.utils.baseItemDtoToDownloadMetadata
import dev.jdtech.jellyfin.utils.deleteDownloadedEpisode import dev.jdtech.jellyfin.utils.deleteDownloadedEpisode
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
import dev.jdtech.jellyfin.utils.itemIsDownloaded
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -61,6 +62,9 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
private val _favorite = MutableLiveData<Boolean>() private val _favorite = MutableLiveData<Boolean>()
val favorite: LiveData<Boolean> = _favorite val favorite: LiveData<Boolean> = _favorite
private val _downloaded = MutableLiveData<Boolean>()
val downloaded: LiveData<Boolean> = _downloaded
private val _error = MutableLiveData<String>() private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error val error: LiveData<String> = _error
@ -75,6 +79,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
_error.value = null _error.value = null
viewModelScope.launch { viewModelScope.launch {
try { try {
_downloaded.value = itemIsDownloaded(itemId)
_item.value = jellyfinRepository.getItem(itemId) _item.value = jellyfinRepository.getItem(itemId)
_actors.value = getActors(_item.value!!) _actors.value = getActors(_item.value!!)
_director.value = getDirector(_item.value!!) _director.value = getDirector(_item.value!!)
@ -201,5 +206,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
fun doneDownloadMedia() { fun doneDownloadMedia() {
_downloadMedia.value = false _downloadMedia.value = false
_downloaded.value = true
} }
} }

View file

@ -107,6 +107,7 @@ constructor(
item.mediaSourceUri.isNotEmpty() -> item.mediaSourceUri item.mediaSourceUri.isNotEmpty() -> item.mediaSourceUri
else -> jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId) else -> jellyfinRepository.getStreamUrl(item.itemId, item.mediaSourceId)
} }
playFromDownloads = item.mediaSourceUri.isNotEmpty()
Timber.d("Stream url: $streamUrl") Timber.d("Stream url: $streamUrl")
val mediaItem = val mediaItem =
@ -156,17 +157,17 @@ constructor(
override fun run() { override fun run() {
viewModelScope.launch { viewModelScope.launch {
if (player.currentMediaItem != null) { if (player.currentMediaItem != null) {
try {
jellyfinRepository.postPlaybackProgress(
UUID.fromString(player.currentMediaItem!!.mediaId),
player.currentPosition.times(10000),
!player.isPlaying
)
} catch (e: Exception) {
Timber.e(e)
}
if(playFromDownloads){ if(playFromDownloads){
postDownloadPlaybackProgress(items[0].mediaSourceUri, player.currentPosition, (player.currentPosition.toDouble()/player.duration.toDouble()).times(100)) //TODO Automaticcaly use the correct item postDownloadPlaybackProgress(items[0].mediaSourceUri, player.currentPosition, (player.currentPosition.toDouble()/player.duration.toDouble()).times(100)) //TODO Automatically use the correct item
}
try {
jellyfinRepository.postPlaybackProgress(
UUID.fromString(player.currentMediaItem!!.mediaId),
player.currentPosition.times(10000),
!player.isPlaying
)
} catch (e: Exception) {
Timber.e(e)
} }
} }
} }

View file

@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.getDownloadPlayerItem
import dev.jdtech.jellyfin.utils.itemIsDownloaded
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -35,8 +37,15 @@ class PlayerViewModel @Inject internal constructor(
fun loadPlayerItems( fun loadPlayerItems(
item: BaseItemDto, item: BaseItemDto,
mediaSourceIndex: Int = 0, mediaSourceIndex: Int = 0,
onVersionSelectRequired: () -> Unit = { Unit } onVersionSelectRequired: () -> Unit = { }
) { ) {
if (itemIsDownloaded(item.id)) {
val playerItem = getDownloadPlayerItem(item.id)
if (playerItem != null) {
loadOfflinePlayerItems(playerItem)
return
}
}
Timber.d("Loading player items for item ${item.id}") Timber.d("Loading player items for item ${item.id}")
if (item.mediaSources.orEmpty().size > 1) { if (item.mediaSources.orEmpty().size > 1) {
onVersionSelectRequired() onVersionSelectRequired()

View file

@ -79,7 +79,6 @@
<string name="force_software_decoding">Force software decoding</string> <string name="force_software_decoding">Force software decoding</string>
<string name="force_software_decoding_summary">Disable hardware decoding and use software decoding. Can be useful if hardware decoding gives weird artifacts.</string> <string name="force_software_decoding_summary">Disable hardware decoding and use software decoding. Can be useful if hardware decoding gives weird artifacts.</string>
<string name="download_button_description">Download</string> <string name="download_button_description">Download</string>
<string name="download_no_storage_permission">Cannot download files without storage permissions</string>
<string name="delete_button_description">Delete</string> <string name="delete_button_description">Delete</string>
<string name="person_detail_title">Person Detail</string> <string name="person_detail_title">Person Detail</string>
<string name="error_getting_person_id">Detail unavailable</string> <string name="error_getting_person_id">Detail unavailable</string>