feat(player-stats): local listening history tab with heatmap and summaries (#849)

* feat(player-stats): local listening history tab with heatmap and summaries

Record play sessions in library.sqlite when the library index is enabled,
add Rust read APIs and Tauri commands for year/day aggregates, and ship the
Player stats UI with session clustering, event-driven live refresh, and a
notice when some servers are excluded from indexing.

* test(player-stats): split play_session repo and expand test coverage

Move the repository into play_session/ (completion, cluster, integration tests),
add remap/purge/FK coverage in Rust, and cover ingestion gates plus live-refresh
hooks on the frontend per spec v0.3.

* docs(release): CHANGELOG and credits for player stats (PR #849)

* fix(player-stats): satisfy tsc and clippy CI gates

Use InternetRadioStation field names in the radio skip test and replace
manual month/day range checks with RangeInclusive::contains.
This commit is contained in:
cucadmuh
2026-05-22 14:07:38 +03:00
committed by GitHub
parent 7afddf7b84
commit 23f7ba02d6
49 changed files with 3059 additions and 19 deletions
@@ -20,12 +20,13 @@ use crate::dto::{
count_local_tracks, local_tracks_max_updated_ms, track_index_nonempty, ArtifactInputDto,
FactInputDto, LibraryAdvancedSearchRequest, LibraryAdvancedSearchResponse,
LibraryCrossServerSearchResponse, LibraryLiveSearchRequest, LibraryLiveSearchResponse, LibraryTrackDto,
LibraryTracksEnvelope, OfflinePathDto, PurgeReportDto, SyncJobDto, SyncStateDto,
LibraryTracksEnvelope, OfflinePathDto, PlaySessionDayDetailDto, PlaySessionHeatmapDayDto,
PlaySessionInputDto, PlaySessionRecentDayDto, PlaySessionYearBoundsDto, PlaySessionYearSummaryDto, PurgeReportDto, SyncJobDto, SyncStateDto,
TrackArtifactDto, TrackFactDto, TrackRefDto,
};
use crate::live_search;
use crate::payload::LibrarySyncProgressPayload;
use crate::repos::{SyncStateRepository, TrackRepository};
use crate::repos::{PlaySessionRepository, SyncStateRepository, TrackRepository};
use crate::runtime::{CurrentJob, LibraryRuntime, SyncSession};
use crate::search::search_tracks;
use crate::store::LibraryStore;
@@ -955,6 +956,53 @@ pub fn library_put_fact(
crate::repos::FactRepository::new(&runtime.store).put(&server_id, &track_id, &fact, now_unix_ms())
}
#[tauri::command]
pub fn library_record_play_session(
runtime: State<'_, LibraryRuntime>,
input: PlaySessionInputDto,
) -> Result<(), String> {
PlaySessionRepository::new(&runtime.store).insert(&input)
}
#[tauri::command]
pub fn library_get_player_stats_year_summary(
runtime: State<'_, LibraryRuntime>,
year: i32,
) -> Result<PlaySessionYearSummaryDto, String> {
PlaySessionRepository::new(&runtime.store).year_summary(year)
}
#[tauri::command]
pub fn library_get_player_stats_heatmap(
runtime: State<'_, LibraryRuntime>,
year: i32,
) -> Result<Vec<PlaySessionHeatmapDayDto>, String> {
PlaySessionRepository::new(&runtime.store).heatmap(year)
}
#[tauri::command]
pub fn library_get_player_stats_day_detail(
runtime: State<'_, LibraryRuntime>,
date_iso: String,
) -> Result<PlaySessionDayDetailDto, String> {
PlaySessionRepository::new(&runtime.store).day_detail(&date_iso)
}
#[tauri::command]
pub fn library_get_player_stats_year_bounds(
runtime: State<'_, LibraryRuntime>,
) -> Result<PlaySessionYearBoundsDto, String> {
PlaySessionRepository::new(&runtime.store).year_bounds()
}
#[tauri::command]
pub fn library_get_player_stats_recent_days(
runtime: State<'_, LibraryRuntime>,
limit: Option<u32>,
) -> Result<Vec<PlaySessionRecentDayDto>, String> {
PlaySessionRepository::new(&runtime.store).recent_days(limit.unwrap_or(30))
}
#[tauri::command]
pub fn library_purge_server(
runtime: State<'_, LibraryRuntime>,
@@ -1013,6 +1061,10 @@ pub fn library_purge_server(
"DELETE FROM track_id_history WHERE server_id = ?1",
params![server_id],
)?;
tx.execute(
"DELETE FROM play_session WHERE server_id = ?1",
params![server_id],
)?;
tx.execute(
"DELETE FROM track WHERE server_id = ?1",
params![server_id],