Save downloads metadata to database (#81)

* Change downloads from metadata files to room database (WIP)

* Disable download progress

* Add file available check + clean up
This commit is contained in:
Jarne Demeulemeester 2022-01-21 17:34:50 +01:00 committed by GitHub
parent b9e5c3b9ba
commit 4b2dd6c672
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 276 additions and 214 deletions

View file

@ -8,6 +8,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.databinding.HomeEpisodeItemBinding import dev.jdtech.jellyfin.databinding.HomeEpisodeItemBinding
import dev.jdtech.jellyfin.models.ContentType
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
import timber.log.Timber import timber.log.Timber
@ -16,18 +17,18 @@ class DownloadEpisodeListAdapter(private val onClickListener: OnClickListener) :
class EpisodeViewHolder(private var binding: HomeEpisodeItemBinding) : class EpisodeViewHolder(private var binding: HomeEpisodeItemBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(episode: PlayerItem) { fun bind(episode: PlayerItem) {
val metadata = episode.metadata!! val metadata = episode.item!!
binding.episode = downloadMetadataToBaseItemDto(episode.metadata) binding.episode = downloadMetadataToBaseItemDto(episode.item)
if (metadata.playedPercentage != null) { if (metadata.playedPercentage != null) {
binding.progressBar.layoutParams.width = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, binding.progressBar.layoutParams.width = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
(metadata.playedPercentage.times(2.24)).toFloat(), binding.progressBar.context.resources.displayMetrics).toInt() (metadata.playedPercentage.times(2.24)).toFloat(), binding.progressBar.context.resources.displayMetrics).toInt()
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
} }
if (metadata.type == "Movie") { if (metadata.type == ContentType.MOVIE) {
binding.primaryName.text = metadata.name binding.primaryName.text = metadata.name
Timber.d(metadata.name) Timber.d(metadata.name)
binding.secondaryName.visibility = View.GONE binding.secondaryName.visibility = View.GONE
} else if (metadata.type == "Episode") { } else if (metadata.type == ContentType.EPISODE) {
binding.primaryName.text = metadata.seriesName binding.primaryName.text = metadata.seriesName
} }
binding.executePendingBindings() binding.executePendingBindings()

View file

@ -8,6 +8,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.databinding.BaseItemBinding import dev.jdtech.jellyfin.databinding.BaseItemBinding
import dev.jdtech.jellyfin.models.ContentType
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
@ -20,9 +21,9 @@ class DownloadViewItemListAdapter(
class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) : class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(item: PlayerItem, fixedWidth: Boolean) { fun bind(item: PlayerItem, fixedWidth: Boolean) {
val metadata = item.metadata!! val metadata = item.item!!
binding.item = downloadMetadataToBaseItemDto(metadata) binding.item = downloadMetadataToBaseItemDto(metadata)
binding.itemName.text = if (metadata.type == "Episode") metadata.seriesName else item.name binding.itemName.text = if (metadata.type == ContentType.EPISODE) metadata.seriesName else item.name
binding.itemCount.visibility = View.GONE binding.itemCount.visibility = View.GONE
if (fixedWidth) { if (fixedWidth) {
binding.itemLayout.layoutParams.width = binding.itemLayout.layoutParams.width =

View file

@ -0,0 +1,16 @@
package dev.jdtech.jellyfin.database
import androidx.room.TypeConverter
import java.util.*
class Converters {
@TypeConverter
fun fromStringToUUID(value: String?): UUID? {
return value?.let { UUID.fromString(it) }
}
@TypeConverter
fun fromUUIDToString(value: UUID?): String? {
return value?.toString()
}
}

View file

@ -0,0 +1,16 @@
package dev.jdtech.jellyfin.database
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import dev.jdtech.jellyfin.models.DownloadItem
@Database(
entities = [DownloadItem::class],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class DownloadDatabase : RoomDatabase() {
abstract val downloadDatabaseDao: DownloadDatabaseDao
}

View file

@ -0,0 +1,29 @@
package dev.jdtech.jellyfin.database
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import dev.jdtech.jellyfin.models.DownloadItem
import java.util.*
@Dao
interface DownloadDatabaseDao {
@Insert()
fun insertItem(downloadItem: DownloadItem)
@Query("select * from downloads where id = :id limit 1")
fun loadItem(id: UUID): DownloadItem?
@Query("select * from downloads")
fun loadItems(): List<DownloadItem>
@Query("delete from downloads where id = :id")
fun deleteItem(id: UUID)
@Query("update downloads set playbackPosition = :playbackPosition, playedPercentage = :playedPercentage where id = :id")
fun updatePlaybackPosition(id: UUID, playbackPosition: Long, playedPercentage: Double)
@Query("update downloads set downloadId = :downloadId where id = :id")
fun updateDownloadId(id: UUID, downloadId: Long)
}

View file

@ -7,6 +7,8 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dev.jdtech.jellyfin.database.DownloadDatabase
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.database.ServerDatabase import dev.jdtech.jellyfin.database.ServerDatabase
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import javax.inject.Singleton import javax.inject.Singleton
@ -27,4 +29,18 @@ object DatabaseModule {
.build() .build()
.serverDatabaseDao .serverDatabaseDao
} }
@Singleton
@Provides
fun provideDownloadDatabaseDao(@ApplicationContext app: Context): DownloadDatabaseDao {
return Room.databaseBuilder(
app.applicationContext,
DownloadDatabase::class.java,
"downloads"
)
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
.downloadDatabaseDao
}
} }

View file

@ -99,7 +99,7 @@ class DownloadFragment : Fragment() {
DownloadFragmentDirections.actionDownloadFragmentToMediaInfoFragment( DownloadFragmentDirections.actionDownloadFragmentToMediaInfoFragment(
UUID.randomUUID(), UUID.randomUUID(),
item.name, item.name,
item.metadata?.type ?: "Unknown", item.item?.type?.type ?: "Unkown",
item, item,
isOffline = true isOffline = true
) )

View file

@ -74,7 +74,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
} }
} }
if(!args.isOffline) { if (!args.isOffline) {
val episodeId: UUID = args.episodeId val episodeId: UUID = args.episodeId
binding.checkButton.setOnClickListener { binding.checkButton.setOnClickListener {
@ -106,8 +106,9 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
binding.downloadButton.setOnClickListener { binding.downloadButton.setOnClickListener {
binding.downloadButton.isEnabled = false binding.downloadButton.isEnabled = false
viewModel.loadDownloadRequestItem(episodeId) viewModel.loadDownloadRequestItem(episodeId)
binding.downloadButton.setImageResource(android.R.color.transparent) binding.downloadButton.setImageResource(R.drawable.ic_download_filled)
binding.progressDownload.isVisible = true //binding.downloadButton.setImageResource(android.R.color.transparent)
//binding.progressDownload.isVisible = true
} }
binding.deleteButton.isVisible = false binding.deleteButton.isVisible = false
@ -142,6 +143,9 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
binding.progressBar.isVisible = true binding.progressBar.isVisible = true
} }
binding.playButton.isEnabled = available
binding.playButton.alpha = if (!available) 0.5F else 1.0F
// Check icon // Check icon
val checkDrawable = when (played) { val checkDrawable = when (played) {
true -> R.drawable.ic_check_filled true -> R.drawable.ic_check_filled
@ -163,7 +167,14 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
} }
binding.downloadButton.setImageResource(downloadDrawable) binding.downloadButton.setImageResource(downloadDrawable)
binding.episodeName.text = String.format(getString(R.string.episode_name_extended), episode.parentIndexNumber, episode.indexNumber, episode.name) binding.downloadButton.isEnabled = !downloaded
binding.episodeName.text = String.format(
getString(R.string.episode_name_extended),
episode.parentIndexNumber,
episode.indexNumber,
episode.name
)
binding.overview.text = episode.overview binding.overview.text = episode.overview
binding.year.text = dateString binding.year.text = dateString
binding.playtime.text = runTime binding.playtime.text = runTime

View file

@ -165,7 +165,9 @@ class MediaInfoFragment : Fragment() {
} }
binding.downloadButton.setOnClickListener { binding.downloadButton.setOnClickListener {
binding.downloadButton.isEnabled = false
viewModel.loadDownloadRequestItem(args.itemId) viewModel.loadDownloadRequestItem(args.itemId)
binding.downloadButton.setImageResource(R.drawable.ic_download_filled)
} }
binding.deleteButton.isVisible = false binding.deleteButton.isVisible = false
@ -190,6 +192,9 @@ class MediaInfoFragment : Fragment() {
binding.communityRating.isVisible = item.communityRating != null binding.communityRating.isVisible = item.communityRating != null
binding.actors.isVisible = actors.isNotEmpty() binding.actors.isVisible = actors.isNotEmpty()
binding.playButton.isEnabled = available
binding.playButton.alpha = if (!available) 0.5F else 1.0F
// Check icon // Check icon
val checkDrawable = when (played) { val checkDrawable = when (played) {
true -> R.drawable.ic_check_filled true -> R.drawable.ic_check_filled
@ -204,6 +209,8 @@ class MediaInfoFragment : Fragment() {
} }
binding.favoriteButton.setImageResource(favoriteDrawable) binding.favoriteButton.setImageResource(favoriteDrawable)
binding.downloadButton.isEnabled = !downloaded
// Download icon // Download icon
val downloadDrawable = when (downloaded) { val downloadDrawable = when (downloaded) {
true -> R.drawable.ic_download_filled true -> R.drawable.ic_download_filled

View file

@ -1,20 +1,25 @@
package dev.jdtech.jellyfin.models package dev.jdtech.jellyfin.models
import android.os.Parcelable import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.util.* import java.util.*
@Parcelize @Parcelize
data class DownloadMetadata( @Entity(tableName = "downloads")
data class DownloadItem(
@PrimaryKey
val id: UUID, val id: UUID,
val type: String?, val type: ContentType,
val name: String,
val played: Boolean,
val overview: String? = null,
val seriesId: UUID? = null,
val seriesName: String? = null, val seriesName: String? = null,
val name: String? = null,
val parentIndexNumber: Int? = null,
val indexNumber: Int? = null, val indexNumber: Int? = null,
val parentIndexNumber: Int? = null,
val playbackPosition: Long? = null, val playbackPosition: Long? = null,
val playedPercentage: Double? = null, val playedPercentage: Double? = null,
val seriesId: UUID? = null, val downloadId: Long? = null,
val played: Boolean? = null,
val overview: String? = null
) : Parcelable ) : Parcelable

View file

@ -8,5 +8,5 @@ import java.util.*
data class DownloadRequestItem( data class DownloadRequestItem(
val uri: String, val uri: String,
val itemId: UUID, val itemId: UUID,
val metadata: DownloadMetadata val item: DownloadItem
) : Parcelable ) : Parcelable

View file

@ -11,5 +11,5 @@ data class PlayerItem(
val mediaSourceId: String, val mediaSourceId: String,
val playbackPosition: Long, val playbackPosition: Long,
val mediaSourceUri: String = "", val mediaSourceUri: String = "",
val metadata: DownloadMetadata? = null val item: DownloadItem? = null
) : Parcelable ) : Parcelable

View file

@ -5,7 +5,8 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import dev.jdtech.jellyfin.models.DownloadMetadata import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.DownloadItem
import dev.jdtech.jellyfin.models.DownloadRequestItem import dev.jdtech.jellyfin.models.DownloadRequestItem
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
@ -17,9 +18,14 @@ import java.util.UUID
var defaultStorage: File? = null var defaultStorage: File? = null
fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context: Context) { fun requestDownload(
downloadDatabase: DownloadDatabaseDao,
uri: Uri,
downloadRequestItem: DownloadRequestItem,
context: Context
) {
val downloadRequest = DownloadManager.Request(uri) val downloadRequest = DownloadManager.Request(uri)
.setTitle(downloadRequestItem.metadata.name) .setTitle(downloadRequestItem.item.name)
.setDescription("Downloading") .setDescription("Downloading")
.setDestinationUri( .setDestinationUri(
Uri.fromFile( Uri.fromFile(
@ -30,101 +36,68 @@ 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())
downloadFile(downloadRequest, context)
createMetadataFile(
downloadRequestItem.metadata,
downloadRequestItem.itemId)
}
private fun createMetadataFile(metadata: DownloadMetadata, itemId: UUID) { try {
val metadataFile = File(defaultStorage, "${itemId}.metadata") downloadDatabase.insertItem(downloadRequestItem.item)
if (!File(defaultStorage, downloadRequestItem.itemId.toString()).exists()) {
metadataFile.writeText("") //This might be necessary to make sure that the metadata file is empty val downloadId = downloadFile(downloadRequest, context)
Timber.d("$downloadId")
if (metadata.type == "Episode") { downloadDatabase.updateDownloadId(downloadRequestItem.itemId, downloadId)
metadataFile.printWriter().use { out ->
out.println(metadata.id)
out.println(metadata.type.toString())
out.println(metadata.seriesName.toString())
out.println(metadata.name.toString())
out.println(metadata.parentIndexNumber.toString())
out.println(metadata.indexNumber.toString())
out.println(metadata.playbackPosition.toString())
out.println(metadata.playedPercentage.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") {
metadataFile.printWriter().use { out ->
out.println(metadata.id)
out.println(metadata.type.toString())
out.println(metadata.name.toString())
out.println(metadata.playbackPosition.toString())
out.println(metadata.playedPercentage.toString())
out.println(metadata.played.toString())
out.println(if (metadata.overview != null) metadata.overview.replace("\n", "\\n") else "")
} }
} catch (e: Exception) {
Timber.e(e)
} }
} }
private fun downloadFile(request: DownloadManager.Request, context: Context) { private fun downloadFile(request: DownloadManager.Request, context: Context): Long {
request.apply { request.apply {
setAllowedOverMetered(false) setAllowedOverMetered(false)
setAllowedOverRoaming(false) setAllowedOverRoaming(false)
} }
context.getSystemService<DownloadManager>()?.enqueue(request) return context.getSystemService<DownloadManager>()!!.enqueue(request)
} }
fun loadDownloadLocation(context: Context) { fun loadDownloadLocation(context: Context) {
defaultStorage = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES) defaultStorage = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES)
} }
fun loadDownloadedEpisodes(): List<PlayerItem> { fun loadDownloadedEpisodes(downloadDatabase: DownloadDatabaseDao): List<PlayerItem> {
val items = mutableListOf<PlayerItem>() val items = downloadDatabase.loadItems()
defaultStorage?.walk()?.forEach { return items.map {
if (it.isFile && it.extension == "") { PlayerItem(
try { name = it.name,
val metadataFile = File(defaultStorage, "${it.name}.metadata").readLines() itemId = it.id,
val metadata = parseMetadataFile(metadataFile) mediaSourceId = "",
items.add( playbackPosition = it.playbackPosition ?: 0,
PlayerItem( mediaSourceUri = File(defaultStorage, it.id.toString()).absolutePath,
name = metadata.name, item = it
itemId = UUID.fromString(it.name), )
mediaSourceId = "",
playbackPosition = metadata.playbackPosition!!,
mediaSourceUri = it.absolutePath,
metadata = metadata
)
)
} catch (e: Exception) {
it.delete()
Timber.e(e)
}
}
} }
return items.toList()
} }
fun itemIsDownloaded(itemId: UUID): Boolean { fun isItemAvailable(itemId: UUID): Boolean {
val file = File(defaultStorage!!, itemId.toString()) return File(defaultStorage, itemId.toString()).exists()
if (file.isFile && file.extension == "") {
if (File(defaultStorage, "${itemId}.metadata").exists()){
return true
}
}
return false
} }
fun getDownloadPlayerItem(itemId: UUID): PlayerItem? { fun isItemDownloaded(downloadDatabaseDao: DownloadDatabaseDao, itemId: UUID): Boolean {
val item = downloadDatabaseDao.loadItem(itemId)
return item != null
}
fun getDownloadPlayerItem(downloadDatabase: DownloadDatabaseDao, itemId: UUID): PlayerItem? {
val file = File(defaultStorage!!, itemId.toString()) val file = File(defaultStorage!!, itemId.toString())
try{ try {
val metadataFile = File(defaultStorage, "${file.name}.metadata").readLines() val metadata = downloadDatabase.loadItem(itemId)
val metadata = parseMetadataFile(metadataFile) if (metadata != null) {
return PlayerItem(metadata.name, UUID.fromString(file.name), "", metadata.playbackPosition!!, file.absolutePath, metadata) return PlayerItem(
metadata.name,
UUID.fromString(file.name),
"",
metadata.playbackPosition!!,
file.absolutePath,
metadata
)
}
} catch (e: Exception) { } catch (e: Exception) {
file.delete() file.delete()
Timber.e(e) Timber.e(e)
@ -132,119 +105,76 @@ fun getDownloadPlayerItem(itemId: UUID): PlayerItem? {
return null return null
} }
fun deleteDownloadedEpisode(uri: String) { fun deleteDownloadedEpisode(downloadDatabase: DownloadDatabaseDao, itemId: UUID) {
try { try {
File(uri).delete() downloadDatabase.deleteItem(itemId)
File("${uri}.metadata").delete() File(defaultStorage, itemId.toString()).delete()
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
} }
} }
fun postDownloadPlaybackProgress(uri: String, playbackPosition: Long, playedPercentage: Double) { fun postDownloadPlaybackProgress(
downloadDatabase: DownloadDatabaseDao,
itemId: UUID,
playbackPosition: Long,
playedPercentage: Double
) {
try { try {
val metadataFile = File("${uri}.metadata") downloadDatabase.updatePlaybackPosition(itemId, playbackPosition, playedPercentage)
val metadataArray = metadataFile.readLines().toMutableList()
if (metadataArray[1] == "Episode") {
metadataArray[6] = playbackPosition.toString()
metadataArray[7] = playedPercentage.toString()
} else if (metadataArray[1] == "Movie") {
metadataArray[3] = playbackPosition.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.printWriter().use { out ->
metadataArray.forEach {
out.println(it)
}
}
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
} }
} }
fun downloadMetadataToBaseItemDto(metadata: DownloadMetadata): BaseItemDto { fun downloadMetadataToBaseItemDto(item: DownloadItem): BaseItemDto {
val userData = UserItemDataDto( val userData = UserItemDataDto(
playbackPositionTicks = metadata.playbackPosition ?: 0, playbackPositionTicks = item.playbackPosition ?: 0,
playedPercentage = metadata.playedPercentage, playedPercentage = item.playedPercentage,
isFavorite = false, isFavorite = false,
playCount = 0, playCount = 0,
played = false played = false
) )
return BaseItemDto( return BaseItemDto(
id = metadata.id,
type = metadata.type,
seriesName = metadata.seriesName,
name = metadata.name,
parentIndexNumber = metadata.parentIndexNumber,
indexNumber = metadata.indexNumber,
userData = userData,
seriesId = metadata.seriesId,
overview = metadata.overview
)
}
fun baseItemDtoToDownloadMetadata(item: BaseItemDto): DownloadMetadata {
return DownloadMetadata(
id = item.id, id = item.id,
type = item.type, type = item.type.type,
seriesName = item.seriesName, seriesName = item.seriesName,
name = item.name, name = item.name,
parentIndexNumber = item.parentIndexNumber, parentIndexNumber = item.parentIndexNumber,
indexNumber = item.indexNumber, indexNumber = item.indexNumber,
playbackPosition = item.userData?.playbackPositionTicks ?: 0, userData = userData,
playedPercentage = item.userData?.playedPercentage,
seriesId = item.seriesId, seriesId = item.seriesId,
played = item.userData?.played,
overview = item.overview overview = item.overview
) )
} }
fun parseMetadataFile(metadataFile: List<String>): DownloadMetadata { fun baseItemDtoToDownloadMetadata(item: BaseItemDto): DownloadItem {
if (metadataFile[1] == "Episode") { return DownloadItem(
return DownloadMetadata( id = item.id,
id = UUID.fromString(metadataFile[0]), type = item.contentType(),
type = metadataFile[1], name = item.name.orEmpty(),
seriesName = metadataFile[2], played = item.userData?.played ?: false,
name = metadataFile[3], seriesId = item.seriesId,
parentIndexNumber = metadataFile[4].toInt(), seriesName = item.seriesName,
indexNumber = metadataFile[5].toInt(), parentIndexNumber = item.parentIndexNumber,
playbackPosition = metadataFile[6].toLong(), indexNumber = item.indexNumber,
playedPercentage = if (metadataFile[7] == "null") { playbackPosition = item.userData?.playbackPositionTicks ?: 0,
null playedPercentage = item.userData?.playedPercentage,
} else { overview = item.overview
metadataFile[7].toDouble() )
},
seriesId = UUID.fromString(metadataFile[8]),
played = metadataFile[9].toBoolean(),
overview = metadataFile[10].replace("\\n", "\n")
)
} else {
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()
},
played = metadataFile[5].toBoolean(),
overview = metadataFile[6].replace("\\n", "\n")
)
}
} }
suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository) { suspend fun syncPlaybackProgress(
val items = loadDownloadedEpisodes() downloadDatabase: DownloadDatabaseDao,
jellyfinRepository: JellyfinRepository
) {
val items = loadDownloadedEpisodes(downloadDatabase)
items.forEach { items.forEach {
try { try {
val localPlaybackProgress = it.metadata?.playbackPosition val localPlaybackProgress = it.item?.playbackPosition
val localPlayedPercentage = it.metadata?.playedPercentage val localPlayedPercentage = it.item?.playedPercentage
val item = jellyfinRepository.getItem(it.itemId) val item = jellyfinRepository.getItem(it.itemId)
val remotePlaybackProgress = item.userData?.playbackPositionTicks?.div(10000) val remotePlaybackProgress = item.userData?.playbackPositionTicks?.div(10000)
@ -253,7 +183,7 @@ suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository) {
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){ if (it.item?.played == true || item.userData?.played == true) {
return@forEach return@forEach
} }
@ -270,8 +200,13 @@ suspend fun syncPlaybackProgress(jellyfinRepository: JellyfinRepository) {
} }
} }
if (playbackProgress != 0.toLong()) { if (playbackProgress != 0L) {
postDownloadPlaybackProgress(it.mediaSourceUri, playbackProgress, playedPercentage) postDownloadPlaybackProgress(
downloadDatabase,
it.itemId,
playbackProgress,
playedPercentage
)
jellyfinRepository.postPlaybackProgress( jellyfinRepository.postPlaybackProgress(
it.itemId, it.itemId,
playbackProgress.times(10000), playbackProgress.times(10000),

View file

@ -1,6 +1,9 @@
package dev.jdtech.jellyfin.viewmodels package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.* import androidx.lifecycle.*
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.ContentType
import dev.jdtech.jellyfin.models.DownloadSection import dev.jdtech.jellyfin.models.DownloadSection
import dev.jdtech.jellyfin.utils.loadDownloadedEpisodes import dev.jdtech.jellyfin.utils.loadDownloadedEpisodes
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -8,8 +11,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
import javax.inject.Inject
class DownloadViewModel : ViewModel() { @HiltViewModel
class DownloadViewModel
@Inject
constructor(
private val downloadDatabase: DownloadDatabaseDao,
) : ViewModel() {
private val uiState = MutableStateFlow<UiState>(UiState.Loading) private val uiState = MutableStateFlow<UiState>(UiState.Loading)
sealed class UiState { sealed class UiState {
@ -30,17 +39,17 @@ class DownloadViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
uiState.emit(UiState.Loading) uiState.emit(UiState.Loading)
try { try {
val items = loadDownloadedEpisodes() val items = loadDownloadedEpisodes(downloadDatabase)
if (items.isEmpty()) { if (items.isEmpty()) {
uiState.emit(UiState.Normal(emptyList())) uiState.emit(UiState.Normal(emptyList()))
return@launch //return@launch
} }
val downloadSections = mutableListOf<DownloadSection>() val downloadSections = mutableListOf<DownloadSection>()
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
DownloadSection( DownloadSection(
UUID.randomUUID(), UUID.randomUUID(),
"Episodes", "Episodes",
items.filter { it.metadata?.type == "Episode" }).let { items.filter { it.item?.type == ContentType.EPISODE }).let {
if (it.items.isNotEmpty()) downloadSections.add( if (it.items.isNotEmpty()) downloadSections.add(
it it
) )
@ -48,7 +57,7 @@ class DownloadViewModel : ViewModel() {
DownloadSection( DownloadSection(
UUID.randomUUID(), UUID.randomUUID(),
"Movies", "Movies",
items.filter { it.metadata?.type == "Movie" }).let { items.filter { it.item?.type == ContentType.MOVIE }).let {
if (it.items.isNotEmpty()) downloadSections.add( if (it.items.isNotEmpty()) downloadSections.add(
it it
) )

View file

@ -5,6 +5,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import androidx.lifecycle.* import androidx.lifecycle.*
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.DownloadRequestItem import dev.jdtech.jellyfin.models.DownloadRequestItem
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
@ -26,7 +27,8 @@ class EpisodeBottomSheetViewModel
@Inject @Inject
constructor( constructor(
private val application: Application, private val application: Application,
private val jellyfinRepository: JellyfinRepository private val jellyfinRepository: JellyfinRepository,
private val downloadDatabase: DownloadDatabaseDao
) : ViewModel() { ) : ViewModel() {
private val uiState = MutableStateFlow<UiState>(UiState.Loading) private val uiState = MutableStateFlow<UiState>(UiState.Loading)
@ -39,6 +41,7 @@ constructor(
val favorite: Boolean, val favorite: Boolean,
val downloaded: Boolean, val downloaded: Boolean,
val downloadEpisode: Boolean, val downloadEpisode: Boolean,
val available: Boolean,
) : UiState() ) : UiState()
object Loading : UiState() object Loading : UiState()
@ -56,6 +59,7 @@ constructor(
var favorite: Boolean = false var favorite: Boolean = false
private var downloaded: Boolean = false private var downloaded: Boolean = false
private var downloadEpisode: Boolean = false private var downloadEpisode: Boolean = false
private var available: Boolean = true
var playerItems: MutableList<PlayerItem> = mutableListOf() var playerItems: MutableList<PlayerItem> = mutableListOf()
private lateinit var downloadRequestItem: DownloadRequestItem private lateinit var downloadRequestItem: DownloadRequestItem
@ -70,7 +74,7 @@ constructor(
dateString = getDateString(tempItem) dateString = getDateString(tempItem)
played = tempItem.userData?.played == true played = tempItem.userData?.played == true
favorite = tempItem.userData?.isFavorite == true favorite = tempItem.userData?.isFavorite == true
downloaded = itemIsDownloaded(episodeId) downloaded = isItemDownloaded(downloadDatabase, episodeId)
uiState.emit( uiState.emit(
UiState.Normal( UiState.Normal(
tempItem, tempItem,
@ -79,7 +83,8 @@ constructor(
played, played,
favorite, favorite,
downloaded, downloaded,
downloadEpisode downloadEpisode,
available,
) )
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -92,7 +97,9 @@ constructor(
viewModelScope.launch { viewModelScope.launch {
uiState.emit(UiState.Loading) uiState.emit(UiState.Loading)
playerItems.add(playerItem) playerItems.add(playerItem)
item = downloadMetadataToBaseItemDto(playerItem.metadata!!) item = downloadMetadataToBaseItemDto(playerItem.item!!)
available = isItemAvailable(playerItem.itemId)
Timber.d("Available: $available")
uiState.emit( uiState.emit(
UiState.Normal( UiState.Normal(
item!!, item!!,
@ -101,7 +108,8 @@ constructor(
played, played,
favorite, favorite,
downloaded, downloaded,
downloadEpisode downloadEpisode,
available,
) )
) )
} }
@ -153,19 +161,17 @@ constructor(
fun loadDownloadRequestItem(itemId: UUID) { fun loadDownloadRequestItem(itemId: UUID) {
viewModelScope.launch { viewModelScope.launch {
//loadEpisode(itemId)
val episode = item val episode = item
val uri = jellyfinRepository.getStreamUrl(itemId, episode?.mediaSources?.get(0)?.id!!) val uri = jellyfinRepository.getStreamUrl(itemId, episode?.mediaSources?.get(0)?.id!!)
Timber.d(uri)
val metadata = baseItemDtoToDownloadMetadata(episode) val metadata = baseItemDtoToDownloadMetadata(episode)
downloadRequestItem = DownloadRequestItem(uri, itemId, metadata) downloadRequestItem = DownloadRequestItem(uri, itemId, metadata)
downloadEpisode = true downloadEpisode = true
requestDownload(Uri.parse(downloadRequestItem.uri), downloadRequestItem, application) requestDownload(downloadDatabase, Uri.parse(downloadRequestItem.uri), downloadRequestItem, application)
} }
} }
fun deleteEpisode() { fun deleteEpisode() {
deleteDownloadedEpisode(playerItems[0].mediaSourceUri) deleteDownloadedEpisode(downloadDatabase, playerItems[0].itemId)
} }
private fun getDateString(item: BaseItemDto): String { private fun getDateString(item: BaseItemDto): String {

View file

@ -9,6 +9,7 @@ import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.adapters.HomeItem import dev.jdtech.jellyfin.adapters.HomeItem
import dev.jdtech.jellyfin.adapters.HomeItem.Section import dev.jdtech.jellyfin.adapters.HomeItem.Section
import dev.jdtech.jellyfin.adapters.HomeItem.ViewItem import dev.jdtech.jellyfin.adapters.HomeItem.ViewItem
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.CollectionType import dev.jdtech.jellyfin.models.CollectionType
import dev.jdtech.jellyfin.models.HomeSection import dev.jdtech.jellyfin.models.HomeSection
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
@ -25,7 +26,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject internal constructor( class HomeViewModel @Inject internal constructor(
private val application: Application, private val application: Application,
private val repository: JellyfinRepository private val repository: JellyfinRepository,
private val downloadDatabase: DownloadDatabaseDao,
) : ViewModel() { ) : ViewModel() {
private val uiState = MutableStateFlow<UiState>(UiState.Loading) private val uiState = MutableStateFlow<UiState>(UiState.Loading)
@ -54,7 +56,7 @@ class HomeViewModel @Inject internal constructor(
val updated = loadDynamicItems() + loadViews() val updated = loadDynamicItems() + loadViews()
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
syncPlaybackProgress(repository) syncPlaybackProgress(downloadDatabase, repository)
} }
uiState.emit(UiState.Normal(updated)) uiState.emit(UiState.Normal(updated))
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -7,14 +7,11 @@ import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.DownloadRequestItem import dev.jdtech.jellyfin.models.DownloadRequestItem
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.baseItemDtoToDownloadMetadata import dev.jdtech.jellyfin.utils.*
import dev.jdtech.jellyfin.utils.deleteDownloadedEpisode
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
import dev.jdtech.jellyfin.utils.itemIsDownloaded
import dev.jdtech.jellyfin.utils.requestDownload
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -32,7 +29,8 @@ class MediaInfoViewModel
@Inject @Inject
constructor( constructor(
private val application: Application, private val application: Application,
private val jellyfinRepository: JellyfinRepository private val jellyfinRepository: JellyfinRepository,
private val downloadDatabase: DownloadDatabaseDao,
) : ViewModel() { ) : ViewModel() {
private val uiState = MutableStateFlow<UiState>(UiState.Loading) private val uiState = MutableStateFlow<UiState>(UiState.Loading)
@ -51,6 +49,7 @@ constructor(
val played: Boolean, val played: Boolean,
val favorite: Boolean, val favorite: Boolean,
val downloaded: Boolean, val downloaded: Boolean,
val available: Boolean,
) : UiState() ) : UiState()
object Loading : UiState() object Loading : UiState()
@ -75,6 +74,7 @@ constructor(
var favorite: Boolean = false var favorite: Boolean = false
private var downloaded: Boolean = false private var downloaded: Boolean = false
private var downloadMedia: Boolean = false private var downloadMedia: Boolean = false
private var available: Boolean = true
private lateinit var downloadRequestItem: DownloadRequestItem private lateinit var downloadRequestItem: DownloadRequestItem
@ -95,7 +95,7 @@ constructor(
dateString = getDateString(tempItem) dateString = getDateString(tempItem)
played = tempItem.userData?.played ?: false played = tempItem.userData?.played ?: false
favorite = tempItem.userData?.isFavorite ?: false favorite = tempItem.userData?.isFavorite ?: false
downloaded = itemIsDownloaded(itemId) downloaded = isItemDownloaded(downloadDatabase, itemId)
if (itemType == "Series") { if (itemType == "Series") {
nextUp = getNextUp(itemId) nextUp = getNextUp(itemId)
seasons = jellyfinRepository.getSeasons(itemId) seasons = jellyfinRepository.getSeasons(itemId)
@ -114,7 +114,8 @@ constructor(
seasons, seasons,
played, played,
favorite, favorite,
downloaded downloaded,
available
) )
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -128,7 +129,7 @@ constructor(
fun loadData(pItem: PlayerItem) { fun loadData(pItem: PlayerItem) {
viewModelScope.launch { viewModelScope.launch {
playerItem = pItem playerItem = pItem
val tempItem = downloadMetadataToBaseItemDto(playerItem.metadata!!) val tempItem = downloadMetadataToBaseItemDto(playerItem.item!!)
item = tempItem item = tempItem
actors = getActors(tempItem) actors = getActors(tempItem)
director = getDirector(tempItem) director = getDirector(tempItem)
@ -139,6 +140,7 @@ constructor(
dateString = "" dateString = ""
played = tempItem.userData?.played ?: false played = tempItem.userData?.played ?: false
favorite = tempItem.userData?.isFavorite ?: false favorite = tempItem.userData?.isFavorite ?: false
available = isItemAvailable(tempItem.id)
uiState.emit( uiState.emit(
UiState.Normal( UiState.Normal(
tempItem, tempItem,
@ -153,7 +155,8 @@ constructor(
seasons, seasons,
played, played,
favorite, favorite,
downloaded downloaded,
available
) )
) )
} }
@ -265,11 +268,11 @@ constructor(
val metadata = baseItemDtoToDownloadMetadata(downloadItem) val metadata = baseItemDtoToDownloadMetadata(downloadItem)
downloadRequestItem = DownloadRequestItem(uri, itemId, metadata) downloadRequestItem = DownloadRequestItem(uri, itemId, metadata)
downloadMedia = true downloadMedia = true
requestDownload(Uri.parse(downloadRequestItem.uri), downloadRequestItem, application) requestDownload(downloadDatabase, Uri.parse(downloadRequestItem.uri), downloadRequestItem, application)
} }
} }
fun deleteItem() { fun deleteItem() {
deleteDownloadedEpisode(playerItem.mediaSourceUri) deleteDownloadedEpisode(downloadDatabase, playerItem.itemId)
} }
} }

View file

@ -16,6 +16,7 @@ import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.mpv.MPVPlayer
import dev.jdtech.jellyfin.mpv.TrackType import dev.jdtech.jellyfin.mpv.TrackType
@ -32,7 +33,8 @@ class PlayerActivityViewModel
@Inject @Inject
constructor( constructor(
application: Application, application: Application,
private val jellyfinRepository: JellyfinRepository private val jellyfinRepository: JellyfinRepository,
private val downloadDatabase: DownloadDatabaseDao
) : ViewModel(), Player.Listener { ) : ViewModel(), Player.Listener {
val player: BasePlayer val player: BasePlayer
@ -156,9 +158,9 @@ constructor(
val runnable = object : Runnable { val runnable = object : Runnable {
override fun run() { override fun run() {
viewModelScope.launch { viewModelScope.launch {
if (player.currentMediaItem != null) { if (player.currentMediaItem != null && player.currentMediaItem!!.mediaId.isNotEmpty()) {
if(playFromDownloads){ if(playFromDownloads){
postDownloadPlaybackProgress(items[0].mediaSourceUri, player.currentPosition, (player.currentPosition.toDouble()/player.duration.toDouble()).times(100)) //TODO Automatically use the correct item postDownloadPlaybackProgress(downloadDatabase, items[0].itemId, player.currentPosition, (player.currentPosition.toDouble()/player.duration.toDouble()).times(100)) //TODO Automatically use the correct item
} }
try { try {
jellyfinRepository.postPlaybackProgress( jellyfinRepository.postPlaybackProgress(

View file

@ -4,10 +4,12 @@ import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.database.DownloadDatabaseDao
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.getDownloadPlayerItem
import dev.jdtech.jellyfin.utils.itemIsDownloaded import dev.jdtech.jellyfin.utils.isItemAvailable
import dev.jdtech.jellyfin.utils.isItemDownloaded
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
@ -21,7 +23,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class PlayerViewModel @Inject internal constructor( class PlayerViewModel @Inject internal constructor(
private val repository: JellyfinRepository private val repository: JellyfinRepository,
private val downloadDatabase: DownloadDatabaseDao
) : ViewModel() { ) : ViewModel() {
private val playerItems = MutableSharedFlow<PlayerItemState>( private val playerItems = MutableSharedFlow<PlayerItemState>(
@ -39,8 +42,8 @@ class PlayerViewModel @Inject internal constructor(
mediaSourceIndex: Int = 0, mediaSourceIndex: Int = 0,
onVersionSelectRequired: () -> Unit = { } onVersionSelectRequired: () -> Unit = { }
) { ) {
if (itemIsDownloaded(item.id)) { if (isItemAvailable(item.id)) {
val playerItem = getDownloadPlayerItem(item.id) val playerItem = getDownloadPlayerItem(downloadDatabase, item.id)
if (playerItem != null) { if (playerItem != null) {
loadOfflinePlayerItems(playerItem) loadOfflinePlayerItems(playerItem)
return return