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

View file

@ -8,6 +8,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.databinding.BaseItemBinding
import dev.jdtech.jellyfin.models.ContentType
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
@ -20,9 +21,9 @@ class DownloadViewItemListAdapter(
class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: PlayerItem, fixedWidth: Boolean) {
val metadata = item.metadata!!
val metadata = item.item!!
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
if (fixedWidth) {
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.android.qualifiers.ApplicationContext
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.ServerDatabaseDao
import javax.inject.Singleton
@ -27,4 +29,18 @@ object DatabaseModule {
.build()
.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(
UUID.randomUUID(),
item.name,
item.metadata?.type ?: "Unknown",
item.item?.type?.type ?: "Unkown",
item,
isOffline = true
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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