fix: record play history

This commit is contained in:
Zhe Fang
2025-12-30 13:02:23 -05:00
parent 9b809983df
commit f39ad54df8
18 changed files with 177 additions and 138 deletions

View File

@@ -1,6 +1,6 @@
namespace BetterLyrics.WinUI3.Constants
{
public static class PlayerID
public static class PlayerId
{
public const string LXMusic = "cn.toside.music.desktop";
public const string LXMusicPortable = "lx-music-desktop.exe";

View File

@@ -42,7 +42,6 @@ namespace BetterLyrics.WinUI3.Controls
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly ILastFMService _lastFMService = Ioc.Default.GetRequiredService<ILastFMService>();
private readonly LyricsRenderer _lyricsRenderer = new();
private readonly FluidBackgroundRenderer _fluidRenderer = new();
@@ -660,22 +659,6 @@ namespace BetterLyrics.WinUI3.Controls
_songPosition += elapsedTime;
_totalPlayedTime += elapsedTime;
_songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
CheckAndScrobbleLastFM();
}
}
private void CheckAndScrobbleLastFM()
{
bool isEnabled = _mediaSessionsService.CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (!isEnabled || _isLastFMTracked) return;
var songInfo = _mediaSessionsService.CurrentSongInfo;
if (songInfo == null || songInfo.Duration <= 0) return;
if (_totalPlayedTime.TotalSeconds >= songInfo.Duration * 0.5)
{
_isLastFMTracked = true;
_lastFMService.TrackAsync(songInfo);
}
}

View File

@@ -54,6 +54,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 总播放时长 -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
@@ -63,14 +64,26 @@
<FontIcon FontSize="14" Glyph="&#xE916;" />
<TextBlock x:Uid="StatsDashboardControlTotalDuration" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<TextBlock
<StackPanel
Margin="0,8,0,0"
Orientation="Horizontal"
Spacing="4">
<TextBlock
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalDuration, Mode=OneWay, Converter={StaticResource MillisecondsToFormattedTimeConverter}}" />
Text="{x:Bind ViewModel.TotalDuration.TotalHours, Mode=OneWay}" />
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Bottom"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Opacity="0.8"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="Hrs" />
</StackPanel>
</StackPanel>
</Border>
<!-- 总播放歌曲次数 -->
<Border Grid.Column="1" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
@@ -87,6 +100,7 @@
</StackPanel>
</Border>
<!-- Top source -->
<Border
Grid.Column="2"
Margin="0,0,0,12"
@@ -113,6 +127,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Top artists -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
@@ -127,7 +142,7 @@
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind ViewModel.TopArtists, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="statsmodels:ArtistPlayCount">
<Grid Margin="0,8">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
@@ -145,8 +160,14 @@
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="{x:Bind PlayCount}" />
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
@@ -154,6 +175,7 @@
</Grid>
</Border>
<!-- Top sources -->
<Border
Grid.Column="1"
Margin="0,0,0,12"
@@ -171,7 +193,7 @@
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind ViewModel.PlayerStats, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:PlayerStatDisplayItem">
<Grid Margin="0,6">
<Grid Margin="0,4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
@@ -184,9 +206,17 @@
<TextBlock
FontSize="13"
Style="{ThemeResource BodyTextBlockStyle}"
Text="{x:Bind PlayerId}" />
<TextBlock Grid.Column="1" Style="{ThemeResource CaptionTextBlockStyle}">
Text="{x:Bind PlayerName}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
<Grid
@@ -213,6 +243,7 @@
</Border>
</Grid>
<!-- Top song -->
<Border
Grid.Row="2"
Margin="0,0,0,20"

View File

@@ -3,18 +3,37 @@ using System;
namespace BetterLyrics.WinUI3.Converter
{
public class MillisecondsToFormattedTimeConverter : IValueConverter
public partial class MillisecondsToFormattedTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int milliseconds)
double? milliseconds = null;
if (value is int iVal) milliseconds = iVal;
else if (value is double dVal) milliseconds = dVal;
else if (value is long lVal) milliseconds = lVal;
if (milliseconds.HasValue)
{
return TimeSpan.FromMilliseconds(milliseconds).ToString(@"mm\:ss\.fff");
}
else if (value is double doubleMilliseconds)
var ts = TimeSpan.FromMilliseconds(milliseconds.Value);
string? format = parameter?.ToString();
if (string.IsNullOrEmpty(format))
{
return TimeSpan.FromMilliseconds(doubleMilliseconds).ToString(@"mm\:ss\.fff");
format = @"mm\:ss\.fff";
}
try
{
return ts.ToString(format);
}
catch (FormatException)
{
return ts.ToString();
}
}
return value?.ToString() ?? "";
}

View File

@@ -41,7 +41,7 @@ namespace BetterLyrics.WinUI3.Extensions
Title = songInfo.Title,
Artist = songInfo.DisplayArtists,
Album = songInfo.Album,
PlayerID = songInfo.PlayerId ?? "N/A",
PlayerId = songInfo.PlayerId ?? "N/A",
TotalDurationMs = songInfo.DurationMs,
DurationPlayedMs = actualPlayedMs,
StartedAt = DateTime.Now.AddMilliseconds(-actualPlayedMs)

View File

@@ -58,7 +58,7 @@ namespace BetterLyrics.WinUI3.Helper
public static string PlayQueuePath => Path.Combine(LocalFolder, "play-queue.m3u");
public static string PlayHistoryPath => Path.Combine(LocalFolder, "play-history.db");
public static string FilesCachePath => Path.Combine(CacheFolder, "files-cache.db");
public static string FilesIndexPath => Path.Combine(LocalFolder, "files-index.db");
public static void EnsureDirectories()
{

View File

@@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
namespace BetterLyrics.WinUI3.Helper
{
public static class PlayerIDHelper
public static class PlayerIdHelper
{
private static readonly List<string> neteaseFamilyRegex =
[
@@ -25,64 +25,64 @@ namespace BetterLyrics.WinUI3.Helper
return false;
}
public static bool IsLXMusic(string? id) => id is PlayerID.LXMusic or PlayerID.LXMusicPortable;
public static bool IsLXMusic(string? id) => id is PlayerId.LXMusic or PlayerId.LXMusicPortable;
public static bool IsAppleMusic(string? id) => id is PlayerID.AppleMusic or PlayerID.AppleMusicAlternative;
public static bool IsAppleMusic(string? id) => id is PlayerId.AppleMusic or PlayerId.AppleMusicAlternative;
public static bool IsBetterLyrics(string? id) => id is PlayerID.BetterLyrics or PlayerID.BetterLyricsDebug;
public static bool IsBetterLyrics(string? id) => id is PlayerId.BetterLyrics or PlayerId.BetterLyricsDebug;
public static string? GetDisplayName(string? id) => id switch
{
PlayerID.Spotify => PlayerName.Spotify,
PlayerID.AppleMusic => PlayerName.AppleMusic,
PlayerID.iTunes => PlayerName.iTunes,
PlayerID.KugouMusic => PlayerName.KugouMusic,
PlayerID.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic,
PlayerID.QQMusic => PlayerName.QQMusic,
PlayerID.LXMusic => PlayerName.LXMusic,
PlayerID.LXMusicPortable => PlayerName.LXMusicPortable,
PlayerID.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11,
PlayerID.AIMP => PlayerName.AIMP,
PlayerID.Foobar2000 => PlayerName.Foobar2000,
PlayerID.MusicBee => PlayerName.MusicBee,
PlayerID.PotPlayer => PlayerName.PotPlayer,
PlayerID.Chrome => PlayerName.Chrome,
PlayerID.Edge => PlayerName.Edge,
PlayerID.BetterLyrics => PlayerName.BetterLyrics,
PlayerID.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
PlayerID.SaltPlayerForWindowsMS => PlayerName.SaltPlayerForWindowsMS,
PlayerID.SaltPlayerForWindowsSteam => PlayerName.SaltPlayerForWindowsSteam,
PlayerID.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerID.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerID.Listen1 => PlayerName.Listen1,
PlayerId.Spotify => PlayerName.Spotify,
PlayerId.AppleMusic => PlayerName.AppleMusic,
PlayerId.iTunes => PlayerName.iTunes,
PlayerId.KugouMusic => PlayerName.KugouMusic,
PlayerId.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic,
PlayerId.QQMusic => PlayerName.QQMusic,
PlayerId.LXMusic => PlayerName.LXMusic,
PlayerId.LXMusicPortable => PlayerName.LXMusicPortable,
PlayerId.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11,
PlayerId.AIMP => PlayerName.AIMP,
PlayerId.Foobar2000 => PlayerName.Foobar2000,
PlayerId.MusicBee => PlayerName.MusicBee,
PlayerId.PotPlayer => PlayerName.PotPlayer,
PlayerId.Chrome => PlayerName.Chrome,
PlayerId.Edge => PlayerName.Edge,
PlayerId.BetterLyrics => PlayerName.BetterLyrics,
PlayerId.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
PlayerId.SaltPlayerForWindowsMS => PlayerName.SaltPlayerForWindowsMS,
PlayerId.SaltPlayerForWindowsSteam => PlayerName.SaltPlayerForWindowsSteam,
PlayerId.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerId.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerId.Listen1 => PlayerName.Listen1,
_ => id,
};
public static string GetLogoPath(string? id) => id switch
{
PlayerID.Spotify => PathHelper.SpotifyLogoPath,
PlayerID.AppleMusic => PathHelper.AppleMusicLogoPath,
PlayerID.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
PlayerID.iTunes => PathHelper.iTunesLogoPath,
PlayerID.KugouMusic => PathHelper.KugouMusicLogoPath,
PlayerID.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,
PlayerID.QQMusic => PathHelper.QQMusicLogoPath,
PlayerID.LXMusic => PathHelper.LXMusicLogoPath,
PlayerID.LXMusicPortable => PathHelper.LXMusicLogoPath,
PlayerID.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath,
PlayerID.AIMP => PathHelper.AIMPLogoPath,
PlayerID.Foobar2000 => PathHelper.Foobar2000LogoPath,
PlayerID.MusicBee => PathHelper.MusicBeeLogoPath,
PlayerID.PotPlayer => PathHelper.PotPlayerLogoPath,
PlayerID.Chrome => PathHelper.ChromeLogoPath,
PlayerID.Edge => PathHelper.EdgeLogoPath,
PlayerID.BetterLyrics => PathHelper.LogoPath,
PlayerID.BetterLyricsDebug => PathHelper.LogoPath,
PlayerID.SaltPlayerForWindowsMS => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.SaltPlayerForWindowsSteam => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerID.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerID.Listen1 => PathHelper.Listen1LogoPath,
PlayerId.Spotify => PathHelper.SpotifyLogoPath,
PlayerId.AppleMusic => PathHelper.AppleMusicLogoPath,
PlayerId.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
PlayerId.iTunes => PathHelper.iTunesLogoPath,
PlayerId.KugouMusic => PathHelper.KugouMusicLogoPath,
PlayerId.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,
PlayerId.QQMusic => PathHelper.QQMusicLogoPath,
PlayerId.LXMusic => PathHelper.LXMusicLogoPath,
PlayerId.LXMusicPortable => PathHelper.LXMusicLogoPath,
PlayerId.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath,
PlayerId.AIMP => PathHelper.AIMPLogoPath,
PlayerId.Foobar2000 => PathHelper.Foobar2000LogoPath,
PlayerId.MusicBee => PathHelper.MusicBeeLogoPath,
PlayerId.PotPlayer => PathHelper.PotPlayerLogoPath,
PlayerId.Chrome => PathHelper.ChromeLogoPath,
PlayerId.Edge => PathHelper.EdgeLogoPath,
PlayerId.BetterLyrics => PathHelper.LogoPath,
PlayerId.BetterLyricsDebug => PathHelper.LogoPath,
PlayerId.SaltPlayerForWindowsMS => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerId.SaltPlayerForWindowsSteam => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerId.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerId.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerId.Listen1 => PathHelper.Listen1LogoPath,
_ => PathHelper.UnknownPlayerLogoPath,
};
}

View File

@@ -35,11 +35,11 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchType LyricsSearchType { get; set; } = LyricsSearchType.Sequential;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int MatchingThreshold { get; set; } = 40;
[JsonIgnore] public string LogoPath => PlayerIDHelper.GetLogoPath(Provider);
[JsonIgnore] public string LogoPath => PlayerIdHelper.GetLogoPath(Provider);
[JsonIgnore] public string? DisplayName => PlayerIDHelper.GetDisplayName(Provider);
[JsonIgnore] public string? DisplayName => PlayerIdHelper.GetDisplayName(Provider);
[JsonIgnore] public bool IsLXMusic => PlayerIDHelper.IsLXMusic(Provider);
[JsonIgnore] public bool IsLXMusic => PlayerIdHelper.IsLXMusic(Provider);
public MediaSourceProviderInfo()
{
@@ -53,7 +53,7 @@ namespace BetterLyrics.WinUI3.Models
IsEnabled = isEnable;
switch (provider)
{
case Constants.PlayerID.AppleMusic:
case Constants.PlayerId.AppleMusic:
// Apple Music 的特性
TimelineSyncThreshold = 1000;
PositionOffset = 1000;

View File

@@ -20,6 +20,6 @@ namespace BetterLyrics.WinUI3.Models
public double TotalDurationMs { get; set; }
[Indexed]
public string PlayerID { get; set; }
public string PlayerId { get; set; }
}
}

View File

@@ -1,4 +1,5 @@
using System;
using BetterLyrics.WinUI3.Helper;
using System;
using System.Collections.Generic;
using System.Text;
@@ -9,7 +10,7 @@ namespace BetterLyrics.WinUI3.Models
public string PlayerId { get; set; }
public int PlayCount { get; set; }
// 这就是 XAML 中 Rectangle Width 绑定的属性
public double DisplayWidth { get; set; }
public string PlayerName => PlayerIdHelper.GetDisplayName(PlayerId);
}
}

View File

@@ -6,7 +6,7 @@ namespace BetterLyrics.WinUI3.Models.Stats
{
public class PlayerStats
{
public string PlayerID { get; set; }
public string PlayerId { get; set; }
public int Count { get; set; }
public double DisplayWidth => (TotalCount > 0) ? (Count / (double)TotalCount) * 150 : 0;

View File

@@ -8,7 +8,6 @@ namespace BetterLyrics.WinUI3.Models.Stats
{
public string Title { get; set; }
public string Artist { get; set; }
public string AlbumArtHash { get; set; }
public int PlayCount { get; set; }
}
}

View File

@@ -44,7 +44,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
_logger = logger;
_localizationService = localizationService;
_settingsService = settingsService;
_db = new SQLiteAsyncConnection(PathHelper.FilesCachePath);
_db = new SQLiteAsyncConnection(PathHelper.FilesIndexPath);
}
public async Task InitializeAsync()

View File

@@ -558,11 +558,11 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
ISearchResult? result;
if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIDHelper.IsNeteaseFamily(songInfo.PlayerId))
if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIdHelper.IsNeteaseFamily(songInfo.PlayerId))
{
result = new NeteaseSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId);
}
else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerID.QQMusic)
else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerId.QQMusic)
{
result = new QQMusicSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId, "");
}

View File

@@ -11,6 +11,7 @@ using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.SettingsService;
@@ -56,6 +57,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private readonly ISettingsService _settingsService;
private readonly IDiscordService _discordService;
private readonly IPlayHistoryService _playHistoryService;
private readonly ILastFMService _lastFMService;
private readonly ILogger<MediaSessionsService> _logger;
private double _lxMusicPositionSeconds = 0;
@@ -79,6 +81,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
ITranslationService libreTranslateService,
ITransliterationService transliterationService,
IPlayHistoryService playHistoryService,
ILastFMService lastFMService,
ILogger<MediaSessionsService> logger)
{
_settingsService = settingsService;
@@ -88,6 +91,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_transliterationService = transliterationService;
_discordService = discordService;
_playHistoryService = playHistoryService;
_lastFMService = lastFMService;
_logger = logger;
_onMediaPropsChangedTimer = _dispatcherQueue.CreateTimer();
@@ -150,7 +154,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
var found = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(s => s.Provider == id);
if (_settingsService.AppSettings.MusicGallerySettings.LyricsWindowStatus.IsOpened)
{
if (PlayerIDHelper.IsBetterLyrics(found?.Provider))
if (PlayerIdHelper.IsBetterLyrics(found?.Provider))
{
return found?.IsEnabled ?? true;
}
@@ -275,7 +279,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
if (PlayerIDHelper.IsLXMusic(sessionId))
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StopSSE();
}
@@ -294,20 +298,20 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
string? fixedAlbum = mediaProperties?.AlbumTitle;
string? songId = null;
if (PlayerIDHelper.IsAppleMusic(sessionId))
if (PlayerIdHelper.IsAppleMusic(sessionId))
{
fixedArtist = mediaProperties?.Artist.Split(" — ").FirstOrDefault();
fixedAlbum = mediaProperties?.Artist.Split(" — ").LastOrDefault();
fixedAlbum = fixedAlbum?.Replace(" - Single", "");
fixedAlbum = fixedAlbum?.Replace(" - EP", "");
}
else if (PlayerIDHelper.IsNeteaseFamily(sessionId))
else if (PlayerIdHelper.IsNeteaseFamily(sessionId))
{
songId = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.NetEaseCloudMusicTrackID))?
.Replace(ExtendedGenreFiled.NetEaseCloudMusicTrackID, "");
}
else if (sessionId == PlayerID.QQMusic)
else if (sessionId == PlayerId.QQMusic)
{
songId = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.QQMusicTrackID))?
@@ -318,8 +322,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.FileName))?
.Replace(ExtendedGenreFiled.FileName, "");
// 统计
if (CurrentSongInfo != null && CurrentSongInfo != SongInfoExtensions.Placeholder)
// 写入播放记录
if (CurrentSongInfo != null && CurrentSongInfo.Title != "N/A")
{
// 必须捕获一个副本给异步任务,因为 CurrentSongInfo 马上就要变了
var lastSong = CurrentSongInfo;
@@ -328,6 +332,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (lastSong.DurationMs > 0 &&
_scrobbleStopwatch.Elapsed.TotalMilliseconds >= (lastSong.DurationMs / 2))
{
// 写入本地播放记录
var playHistoryItem = CurrentSongInfo.ToPlayHistoryItem(_scrobbleStopwatch.Elapsed.TotalMilliseconds);
if (playHistoryItem != null)
{
@@ -335,6 +340,13 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_ = Task.Run(() => _playHistoryService.AddLogAsync(playHistoryItem));
_logger.LogInformation($"[Scrobble] 结算成功: {lastSong.Title}");
}
// 写入 Last.fm 播放记录
var isLastFMEnabled = CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (isLastFMEnabled)
{
// 后台
_ = Task.Run(() => _lastFMService.TrackAsync(lastSong));
}
}
}
_scrobbleStopwatch.Restart();
@@ -350,7 +362,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
LinkedFileName = linkedFileName
};
if (PlayerIDHelper.IsLXMusic(sessionId))
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StartSSE();
}
@@ -359,7 +371,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
StopSSE();
}
if (PlayerIDHelper.IsLXMusic(sessionId) && _lxMusicAlbumArtBytes != null)
if (PlayerIdHelper.IsLXMusic(sessionId) && _lxMusicAlbumArtBytes != null)
{
_SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer();
}
@@ -550,7 +562,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
if (PlayerIDHelper.IsLXMusic(CurrentSongInfo?.PlayerId))
if (PlayerIdHelper.IsLXMusic(CurrentSongInfo?.PlayerId))
{
var data = JsonSerializer.Deserialize(e.Message, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.ValueKind == JsonValueKind.Number)

View File

@@ -71,17 +71,15 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
{
await InitializeAsync();
// 使用 SQL 查询比在内存里 GroupBy 更快且省内存
// SQLite 语法: Group By Title 和 Artist
string query = @"
SELECT Title, Artist, AlbumArtHash, COUNT(*) as PlayCount
SELECT Title, Artist, COUNT(*) as PlayCount
FROM PlayHistory
WHERE StartedAt >= ? AND StartedAt <= ?
GROUP BY Title, Artist
ORDER BY PlayCount DESC
LIMIT ?";
// 注意SQLite存的是Ticks或者ISO8601sqlite-net-pcl会自动处理DateTime参数
return await _db.QueryAsync<SongPlayCount>(query, start, end, limit);
}
@@ -126,10 +124,10 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
await InitializeAsync();
string query = @"
SELECT PlayerID, COUNT(*) as Count
SELECT PlayerId, COUNT(*) as Count
FROM PlayHistory
WHERE StartedAt >= ? AND StartedAt <= ?
GROUP BY PlayerID
GROUP BY PlayerId
ORDER BY Count DESC";
return await _db.QueryAsync<PlayerStats>(query, start, end);
@@ -265,7 +263,7 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
Title = song.Title,
Artist = song.Artist,
Album = song.Album,
PlayerID = playerId,
PlayerId = playerId,
StartedAt = startedAt,
TotalDurationMs = totalDurationMs,
DurationPlayedMs = playedDurationMs

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Stats;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
@@ -44,37 +45,30 @@ namespace BetterLyrics.WinUI3.ViewModels
try
{
// 1. 计算时间范围
var (start, end) = CalculateDateRange(range);
// 2. 并行获取所有数据 (性能优化:不等待一个查完再查下一个,而是同时查)
var durationTask = _playHistoryService.GetTotalListeningDurationAsync(start, end);
var logsTask = _playHistoryService.GetLogsByDateRangeAsync(start, end); // 用来算总数
var logsTask = _playHistoryService.GetLogsByDateRangeAsync(start, end);
var topSongsTask = _playHistoryService.GetTopSongsAsync(start, end, 10);
var topArtistsTask = _playHistoryService.GetTopArtistsAsync(start, end, 5); // 只要前5名
var topArtistsTask = _playHistoryService.GetTopArtistsAsync(start, end, 10);
var playersTask = _playHistoryService.GetPlayerDistributionAsync(start, end);
await Task.WhenAll(durationTask, logsTask, topSongsTask, topArtistsTask, playersTask);
// 3. 更新 UI 数据
TotalDuration = await durationTask;
var logs = await logsTask;
TotalTracksPlayed = logs.Count;
// 更新歌曲列表
TopSongs.Clear();
foreach (var item in await topSongsTask) TopSongs.Add(item);
// 更新歌手列表
TopArtists.Clear();
foreach (var item in await topArtistsTask) TopArtists.Add(item);
// 更新播放器分布 (需要特殊处理进度条宽度)
UpdatePlayerStats(await playersTask);
}
catch (Exception ex)
{
// 这里可以记录日志 _logger.LogError(ex, ...)
System.Diagnostics.Debug.WriteLine($"Error loading stats: {ex.Message}");
}
finally
@@ -98,7 +92,7 @@ namespace BetterLyrics.WinUI3.ViewModels
if (stats == null || stats.Count == 0)
{
TopPlayerName = "None";
TopPlayerName = "N/A";
return;
}
@@ -106,7 +100,7 @@ namespace BetterLyrics.WinUI3.ViewModels
if (maxCount == 0) maxCount = 1;
var topPlayer = stats.OrderByDescending(x => x.Count).FirstOrDefault();
TopPlayerName = topPlayer?.PlayerID ?? "None";
TopPlayerName = PlayerIdHelper.GetDisplayName(topPlayer?.PlayerId) ?? "N/A";
foreach (var item in stats.OrderByDescending(x => x.Count))
{
@@ -117,7 +111,7 @@ namespace BetterLyrics.WinUI3.ViewModels
PlayerStats.Add(new PlayerStatDisplayItem
{
PlayerId = item.PlayerID,
PlayerId = item.PlayerId,
PlayCount = item.Count,
DisplayWidth = calculatedWidth
});
@@ -126,32 +120,35 @@ namespace BetterLyrics.WinUI3.ViewModels
private (DateTime Start, DateTime End) CalculateDateRange(StatsRange range)
{
DateTime now = DateTime.Now;
DateTime start = now;
DateTime nowLocal = DateTime.Now;
DateTime startLocal = nowLocal.Date; // 默认为本地今天 00:00
switch (range)
{
case StatsRange.Day:
start = now.Date; // 今天 00:00
break;
case StatsRange.Week:
// 假设周一为一周开始
int diff = (7 + (now.DayOfWeek - DayOfWeek.Monday)) % 7;
start = now.Date.AddDays(-1 * diff);
int dayOfWeek = (int)nowLocal.DayOfWeek;
if (dayOfWeek == 0) dayOfWeek = 7; // 处理周日
startLocal = nowLocal.Date.AddDays(-(dayOfWeek - 1));
break;
case StatsRange.Month:
start = new DateTime(now.Year, now.Month, 1);
startLocal = new DateTime(nowLocal.Year, nowLocal.Month, 1);
break;
case StatsRange.Quarter:
int quarter = (now.Month - 1) / 3 + 1;
start = new DateTime(now.Year, (quarter - 1) * 3 + 1, 1);
int quarterStartMonth = (nowLocal.Month - 1) / 3 * 3 + 1;
startLocal = new DateTime(nowLocal.Year, quarterStartMonth, 1);
break;
case StatsRange.Year:
start = new DateTime(now.Year, 1, 1);
startLocal = new DateTime(nowLocal.Year, 1, 1);
break;
}
return (start, now);
// 数据库里的 StartedAt 是 UTC所以查询条件必须也是 UTC
DateTime startUtc = startLocal.ToUniversalTime();
DateTime endUtc = nowLocal.ToUniversalTime();
return (startUtc, endUtc);
}
}
}

View File

@@ -102,7 +102,6 @@ namespace BetterLyrics.WinUI3.Views
private void NowPlayingBar_SongInfoTapped(object sender, System.EventArgs e)
{
NowPlayingBar.IsPlayingQueueOpened = false;
NowPlayingBar.ShowSongInfo = false;
NowPlayingBar.ShowTime = true;
NowPlayingBar.IsAutoHideEnabled = true;