diff --git a/app/src/main/java/dev/jdtech/jellyfin/adapters/DownloadEpisodeListAdapter.kt b/app/src/main/java/dev/jdtech/jellyfin/adapters/DownloadEpisodeListAdapter.kt index be1c8527..f16e9c1b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/adapters/DownloadEpisodeListAdapter.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/adapters/DownloadEpisodeListAdapter.kt @@ -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() diff --git a/app/src/main/java/dev/jdtech/jellyfin/adapters/DownloadViewItemListAdapter.kt b/app/src/main/java/dev/jdtech/jellyfin/adapters/DownloadViewItemListAdapter.kt index b1c4ee4d..32eed6c4 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/adapters/DownloadViewItemListAdapter.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/adapters/DownloadViewItemListAdapter.kt @@ -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 = diff --git a/app/src/main/java/dev/jdtech/jellyfin/database/Converters.kt b/app/src/main/java/dev/jdtech/jellyfin/database/Converters.kt new file mode 100644 index 00000000..a63d80a3 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/database/Converters.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/database/DownloadDatabase.kt b/app/src/main/java/dev/jdtech/jellyfin/database/DownloadDatabase.kt new file mode 100644 index 00000000..27483d6f --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/database/DownloadDatabase.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/database/DownloadDatabaseDao.kt b/app/src/main/java/dev/jdtech/jellyfin/database/DownloadDatabaseDao.kt new file mode 100644 index 00000000..c93191e3 --- /dev/null +++ b/app/src/main/java/dev/jdtech/jellyfin/database/DownloadDatabaseDao.kt @@ -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 + + @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) +} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/di/DatabaseModule.kt b/app/src/main/java/dev/jdtech/jellyfin/di/DatabaseModule.kt index 9f13b4e5..4318ba80 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/di/DatabaseModule.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/di/DatabaseModule.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/DownloadFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/DownloadFragment.kt index 6f638f84..95785491 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/DownloadFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/DownloadFragment.kt @@ -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 ) diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index 7c71a9d4..73daefef 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -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 diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt index 4560024e..cf7b0e34 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/MediaInfoFragment.kt @@ -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 diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/DownloadMetadata.kt b/app/src/main/java/dev/jdtech/jellyfin/models/DownloadItem.kt similarity index 58% rename from app/src/main/java/dev/jdtech/jellyfin/models/DownloadMetadata.kt rename to app/src/main/java/dev/jdtech/jellyfin/models/DownloadItem.kt index 49a41d82..42320509 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/models/DownloadMetadata.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/models/DownloadItem.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/DownloadRequestItem.kt b/app/src/main/java/dev/jdtech/jellyfin/models/DownloadRequestItem.kt index 32ad1e02..f5b82a0c 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/models/DownloadRequestItem.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/models/DownloadRequestItem.kt @@ -8,5 +8,5 @@ import java.util.* data class DownloadRequestItem( val uri: String, val itemId: UUID, - val metadata: DownloadMetadata + val item: DownloadItem ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt b/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt index 7f48644d..abc7109c 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/utils/DownloadUtilities.kt b/app/src/main/java/dev/jdtech/jellyfin/utils/DownloadUtilities.kt index 31536e4f..c652e023 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/utils/DownloadUtilities.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/utils/DownloadUtilities.kt @@ -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) -} -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 "") + 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 downloadFile(request: DownloadManager.Request, context: Context) { +private fun downloadFile(request: DownloadManager.Request, context: Context): Long { request.apply { setAllowedOverMetered(false) setAllowedOverRoaming(false) } - context.getSystemService()?.enqueue(request) + return context.getSystemService()!!.enqueue(request) } fun loadDownloadLocation(context: Context) { defaultStorage = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES) } -fun loadDownloadedEpisodes(): List { - val items = mutableListOf() - defaultStorage?.walk()?.forEach { - if (it.isFile && it.extension == "") { - try { - val metadataFile = File(defaultStorage, "${it.name}.metadata").readLines() - val metadata = parseMetadataFile(metadataFile) - items.add( - PlayerItem( - name = metadata.name, - itemId = UUID.fromString(it.name), - mediaSourceId = "", - playbackPosition = metadata.playbackPosition!!, - mediaSourceUri = it.absolutePath, - metadata = metadata - ) - ) - } catch (e: Exception) { - it.delete() - Timber.e(e) - } - - } +fun loadDownloadedEpisodes(downloadDatabase: DownloadDatabaseDao): List { + val items = downloadDatabase.loadItems() + return items.map { + PlayerItem( + name = it.name, + itemId = it.id, + mediaSourceId = "", + playbackPosition = it.playbackPosition ?: 0, + mediaSourceUri = File(defaultStorage, it.id.toString()).absolutePath, + item = it + ) } - 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): 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") - ) - } 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") - ) - } +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 + ) } -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), diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/DownloadViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/DownloadViewModel.kt index 873deee7..fdeadeed 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/DownloadViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/DownloadViewModel.kt @@ -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.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() 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 ) diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt index 4001bcf3..f610ca0b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/EpisodeBottomSheetViewModel.kt @@ -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.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 = 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 { diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt index faedff31..2eae854b 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt @@ -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.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) { diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt index 2b74f507..4b16c03f 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/MediaInfoViewModel.kt @@ -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.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) } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index ee1ccd82..45472e54 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -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( diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index 36256014..10888c76 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -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( @@ -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