mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 23:35:44 +00:00
fix(playback): ReplayGain prefetch, gapless UI sync, library peak column (#1231)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- ReplayGain track peak for anti-clipping bind (OpenSubsonic replayGain.trackPeak).
|
||||
ALTER TABLE track ADD COLUMN replay_gain_peak REAL;
|
||||
@@ -1438,6 +1438,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -213,6 +213,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -208,6 +208,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -1492,6 +1492,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: Some(format!("hash-{id}")),
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -212,6 +212,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -106,6 +106,7 @@ pub struct LibraryTrackDto {
|
||||
pub bpm_source: Option<String>,
|
||||
pub replay_gain_track_db: Option<f64>,
|
||||
pub replay_gain_album_db: Option<f64>,
|
||||
pub replay_gain_peak: Option<f64>,
|
||||
|
||||
pub server_updated_at: Option<i64>,
|
||||
pub server_created_at: Option<i64>,
|
||||
@@ -157,6 +158,7 @@ impl LibraryTrackDto {
|
||||
bpm_source: None,
|
||||
replay_gain_track_db: row.replay_gain_track_db,
|
||||
replay_gain_album_db: row.replay_gain_album_db,
|
||||
replay_gain_peak: row.replay_gain_peak,
|
||||
server_updated_at: row.server_updated_at,
|
||||
server_created_at: row.server_created_at,
|
||||
synced_at: row.synced_at,
|
||||
@@ -750,6 +752,7 @@ mod tests {
|
||||
bpm: Some(120),
|
||||
replay_gain_track_db: Some(-1.2),
|
||||
replay_gain_album_db: Some(-0.8),
|
||||
replay_gain_peak: Some(0.95),
|
||||
content_hash: Some("deadbeef".into()),
|
||||
server_updated_at: Some(1_700_000_000),
|
||||
server_created_at: Some(1_699_000_000),
|
||||
@@ -780,6 +783,7 @@ mod tests {
|
||||
"mbidRecording",
|
||||
"replayGainTrackDb",
|
||||
"replayGainAlbumDb",
|
||||
"replayGainPeak",
|
||||
"serverUpdatedAt",
|
||||
"syncedAt",
|
||||
"rawJson",
|
||||
|
||||
@@ -230,6 +230,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -288,6 +288,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -375,6 +375,7 @@ fn map_live_hit_row(row: &rusqlite::Row<'_>, offset: usize) -> rusqlite::Result<
|
||||
mbid_recording: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
synced_at: row.get(offset + 21)?,
|
||||
@@ -429,6 +430,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -180,6 +180,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -233,6 +233,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -34,6 +34,7 @@ fn seed_track(store: &LibraryStore, server_id: &str, track_id: &str, duration_se
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
@@ -79,6 +80,7 @@ fn row_with_id_hash(server: &str, id: &str, hash: &str, path: &str) -> TrackRow
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: if hash.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -503,6 +505,7 @@ fn recent_plays_includes_album_cover_metadata() {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -53,6 +53,7 @@ pub struct TrackRow {
|
||||
pub bpm: Option<i64>,
|
||||
pub replay_gain_track_db: Option<f64>,
|
||||
pub replay_gain_album_db: Option<f64>,
|
||||
pub replay_gain_peak: Option<f64>,
|
||||
pub content_hash: Option<String>,
|
||||
pub server_updated_at: Option<i64>,
|
||||
pub server_created_at: Option<i64>,
|
||||
@@ -150,6 +151,7 @@ impl<'a> TrackRepository<'a> {
|
||||
r.bpm,
|
||||
r.replay_gain_track_db,
|
||||
r.replay_gain_album_db,
|
||||
r.replay_gain_peak,
|
||||
r.content_hash,
|
||||
r.server_updated_at,
|
||||
r.server_created_at,
|
||||
@@ -189,6 +191,7 @@ impl<'a> TrackRepository<'a> {
|
||||
r.bpm,
|
||||
r.replay_gain_track_db,
|
||||
r.replay_gain_album_db,
|
||||
r.replay_gain_peak,
|
||||
r.content_hash,
|
||||
r.server_updated_at,
|
||||
r.server_created_at,
|
||||
@@ -499,6 +502,7 @@ impl<'a> TrackRepository<'a> {
|
||||
r.bpm,
|
||||
r.replay_gain_track_db,
|
||||
r.replay_gain_album_db,
|
||||
r.replay_gain_peak,
|
||||
r.content_hash,
|
||||
r.server_updated_at,
|
||||
r.server_created_at,
|
||||
@@ -673,27 +677,27 @@ const TRACK_COLUMNS: &str = "\
|
||||
album_artist, duration_sec, track_number, disc_number, year, genre, suffix, \
|
||||
bit_rate, size_bytes, cover_art_id, starred_at, user_rating, play_count, \
|
||||
played_at, server_path, library_id, isrc, mbid_recording, bpm, \
|
||||
replay_gain_track_db, replay_gain_album_db, content_hash, server_updated_at, \
|
||||
replay_gain_track_db, replay_gain_album_db, replay_gain_peak, content_hash, server_updated_at, \
|
||||
server_created_at, deleted, synced_at, raw_json";
|
||||
|
||||
const SELECT_TRACK_BY_ID: &str = "SELECT server_id, id, title, title_sort, artist, artist_id, \
|
||||
album, album_id, album_artist, duration_sec, track_number, disc_number, year, genre, suffix, \
|
||||
bit_rate, size_bytes, cover_art_id, starred_at, user_rating, play_count, played_at, \
|
||||
server_path, library_id, isrc, mbid_recording, bpm, replay_gain_track_db, replay_gain_album_db, \
|
||||
server_path, library_id, isrc, mbid_recording, bpm, replay_gain_track_db, replay_gain_album_db, replay_gain_peak, \
|
||||
content_hash, server_updated_at, server_created_at, deleted, synced_at, raw_json \
|
||||
FROM track WHERE server_id = ?1 AND id = ?2 AND deleted = 0";
|
||||
|
||||
const SELECT_TRACK_BY_ID_ONLY: &str = "SELECT server_id, id, title, title_sort, artist, artist_id, \
|
||||
album, album_id, album_artist, duration_sec, track_number, disc_number, year, genre, suffix, \
|
||||
bit_rate, size_bytes, cover_art_id, starred_at, user_rating, play_count, played_at, \
|
||||
server_path, library_id, isrc, mbid_recording, bpm, replay_gain_track_db, replay_gain_album_db, \
|
||||
server_path, library_id, isrc, mbid_recording, bpm, replay_gain_track_db, replay_gain_album_db, replay_gain_peak, \
|
||||
content_hash, server_updated_at, server_created_at, deleted, synced_at, raw_json \
|
||||
FROM track WHERE id = ?1 AND deleted = 0";
|
||||
|
||||
const SELECT_TRACKS_BY_ALBUM: &str = "SELECT server_id, id, title, title_sort, artist, artist_id, \
|
||||
album, album_id, album_artist, duration_sec, track_number, disc_number, year, genre, suffix, \
|
||||
bit_rate, size_bytes, cover_art_id, starred_at, user_rating, play_count, played_at, \
|
||||
server_path, library_id, isrc, mbid_recording, bpm, replay_gain_track_db, replay_gain_album_db, \
|
||||
server_path, library_id, isrc, mbid_recording, bpm, replay_gain_track_db, replay_gain_album_db, replay_gain_peak, \
|
||||
content_hash, server_updated_at, server_created_at, deleted, synced_at, raw_json \
|
||||
FROM track WHERE server_id = ?1 AND album_id = ?2 AND deleted = 0 \
|
||||
ORDER BY disc_number ASC NULLS LAST, track_number ASC NULLS LAST, id ASC";
|
||||
@@ -733,12 +737,13 @@ pub(crate) fn row_to_track_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Trac
|
||||
bpm: row.get(26)?,
|
||||
replay_gain_track_db: row.get(27)?,
|
||||
replay_gain_album_db: row.get(28)?,
|
||||
content_hash: row.get(29)?,
|
||||
server_updated_at: row.get(30)?,
|
||||
server_created_at: row.get(31)?,
|
||||
deleted: row.get::<_, i64>(32)? != 0,
|
||||
synced_at: row.get(33)?,
|
||||
raw_json: row.get(34)?,
|
||||
replay_gain_peak: row.get(29)?,
|
||||
content_hash: row.get(30)?,
|
||||
server_updated_at: row.get(31)?,
|
||||
server_created_at: row.get(32)?,
|
||||
deleted: row.get::<_, i64>(33)? != 0,
|
||||
synced_at: row.get(34)?,
|
||||
raw_json: row.get(35)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -748,60 +753,8 @@ INSERT INTO track (
|
||||
album_artist, duration_sec, track_number, disc_number, year, genre, suffix,
|
||||
bit_rate, size_bytes, cover_art_id, starred_at, user_rating, play_count,
|
||||
played_at, server_path, library_id, isrc, mbid_recording, bpm,
|
||||
replay_gain_track_db, replay_gain_album_db, content_hash, server_updated_at,
|
||||
replay_gain_track_db, replay_gain_album_db, replay_gain_peak, content_hash, server_updated_at,
|
||||
server_created_at, deleted, synced_at, raw_json
|
||||
) VALUES (
|
||||
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17,
|
||||
?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32,
|
||||
?33, ?34, ?35
|
||||
)
|
||||
ON CONFLICT(server_id, id) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
title_sort = excluded.title_sort,
|
||||
artist = excluded.artist,
|
||||
artist_id = excluded.artist_id,
|
||||
album = excluded.album,
|
||||
album_id = excluded.album_id,
|
||||
album_artist = excluded.album_artist,
|
||||
duration_sec = excluded.duration_sec,
|
||||
track_number = excluded.track_number,
|
||||
disc_number = excluded.disc_number,
|
||||
year = excluded.year,
|
||||
genre = excluded.genre,
|
||||
suffix = excluded.suffix,
|
||||
bit_rate = excluded.bit_rate,
|
||||
size_bytes = excluded.size_bytes,
|
||||
cover_art_id = excluded.cover_art_id,
|
||||
starred_at = excluded.starred_at,
|
||||
user_rating = excluded.user_rating,
|
||||
play_count = excluded.play_count,
|
||||
played_at = excluded.played_at,
|
||||
server_path = excluded.server_path,
|
||||
library_id = excluded.library_id,
|
||||
isrc = excluded.isrc,
|
||||
mbid_recording = excluded.mbid_recording,
|
||||
bpm = excluded.bpm,
|
||||
replay_gain_track_db = excluded.replay_gain_track_db,
|
||||
replay_gain_album_db = excluded.replay_gain_album_db,
|
||||
-- E2: never let a sync (which passes NULL content_hash) clobber the
|
||||
-- playback-derived md5_16kb written via library_patch_track / the analysis
|
||||
-- bridge. A non-empty incoming hash still wins.
|
||||
content_hash = COALESCE(NULLIF(excluded.content_hash, ''), track.content_hash),
|
||||
server_updated_at = excluded.server_updated_at,
|
||||
server_created_at = excluded.server_created_at,
|
||||
deleted = excluded.deleted,
|
||||
synced_at = excluded.synced_at,
|
||||
raw_json = excluded.raw_json
|
||||
"#;
|
||||
|
||||
const UPSERT_INITIAL_RESYNC_SQL: &str = r#"
|
||||
INSERT INTO track (
|
||||
server_id, id, title, title_sort, artist, artist_id, album, album_id,
|
||||
album_artist, duration_sec, track_number, disc_number, year, genre, suffix,
|
||||
bit_rate, size_bytes, cover_art_id, starred_at, user_rating, play_count,
|
||||
played_at, server_path, library_id, isrc, mbid_recording, bpm,
|
||||
replay_gain_track_db, replay_gain_album_db, content_hash, server_updated_at,
|
||||
server_created_at, deleted, synced_at, raw_json, resync_gen
|
||||
) VALUES (
|
||||
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17,
|
||||
?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32,
|
||||
@@ -835,6 +788,60 @@ ON CONFLICT(server_id, id) DO UPDATE SET
|
||||
bpm = excluded.bpm,
|
||||
replay_gain_track_db = excluded.replay_gain_track_db,
|
||||
replay_gain_album_db = excluded.replay_gain_album_db,
|
||||
replay_gain_peak = excluded.replay_gain_peak,
|
||||
-- E2: never let a sync (which passes NULL content_hash) clobber the
|
||||
-- playback-derived md5_16kb written via library_patch_track / the analysis
|
||||
-- bridge. A non-empty incoming hash still wins.
|
||||
content_hash = COALESCE(NULLIF(excluded.content_hash, ''), track.content_hash),
|
||||
server_updated_at = excluded.server_updated_at,
|
||||
server_created_at = excluded.server_created_at,
|
||||
deleted = excluded.deleted,
|
||||
synced_at = excluded.synced_at,
|
||||
raw_json = excluded.raw_json
|
||||
"#;
|
||||
|
||||
const UPSERT_INITIAL_RESYNC_SQL: &str = r#"
|
||||
INSERT INTO track (
|
||||
server_id, id, title, title_sort, artist, artist_id, album, album_id,
|
||||
album_artist, duration_sec, track_number, disc_number, year, genre, suffix,
|
||||
bit_rate, size_bytes, cover_art_id, starred_at, user_rating, play_count,
|
||||
played_at, server_path, library_id, isrc, mbid_recording, bpm,
|
||||
replay_gain_track_db, replay_gain_album_db, replay_gain_peak, content_hash, server_updated_at,
|
||||
server_created_at, deleted, synced_at, raw_json, resync_gen
|
||||
) VALUES (
|
||||
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17,
|
||||
?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32,
|
||||
?33, ?34, ?35, ?36, ?37
|
||||
)
|
||||
ON CONFLICT(server_id, id) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
title_sort = excluded.title_sort,
|
||||
artist = excluded.artist,
|
||||
artist_id = excluded.artist_id,
|
||||
album = excluded.album,
|
||||
album_id = excluded.album_id,
|
||||
album_artist = excluded.album_artist,
|
||||
duration_sec = excluded.duration_sec,
|
||||
track_number = excluded.track_number,
|
||||
disc_number = excluded.disc_number,
|
||||
year = excluded.year,
|
||||
genre = excluded.genre,
|
||||
suffix = excluded.suffix,
|
||||
bit_rate = excluded.bit_rate,
|
||||
size_bytes = excluded.size_bytes,
|
||||
cover_art_id = excluded.cover_art_id,
|
||||
starred_at = excluded.starred_at,
|
||||
user_rating = excluded.user_rating,
|
||||
play_count = excluded.play_count,
|
||||
played_at = excluded.played_at,
|
||||
server_path = excluded.server_path,
|
||||
library_id = excluded.library_id,
|
||||
isrc = excluded.isrc,
|
||||
mbid_recording = excluded.mbid_recording,
|
||||
bpm = excluded.bpm,
|
||||
replay_gain_track_db = excluded.replay_gain_track_db,
|
||||
replay_gain_album_db = excluded.replay_gain_album_db,
|
||||
replay_gain_peak = excluded.replay_gain_peak,
|
||||
content_hash = COALESCE(NULLIF(excluded.content_hash, ''), track.content_hash),
|
||||
server_updated_at = excluded.server_updated_at,
|
||||
server_created_at = excluded.server_created_at,
|
||||
@@ -887,6 +894,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: Some("hash-abc".into()),
|
||||
server_updated_at: Some(1_700_000_000),
|
||||
server_created_at: Some(1_699_000_000),
|
||||
|
||||
@@ -356,6 +356,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -12,11 +12,14 @@ use tauri::Manager;
|
||||
///
|
||||
/// Migration checklist (wiring, data backfill, open/swap path):
|
||||
/// psysonic-workdocs `ai/agent-rules/08-library-db-migrations.md`.
|
||||
pub const LIBRARY_DB_SCHEMA_VERSION: i64 = 14;
|
||||
pub const LIBRARY_DB_SCHEMA_VERSION: i64 = 15;
|
||||
|
||||
/// One-time data repair after migration 014 (`artist.name_sort`).
|
||||
pub(crate) const ARTIST_NAME_SORT_RECONCILE_ID: &str = "artist_name_sort_reconcile_v1";
|
||||
|
||||
/// One-time backfill after migration 015 (`track.replay_gain_peak`).
|
||||
pub(crate) const REPLAY_GAIN_PEAK_RECONCILE_ID: &str = "replay_gain_peak_reconcile_v1";
|
||||
|
||||
/// Lowest applied schema version the current code can advance from purely
|
||||
/// additively. If a DB carries a version below this, the breaking-bump hook
|
||||
/// fires (spec §5.7 / P22): the library is treated as incompatible, must be
|
||||
@@ -38,6 +41,8 @@ pub(crate) const MIGRATION_013_ARTIST_ARTWORK_LOOKUP: &str =
|
||||
include_str!("../migrations/013_artist_artwork_lookup.sql");
|
||||
pub(crate) const MIGRATION_014_ARTIST_NAME_SORT: &str =
|
||||
include_str!("../migrations/014_artist_name_sort.sql");
|
||||
pub(crate) const MIGRATION_015_REPLAY_GAIN_PEAK: &str =
|
||||
include_str!("../migrations/015_replay_gain_peak.sql");
|
||||
|
||||
/// Embedded migrations. Ordered ascending by `version`; the runner sorts
|
||||
/// defensively before applying so the source order can stay readable.
|
||||
@@ -46,6 +51,7 @@ const MIGRATIONS: &[(i64, &str)] = &[
|
||||
(12, MIGRATION_012_TRACK_GENRE_LEGACY),
|
||||
(13, MIGRATION_013_ARTIST_ARTWORK_LOOKUP),
|
||||
(14, MIGRATION_014_ARTIST_NAME_SORT),
|
||||
(15, MIGRATION_015_REPLAY_GAIN_PEAK),
|
||||
];
|
||||
|
||||
/// Idempotent repair — also runs after the migration runner on every open so
|
||||
@@ -589,6 +595,7 @@ fn open_database_connections(db_path: &Path) -> rusqlite::Result<(Connection, Co
|
||||
fn prepare_write_connection_for_open(conn: &Connection) -> rusqlite::Result<()> {
|
||||
run_migrations(conn)?;
|
||||
maybe_reconcile_artist_name_sort(conn)?;
|
||||
maybe_reconcile_replay_gain_peak(conn)?;
|
||||
ensure_genre_tags_schema(conn)?;
|
||||
checkpoint_wal_conn(conn, "open")?;
|
||||
Ok(())
|
||||
@@ -721,6 +728,68 @@ fn repair_artist_name_sort_keys(conn: &Connection) -> rusqlite::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn replay_gain_peak_column_exists(conn: &Connection) -> rusqlite::Result<bool> {
|
||||
let column_exists: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('track') WHERE name = 'replay_gain_peak'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap_or(0);
|
||||
Ok(column_exists > 0)
|
||||
}
|
||||
|
||||
fn replay_gain_peak_reconcile_completed(conn: &Connection) -> rusqlite::Result<bool> {
|
||||
let completed: Option<Option<i64>> = conn
|
||||
.query_row(
|
||||
"SELECT completed_at FROM library_data_migration WHERE id = ?1",
|
||||
params![REPLAY_GAIN_PEAK_RECONCILE_ID],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()?;
|
||||
Ok(completed.flatten().is_some())
|
||||
}
|
||||
|
||||
fn mark_replay_gain_peak_reconcile_completed(conn: &Connection) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO library_data_migration (id, cursor_rowid, started_at, completed_at) \
|
||||
VALUES (?1, 0, strftime('%s','now'), strftime('%s','now')) \
|
||||
ON CONFLICT(id) DO UPDATE SET completed_at = excluded.completed_at",
|
||||
params![REPLAY_GAIN_PEAK_RECONCILE_ID],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One-time backfill after schema 015 — project peak from stored `raw_json`.
|
||||
fn repair_replay_gain_peak_from_raw_json(conn: &Connection) -> rusqlite::Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE track SET replay_gain_peak = json_extract(raw_json, '$.replayGain.trackPeak') \
|
||||
WHERE replay_gain_peak IS NULL \
|
||||
AND json_type(json_extract(raw_json, '$.replayGain.trackPeak')) = 'real'",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(
|
||||
"UPDATE track SET replay_gain_peak = json_extract(raw_json, '$.rgTrackPeak') \
|
||||
WHERE replay_gain_peak IS NULL \
|
||||
AND json_type(json_extract(raw_json, '$.rgTrackPeak')) = 'real'",
|
||||
[],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One-time reconcile after schema 015 — not on every open.
|
||||
fn maybe_reconcile_replay_gain_peak(conn: &Connection) -> rusqlite::Result<()> {
|
||||
if !replay_gain_peak_column_exists(conn)? {
|
||||
return Ok(());
|
||||
}
|
||||
if replay_gain_peak_reconcile_completed(conn)? {
|
||||
return Ok(());
|
||||
}
|
||||
repair_replay_gain_peak_from_raw_json(conn)?;
|
||||
mark_replay_gain_peak_reconcile_completed(conn)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_migrations(conn: &Connection) -> rusqlite::Result<MigrationOutcome> {
|
||||
run_migrations_with(
|
||||
conn,
|
||||
|
||||
@@ -639,6 +639,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: Some(server_updated_at),
|
||||
server_created_at: None,
|
||||
|
||||
@@ -2265,6 +2265,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
@@ -2306,6 +2307,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
@@ -70,6 +70,10 @@ pub fn subsonic_song_to_track_row(
|
||||
.get("replayGain")
|
||||
.and_then(|rg| rg.get("albumGain"))
|
||||
.and_then(|v| v.as_f64()),
|
||||
replay_gain_peak: raw_value
|
||||
.get("replayGain")
|
||||
.and_then(|rg| rg.get("trackPeak"))
|
||||
.and_then(|v| v.as_f64()),
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
@@ -134,6 +138,7 @@ pub fn navidrome_song_to_track_row(
|
||||
bpm: raw.get("bpm").and_then(|v| v.as_i64()),
|
||||
replay_gain_track_db: raw.get("rgTrackGain").and_then(|v| v.as_f64()),
|
||||
replay_gain_album_db: raw.get("rgAlbumGain").and_then(|v| v.as_f64()),
|
||||
replay_gain_peak: raw.get("rgTrackPeak").and_then(|v| v.as_f64()),
|
||||
content_hash: None,
|
||||
server_updated_at,
|
||||
server_created_at: raw
|
||||
@@ -258,7 +263,7 @@ mod tests {
|
||||
"track": 3,
|
||||
"year": 2024,
|
||||
"musicBrainzId": "mb-1",
|
||||
"replayGain": { "trackGain": -1.2, "albumGain": -0.8 }
|
||||
"replayGain": { "trackGain": -1.2, "albumGain": -0.8, "trackPeak": 0.91 }
|
||||
});
|
||||
let song: Song = serde_json::from_value(raw.clone()).unwrap();
|
||||
let row = subsonic_song_to_track_row("s1", &song, &raw, 1_000, Some("lib-fb"));
|
||||
@@ -268,6 +273,7 @@ mod tests {
|
||||
assert_eq!(row.mbid_recording.as_deref(), Some("mb-1"));
|
||||
assert_eq!(row.replay_gain_track_db, Some(-1.2));
|
||||
assert_eq!(row.replay_gain_album_db, Some(-0.8));
|
||||
assert_eq!(row.replay_gain_peak, Some(0.91));
|
||||
// Fallback library_id kicks in when the song didn't ship one.
|
||||
assert_eq!(row.library_id.as_deref(), Some("lib-fb"));
|
||||
assert!(row.raw_json.contains("replayGain"));
|
||||
|
||||
@@ -271,6 +271,7 @@ mod tests {
|
||||
bpm: None,
|
||||
replay_gain_track_db: None,
|
||||
replay_gain_album_db: None,
|
||||
replay_gain_peak: None,
|
||||
content_hash: None,
|
||||
server_updated_at: None,
|
||||
server_created_at: None,
|
||||
|
||||
Reference in New Issue
Block a user