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.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()
|
||||
|
|
|
@ -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 =
|
||||
|
|
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.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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -8,5 +8,5 @@ import java.util.*
|
|||
data class DownloadRequestItem(
|
||||
val uri: String,
|
||||
val itemId: UUID,
|
||||
val metadata: DownloadMetadata
|
||||
val item: DownloadItem
|
||||
) : Parcelable
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
||||
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 "")
|
||||
} 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 {
|
||||
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 isItemAvailable(itemId: UUID): Boolean {
|
||||
return File(defaultStorage, itemId.toString()).exists()
|
||||
}
|
||||
|
||||
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 isItemDownloaded(downloadDatabaseDao: DownloadDatabaseDao, itemId: UUID): Boolean {
|
||||
val item = downloadDatabaseDao.loadItem(itemId)
|
||||
return item != null
|
||||
}
|
||||
|
||||
fun getDownloadPlayerItem(itemId: UUID): PlayerItem? {
|
||||
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)
|
||||
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),
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue