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
+29
View File
@@ -62,4 +62,33 @@ export const statistics = {
exportSaving: 'Speichere…',
exportSaved: 'Gespeichert.',
exportSaveFailed: 'Bild konnte nicht gespeichert werden.',
tabServer: 'Server-Statistik',
tabPlayer: 'Player-Statistik',
playerEmpty: 'Hör zu — dein lokaler Verlauf erscheint hier, sobald der Library-Index aktiv ist.',
playerSummaryTime: 'Hörzeit',
playerListeningDayShort: '{{count}}d',
playerListeningHourShort: '{{count}}h',
playerListeningMinuteShort: '{{count}}min',
playerSummarySessions: 'Sitzungen',
playerSummaryTracks: 'Titel',
playerSummaryUniqueTracks: 'Einzigartige Titel',
playerSummaryDays: 'Days',
playerPartialIndexNotice: 'Einige Server sind vom lokalen Bibliotheksindex ausgeschlossen. Wiedergaben auf diesen Servern werden nicht in der Player-Statistik erfasst.',
playerPartialIndexSettings: 'Bibliothekseinstellungen',
playerSummaryCompletion: 'Voll / teilweise',
playerYearPrev: 'Vorheriges Jahr',
playerYearNext: 'Nächstes Jahr',
playerHeatmapLegend: 'Dunkler = mehr gehörte Titel an diesem Tag.',
playerHeatmapLess: 'Weniger',
playerHeatmapMore: 'Mehr',
playerDaySessions: '{{count}} Sitzungen',
playerDayTrackPlays: '{{count}} Titel',
playerDayFullPartial: '{{full}} voll · {{partial}} teilweise',
playerRecentDaysTitle: 'Letzte Tage',
playerDayToday: 'Heute',
playerDayYesterday: 'Gestern',
playerListenedSecShort: '{{seconds}} s',
playerListenedMinDecimal: '{{minutes}} Min.',
completionFull: 'Vollständig',
completionPartial: 'Teilweise',
};
+29
View File
@@ -62,4 +62,33 @@ export const statistics = {
exportSaving: 'Saving…',
exportSaved: 'Saved.',
exportSaveFailed: 'Could not save the image.',
tabServer: 'Server stats',
tabPlayer: 'Player stats',
playerEmpty: 'Start listening — your local play history will appear here once the library index is enabled.',
playerSummaryTime: 'Listening time',
playerListeningDayShort: '{{count}}d',
playerListeningHourShort: '{{count}}h',
playerListeningMinuteShort: '{{count}}m',
playerSummarySessions: 'Sessions',
playerSummaryTracks: 'Track plays',
playerSummaryUniqueTracks: 'Unique tracks',
playerSummaryDays: 'Days',
playerPartialIndexNotice: 'Some servers are excluded from the local library index. Listening on those servers is not recorded in player statistics.',
playerPartialIndexSettings: 'Library settings',
playerSummaryCompletion: 'Full / partial',
playerYearPrev: 'Previous year',
playerYearNext: 'Next year',
playerHeatmapLegend: 'Darker cells = more tracks played that day.',
playerHeatmapLess: 'Less',
playerHeatmapMore: 'More',
playerDaySessions: '{{count}} sessions',
playerDayTrackPlays: '{{count}} tracks',
playerDayFullPartial: '{{full}} full · {{partial}} partial',
playerRecentDaysTitle: 'Recent days',
playerDayToday: 'Today',
playerDayYesterday: 'Yesterday',
playerListenedSecShort: '{{seconds}}s',
playerListenedMinDecimal: '{{minutes}} min',
completionFull: 'Full listen',
completionPartial: 'Partial',
};
+29
View File
@@ -44,4 +44,33 @@ export const statistics = {
topRatedArtists: 'Artistas Mejor Calificados',
noRatedSongs: 'Aún no hay canciones calificadas. Califica canciones en la vista de álbum o playlist.',
noRatedArtists: 'Aún no hay artistas calificados.',
tabServer: 'Estadísticas del servidor',
tabPlayer: 'Estadísticas del reproductor',
playerEmpty: 'Empieza a escuchar — tu historial local aparecerá aquí con el índice de biblioteca activo.',
playerSummaryTime: 'Tiempo de escucha',
playerListeningDayShort: '{{count}}d',
playerListeningHourShort: '{{count}}h',
playerListeningMinuteShort: '{{count}}m',
playerSummarySessions: 'Sesiones',
playerSummaryTracks: 'Pistas',
playerSummaryUniqueTracks: 'Pistas únicas',
playerSummaryDays: 'Days',
playerPartialIndexNotice: 'Algunos servidores están excluidos del índice local de biblioteca. La escucha en esos servidores no se registra en las estadísticas del reproductor.',
playerPartialIndexSettings: 'Ajustes de biblioteca',
playerSummaryCompletion: 'Completas / parciales',
playerYearPrev: 'Año anterior',
playerYearNext: 'Año siguiente',
playerHeatmapLegend: 'Más oscuro = más pistas escuchadas ese día.',
playerHeatmapLess: 'Menos',
playerHeatmapMore: 'Más',
playerDaySessions: '{{count}} sesiones',
playerDayTrackPlays: '{{count}} pistas',
playerDayFullPartial: '{{full}} completas · {{partial}} parciales',
playerRecentDaysTitle: 'Días recientes',
playerDayToday: 'Hoy',
playerDayYesterday: 'Ayer',
playerListenedSecShort: '{{seconds}} s',
playerListenedMinDecimal: '{{minutes}} min',
completionFull: 'Escucha completa',
completionPartial: 'Parcial',
};
+29
View File
@@ -44,4 +44,33 @@ export const statistics = {
topRatedArtists: 'Artistes les mieux notés',
noRatedSongs: 'Aucun morceau noté. Notez des morceaux dans la vue album ou playlist.',
noRatedArtists: 'Aucun artiste noté.',
tabServer: 'Stats serveur',
tabPlayer: 'Stats lecteur',
playerEmpty: 'Commencez à écouter — lhistorique local apparaîtra ici avec lindex bibliothèque activé.',
playerSummaryTime: 'Temps d’écoute',
playerListeningDayShort: '{{count}}j',
playerListeningHourShort: '{{count}}h',
playerListeningMinuteShort: '{{count}}m',
playerSummarySessions: 'Sessions',
playerSummaryTracks: 'Morceaux',
playerSummaryUniqueTracks: 'Morceaux uniques',
playerSummaryDays: 'Days',
playerPartialIndexNotice: 'Certains serveurs sont exclus de lindex local de bibliothèque. L’écoute sur ces serveurs nest pas enregistrée dans les statistiques du lecteur.',
playerPartialIndexSettings: 'Paramètres bibliothèque',
playerSummaryCompletion: 'Complets / partiels',
playerYearPrev: 'Année précédente',
playerYearNext: 'Année suivante',
playerHeatmapLegend: 'Plus foncé = plus de morceaux écoutés ce jour-là.',
playerHeatmapLess: 'Moins',
playerHeatmapMore: 'Plus',
playerDaySessions: '{{count}} sessions',
playerDayTrackPlays: '{{count}} morceaux',
playerDayFullPartial: '{{full}} complets · {{partial}} partiels',
playerRecentDaysTitle: 'Jours récents',
playerDayToday: "Aujourd'hui",
playerDayYesterday: 'Hier',
playerListenedSecShort: '{{seconds}} s',
playerListenedMinDecimal: '{{minutes}} min',
completionFull: 'Écoute complète',
completionPartial: 'Partiel',
};
+29
View File
@@ -44,4 +44,33 @@ export const statistics = {
topRatedArtists: 'Høyest vurderte artister',
noRatedSongs: 'Ingen vurderte spor ennå. Vurder spor i album- eller spillelistevisning.',
noRatedArtists: 'Ingen vurderte artister ennå.',
tabServer: 'Serverstatistikk',
tabPlayer: 'Spillerstatistikk',
playerEmpty: 'Begynn å lytte — lokal historikk vises her når bibliotekindeks er aktivert.',
playerSummaryTime: 'Lyttetid',
playerListeningDayShort: '{{count}}d',
playerListeningHourShort: '{{count}}t',
playerListeningMinuteShort: '{{count}}m',
playerSummarySessions: 'Økter',
playerSummaryTracks: 'Spor',
playerSummaryUniqueTracks: 'Unike spor',
playerSummaryDays: 'Days',
playerPartialIndexNotice: 'Noen servere er ekskludert fra det lokale biblioteksindeks. Lytting på disse serverne registreres ikke i spillerstatistikken.',
playerPartialIndexSettings: 'Bibliotekinnstillinger',
playerSummaryCompletion: 'Full / delvis',
playerYearPrev: 'Forrige år',
playerYearNext: 'Neste år',
playerHeatmapLegend: 'Mørkere = flere spor den dagen.',
playerHeatmapLess: 'Mindre',
playerHeatmapMore: 'Mer',
playerDaySessions: '{{count}} økter',
playerDayTrackPlays: '{{count}} spor',
playerDayFullPartial: '{{full}} full · {{partial}} delvis',
playerRecentDaysTitle: 'Siste dager',
playerDayToday: 'I dag',
playerDayYesterday: 'I går',
playerListenedSecShort: '{{seconds}} s',
playerListenedMinDecimal: '{{minutes}} min',
completionFull: 'Full lytting',
completionPartial: 'Delvis',
};
+29
View File
@@ -44,4 +44,33 @@ export const statistics = {
topRatedArtists: 'Best beoordeelde artiesten',
noRatedSongs: 'Nog geen beoordeelde nummers. Beoordeel nummers in album- of afspeellijstweergave.',
noRatedArtists: 'Nog geen beoordeelde artiesten.',
tabServer: 'Serverstatistieken',
tabPlayer: 'Spelerstatistieken',
playerEmpty: 'Begin te luisteren — je lokale geschiedenis verschijnt hier zodra de bibliotheekindex aan staat.',
playerSummaryTime: 'Luistertijd',
playerListeningDayShort: '{{count}}d',
playerListeningHourShort: '{{count}}u',
playerListeningMinuteShort: '{{count}}m',
playerSummarySessions: 'Sessies',
playerSummaryTracks: 'Nummers',
playerSummaryUniqueTracks: 'Unieke nummers',
playerSummaryDays: 'Days',
playerPartialIndexNotice: 'Sommige servers zijn uitgesloten van de lokale bibliotheekindex. Luisteren op die servers wordt niet vastgelegd in de playerstatistieken.',
playerPartialIndexSettings: 'Bibliotheekinstellingen',
playerSummaryCompletion: 'Volledig / gedeeltelijk',
playerYearPrev: 'Vorig jaar',
playerYearNext: 'Volgend jaar',
playerHeatmapLegend: 'Donkerder = meer nummers geluisterd op die dag.',
playerHeatmapLess: 'Minder',
playerHeatmapMore: 'Meer',
playerDaySessions: '{{count}} sessies',
playerDayTrackPlays: '{{count}} nummers',
playerDayFullPartial: '{{full}} volledig · {{partial}} gedeeltelijk',
playerRecentDaysTitle: 'Recente dagen',
playerDayToday: 'Vandaag',
playerDayYesterday: 'Gisteren',
playerListenedSecShort: '{{seconds}} s',
playerListenedMinDecimal: '{{minutes}} min',
completionFull: 'Volledig geluisterd',
completionPartial: 'Gedeeltelijk',
};
+29
View File
@@ -62,4 +62,33 @@ export const statistics = {
exportSaving: 'Se salvează…',
exportSaved: 'Salvat.',
exportSaveFailed: 'Nu s-a putut salva imaginea.',
tabServer: 'Statistici server',
tabPlayer: 'Statistici player',
playerEmpty: 'Începe să asculți — istoricul local va apărea aici când indexul bibliotecii este activ.',
playerSummaryTime: 'Timp de ascultare',
playerListeningDayShort: '{{count}}z',
playerListeningHourShort: '{{count}}h',
playerListeningMinuteShort: '{{count}}m',
playerSummarySessions: 'Sesiuni',
playerSummaryTracks: 'Piese',
playerSummaryUniqueTracks: 'Piese unice',
playerSummaryDays: 'Days',
playerPartialIndexNotice: 'Unele servere sunt excluse din indexul local al bibliotecii. Ascultarea pe aceste servere nu este înregistrată în statisticile playerului.',
playerPartialIndexSettings: 'Setări bibliotecă',
playerSummaryCompletion: 'Complete / parțiale',
playerYearPrev: 'Anul anterior',
playerYearNext: 'Anul următor',
playerHeatmapLegend: 'Mai închis = mai multe piese ascultate în acea zi.',
playerHeatmapLess: 'Mai puțin',
playerHeatmapMore: 'Mai mult',
playerDaySessions: '{{count}} sesiuni',
playerDayTrackPlays: '{{count}} piese',
playerDayFullPartial: '{{full}} complete · {{partial}} parțiale',
playerRecentDaysTitle: 'Zile recente',
playerDayToday: 'Astăzi',
playerDayYesterday: 'Ieri',
playerListenedSecShort: '{{seconds}} s',
playerListenedMinDecimal: '{{minutes}} min',
completionFull: 'Ascultare completă',
completionPartial: 'Parțial',
};
+29
View File
@@ -55,4 +55,33 @@ export const statistics = {
topRatedArtists: 'Лучшие по рейтингу исполнители',
noRatedSongs: 'Нет оценённых треков. Ставьте оценки в альбомах или плейлистах.',
noRatedArtists: 'Нет оценённых исполнителей.',
tabServer: 'Статистика сервера',
tabPlayer: 'Статистика плеера',
playerEmpty: 'Начните слушать — локальная история появится здесь при включённом индексе библиотеки.',
playerSummaryTime: 'Время прослушивания',
playerListeningDayShort: '{{count}}д',
playerListeningHourShort: '{{count}}ч',
playerListeningMinuteShort: '{{count}}мин',
playerSummarySessions: 'Сессии',
playerSummaryTracks: 'Треки',
playerSummaryUniqueTracks: 'Уникальные треки',
playerSummaryDays: 'Days',
playerPartialIndexNotice: 'Не все серверы включены в локальный индекс библиотеки. Прослушивание с выключенных серверов не попадает в эту статистику.',
playerPartialIndexSettings: 'Настройки библиотеки',
playerSummaryCompletion: 'Полные / частичные',
playerYearPrev: 'Предыдущий год',
playerYearNext: 'Следующий год',
playerHeatmapLegend: 'Темнее — больше прослушанных треков за день.',
playerHeatmapLess: 'Меньше',
playerHeatmapMore: 'Больше',
playerDaySessions: '{{count}} сессий',
playerDayTrackPlays: '{{count}} треков',
playerDayFullPartial: '{{full}} полных · {{partial}} частичных',
playerRecentDaysTitle: 'Недавние дни',
playerDayToday: 'Сегодня',
playerDayYesterday: 'Вчера',
playerListenedSecShort: '{{seconds}} с',
playerListenedMinDecimal: '{{minutes}} мин',
completionFull: 'Прослушано',
completionPartial: 'Частично',
};
+29
View File
@@ -44,4 +44,33 @@ export const statistics = {
topRatedArtists: '最高评分艺人',
noRatedSongs: '暂无已评分歌曲。请在专辑或播放列表中为歌曲评分。',
noRatedArtists: '暂无已评分艺人。',
tabServer: '服务器统计',
tabPlayer: '播放器统计',
playerEmpty: '开始收听 — 启用媒体库索引后,本地播放历史将显示在这里。',
playerSummaryTime: '收听时长',
playerListeningDayShort: '{{count}}天',
playerListeningHourShort: '{{count}}小时',
playerListeningMinuteShort: '{{count}}分钟',
playerSummarySessions: '会话数',
playerSummaryTracks: '曲目',
playerSummaryUniqueTracks: '独立曲目',
playerSummaryDays: 'Days',
playerPartialIndexNotice: '部分服务器未纳入本地库索引,在这些服务器上的收听不会计入播放器统计。',
playerPartialIndexSettings: '库设置',
playerSummaryCompletion: '完整 / 部分',
playerYearPrev: '上一年',
playerYearNext: '下一年',
playerHeatmapLegend: '颜色越深表示当天收听的曲目越多。',
playerHeatmapLess: '少',
playerHeatmapMore: '多',
playerDaySessions: '{{count}} 次会话',
playerDayTrackPlays: '{{count}} 首曲目',
playerDayFullPartial: '{{full}} 完整 · {{partial}} 部分',
playerRecentDaysTitle: '最近几天',
playerDayToday: '今天',
playerDayYesterday: '昨天',
playerListenedSecShort: '{{seconds}} 秒',
playerListenedMinDecimal: '{{minutes}} 分钟',
completionFull: '完整收听',
completionPartial: '部分',
};