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:
parent
b9e5c3b9ba
commit
4b2dd6c672
19 changed files with 276 additions and 214 deletions
|
@ -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()
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
16
app/src/main/java/dev/jdtech/jellyfin/database/Converters.kt
Normal file
16
app/src/main/java/dev/jdtech/jellyfin/database/Converters.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue