Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
013cfc3
perf(scanner): éliminer le double scan MediaStore au démarrage et opt…
github-actions[bot] Aug 16, 2026
040f43f
perf(domain): précompiler et mémoïser les Regex dans TitleCleaner, Fo…
github-actions[bot] Aug 16, 2026
50e609f
perf(compose): virtualiser les rangées de l'écran d'accueil dans Lazy…
github-actions[bot] Aug 16, 2026
d0dc0b2
perf(player): découpler la synchronisation de position pour éviter la…
github-actions[bot] Aug 16, 2026
c5d5636
perf(db): resoudre probleme N+1 playlists, batch writes et indexer pl…
github-actions[bot] Aug 16, 2026
1ca2792
perf(library): eviter re-filtrage synchrone et deporter calculs TMDB/…
github-actions[bot] Aug 16, 2026
c76ceb9
perf(sorting): optimiser complexite comparateur tri et lookups O(1)
github-actions[bot] Aug 16, 2026
b9a911b
perf(ui): memoiser allocations graphiques et formats dans VideoCard
github-actions[bot] Aug 16, 2026
4e86fcc
fix(detekt): supprimer l'avertissement LongParameterList sur LibraryV…
github-actions[bot] Aug 16, 2026
095bae6
refactor(sorting): factoriser les comparateurs pour respecter les reg…
github-actions[bot] Aug 16, 2026
37d8fda
Merge PR #161: perf(ui): mémoïser les allocations d'images (ColorFilt…
github-actions[bot] Aug 16, 2026
01a6df4
Merge PR #160: perf(sorting): optimiser la complexité du comparateur …
github-actions[bot] Aug 16, 2026
d99dbd0
Merge PR #159: perf(library): éviter le re-filtrage et re-tri synchro…
github-actions[bot] Aug 16, 2026
0947a14
Merge PR #158: perf(db): résoudre le problème N+1 sur les playlists, …
github-actions[bot] Aug 16, 2026
a4f4e1a
Merge PR #157: perf(player): découpler la synchronisation de position…
github-actions[bot] Aug 16, 2026
d45be3f
Merge PR #156: perf(compose): virtualiser les rangées de l'écran d'ac…
github-actions[bot] Aug 16, 2026
065b7f3
Merge PR #155: perf(domain): précompiler et mémoïser les Regex dans T…
github-actions[bot] Aug 16, 2026
cb02fd8
Merge PR #154: perf(scanner): éliminer le double scan MediaStore au d…
github-actions[bot] Aug 16, 2026
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