Now we also record the scrobble source
This commit is contained in:
81
src/db.rs
81
src/db.rs
@@ -64,6 +64,8 @@ pub struct NewScrobble {
|
||||
pub track_duration_secs: Option<i64>,
|
||||
pub played_duration_secs: i64,
|
||||
pub scrobbled_at: String,
|
||||
/// The source that produced this scrobble (e.g. "MPD", "Qobuz").
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
/// Aggregate overview statistics for a given time period.
|
||||
@@ -119,6 +121,15 @@ pub struct TopGenre {
|
||||
pub listen_time_secs: i64,
|
||||
}
|
||||
|
||||
/// A row in the "top sources" breakdown, showing which player or source
|
||||
/// contributed most scrobbles (e.g. "MPD", "Qobuz").
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct TopSource {
|
||||
pub source: String,
|
||||
pub scrobbles: i64,
|
||||
pub listen_time_secs: i64,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Database initialization
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -186,8 +197,15 @@ fn init_schema(conn: &Connection) -> Result<()> {
|
||||
scrobbled_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_scrobbled_at ON scrobbles(scrobbled_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_artist ON scrobbles(artist);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_artist ON scrobbles(artist);",
|
||||
)?;
|
||||
// Add the source column to existing databases. SQLite returns an error if
|
||||
// the column already exists, which we silently ignore for idempotency.
|
||||
conn.execute(
|
||||
"ALTER TABLE scrobbles ADD COLUMN source TEXT",
|
||||
[],
|
||||
).ok();
|
||||
conn.execute_batch("
|
||||
-- Cache table for album metadata (cover art, genres) from MusicBrainz.
|
||||
-- Populated by the 'enrich' command or automatically when generating
|
||||
-- HTML reports. Rows with cover_url = NULL are re-tried on next run.
|
||||
@@ -212,8 +230,8 @@ fn init_schema(conn: &Connection) -> Result<()> {
|
||||
/// Insert a new scrobble record and return its auto-generated row ID.
|
||||
pub fn insert_scrobble(conn: &Connection, s: &NewScrobble) -> Result<i64> {
|
||||
conn.execute(
|
||||
"INSERT INTO scrobbles (artist, album, title, track_duration_secs, played_duration_secs, scrobbled_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
"INSERT INTO scrobbles (artist, album, title, track_duration_secs, played_duration_secs, scrobbled_at, source)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![
|
||||
s.artist,
|
||||
s.album,
|
||||
@@ -221,6 +239,7 @@ pub fn insert_scrobble(conn: &Connection, s: &NewScrobble) -> Result<i64> {
|
||||
s.track_duration_secs,
|
||||
s.played_duration_secs,
|
||||
s.scrobbled_at,
|
||||
s.source,
|
||||
],
|
||||
)?;
|
||||
Ok(conn.last_insert_rowid())
|
||||
@@ -581,6 +600,40 @@ pub fn top_genres(conn: &Connection, period: &str, limit: i64) -> Result<Vec<Top
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Get the top sources (players) by scrobble count for the given period,
|
||||
/// ordered by scrobble count descending. Rows with a NULL source are excluded
|
||||
/// — these are historical records written before the `source` column existed.
|
||||
pub fn top_sources(conn: &Connection, period: &str) -> Result<Vec<TopSource>> {
|
||||
let (whr, p) = where_clause(period);
|
||||
// Extend the WHERE clause to also filter out NULL sources.
|
||||
let source_filter = if whr.is_empty() {
|
||||
" WHERE source IS NOT NULL".to_string()
|
||||
} else {
|
||||
format!("{} AND source IS NOT NULL", whr)
|
||||
};
|
||||
let sql = format!(
|
||||
"SELECT source, COUNT(*) as scrobbles, COALESCE(SUM(played_duration_secs), 0) as listen_time
|
||||
FROM scrobbles{}
|
||||
GROUP BY source
|
||||
ORDER BY scrobbles DESC, listen_time DESC, source ASC",
|
||||
source_filter
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let mapper = |row: &rusqlite::Row| {
|
||||
Ok(TopSource {
|
||||
source: row.get(0)?,
|
||||
scrobbles: row.get(1)?,
|
||||
listen_time_secs: row.get(2)?,
|
||||
})
|
||||
};
|
||||
if p.is_empty() {
|
||||
stmt.query_map([], mapper)?.collect()
|
||||
} else {
|
||||
stmt.query_map(params![p[0], p[1]], mapper)?.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the most recent scrobbles for the given period, ordered by
|
||||
/// timestamp descending (newest first), limited to `limit` entries.
|
||||
pub fn recent_scrobbles(conn: &Connection, period: &str, limit: i64) -> Result<Vec<Scrobble>> {
|
||||
@@ -919,6 +972,7 @@ mod tests {
|
||||
track_duration_secs: Some(186),
|
||||
played_duration_secs: 186,
|
||||
scrobbled_at: "2026-03-19T10:00:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
NewScrobble {
|
||||
artist: "††† (Crosses)".to_string(),
|
||||
@@ -927,6 +981,7 @@ mod tests {
|
||||
track_duration_secs: Some(215),
|
||||
played_duration_secs: 200,
|
||||
scrobbled_at: "2026-03-19T10:05:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
NewScrobble {
|
||||
artist: "Deftones".to_string(),
|
||||
@@ -935,6 +990,7 @@ mod tests {
|
||||
track_duration_secs: Some(291),
|
||||
played_duration_secs: 291,
|
||||
scrobbled_at: "2026-03-19T10:10:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
NewScrobble {
|
||||
artist: "Deftones".to_string(),
|
||||
@@ -943,6 +999,7 @@ mod tests {
|
||||
track_duration_secs: Some(290),
|
||||
played_duration_secs: 250,
|
||||
scrobbled_at: "2026-03-18T14:00:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
NewScrobble {
|
||||
artist: "††† (Crosses)".to_string(),
|
||||
@@ -951,6 +1008,7 @@ mod tests {
|
||||
track_duration_secs: Some(186),
|
||||
played_duration_secs: 180,
|
||||
scrobbled_at: "2026-03-12T09:00:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
];
|
||||
for s in &scrobbles {
|
||||
@@ -976,6 +1034,7 @@ mod tests {
|
||||
track_duration_secs: Some(186),
|
||||
played_duration_secs: 186,
|
||||
scrobbled_at: "2026-03-19T10:00:00".to_string(),
|
||||
source: "test".into(),
|
||||
};
|
||||
// First insert should get row ID 1.
|
||||
let id = insert_scrobble(&conn, &s).unwrap();
|
||||
@@ -1078,6 +1137,7 @@ mod tests {
|
||||
track_duration_secs: Some(200),
|
||||
played_duration_secs: 200,
|
||||
scrobbled_at: "2026-03-19T10:00:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1090,6 +1150,7 @@ mod tests {
|
||||
track_duration_secs: Some(180),
|
||||
played_duration_secs: 180,
|
||||
scrobbled_at: "2026-03-19T10:01:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1143,6 +1204,7 @@ mod tests {
|
||||
track_duration_secs: Some(120),
|
||||
played_duration_secs: 120,
|
||||
scrobbled_at: "2026-03-19T11:00:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1155,6 +1217,7 @@ mod tests {
|
||||
track_duration_secs: Some(320),
|
||||
played_duration_secs: 320,
|
||||
scrobbled_at: "2026-03-19T11:05:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1181,6 +1244,7 @@ mod tests {
|
||||
track_duration_secs: Some(150),
|
||||
played_duration_secs: 150,
|
||||
scrobbled_at: "2026-03-19T12:00:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1193,6 +1257,7 @@ mod tests {
|
||||
track_duration_secs: Some(240),
|
||||
played_duration_secs: 240,
|
||||
scrobbled_at: "2026-03-19T12:05:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1229,6 +1294,7 @@ mod tests {
|
||||
track_duration_secs: Some(300),
|
||||
played_duration_secs: 300,
|
||||
scrobbled_at: (*ts).to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1281,6 +1347,7 @@ mod tests {
|
||||
track_duration_secs: Some(100),
|
||||
played_duration_secs: 100,
|
||||
scrobbled_at: "2026-03-19T13:00:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1293,6 +1360,7 @@ mod tests {
|
||||
track_duration_secs: Some(360),
|
||||
played_duration_secs: 360,
|
||||
scrobbled_at: "2026-03-19T13:05:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1338,6 +1406,7 @@ mod tests {
|
||||
track_duration_secs: None,
|
||||
played_duration_secs: 240,
|
||||
scrobbled_at: "2026-03-19T12:00:00".to_string(),
|
||||
source: "test".into(),
|
||||
};
|
||||
insert_scrobble(&conn, &s).unwrap();
|
||||
let recent = recent_scrobbles(&conn, "all", 1).unwrap();
|
||||
@@ -1359,6 +1428,7 @@ mod tests {
|
||||
track_duration_secs: Some(100),
|
||||
played_duration_secs: 100,
|
||||
scrobbled_at: "2026-03-19T10:00:00".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1371,6 +1441,7 @@ mod tests {
|
||||
track_duration_secs: Some(120),
|
||||
played_duration_secs: 120,
|
||||
scrobbled_at: "2026-03-19T12:34:56".to_string(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1404,6 +1475,7 @@ mod tests {
|
||||
track_duration_secs: Some(180),
|
||||
played_duration_secs: 180,
|
||||
scrobbled_at: old_day.clone(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1419,6 +1491,7 @@ mod tests {
|
||||
track_duration_secs: Some(200),
|
||||
played_duration_secs: 200,
|
||||
scrobbled_at: recent,
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -1178,6 +1178,7 @@ mod tests {
|
||||
track_duration_secs: Some(291),
|
||||
played_duration_secs: 291,
|
||||
scrobbled_at: "2026-03-19T10:00:00".into(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1190,6 +1191,7 @@ mod tests {
|
||||
track_duration_secs: Some(215),
|
||||
played_duration_secs: 200,
|
||||
scrobbled_at: "2026-03-19T10:05:00".into(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1294,6 +1296,7 @@ mod tests {
|
||||
track_duration_secs: Some(180),
|
||||
played_duration_secs: 160,
|
||||
scrobbled_at: "2026-03-19T10:00:00".into(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -405,7 +405,8 @@ fn run_watch(player: &str, use_mpris: bool, mpd_config: Option<mpd::MpdConfig>,
|
||||
// Receives events from both reader threads and the Ctrl+C handler,
|
||||
// and feeds them into the MPRIS ScrobbleTracker state machine.
|
||||
// When MPRIS is disabled, this loop exits immediately on shutdown.
|
||||
let mut tracker = watcher::create_db_tracker(conn);
|
||||
let source = watcher::normalise_player_name(player);
|
||||
let mut tracker = watcher::create_db_tracker(conn, source);
|
||||
let mut eof_count = 0;
|
||||
let mut flushed = false;
|
||||
|
||||
|
||||
@@ -603,7 +603,7 @@ pub fn run_mpd_watch(config: &MpdConfig, conn: Arc<Mutex<Connection>>, running:
|
||||
// Keep a clone of `conn` for inline cover extraction. The tracker closure
|
||||
// owns the original; both share the same underlying Mutex<Connection>.
|
||||
let conn_for_covers = conn.clone();
|
||||
let mut tracker = watcher::create_db_tracker(conn);
|
||||
let mut tracker = watcher::create_db_tracker(conn, "MPD".to_string());
|
||||
|
||||
// Outer loop: reconnect on failure until shutdown is requested.
|
||||
while running.load(Ordering::SeqCst) {
|
||||
|
||||
@@ -210,6 +210,8 @@ pub struct ReportData {
|
||||
pub top_tracks: Vec<db::TopTrack>,
|
||||
/// Top genres derived from cached album metadata.
|
||||
pub top_genres: Vec<db::TopGenre>,
|
||||
/// Breakdown of scrobbles by source (player/service).
|
||||
pub top_sources: Vec<db::TopSource>,
|
||||
/// Most recent scrobbles (newest first), limited to 20.
|
||||
pub recent_scrobbles: Vec<db::Scrobble>,
|
||||
}
|
||||
@@ -529,6 +531,7 @@ pub fn gather_report(
|
||||
top_albums: db::top_albums(conn, period, limit)?,
|
||||
top_tracks: db::top_tracks(conn, period, limit)?,
|
||||
top_genres: db::top_genres(conn, period, limit)?,
|
||||
top_sources: db::top_sources(conn, period)?,
|
||||
// Recent scrobbles are always limited to 20, regardless of the --limit flag.
|
||||
recent_scrobbles: db::recent_scrobbles(conn, period, 20)?,
|
||||
})
|
||||
@@ -584,6 +587,28 @@ pub fn print_terminal_report(data: &ReportData) {
|
||||
println!(" Mood: {}", mood.join(" · "));
|
||||
}
|
||||
|
||||
// --- Source Breakdown ---
|
||||
if !data.top_sources.is_empty() {
|
||||
println!("\n Sources");
|
||||
let rows: Vec<Vec<String>> = data
|
||||
.top_sources
|
||||
.iter()
|
||||
.map(|s| {
|
||||
vec![
|
||||
s.source.clone(),
|
||||
s.scrobbles.to_string(),
|
||||
format_duration(s.listen_time_secs),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
print_box_table(
|
||||
&["Source", "Scrobbles", "Time"],
|
||||
&rows,
|
||||
None,
|
||||
&[0],
|
||||
);
|
||||
}
|
||||
|
||||
// --- Top Artists ---
|
||||
if !data.top_artists.is_empty() {
|
||||
println!("\n Top Artists");
|
||||
@@ -881,6 +906,30 @@ pub fn render_html_report(conn: &Connection, limit: i64, all_time_limit: i64) ->
|
||||
write_kpi(&mut h, "Tracks", &data.overview.unique_tracks.to_string());
|
||||
h.close("</div>");
|
||||
|
||||
// Source breakdown — simple bar table showing scrobbles per player/service.
|
||||
// Only rendered when there is more than one source, or when the single
|
||||
// source differs from the default expectation, so it doesn't clutter
|
||||
// reports for users with a single player.
|
||||
if !data.top_sources.is_empty() {
|
||||
let source_rows: Vec<BarRow> = data
|
||||
.top_sources
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, s)| BarRow {
|
||||
cells: vec![(i + 1).to_string(), s.source.clone()],
|
||||
value: s.scrobbles,
|
||||
suffix: format_duration(s.listen_time_secs),
|
||||
cover: None,
|
||||
})
|
||||
.collect();
|
||||
write_bar_table(
|
||||
&mut h,
|
||||
"Sources",
|
||||
&["#", "Source", "Scrobbles", ""],
|
||||
&source_rows,
|
||||
);
|
||||
}
|
||||
|
||||
// Album cover grid — shown first after KPIs to visually illustrate
|
||||
// the period's listening at a glance.
|
||||
//
|
||||
@@ -1633,6 +1682,7 @@ mod tests {
|
||||
track_duration_secs: Some(186),
|
||||
played_duration_secs: 186,
|
||||
scrobbled_at: "2026-03-19T10:00:00".into(),
|
||||
source: "test".into(),
|
||||
},
|
||||
db::NewScrobble {
|
||||
artist: "††† (Crosses)".into(),
|
||||
@@ -1641,6 +1691,7 @@ mod tests {
|
||||
track_duration_secs: Some(215),
|
||||
played_duration_secs: 200,
|
||||
scrobbled_at: "2026-03-19T10:05:00".into(),
|
||||
source: "test".into(),
|
||||
},
|
||||
db::NewScrobble {
|
||||
artist: "Deftones".into(),
|
||||
@@ -1649,6 +1700,7 @@ mod tests {
|
||||
track_duration_secs: Some(291),
|
||||
played_duration_secs: 291,
|
||||
scrobbled_at: "2026-03-19T10:10:00".into(),
|
||||
source: "test".into(),
|
||||
},
|
||||
];
|
||||
for s in &scrobbles {
|
||||
@@ -1687,6 +1739,7 @@ mod tests {
|
||||
track_duration_secs: Some(180),
|
||||
played_duration_secs: 180,
|
||||
scrobbled_at: "2026-03-19T10:00:00".into(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1709,6 +1762,7 @@ mod tests {
|
||||
track_duration_secs: Some(180),
|
||||
played_duration_secs: 170,
|
||||
scrobbled_at: "2026-03-19T10:00:00".into(),
|
||||
source: "test".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -131,16 +131,19 @@ pub struct ScrobbleTracker<F: FnMut(NewScrobble)> {
|
||||
playing_since: Option<Instant>,
|
||||
accumulated_secs: f64,
|
||||
scrobble_fn: F,
|
||||
/// The source label recorded alongside each scrobble (e.g. `"MPD"`).
|
||||
source: String,
|
||||
}
|
||||
|
||||
impl<F: FnMut(NewScrobble)> ScrobbleTracker<F> {
|
||||
pub fn new(scrobble_fn: F) -> Self {
|
||||
pub fn new(scrobble_fn: F, source: String) -> Self {
|
||||
Self {
|
||||
current_track: None,
|
||||
is_playing: false,
|
||||
playing_since: None,
|
||||
accumulated_secs: 0.0,
|
||||
scrobble_fn,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +268,7 @@ impl<F: FnMut(NewScrobble)> ScrobbleTracker<F> {
|
||||
track_duration_secs: track_dur,
|
||||
played_duration_secs: self.accumulated_secs.round() as i64,
|
||||
scrobbled_at: now,
|
||||
source: self.source.clone(),
|
||||
};
|
||||
(self.scrobble_fn)(scrobble);
|
||||
}
|
||||
@@ -329,6 +333,35 @@ pub fn parse_status_line(line: &str) -> Option<Event> {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Player name normalisation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Normalise a D-Bus player name (e.g. `"com.blitzfc.qbz"`) into a
|
||||
/// human-readable source label (e.g. `"Qobuz"`).
|
||||
///
|
||||
/// Logic:
|
||||
/// 1. Take the last `.`-separated segment of the D-Bus name.
|
||||
/// 2. Apply known mappings (matched case-insensitively):
|
||||
/// - `"qbz"` or `"qbz2"` → `"Qobuz"`
|
||||
/// 3. Otherwise: capitalise the first character of the segment.
|
||||
pub fn normalise_player_name(player: &str) -> String {
|
||||
let segment = player.rsplit('.').next().unwrap_or(player);
|
||||
match segment.to_lowercase().as_str() {
|
||||
"qbz" | "qbz2" => "Qobuz".to_string(),
|
||||
_ => {
|
||||
let mut chars = segment.chars();
|
||||
match chars.next() {
|
||||
None => String::new(),
|
||||
Some(first) => {
|
||||
let upper: String = first.to_uppercase().collect();
|
||||
upper + chars.as_str()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Factory for production use
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -339,23 +372,28 @@ pub fn parse_status_line(line: &str) -> Option<Event> {
|
||||
/// to stderr. The `Arc<Mutex<Connection>>` is shared with the main thread
|
||||
/// but only accessed from the main event loop (single-threaded), so contention
|
||||
/// is minimal.
|
||||
///
|
||||
/// The `source` parameter names the player or service that produced these
|
||||
/// scrobbles (e.g. `"MPD"`, `"Qobuz"`). It is stored alongside each scrobble
|
||||
/// and shown in source-breakdown reports.
|
||||
pub fn create_db_tracker(
|
||||
conn: std::sync::Arc<std::sync::Mutex<Connection>>,
|
||||
source: String,
|
||||
) -> ScrobbleTracker<impl FnMut(NewScrobble)> {
|
||||
ScrobbleTracker::new(move |scrobble: NewScrobble| {
|
||||
let conn = conn.lock().unwrap();
|
||||
match db::insert_scrobble(&conn, &scrobble) {
|
||||
Ok(_) => {
|
||||
eprintln!(
|
||||
"[scrobbled] {} - {} ({}s)",
|
||||
scrobble.artist, scrobble.title, scrobble.played_duration_secs
|
||||
"[scrobbled] {}: {} - {} ({}s)",
|
||||
scrobble.source, scrobble.artist, scrobble.title, scrobble.played_duration_secs
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[error] Failed to insert scrobble: {}", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
}, source)
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -380,6 +418,8 @@ pub struct TestableTracker {
|
||||
pub scrobbled: Vec<NewScrobble>,
|
||||
/// The current simulated time, in seconds since the start of the test.
|
||||
clock_secs: f64,
|
||||
/// Source label attached to every scrobble produced during the test.
|
||||
source: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -392,6 +432,7 @@ impl TestableTracker {
|
||||
accumulated_secs: 0.0,
|
||||
scrobbled: Vec::new(),
|
||||
clock_secs: 0.0,
|
||||
source: "test".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +467,7 @@ impl TestableTracker {
|
||||
track_duration_secs: track_dur,
|
||||
played_duration_secs: self.accumulated_secs.round() as i64,
|
||||
scrobbled_at: format!("test-time-{}", self.clock_secs),
|
||||
source: self.source.clone(),
|
||||
};
|
||||
self.scrobbled.push(scrobble);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user