mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
feat: sync OpenSubsonic replay gain
Store the per-track replay gain a server reports on OpenSubsonic's `Child.replayGain` so playback can normalise loudness later. - add trackGain/albumGain/trackPeak/albumPeak (REAL) to the songs table and the 27 -> 28 migration - parse the nested `<replayGain>` element, folding the child-element dispatch into a single `songChild` shared by the search and album paths - teach the sync upsert to bind Float, which it previously rejected baseGain and fallbackGain are omitted: the servers that send the object do not populate them, so they would only ever be null columns. Nothing applies the values yet; this slice only mirrors them.
This commit is contained in:
@@ -384,6 +384,16 @@ val MIGRATION_26_27 =
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_27_28 =
|
||||
object : Migration(27, 28) {
|
||||
override suspend fun migrate(connection: SQLiteConnection) {
|
||||
connection.execSQL("ALTER TABLE `songs` ADD COLUMN `trackGain` REAL")
|
||||
connection.execSQL("ALTER TABLE `songs` ADD COLUMN `albumGain` REAL")
|
||||
connection.execSQL("ALTER TABLE `songs` ADD COLUMN `trackPeak` REAL")
|
||||
connection.execSQL("ALTER TABLE `songs` ADD COLUMN `albumPeak` REAL")
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors the FTS5 table and content-sync triggers Room generates for the @Fts5 entities.
|
||||
private suspend fun createSearchIndex(
|
||||
connection: SQLiteConnection,
|
||||
@@ -444,4 +454,5 @@ val MIGRATIONS: Array<Migration> =
|
||||
MIGRATION_24_25,
|
||||
MIGRATION_25_26,
|
||||
MIGRATION_26_27,
|
||||
MIGRATION_27_28,
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ import com.subtracks.data.model.UpNextEntry
|
||||
SongDownload::class,
|
||||
PendingAction::class,
|
||||
],
|
||||
version = 27,
|
||||
version = 28,
|
||||
exportSchema = true,
|
||||
)
|
||||
@ColumnTypeConverters(
|
||||
|
||||
@@ -264,6 +264,10 @@ data class Song(
|
||||
@ColumnInfo(defaultValue = "0") val created: Long = 0,
|
||||
@ColumnInfo(defaultValue = "0") val playCount: Long = 0,
|
||||
val played: Long? = null,
|
||||
val trackGain: Float? = null,
|
||||
val albumGain: Float? = null,
|
||||
val trackPeak: Float? = null,
|
||||
val albumPeak: Float? = null,
|
||||
@Ignore val genres: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ class SubsonicSource(
|
||||
),
|
||||
tag = "song",
|
||||
create = { SubsonicXml.songDraft(id, it) },
|
||||
onChild = { song, name, attrs -> if (name == "genres") song.addGenre(attrs) },
|
||||
onChild = SubsonicXml::songChild,
|
||||
).collect { batch ->
|
||||
raw += batch.size
|
||||
val accepted = batch.map { it.toSong() }.filter { it.id.isNotEmpty() }
|
||||
@@ -267,7 +267,7 @@ class SubsonicSource(
|
||||
params = mapOf("id" to albumId),
|
||||
tag = "song",
|
||||
create = { SubsonicXml.songDraft(id, it) },
|
||||
onChild = { song, name, attrs -> if (name == "genres") song.addGenre(attrs) },
|
||||
onChild = SubsonicXml::songChild,
|
||||
).collect { batch ->
|
||||
val accepted = batch.map { it.toSong() }.filter { it.id.isNotEmpty() }
|
||||
accept(accepted)
|
||||
|
||||
@@ -78,6 +78,17 @@ object SubsonicXml {
|
||||
played = attrs.dateAttr("played"),
|
||||
)
|
||||
|
||||
internal fun songChild(
|
||||
entity: SongDraft,
|
||||
name: String,
|
||||
attrs: Attributes,
|
||||
) {
|
||||
when (name) {
|
||||
"genres" -> entity.addGenre(attrs)
|
||||
"replayGain" -> entity.addReplayGain(attrs)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun playlist(
|
||||
sourceId: Long,
|
||||
attrs: Attributes,
|
||||
@@ -184,6 +195,10 @@ internal class SongDraft(
|
||||
private val created: Long = 0L,
|
||||
private val playCount: Long = 0L,
|
||||
private val played: Long? = null,
|
||||
private var trackGain: Float? = null,
|
||||
private var albumGain: Float? = null,
|
||||
private var trackPeak: Float? = null,
|
||||
private var albumPeak: Float? = null,
|
||||
private val genres: MutableList<String> = mutableListOf(),
|
||||
) {
|
||||
fun addGenre(attrs: Attributes) {
|
||||
@@ -194,6 +209,13 @@ internal class SongDraft(
|
||||
?.let { genres += it }
|
||||
}
|
||||
|
||||
fun addReplayGain(attrs: Attributes) {
|
||||
trackGain = attrs.floatAttr("trackGain")
|
||||
albumGain = attrs.floatAttr("albumGain")
|
||||
trackPeak = attrs.floatAttr("trackPeak")
|
||||
albumPeak = attrs.floatAttr("albumPeak")
|
||||
}
|
||||
|
||||
fun toSong(): Song =
|
||||
Song(
|
||||
sourceId = sourceId,
|
||||
@@ -211,6 +233,10 @@ internal class SongDraft(
|
||||
created = created,
|
||||
playCount = playCount,
|
||||
played = played,
|
||||
trackGain = trackGain,
|
||||
albumGain = albumGain,
|
||||
trackPeak = trackPeak,
|
||||
albumPeak = albumPeak,
|
||||
genres = genres.toList(),
|
||||
)
|
||||
}
|
||||
@@ -219,6 +245,8 @@ internal fun Attributes.attr(name: String): String = getValue(name) ?: ""
|
||||
|
||||
internal fun Attributes.longAttr(name: String): Long? = attr(name).takeIf { it.isNotEmpty() }?.toLongOrNull()
|
||||
|
||||
internal fun Attributes.floatAttr(name: String): Float? = attr(name).takeIf { it.isNotEmpty() }?.toFloatOrNull()
|
||||
|
||||
internal fun Attributes.dateAttr(name: String): Long? = attr(name).takeIf { it.isNotEmpty() }?.let(IsoDate::parse)
|
||||
|
||||
private const val ROOT_TAG = "subsonic-response"
|
||||
|
||||
@@ -314,6 +314,10 @@ private val songColumns =
|
||||
"created",
|
||||
"playCount",
|
||||
"played",
|
||||
"trackGain",
|
||||
"albumGain",
|
||||
"trackPeak",
|
||||
"albumPeak",
|
||||
)
|
||||
|
||||
private val playlistColumns = listOf("sourceId", "id", "name", "comment", "coverArt", "songCount", "created", "changed", "duration")
|
||||
@@ -358,6 +362,10 @@ private fun Song.values() =
|
||||
created,
|
||||
playCount,
|
||||
played,
|
||||
trackGain,
|
||||
albumGain,
|
||||
trackPeak,
|
||||
albumPeak,
|
||||
)
|
||||
|
||||
private fun SongGenre.values() = listOf<Any?>(sourceId, songId, position, genre)
|
||||
@@ -401,6 +409,7 @@ private fun SQLiteStatement.bind(
|
||||
is Int -> bindLong(index, value.toLong())
|
||||
is Boolean -> bindLong(index, if (value) 1L else 0L)
|
||||
is Double -> bindDouble(index, value)
|
||||
is Float -> bindDouble(index, value.toDouble())
|
||||
is String -> bindText(index, value)
|
||||
else -> error("Cannot bind ${value::class}")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user