Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions native/app/schemas/com.localstream.app.data.db.AppDatabase/3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "d26195f2f513711c07e8e858973d8cb1",
"entities": [
{
"tableName": "watched_items",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `watched` INTEGER NOT NULL, `watched_at` INTEGER NOT NULL, `media_store_id` INTEGER, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "watched",
"columnName": "watched",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "watchedAt",
"columnName": "watched_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaStoreId",
"columnName": "media_store_id",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
}
},
{
"tableName": "playback_state",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `progress_pct` REAL NOT NULL, `position_ms` INTEGER NOT NULL, `last_played_at` INTEGER NOT NULL, `media_store_id` INTEGER, PRIMARY KEY(`name`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "progressPct",
"columnName": "progress_pct",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "positionMs",
"columnName": "position_ms",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastPlayedAt",
"columnName": "last_played_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaStoreId",
"columnName": "media_store_id",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"name"
]
},
"indices": [
{
"name": "index_playback_state_last_played_at",
"unique": false,
"columnNames": [
"last_played_at"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_playback_state_last_played_at` ON `${TABLE_NAME}` (`last_played_at`)"
},
{
"name": "index_playback_state_progress_pct",
"unique": false,
"columnNames": [
"progress_pct"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_playback_state_progress_pct` ON `${TABLE_NAME}` (`progress_pct`)"
}
]
},
{
"tableName": "playlist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "created_at",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "playlist_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` TEXT NOT NULL, `video_name` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `video_name`), FOREIGN KEY(`playlist_id`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "playlistId",
"columnName": "playlist_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "videoName",
"columnName": "video_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"playlist_id",
"video_name"
]
},
"indices": [
{
"name": "index_playlist_item_playlist_id",
"unique": false,
"columnNames": [
"playlist_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_item_playlist_id` ON `${TABLE_NAME}` (`playlist_id`)"
}
],
"foreignKeys": [
{
"table": "playlist",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"playlist_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "tmdb_metadata",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query_key` TEXT NOT NULL, `json` TEXT NOT NULL, `fetched_at` INTEGER NOT NULL, PRIMARY KEY(`query_key`))",
"fields": [
{
"fieldPath": "queryKey",
"columnName": "query_key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "json",
"columnName": "json",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fetchedAt",
"columnName": "fetched_at",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"query_key"
]
}
}
],
"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, 'd26195f2f513711c07e8e858973d8cb1')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import com.localstream.app.data.db.entity.WatchedItemEntity
PlaylistItemEntity::class,
TmdbMetadataEntity::class,
],
version = 2,
version = 3,
exportSchema = true,
)
abstract class AppDatabase : RoomDatabase() {
Expand Down Expand Up @@ -58,6 +58,13 @@ abstract class AppDatabase : RoomDatabase() {
}
}

val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE INDEX IF NOT EXISTS `index_playback_state_last_played_at` ON `playback_state` (`last_played_at`)")
db.execSQL("CREATE INDEX IF NOT EXISTS `index_playback_state_progress_pct` ON `playback_state` (`progress_pct`)")
}
}

@Volatile
private var INSTANCE: AppDatabase? = null

Expand All @@ -68,7 +75,7 @@ abstract class AppDatabase : RoomDatabase() {
AppDatabase::class.java,
DB_NAME,
)
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
.also { INSTANCE = it }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import com.localstream.app.data.db.entity.PlaylistEntity
import com.localstream.app.data.db.entity.PlaylistItemEntity
import kotlinx.coroutines.flow.Flow

import com.localstream.app.data.db.entity.PlaylistWithItems

@Dao
@Suppress("TooManyFunctions")
interface PlaylistDao {
Expand All @@ -18,9 +20,17 @@ interface PlaylistDao {
@Query("SELECT * FROM playlist ORDER BY created_at ASC")
fun observePlaylists(): Flow<List<PlaylistEntity>>

@Transaction
@Query("SELECT * FROM playlist ORDER BY created_at ASC")
fun observePlaylistsWithItems(): Flow<List<PlaylistWithItems>>

@Query("SELECT * FROM playlist ORDER BY created_at ASC")
suspend fun getAllPlaylists(): List<PlaylistEntity>

@Transaction
@Query("SELECT * FROM playlist ORDER BY created_at ASC")
suspend fun getPlaylistsWithItems(): List<PlaylistWithItems>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertPlaylist(playlist: PlaylistEntity)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ interface TmdbMetadataDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMetadata(entity: TmdbMetadataEntity)

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMetadataList(entities: List<TmdbMetadataEntity>)

@Query("DELETE FROM tmdb_metadata WHERE query_key = :queryKey")
suspend fun deleteMetadata(queryKey: String)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ interface WatchedItemDao {
@Query("DELETE FROM watched_items WHERE name = :name")
suspend fun deleteByName(name: String)

@Query("DELETE FROM watched_items WHERE name IN (:names)")
suspend fun deleteByNames(names: List<String>)

@Query("DELETE FROM watched_items")
suspend fun deleteAll()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

import androidx.room.Index

/**
* Table Room : progression de lecture.
* Fusionne les trois cl\u00e9s localStorage : watchProgress, watchPositions et recentlyWatched.
* Fusionne les trois clés localStorage : watchProgress, watchPositions et recentlyWatched.
*
* - [progressPct] : pourcentage (0.0 – 100.0), \u00e9quivalent "watchProgress"
* - [positionMs] : position en millisecondes, \u00e9quivalent "watchPositions"
* - [lastPlayedAt] : timestamp ms, permet de r\u00e9construire "recentlyWatched" par tri DESC
* - [mediaStoreId] : nullable, fiabilise la cl\u00e9 \u00e0 terme (renommage de fichier)
* - [progressPct] : pourcentage (0.0 – 100.0), équivalent "watchProgress"
* - [positionMs] : position en millisecondes, équivalent "watchPositions"
* - [lastPlayedAt] : timestamp ms, permet de reconstruire "recentlyWatched" par tri DESC
* - [mediaStoreId] : nullable, fiabilise la clé à terme (renommage de fichier)
*/
@Entity(tableName = "playback_state")
@Entity(
tableName = "playback_state",
indices = [
Index(value = ["last_played_at"]),
Index(value = ["progress_pct"]),
],
)
data class PlaybackStateEntity(
@PrimaryKey
@ColumnInfo(name = "name") val name: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,16 @@ data class PlaylistItemEntity(
@ColumnInfo(name = "video_name") val videoName: String,
/** Position 0-based dans la playlist. */
@ColumnInfo(name = "position") val position: Int = 0,
)

/**
* Relation 1:N entre une Playlist et ses items pour chargement optimisé sans N+1.
*/
data class PlaylistWithItems(
@androidx.room.Embedded val playlist: PlaylistEntity,
@androidx.room.Relation(
parentColumn = "id",
entityColumn = "playlist_id",
)
val items: List<PlaylistItemEntity> = emptyList(),
)
Loading
Loading