feat: add Room entities, DAOs, database, and DataStore preferences

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-10 01:04:55 -04:00
parent 2a9b21b67f
commit b3d22650c7
15 changed files with 650 additions and 1 deletions

View File

@@ -1,5 +1,17 @@
package xyz.cottongin.radio247
import android.app.Application
import androidx.room.Room
import xyz.cottongin.radio247.data.db.RadioDatabase
import xyz.cottongin.radio247.data.prefs.RadioPreferences
class RadioApplication : Application()
class RadioApplication : Application() {
val database: RadioDatabase by lazy {
Room.databaseBuilder(this, RadioDatabase::class.java, "radio_database")
.build()
}
val preferences: RadioPreferences by lazy {
RadioPreferences(this)
}
}

View File

@@ -0,0 +1,22 @@
package xyz.cottongin.radio247.data.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import xyz.cottongin.radio247.data.model.ConnectionSpan
import kotlinx.coroutines.flow.Flow
@Dao
interface ConnectionSpanDao {
@Insert
suspend fun insert(span: ConnectionSpan): Long
@Query("UPDATE connection_spans SET endedAt = :endedAt WHERE id = :id")
suspend fun updateEndedAt(id: Long, endedAt: Long)
@Query("SELECT * FROM connection_spans WHERE sessionId = :sessionId ORDER BY startedAt DESC")
fun getBySession(sessionId: Long): Flow<List<ConnectionSpan>>
@Query("SELECT * FROM connection_spans WHERE endedAt IS NULL LIMIT 1")
suspend fun getActiveSpan(): ConnectionSpan?
}

View File

@@ -0,0 +1,22 @@
package xyz.cottongin.radio247.data.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import xyz.cottongin.radio247.data.model.ListeningSession
import kotlinx.coroutines.flow.Flow
@Dao
interface ListeningSessionDao {
@Insert
suspend fun insert(session: ListeningSession): Long
@Query("UPDATE listening_sessions SET endedAt = :endedAt WHERE id = :id")
suspend fun updateEndedAt(id: Long, endedAt: Long)
@Query("SELECT * FROM listening_sessions WHERE endedAt IS NULL LIMIT 1")
suspend fun getActiveSession(): ListeningSession?
@Query("SELECT * FROM listening_sessions ORDER BY startedAt DESC LIMIT :limit")
fun getRecentSessions(limit: Int = 50): Flow<List<ListeningSession>>
}

View File

@@ -0,0 +1,22 @@
package xyz.cottongin.radio247.data.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import xyz.cottongin.radio247.data.model.MetadataSnapshot
import kotlinx.coroutines.flow.Flow
@Dao
interface MetadataSnapshotDao {
@Insert
suspend fun insert(snapshot: MetadataSnapshot): Long
@Query("SELECT * FROM metadata_snapshots WHERE stationId = :stationId ORDER BY timestamp DESC LIMIT :limit")
fun getByStation(stationId: Long, limit: Int = 50): Flow<List<MetadataSnapshot>>
@Query("SELECT * FROM metadata_snapshots ORDER BY timestamp DESC LIMIT :limit")
fun getRecent(limit: Int = 100): Flow<List<MetadataSnapshot>>
@Query("SELECT * FROM metadata_snapshots WHERE artist LIKE '%' || :query || '%' OR title LIKE '%' || :query || '%' ORDER BY timestamp DESC")
fun search(query: String): Flow<List<MetadataSnapshot>>
}

View File

@@ -0,0 +1,33 @@
package xyz.cottongin.radio247.data.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import xyz.cottongin.radio247.data.model.Playlist
import kotlinx.coroutines.flow.Flow
@Dao
interface PlaylistDao {
@Query("SELECT * FROM playlists ORDER BY starred DESC, sortOrder ASC")
fun getAllPlaylists(): Flow<List<Playlist>>
@Query("SELECT * FROM playlists WHERE id = :id")
suspend fun getPlaylistById(id: Long): Playlist?
@Insert
suspend fun insert(playlist: Playlist): Long
@Update
suspend fun update(playlist: Playlist)
@Delete
suspend fun delete(playlist: Playlist)
@Query("UPDATE playlists SET sortOrder = :sortOrder WHERE id = :id")
suspend fun updateSortOrder(id: Long, sortOrder: Int)
@Query("UPDATE playlists SET starred = :starred WHERE id = :id")
suspend fun toggleStarred(id: Long, starred: Boolean)
}

View File

@@ -0,0 +1,28 @@
package xyz.cottongin.radio247.data.db
import androidx.room.Database
import androidx.room.RoomDatabase
import xyz.cottongin.radio247.data.model.ConnectionSpan
import xyz.cottongin.radio247.data.model.ListeningSession
import xyz.cottongin.radio247.data.model.MetadataSnapshot
import xyz.cottongin.radio247.data.model.Playlist
import xyz.cottongin.radio247.data.model.Station
@Database(
entities = [
Station::class,
Playlist::class,
MetadataSnapshot::class,
ListeningSession::class,
ConnectionSpan::class
],
version = 1,
exportSchema = true
)
abstract class RadioDatabase : RoomDatabase() {
abstract fun stationDao(): StationDao
abstract fun playlistDao(): PlaylistDao
abstract fun metadataSnapshotDao(): MetadataSnapshotDao
abstract fun listeningSessionDao(): ListeningSessionDao
abstract fun connectionSpanDao(): ConnectionSpanDao
}

View File

@@ -0,0 +1,39 @@
package xyz.cottongin.radio247.data.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import xyz.cottongin.radio247.data.model.Station
import kotlinx.coroutines.flow.Flow
@Dao
interface StationDao {
@Query("SELECT * FROM stations ORDER BY starred DESC, sortOrder ASC")
fun getAllStations(): Flow<List<Station>>
@Query("SELECT * FROM stations WHERE playlistId = :playlistId ORDER BY starred DESC, sortOrder ASC")
fun getStationsByPlaylist(playlistId: Long): Flow<List<Station>>
@Query("SELECT * FROM stations WHERE playlistId IS NULL ORDER BY starred DESC, sortOrder ASC")
fun getUnsortedStations(): Flow<List<Station>>
@Query("SELECT * FROM stations WHERE id = :id")
suspend fun getStationById(id: Long): Station?
@Insert
suspend fun insert(station: Station): Long
@Update
suspend fun update(station: Station)
@Delete
suspend fun delete(station: Station)
@Query("UPDATE stations SET sortOrder = :sortOrder WHERE id = :id")
suspend fun updateSortOrder(id: Long, sortOrder: Int)
@Query("UPDATE stations SET starred = :starred WHERE id = :id")
suspend fun toggleStarred(id: Long, starred: Boolean)
}

View File

@@ -0,0 +1,23 @@
package xyz.cottongin.radio247.data.model
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "connection_spans",
foreignKeys = [ForeignKey(
entity = ListeningSession::class,
parentColumns = ["id"],
childColumns = ["sessionId"],
onDelete = ForeignKey.CASCADE
)],
indices = [Index("sessionId")]
)
data class ConnectionSpan(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val sessionId: Long,
val startedAt: Long,
val endedAt: Long? = null
)

View File

@@ -0,0 +1,23 @@
package xyz.cottongin.radio247.data.model
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "listening_sessions",
foreignKeys = [ForeignKey(
entity = Station::class,
parentColumns = ["id"],
childColumns = ["stationId"],
onDelete = ForeignKey.CASCADE
)],
indices = [Index("stationId")]
)
data class ListeningSession(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val stationId: Long,
val startedAt: Long,
val endedAt: Long? = null
)

View File

@@ -0,0 +1,25 @@
package xyz.cottongin.radio247.data.model
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "metadata_snapshots",
foreignKeys = [ForeignKey(
entity = Station::class,
parentColumns = ["id"],
childColumns = ["stationId"],
onDelete = ForeignKey.CASCADE
)],
indices = [Index("stationId"), Index("timestamp")]
)
data class MetadataSnapshot(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val stationId: Long,
val title: String? = null,
val artist: String? = null,
val artworkUrl: String? = null,
val timestamp: Long
)

View File

@@ -0,0 +1,12 @@
package xyz.cottongin.radio247.data.model
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "playlists")
data class Playlist(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
val sortOrder: Int = 0,
val starred: Boolean = false
)

View File

@@ -0,0 +1,26 @@
package xyz.cottongin.radio247.data.model
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "stations",
foreignKeys = [ForeignKey(
entity = Playlist::class,
parentColumns = ["id"],
childColumns = ["playlistId"],
onDelete = ForeignKey.SET_NULL
)],
indices = [Index("playlistId")]
)
data class Station(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
val url: String,
val playlistId: Long? = null,
val sortOrder: Int = 0,
val starred: Boolean = false,
val defaultArtworkUrl: String? = null
)

View File

@@ -0,0 +1,37 @@
package xyz.cottongin.radio247.data.prefs
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class RadioPreferences(private val context: Context) {
private val dataStore = context.dataStore
val stayConnected: Flow<Boolean> = dataStore.data.map { it[STAY_CONNECTED] ?: false }
val bufferMs: Flow<Int> = dataStore.data.map { it[BUFFER_MS] ?: 0 }
val lastStationId: Flow<Long?> = dataStore.data.map { it[LAST_STATION_ID] }
suspend fun setStayConnected(value: Boolean) {
dataStore.edit { it[STAY_CONNECTED] = value }
}
suspend fun setBufferMs(value: Int) {
dataStore.edit { it[BUFFER_MS] = value }
}
suspend fun setLastStationId(value: Long) {
dataStore.edit { it[LAST_STATION_ID] = value }
}
companion object {
private val Context.dataStore by preferencesDataStore(name = "radio_prefs")
private val STAY_CONNECTED = booleanPreferencesKey("stay_connected")
private val BUFFER_MS = intPreferencesKey("buffer_ms")
private val LAST_STATION_ID = longPreferencesKey("last_station_id")
}
}