Skip to content

Commit

Permalink
Merge pull request #1 from neBM/download
Browse files Browse the repository at this point in the history
Refactor and simplify Room database
  • Loading branch information
7ritn committed Jul 8, 2024
2 parents d3b7ae5 + 51d0ea2 commit 051de01
Show file tree
Hide file tree
Showing 31 changed files with 492 additions and 393 deletions.
34 changes: 5 additions & 29 deletions app/schemas/org.jellyfin.mobile.data.JellyfinDatabase/3.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "4366575e1609ecf3e2c44abd106d51d6",
"identityHash": "26b179bda28d76008389cab4ce8cb631",
"entities": [
{
"tableName": "Server",
Expand Down Expand Up @@ -115,49 +115,25 @@
},
{
"tableName": "Download",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `item_id` TEXT NOT NULL, `media_uri` TEXT NOT NULL, `media_source` TEXT NOT NULL, `download_folder_uri` TEXT NOT NULL, `download_length` INTEGER NOT NULL)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`item_id` TEXT NOT NULL, `media_source` TEXT NOT NULL, PRIMARY KEY(`item_id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "itemId",
"columnName": "item_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mediaUri",
"columnName": "media_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mediaSource",
"columnName": "media_source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadFolderUri",
"columnName": "download_folder_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadLength",
"columnName": "download_length",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"autoGenerate": false,
"columnNames": [
"id"
"item_id"
]
},
"indices": [
Expand All @@ -177,7 +153,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4366575e1609ecf3e2c44abd106d51d6')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '26b179bda28d76008389cab4ce8cb631')"
]
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/org/jellyfin/mobile/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class MainViewModel(

private suspend fun refreshServer() {
val serverEntity = apiClientController.loadSavedServer()
_serverState.value = serverEntity?.let { entity -> ServerState.Available(entity) } ?: ServerState.Unset
_serverState.value = serverEntity?.let { entity -> ServerState.Available(entity) } ?: ServerState.Unset
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package org.jellyfin.mobile.app

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jellyfin.mobile.data.dao.DownloadDao
import org.jellyfin.mobile.data.dao.ServerDao
import org.jellyfin.mobile.data.dao.UserDao
import org.jellyfin.mobile.data.entity.DownloadEntity
import org.jellyfin.mobile.data.entity.ServerEntity
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.ApiClient
Expand All @@ -17,7 +15,7 @@ class ApiClientController(
private val jellyfin: Jellyfin,
private val apiClient: ApiClient,
private val serverDao: ServerDao,
private val userDao: UserDao
private val userDao: UserDao,
) {
private val baseDeviceInfo: DeviceInfo
get() = jellyfin.options.deviceInfo!!
Expand Down
7 changes: 3 additions & 4 deletions app/src/main/java/org/jellyfin/mobile/app/AppModule.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.jellyfin.mobile.app

import org.jellyfin.mobile.downloads.DownloadsViewModel
import android.content.Context
import coil.ImageLoader
import com.google.android.exoplayer2.database.DatabaseProvider
Expand All @@ -26,6 +25,7 @@ import org.chromium.net.CronetEngine
import org.chromium.net.CronetProvider
import org.jellyfin.mobile.MainViewModel
import org.jellyfin.mobile.bridge.NativePlayer
import org.jellyfin.mobile.downloads.DownloadsViewModel
import org.jellyfin.mobile.events.ActivityEventHandler
import org.jellyfin.mobile.player.audio.car.LibraryBrowser
import org.jellyfin.mobile.player.deviceprofile.DeviceProfileBuilder
Expand Down Expand Up @@ -88,11 +88,11 @@ val applicationModule = module {
single { QualityOptionsProvider() }

// ExoPlayer factories
single <DatabaseProvider>{
single<DatabaseProvider> {
val dbProvider = StandaloneDatabaseProvider(get<Context>())
dbProvider
}
single <Cache> {
single<Cache> {
val downloadPath = File(get<Context>().filesDir, Constants.DOWNLOAD_PATH)
if (!downloadPath.exists()) {
downloadPath.mkdirs()
Expand Down Expand Up @@ -125,7 +125,6 @@ val applicationModule = module {
}

DefaultDataSource.Factory(context, baseDataSourceFactory)

}

single<CacheDataSource.Factory> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package org.jellyfin.mobile.data

import androidx.room.Database
import androidx.room.RoomDatabase
import org.jellyfin.mobile.data.dao.DownloadDao
import org.jellyfin.mobile.data.dao.ServerDao
import org.jellyfin.mobile.data.dao.UserDao
import org.jellyfin.mobile.data.dao.DownloadDao
import org.jellyfin.mobile.data.entity.DownloadEntity
import org.jellyfin.mobile.data.entity.ServerEntity
import org.jellyfin.mobile.data.entity.UserEntity
import org.jellyfin.mobile.data.entity.DownloadEntity

@Database(entities = [ServerEntity::class, UserEntity::class, DownloadEntity::class], version = 3)
abstract class JellyfinDatabase : RoomDatabase() {
Expand Down
11 changes: 2 additions & 9 deletions app/src/main/java/org/jellyfin/mobile/data/dao/DownloadDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,8 @@ interface DownloadDao {
fun getAllDownloads(): Flow<List<DownloadEntity>>

@Query("SELECT * FROM $TABLE_NAME WHERE item_id LIKE :downloadId")
suspend fun get(downloadId: String): DownloadEntity

@Query("SELECT download_folder_uri FROM $TABLE_NAME WHERE item_id LIKE :downloadId")
suspend fun getDownloadFolderUri(downloadId: String): String

@Query("SELECT media_uri FROM $TABLE_NAME WHERE item_id LIKE :downloadId")
suspend fun getMediaUri(downloadId: String): String
suspend fun get(downloadId: String): DownloadEntity?

@Query("SELECT EXISTS(SELECT * FROM $TABLE_NAME WHERE item_id LIKE :downloadId)")
suspend fun downloadExists(downloadId : String) : Boolean

suspend fun downloadExists(downloadId: String): Boolean
}
97 changes: 78 additions & 19 deletions app/src/main/java/org/jellyfin/mobile/data/entity/DownloadEntity.kt
Original file line number Diff line number Diff line change
@@ -1,43 +1,102 @@
package org.jellyfin.mobile.data.entity

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.Json.Default.decodeFromString
import org.jellyfin.mobile.data.entity.DownloadEntity.Key.ITEM_ID
import org.jellyfin.mobile.data.entity.DownloadEntity.Key.TABLE_NAME
import org.jellyfin.mobile.player.source.LocalJellyfinMediaSource
import org.jellyfin.mobile.utils.Constants
import java.io.File
import java.util.Locale

@Entity(
tableName = TABLE_NAME,
indices = [
Index(value = [ITEM_ID], unique = true),
],
)
@TypeConverters(LocalJellyfinMediaSourceConverter::class)
data class DownloadEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = ID)
val id: Long,
@PrimaryKey
@ColumnInfo(name = ITEM_ID)
val itemId: String,
@ColumnInfo(name = MEDIA_URI)
val mediaUri: String,
@ColumnInfo(name = MEDIA_SOURCE)
val mediaSource: String,
@ColumnInfo(name = DOWNLOAD_FOLDER_URI)
val downloadFolderUri: String,
@ColumnInfo(name = DOWNLOAD_LENGTH)
val downloadLength: Long
val mediaSource: LocalJellyfinMediaSource,
) {
constructor(itemId: String, mediaUri: String, mediaSource: String, downloadFolderUri: String, downloadLength: Long) :
this(0, itemId, mediaUri, mediaSource, downloadFolderUri, downloadLength)
/**
* Converts the [mediaSource] string to a [LocalJellyfinMediaSource] object.
*
* @param startTimeMs The start time in milliseconds. If null, the default start time is used.
* @param audioStreamIndex The index of the audio stream to select. If null, the default audio stream is used.
* @param subtitleStreamIndex The index of the subtitle stream to select. If -1, subtitles are disabled. If null, the default subtitle stream is used.
*/
fun asMediaSource(
startTimeMs: Long? = null,
audioStreamIndex: Int? = null,
subtitleStreamIndex: Int? = null,
): LocalJellyfinMediaSource = mediaSource
.also { localJellyfinMediaSource ->
startTimeMs
?.let { localJellyfinMediaSource.startTimeMs = it }
audioStreamIndex
?.let { localJellyfinMediaSource.mediaStreams[it] }
?.let(localJellyfinMediaSource::selectAudioStream)
subtitleStreamIndex
?.run {
takeUnless { it == -1 }
?.let { localJellyfinMediaSource.mediaStreams[it] }
?: localJellyfinMediaSource.selectSubtitleStream(null)
}
}

constructor(mediaSource: LocalJellyfinMediaSource) :
this(mediaSource.id, mediaSource)

@Ignore
val thumbnail: Bitmap? = BitmapFactory.decodeFile(
File(mediaSource.localDirectoryUri, Constants.DOWNLOAD_THUMBNAIL_FILENAME).canonicalPath,
)

@Ignore
val fileSize: String = formatFileSize(mediaSource.downloadSize)

@Ignore
private fun formatFileSize(bytes: Long): String {
val units = arrayOf("B", "KB", "MB", "GB", "TB")
var size = bytes.toDouble()
var unitIndex = 0

while (size >= BYTES_PER_BINARY_UNIT && unitIndex < units.lastIndex) {
size /= BYTES_PER_BINARY_UNIT
unitIndex++
}

return "%.1f %s".format(Locale.ROOT, size, units[unitIndex])
}

companion object Key {
const val TABLE_NAME = "Download"
const val ID = "id"
const val ITEM_ID = "item_id"
const val MEDIA_URI = "media_uri"
const val MEDIA_SOURCE = "media_source"
const val DOWNLOAD_FOLDER_URI = "download_folder_uri"
const val DOWNLOAD_LENGTH = "download_length"
const val BYTES_PER_BINARY_UNIT: Int = 1024
const val TABLE_NAME: String = "Download"
const val ID: String = "id"
const val ITEM_ID: String = "item_id"
const val MEDIA_SOURCE: String = "media_source"
}
}

class LocalJellyfinMediaSourceConverter {
@TypeConverter
fun toLocalJellyfinMediaSource(value: String): LocalJellyfinMediaSource = decodeFromString(value)

@TypeConverter
fun fromLocalJellyfinMediaSource(value: LocalJellyfinMediaSource): String = Json.encodeToString(value)
}
31 changes: 0 additions & 31 deletions app/src/main/java/org/jellyfin/mobile/downloads/DownloadItem.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.koin.core.component.inject
import java.util.concurrent.Executors

object DownloadServiceUtil : KoinComponent {
private const val DOWNLOAD_THREADS = 6

private val context: Context by inject()
private val databaseProvider: DatabaseProvider by inject()
Expand Down Expand Up @@ -53,12 +54,11 @@ object DownloadServiceUtil : KoinComponent {
DefaultDownloadIndex(databaseProvider),
DefaultDownloaderFactory(
downloadDataCache,
Executors.newFixedThreadPool(6),
Executors.newFixedThreadPool(DOWNLOAD_THREADS),
),
)
downloadTracker =
DownloadTracker(downloadManager!!)

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadIndex
import com.google.android.exoplayer2.offline.DownloadManager
import com.google.common.base.Preconditions
import timber.log.Timber
import java.io.IOException
import java.util.concurrent.CopyOnWriteArraySet

Expand All @@ -17,7 +18,6 @@ class DownloadTracker(downloadManager: DownloadManager) {
private val downloads: HashMap<Uri, Download> = HashMap()
private val downloadIndex: DownloadIndex


init {
downloadIndex = downloadManager.downloadIndex
downloadManager.addListener(DownloadManagerListener())
Expand Down Expand Up @@ -56,10 +56,11 @@ class DownloadTracker(downloadManager: DownloadManager) {
}
}
} catch (e: IOException) {
Timber.e(e, "Failed to load downloads")
}
}

private inner class DownloadManagerListener : DownloadManager.Listener {
private inner class DownloadManagerListener : DownloadManager.Listener {
override fun onDownloadChanged(downloadManager: DownloadManager, download: Download, finalException: Exception?) {
downloads[download.request.uri] = download
for (listener in listeners) {
Expand Down
Loading

0 comments on commit 051de01

Please sign in to comment.