Compare commits

...

13 Commits

Author SHA1 Message Date
Zhe Fang
a13bb6e8e4 chores: bump to 1.2.243.0 2026-01-04 16:43:02 -05:00
Zhe Fang
0b436c1ea9 chores: rollback UpdateLyrics 2026-01-04 16:21:33 -05:00
Zhe Fang
5d332fdfc6 fix: media sessions record issue 2026-01-04 16:02:11 -05:00
Zhe Fang
572d2cd8ba fix 2026-01-04 14:54:15 -05:00
Zhe Fang
1e5a95c55e chores: bump to v1.2.240.0 2026-01-04 12:05:53 -05:00
Zhe Fang
18ce6d3a57 chores: improve thanks list 2026-01-04 11:39:47 -05:00
Zhe Fang
427aed6857 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-04 09:51:53 -05:00
Zhe Fang
ebfa484a2e fix: GSMTC 2026-01-04 09:51:51 -05:00
Zhe Fang
3ef9d81bea Update README.CN.md 2026-01-04 06:45:25 -05:00
Zhe Fang
e999d07834 Update README.md 2026-01-04 06:43:52 -05:00
Zhe Fang
838b8de94f Update README.md 2026-01-04 06:43:20 -05:00
Zhe Fang
b3059dbeb1 chores: improve GSMTC service 2026-01-03 22:02:08 -05:00
Zhe Fang
6fea88a6a1 fix: stats dashboard ui 2026-01-03 17:27:47 -05:00
41 changed files with 734 additions and 702 deletions

View File

@@ -12,7 +12,7 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.2.237.0" />
Version="1.2.243.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

View File

@@ -260,6 +260,7 @@ namespace BetterLyrics.WinUI3
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(Serilog.Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Error)
.WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

View File

@@ -42,6 +42,7 @@
<None Remove="Controls\LyricsWindowSwitchControl.xaml" />
<None Remove="Controls\MediaSettingsControl.xaml" />
<None Remove="Controls\NowPlayingBar.xaml" />
<None Remove="Controls\PatronControl.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" />
<None Remove="Controls\PlayQueue.xaml" />
<None Remove="Controls\PropertyRow.xaml" />
@@ -246,6 +247,9 @@
<Content Update="Assets\Question.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\RevolvingHearts.gif">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\SaltPlayerForWindows.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -262,6 +266,11 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PatronControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\GhostSliderStyle.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -6,5 +6,6 @@ namespace BetterLyrics.WinUI3.Constants
{
public static readonly TimeSpan DebounceTimeout = TimeSpan.FromMilliseconds(250);
public static readonly TimeSpan AnimationDuration = TimeSpan.FromMilliseconds(350);
public static readonly TimeSpan WaitingDuration = TimeSpan.FromMilliseconds(300);
}
}

View File

@@ -57,12 +57,12 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="GitHub" NavigateUri="{x:Bind const:Link.BetterLyricsGitHub}" />
<HyperlinkButton x:Uid="UserGuide" NavigateUri="{x:Bind const:Link.UserGuide}" />
<HyperlinkButton x:Uid="PrivacyPolicy" NavigateUri="{x:Bind const:Link.PrivacyPolicy}" />
<HyperlinkButton x:Uid="TermsOfService" NavigateUri="{x:Bind const:Link.TermsOfService}" />
</StackPanel>
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
@@ -70,18 +70,18 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageFeedback" />
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="QQ 反馈交流群" NavigateUri="{x:Bind const:Link.QQGroup}" />
<HyperlinkButton Content="Discord" NavigateUri="{x:Bind const:Link.Discord}" />
<HyperlinkButton Content="Telegram" NavigateUri="{x:Bind const:Link.Telegram}" />
</StackPanel>
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageDonation" />
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="Buy Me a Coffee" NavigateUri="{x:Bind const:Link.BuyMeACoffee}" />
<HyperlinkButton Content="PayPal" NavigateUri="{x:Bind const:Link.PayPal}" />
<HyperlinkButton
@@ -117,7 +117,7 @@
</HyperlinkButton.ContextFlyout>
</HyperlinkButton>
<HyperlinkButton Content="爱发电" NavigateUri="{x:Bind const:Link.Afdian}" />
</StackPanel>
</dev:WrapPanel>
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -136,15 +136,6 @@
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageThanksList">
<Button
Click="Patron_Click"
Content="{ui:FontIcon FontSize=16,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE7FD;}"
Style="{StaticResource AccentButtonStyle}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
<dev:SettingsExpander.ItemsFooter>
<InfoBar
@@ -157,6 +148,96 @@
</dev:SettingsExpander.ItemsFooter>
</dev:SettingsExpander>
<dev:SettingsExpander x:Uid="SettingsPageThanksList">
<dev:SettingsExpander.HeaderIcon>
<ImageIcon Source="ms-appx:///Assets/RevolvingHearts.gif" />
</dev:SettingsExpander.HeaderIcon>
<dev:SettingsExpander.Items>
<!-- 贡献者 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<RichTextBlock>
<Paragraph>
<Run x:Uid="SetingsPageContributors" />
<Run Text="(Code)" />
</Paragraph>
</RichTextBlock>
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="jayfunc" NavigateUri="https://github.com/jayfunc" />
<HyperlinkButton Content="Raspberry-Monster" NavigateUri="https://github.com/Raspberry-Monster" />
<HyperlinkButton Content="ZHider" NavigateUri="https://github.com/ZHider" />
<HyperlinkButton Content="kusutori" NavigateUri="https://github.com/kusutori" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 贡献者 (Translator) -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<RichTextBlock>
<Paragraph>
<Run x:Uid="SetingsPageContributors" />
<Run Text="(Translator)" />
</Paragraph>
</RichTextBlock>
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="borcolasky" NavigateUri="https://crowdin.com/profile/borcolasky" />
<HyperlinkButton Content="SuHeAndZl" NavigateUri="https://crowdin.com/profile/SuHeAndZl" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 赞助 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SettingsPagePatrons" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<uc:PatronControl Date="Dec 3, 2025" PatronName="YE" />
<uc:PatronControl Date="Nov 23, 2025" PatronName="**玄" />
<uc:PatronControl Date="Nov 21, 2025" PatronName="**智" />
<uc:PatronControl Date="Nov 17, 2025" PatronName="*鹤" />
<uc:PatronControl Date="Nov 2, 2025" PatronName="借过" />
<uc:PatronControl Date="Aug 28, 2025" PatronName="**华" />
<TextBlock x:Uid="SettingsPageUserWhoPurchased" Margin="12,8" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 特别鸣谢 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageSpecialThanks" />
<TextBlock x:Uid="SettingsPageYouNowUsing" Margin="0,8" />
</StackPanel>
</dev:SettingsCard>
<!-- 代码参考 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageDeps" />
<HyperlinkButton Margin="-12,0,0,0" NavigateUri="https://github.com/jayfunc/BetterLyrics/network/dependencies">
<TextBlock x:Uid="SetingsPageDeps" />
</HyperlinkButton>
</StackPanel>
</dev:SettingsCard>
<!-- UI/UX 参考 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageUIUXRef" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="refined-now-playing-netease" NavigateUri="https://github.com/solstice23/refined-now-playing-netease" />
<HyperlinkButton Content="Lyricify" NavigateUri="https://github.com/WXRIW/Lyricify-App" />
<HyperlinkButton Content="椒盐音乐 Salt Player" NavigateUri="https://moriafly.com/program/salt-player" />
<HyperlinkButton Content="MyToolBar" NavigateUri="https://github.com/TwilightLemon/MyToolBar" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageMockMusicPlaying">
<HyperlinkButton x:Uid="SettingsPagePlayingMockMusicButton" NavigateUri="https://soundcloud.com/carlyraejepsen/cut-to-the-feeling" />
</dev:SettingsCard>
@@ -209,194 +290,28 @@
Value="{x:Bind ViewModel.AppSettings.AdvancedSettings.FPS, Mode=TwoWay}" />
</dev:SettingsCard>
<RichTextBlock
Margin="0,16,0,0"
HorizontalAlignment="Center"
HorizontalTextAlignment="Center"
LineHeight="28">
<Paragraph FontWeight="Bold">
<Run Text="{x:Bind const:App.AppName}" />
</Paragraph>
<Paragraph>
<Run Text="An elegant and deeply customizable lyrics visualizer &amp; versatile music player" />
</Paragraph>
<Paragraph>
<Run Text="Proudly built by" />
<Hyperlink NavigateUri="{x:Bind const:Link.AuthorGitHub}">
<Run Text="{x:Bind const:App.AppAuthor}" />
</Hyperlink>
</Paragraph>
</RichTextBlock>
</StackPanel>
</Grid>
</ScrollViewer>
<Grid
x:Name="CreditsReel"
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
Opacity="0"
SizeChanged="CreditsReel_SizeChanged"
Tapped="CreditsReel_Tapped"
Visibility="Collapsed">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<ScrollViewer
x:Name="CreditsReelScrollViewer"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollMode="Disabled">
<RichTextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
HorizontalTextAlignment="Center"
LineHeight="28"
PointerEntered="RichTextBlock_PointerEntered"
PointerExited="RichTextBlock_PointerExited">
<Paragraph x:Name="CreditsReelHeader" />
<!-- 贡献者 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageContributors" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/jayfunc">
<Run Text="jayfunc" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Raspberry-Monster">
<Run Text="Raspberry-Monster" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/ZHider">
<Run Text="ZHider" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/kusutori">
<Run Text="kusutori" />
</Hyperlink>
</Paragraph>
<!-- 赞助 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SettingsPagePatrons" />
</Paragraph>
<Paragraph>
<Run Text="YE" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Dec 3, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**玄" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 23, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**智" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 21, 2025" />
</Paragraph>
<Paragraph>
<Run Text="*鹤" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 17, 2025" />
</Paragraph>
<Paragraph>
<Run Text="借过" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 2, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**华" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Aug 28, 2025" />
</Paragraph>
<Paragraph>
<Run x:Uid="SettingsPageUserWhoPurchased" />
</Paragraph>
<!-- 特别鸣谢 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageSpecialThanks" />
</Paragraph>
<Paragraph>
<Run x:Uid="SettingsPageYouNowUsing" />
</Paragraph>
<!-- 代码参考 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageDeps" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://gist.github.com/mcworkaholic/82fbf203e3f1043bbe534b5b2974c0ce">
<Run Text="Get album artwork from ITunes (with Python3 or C#)" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://stackoverflow.com/a/32013610/11048731">
<Run Text="FullyObservableCollection" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Storyteller-Studios/Impressionist">
<Run Text="Impressionist" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Storyteller-Studios/ColorThief.WinUI3">
<Run Text="ColorThief.WinUI3" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Johnwikix/SpectrumVisualization">
<Run Text="SpectrumVisualization" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.shadertoy.com/view/Mdt3Df">
<Run Text="Snow (as shown in sweden)" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.shadertoy.com/view/lllSR2">
<Run Text="w10" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/mo-jinran/Taskbar-Lyrics">
<Run Text="Taskbar-Lyrics" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/jayfunc/BetterLyrics/network/dependencies">
<Run Text="..." />
</Hyperlink>
</Paragraph>
<!-- UI/UX 设计参考 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageUIUXRef" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/solstice23/refined-now-playing-netease">
<Run Text="refined-now-playing-netease" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/WXRIW/Lyricify-App">
<Run Text="Lyricify" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://moriafly.com/program/salt-player">
<Run Text="椒盐音乐 Salt Player" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/TwilightLemon/MyToolBar">
<Run Text="MyToolBar" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="">
<Run Text="" />
</Hyperlink>
</Paragraph>
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run Text="{x:Bind const:App.AppName}" />
</Paragraph>
<Paragraph>
<Run Text="Proudly built by" />
<Hyperlink NavigateUri="{x:Bind const:Link.AuthorGitHub}">
<Run Text="{x:Bind const:App.AppAuthor}" />
</Hyperlink>
</Paragraph>
<Paragraph x:Name="CreditsReelFooter" />
</RichTextBlock>
</ScrollViewer>
</Grid>
</Grid>
</UserControl>

View File

@@ -11,7 +11,6 @@ namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class AboutControl : UserControl
{
private bool _isCreditsScrolling = false;
public AboutControlViewModel ViewModel => (AboutControlViewModel)DataContext;
public AboutControl()
@@ -20,47 +19,6 @@ namespace BetterLyrics.WinUI3.Controls
DataContext = Ioc.Default.GetRequiredService<AboutControlViewModel>();
}
private async void Patron_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
CompositionTarget.Rendering += CompositionTarget_Rendering;
CreditsReel.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
CreditsReel.Opacity = 1;
_isCreditsScrolling = true;
}
private void CompositionTarget_Rendering(object? sender, object e)
{
if (_isCreditsScrolling)
{
CreditsReelScrollViewer.ChangeView(null, CreditsReelScrollViewer.VerticalOffset + 0.5, null);
}
}
private async void CreditsReel_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
CreditsReel.Opacity = 0;
await Task.Delay(Constants.Time.AnimationDuration);
CreditsReel.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
CompositionTarget.Rendering -= CompositionTarget_Rendering;
CreditsReelScrollViewer.ChangeView(null, 0, null);
}
private void CreditsReel_SizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
{
CreditsReelHeader.LineHeight = e.NewSize.Height;
CreditsReelFooter.LineHeight = e.NewSize.Height / 2;
}
private void RichTextBlock_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
_isCreditsScrolling = false;
}
private void RichTextBlock_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
_isCreditsScrolling = true;
}
private void WeChat_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
WeChatFlyout.ShowAt(WeChatButton);

View File

@@ -31,7 +31,7 @@ namespace BetterLyrics.WinUI3.Controls
public sealed partial class LyricsCanvas : UserControl,
IRecipient<PropertyChangedMessage<TimeSpan>>,
IRecipient<PropertyChangedMessage<LyricsData?>>,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<SongInfo>>,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<double>>,
IRecipient<PropertyChangedMessage<bool>>,
@@ -343,7 +343,7 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings;
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
double songDuration = _gsmtcService.CurrentSongInfo?.DurationMs ?? 0;
double songDuration = _gsmtcService.CurrentSongInfo.DurationMs;
bool isForceWordByWord = _settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect;
Color overlayColor;
@@ -726,7 +726,7 @@ namespace BetterLyrics.WinUI3.Controls
}
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
public void Receive(PropertyChangedMessage<SongInfo> message)
{
if (message.Sender is IGSMTCService)
{
@@ -891,5 +891,6 @@ namespace BetterLyrics.WinUI3.Controls
}
}
}
}
}

View File

@@ -60,16 +60,16 @@
</interactivity:Interaction.Behaviors>
<Grid VerticalAlignment="Center" CornerRadius="4">
<local:ImageSwitcher
x:Name="AlbumArtImageSwitcher"
Width="36"
Height="36" />
Height="36"
Source="{x:Bind ViewModel.GSMTCService.AlbumArtBitmapImage, Mode=OneWay}" />
</Grid>
<StackPanel VerticalAlignment="Center">
<TextBlock x:Name="TitleTextBlock" />
<TextBlock Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, Mode=OneWay}" />
<TextBlock
x:Name="ArtistsTextBlock"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DisplayArtists, Mode=OneWay}" />
</StackPanel>
</StackPanel>
@@ -423,7 +423,8 @@
Maximum="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
Minimum="0"
Style="{StaticResource GhostSliderStyle}"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}" />
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}"
Value="{x:Bind ViewModel.GSMTCService.CurrentPosition.TotalSeconds, Mode=OneWay}" />
<Grid
x:Name="TimelineSliderLyricsLineInfo"

View File

@@ -21,10 +21,7 @@ using BetterLyrics.WinUI3.Extensions;
namespace BetterLyrics.WinUI3.Controls;
public sealed partial class NowPlayingBar : UserControl,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<BitmapImage?>>,
IRecipient<PropertyChangedMessage<TimeSpan>>
public sealed partial class NowPlayingBar : UserControl
{
public NowPlayingBarViewModel ViewModel => (NowPlayingBarViewModel)DataContext;
@@ -110,8 +107,6 @@ public sealed partial class NowPlayingBar : UserControl,
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<NowPlayingBarViewModel>();
WeakReferenceMessenger.Default.RegisterAll(this);
}
private static void OnDependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -311,38 +306,4 @@ public sealed partial class NowPlayingBar : UserControl,
{
PlaybackOrder = PlaybackOrder.GetNext();
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
{
TitleTextBlock.Text = message.NewValue?.Title;
ArtistsTextBlock.Text = message.NewValue?.DisplayArtists;
}
}
}
public void Receive(PropertyChangedMessage<BitmapImage?> message)
{
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IGSMTCService.AlbumArtBitmapImage))
{
AlbumArtImageSwitcher.Source = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IGSMTCService.CurrentPosition))
{
TimelineSlider.Value = message.NewValue.TotalSeconds;
}
}
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.PatronControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Margin="12,8">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{x:Bind PatronName, Mode=OneWay}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Date, Mode=OneWay}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,46 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class PatronControl : UserControl
{
public string PatronName
{
get { return (string)GetValue(PatronNameProperty); }
set { SetValue(PatronNameProperty, value); }
}
public static readonly DependencyProperty PatronNameProperty =
DependencyProperty.Register(nameof(PatronName), typeof(string), typeof(PatronControl), new PropertyMetadata(""));
public string Date
{
get { return (string)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
public static readonly DependencyProperty DateProperty =
DependencyProperty.Register(nameof(Date), typeof(string), typeof(PatronControl), new PropertyMetadata(""));
public PatronControl()
{
InitializeComponent();
}
}
}

View File

@@ -26,49 +26,82 @@
</Style>
</UserControl.Resources>
<Grid Margin="0,20,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="36,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ProgressBar
Grid.Row="0"
Background="Transparent"
IsIndeterminate="{x:Bind ViewModel.IsLoading, Mode=OneWay}"
Visibility="{x:Bind ViewModel.IsLoading, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<StackPanel Orientation="Horizontal" Spacing="12">
<ComboBox
x:Uid="StatsDashboardControlTimeRange"
Header="Time Range"
SelectedIndex="{x:Bind ViewModel.SelectedTimeRange, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="StatsDashboardControlToday" />
<ComboBoxItem x:Uid="StatsDashboardControlThisWeek" />
<ComboBoxItem x:Uid="StatsDashboardControlThisMonth" />
<ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" />
<ComboBoxItem x:Uid="StatsDashboardControlThisYear" />
<ComboBoxItem x:Uid="StatsDashboardControlCustom" />
</ComboBox>
<StackPanel
Margin="0,0,0,5"
VerticalAlignment="Bottom"
Orientation="Horizontal"
Spacing="8"
Visibility="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<CalendarDatePicker x:Uid="StatsDashboardControlStart" Date="{x:Bind ViewModel.CustomStartDate, Mode=TwoWay}" />
<TextBlock
Margin="0,26,0,0"
VerticalAlignment="Center"
Text="-" />
<CalendarDatePicker x:Uid="StatsDashboardControlEnd" Date="{x:Bind ViewModel.CustomEndDate, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.GSMTCService.IsScrobbled, Mode=OneWay, Converter={StaticResource BoolNegationToVisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<InfoBar
x:Uid="StatsDashboardControlRecording"
Grid.Row="0"
IsClosable="False"
IsOpen="True"
Message="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, Mode=OneWay}" />
<ProgressBar
Grid.Row="1"
Background="Transparent"
Maximum="{x:Bind ViewModel.GSMTCService.TargetScrobbledDuration.TotalSeconds, Mode=OneWay}"
ShowPaused="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
Value="{x:Bind ViewModel.GSMTCService.ScrobbledDuration.TotalSeconds, Mode=OneWay}" />
</Grid>
<ScrollViewer Grid.Row="1" Padding="36,0">
<controls:WrapPanel
Grid.Row="2"
Margin="36,36,36,12"
HorizontalSpacing="12"
Orientation="Horizontal"
VerticalSpacing="12">
<ComboBox
x:Uid="StatsDashboardControlTimeRange"
Header="Time Range"
SelectedIndex="{x:Bind ViewModel.SelectedTimeRange, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="StatsDashboardControlToday" />
<ComboBoxItem x:Uid="StatsDashboardControlThisWeek" />
<ComboBoxItem x:Uid="StatsDashboardControlThisMonth" />
<ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" />
<ComboBoxItem x:Uid="StatsDashboardControlThisYear" />
<ComboBoxItem x:Uid="StatsDashboardControlCustom" />
</ComboBox>
<CalendarDatePicker
x:Uid="StatsDashboardControlStart"
Date="{x:Bind ViewModel.CustomStartDate, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}" />
<TimePicker
VerticalAlignment="Bottom"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}"
Time="{x:Bind ViewModel.CustomStartTime, Mode=TwoWay}" />
<CalendarDatePicker
x:Uid="StatsDashboardControlEnd"
Date="{x:Bind ViewModel.CustomEndDate, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}" />
<TimePicker
VerticalAlignment="Bottom"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}"
Time="{x:Bind ViewModel.CustomEndTime, Mode=TwoWay}" />
<Button
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.RefreshDataCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE72C;}" />
</controls:WrapPanel>
<ScrollViewer Grid.Row="3" Padding="36,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
@@ -367,11 +400,11 @@
</Grid>
</ScrollViewer>
<Button
Grid.Row="1"
<!--<Button
Grid.Row="2"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.GenerateTestDataCommand}"
Content="Generate test data" />
Content="Generate test data" />-->
</Grid>
</UserControl>

View File

@@ -5,7 +5,7 @@ namespace BetterLyrics.WinUI3.Extensions
{
public static class SongInfoExtensions
{
public static SongInfo Placeholder => new SongInfo
public static SongInfo Placeholder => new()
{
Title = "N/A",
Album = "N/A",
@@ -44,7 +44,7 @@ namespace BetterLyrics.WinUI3.Extensions
PlayerId = songInfo.PlayerId ?? "N/A",
TotalDurationMs = songInfo.DurationMs,
DurationPlayedMs = actualPlayedMs,
StartedAt = DateTime.Now.AddMilliseconds(-actualPlayedMs)
StartedAt = DateTime.FromBinary(songInfo.StartedAt)
};
}
}

View File

@@ -6,7 +6,7 @@ using System;
namespace BetterLyrics.WinUI3.Models
{
public partial class SongInfo : ObservableObject, ICloneable
public partial class SongInfo : ObservableRecipient, ICloneable
{
[ObservableProperty]
public partial string Album { get; set; }
@@ -26,6 +26,8 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty]
public partial string? SongId { get; set; } = null;
[ObservableProperty] public partial long StartedAt { get; set; } = DateTime.Now.ToBinary();
public string? LinkedFileName { get; set; } = null;
public double Duration => DurationMs / 1000;
@@ -45,6 +47,7 @@ namespace BetterLyrics.WinUI3.Models
PlayerId = this.PlayerId,
SongId = this.SongId,
LinkedFileName = this.LinkedFileName,
StartedAt = this.StartedAt,
};
}

View File

@@ -42,15 +42,13 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
{
_logger.LogInformation("RefreshArtAlbum");
if (CurrentSongInfo == null)
IBuffer? buffer = null;
if (CurrentSongInfo != SongInfoExtensions.Placeholder)
{
_logger.LogWarning("CurrentSongInfo == null");
return;
buffer = await Task.Run(async () => await _albumArtSearchService.SearchAsync(CurrentSongInfo, _SMTCAlbumArtBuffer, token), token);
if (token.IsCancellationRequested) return;
}
IBuffer? buffer = await Task.Run(async () => await _albumArtSearchService.SearchAsync(CurrentSongInfo, _SMTCAlbumArtBuffer, token), token);
if (token.IsCancellationRequested) return;
if (buffer == null)
{
using var placeHolderStream = await ImageHelper.GetAlbumArtPlaceholderAsync();

View File

@@ -3,7 +3,9 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Parsers.LyricsParser;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.WinUI;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using System.Threading;
using System.Threading.Tasks;
@@ -24,15 +26,10 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
CurrentLyricsSearchResult = null;
CurrentLyricsData = LyricsData.GetLoadingPlaceholder();
if (CurrentSongInfo != null)
if (CurrentSongInfo != SongInfoExtensions.Placeholder)
{
CurrentLyricsSearchResult = await Task.Run(async () => await _lyrcsSearchService.SearchSmartlyAsync(
CurrentSongInfo,
true,
CurrentMediaSourceProviderInfo?.LyricsSearchType,
token),
token);
if (token.IsCancellationRequested) return;
CurrentSongInfo, true, CurrentMediaSourceProviderInfo?.LyricsSearchType, token), token);
if (CurrentLyricsSearchResult != null)
{
@@ -40,14 +37,14 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
(CurrentLyricsData, CurrentLyricsSearchResult.TransliterationProvider, CurrentLyricsSearchResult.TranslationProvider) =
await Task.Run(async () => await lyricsParser.Parse(
_translationService,
_transliterationService,
_settingsService.AppSettings.TranslationSettings,
CurrentLyricsSearchResult,
token),
token);
_translationService, _transliterationService, _settingsService.AppSettings.TranslationSettings, CurrentLyricsSearchResult, token), token);
}
}
if (CurrentLyricsSearchResult == null)
{
CurrentLyricsData = LyricsData.GetNotfoundPlaceholder();
}
}
public async void UpdateLyrics()

View File

@@ -26,6 +26,7 @@ using CommunityToolkit.WinUI;
using EvtSource;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Diagnostics;
@@ -50,6 +51,8 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
private readonly MediaManager _mediaManager = new();
private IBuffer? _SMTCAlbumArtBuffer = null;
private MediaManager.MediaSession? _currentDesiredSession = null;
private readonly IAlbumArtSearchService _albumArtSearchService;
private readonly ILyricsSearchService _lyrcsSearchService;
private readonly ITranslationService _translationService;
@@ -64,12 +67,15 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
private byte[]? _lxMusicAlbumArtBytes = null;
private readonly DispatcherQueueTimer? _onMediaPropsChangedTimer;
private readonly DispatcherTimer _scrobbleTimer;
private readonly Stopwatch _scrobbleStopwatch = new();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsScrobbled { get; set; } = false;
[ObservableProperty] public partial TimeSpan ScrobbledDuration { get; set; } = TimeSpan.Zero;
[ObservableProperty] public partial TimeSpan TargetScrobbledDuration { get; set; } = TimeSpan.Zero;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool CurrentIsPlaying { get; private set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TimeSpan CurrentPosition { get; private set; } = TimeSpan.Zero;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SongInfo? CurrentSongInfo { get; private set; } = SongInfoExtensions.Placeholder;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SongInfo CurrentSongInfo { get; private set; } = SongInfoExtensions.Placeholder;
[ObservableProperty] public partial MediaSourceProviderInfo? CurrentMediaSourceProviderInfo { get; set; }
@@ -94,6 +100,10 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
_lastFMService = lastFMService;
_logger = logger;
_scrobbleTimer = new();
_scrobbleTimer.Interval = TimeSpan.FromSeconds(1);
_scrobbleTimer.Tick += ScrobbleTimer_Tick;
_onMediaPropsChangedTimer = _dispatcherQueue.CreateTimer();
_settingsService.AppSettings.MediaSourceProvidersInfo.ItemPropertyChanged += MediaSourceProvidersInfo_ItemPropertyChanged;
@@ -106,6 +116,41 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
InitMediaManager();
}
private void ScrobbleTimer_Tick(object? sender, object e)
{
if (!IsScrobbled)
{
if (!string.IsNullOrWhiteSpace(CurrentSongInfo.Title) && CurrentSongInfo.Title != "N/A")
{
ScrobbledDuration += _scrobbleTimer.Interval;
if (ScrobbledDuration >= TargetScrobbledDuration)
{
// 写入本地播放记录
var playHistoryItem = CurrentSongInfo.ToPlayHistoryItem(ScrobbledDuration.TotalMilliseconds);
if (playHistoryItem != null)
{
// 后台
_ = Task.Run(async () =>
{
await _playHistoryService.AddLogAsync(playHistoryItem);
});
_logger.LogInformation("ScrobbleTimer_Tick: {} scrobbled", CurrentSongInfo.Title);
}
// 写入 Last.fm 播放记录
var isLastFMEnabled = CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (isLastFMEnabled)
{
// 后台
_ = Task.Run(() => _lastFMService.TrackAsync(CurrentSongInfo));
}
IsScrobbled = true;
ScrobbledDuration = TimeSpan.Zero;
}
}
}
}
private void MappedSongSearchQueries_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
UpdateLyrics();
@@ -143,10 +188,9 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
}
}
private MediaSourceProviderInfo? GetCurrentMediaSourceProviderInfo()
private MediaSourceProviderInfo? GetCurrentDesiredMediaSourceProviderInfo()
{
var desiredSession = GetCurrentSession();
return _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == desiredSession?.Id);
return _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == _currentDesiredSession?.Id);
}
private bool IsMediaSourceEnabled(string id)
@@ -176,6 +220,10 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
private void InitMediaManager()
{
_mediaManager.Start();
_mediaManager.CurrentMediaSessions.ToList().ForEach(x => RecordMediaSession(x.Value.Id));
_mediaManager.OnAnySessionOpened += MediaManager_OnAnySessionOpened;
_mediaManager.OnAnySessionClosed += MediaManager_OnAnySessionClosed;
_mediaManager.OnFocusedSessionChanged += MediaManager_OnFocusedSessionChanged;
@@ -183,221 +231,157 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
_mediaManager.OnAnyPlaybackStateChanged += MediaManager_OnAnyPlaybackStateChanged;
_mediaManager.OnAnyTimelinePropertyChanged += MediaManager_OnAnyTimelinePropertyChanged;
_mediaManager.Start();
MediaManager_OnFocusedSessionChanged(null);
_mediaManager.CurrentMediaSessions.ToList().ForEach(x => RecordMediaSourceProviderInfo(x.Value));
OnDesiredSessionChanged(true);
}
private async void MediaManager_OnFocusedSessionChanged(MediaManager.MediaSession? mediaSession)
private void OnDesiredSessionChanged(bool firstTime = false)
{
if (!_mediaManager.IsStarted) return;
await SendFocusedMessagesAsync();
}
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties? timelineProperties)
{
_dispatcherQueue.TryEnqueue(() =>
var desiredSession = GetCurrentDesiredSession();
if (firstTime || desiredSession != _currentDesiredSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
_currentDesiredSession = desiredSession;
if (_currentDesiredSession == null)
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
return;
}
var desiredSession = GetCurrentSession();
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
SendNullMessages();
}
else
{
if (IsMediaSourceTimelineSyncEnabled(mediaSession.Id))
{
CurrentPosition = timelineProperties?.Position ?? TimeSpan.Zero;
CurrentSongInfo?.DurationMs = timelineProperties?.EndTime.TotalMilliseconds ?? 0;
}
_ = SendFocusedMessagesAsync();
}
}
}
private void MediaManager_OnFocusedSessionChanged(MediaManager.MediaSession? mediaSession)
{
OnDesiredSessionChanged();
}
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties timelineProperties)
{
_dispatcherQueue.TryEnqueue(() =>
{
if (mediaSession != _currentDesiredSession) return;
CurrentPosition = timelineProperties.Position;
CurrentSongInfo.DurationMs = timelineProperties.EndTime.TotalMilliseconds;
UpdateTargetScrobbledDuration();
if (CurrentPosition.TotalSeconds == 0)
{
IsScrobbled = false;
ScrobbledDuration = TimeSpan.Zero;
}
});
}
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo? playbackInfo)
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo playbackInfo)
{
_dispatcherQueue.TryEnqueue(() =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
{
CurrentIsPlaying = false;
return;
}
if (mediaSession != _currentDesiredSession) return;
var desiredSession = GetCurrentSession();
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
CurrentIsPlaying = playbackInfo.PlaybackStatus switch
{
CurrentIsPlaying = false;
}
else
{
CurrentIsPlaying = playbackInfo?.PlaybackStatus switch
{
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false,
};
}
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false,
};
if (CurrentIsPlaying)
{
_scrobbleStopwatch.Start();
_scrobbleTimer.Start();
}
else
{
_scrobbleStopwatch.Stop();
_scrobbleTimer.Stop();
}
});
}
private void MediaManager_OnAnyMediaPropertyChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProperties)
private void MediaManager_OnAnyMediaPropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionMediaProperties mediaProperties)
{
_onMediaPropsChangedTimer?.Debounce(() =>
{
_dispatcherQueue.TryEnqueue(async () =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
if (mediaSession != _currentDesiredSession) return;
string sessionId = mediaSession.Id;
var currentMediaSourceProviderInfo = GetCurrentDesiredMediaSourceProviderInfo();
if (currentMediaSourceProviderInfo?.ResetPositionOffsetOnSongChanged == true)
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
currentMediaSourceProviderInfo?.PositionOffset = 0;
}
string? sessionId = mediaSession?.Id;
string fixedTitle = mediaProperties.Title;
string fixedArtist = mediaProperties.Artist;
string fixedAlbum = mediaProperties.AlbumTitle;
string? songId = null;
var desiredSession = GetCurrentSession();
if (mediaSession != desiredSession) return;
if (sessionId != null && !IsMediaSourceEnabled(sessionId))
if (PlayerIdHelper.IsAppleMusic(sessionId))
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
fixedArtist = mediaProperties.Artist.Split(" — ").First();
fixedAlbum = mediaProperties.Artist.Split(" — ").Last();
fixedAlbum = fixedAlbum.Replace(" - Single", "");
fixedAlbum = fixedAlbum.Replace(" - EP", "");
}
else if (PlayerIdHelper.IsNeteaseFamily(sessionId))
{
songId = mediaProperties.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.NetEaseCloudMusicTrackID))?
.Replace(ExtendedGenreFiled.NetEaseCloudMusicTrackID, "");
}
else if (sessionId == PlayerId.QQMusic)
{
songId = mediaProperties.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.QQMusicTrackID))?
.Replace(ExtendedGenreFiled.QQMusicTrackID, "");
}
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StopSSE();
}
var linkedFileName = mediaProperties.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.FileName))?
.Replace(ExtendedGenreFiled.FileName, "");
_SMTCAlbumArtBuffer = null;
CurrentSongInfo = new()
{
Title = fixedTitle,
Artists = fixedArtist.SplitByCommonSplitter(),
Album = fixedAlbum,
DurationMs = mediaSession.ControlSession.GetTimelineProperties().EndTime.TotalMilliseconds,
PlayerId = sessionId,
SongId = songId,
LinkedFileName = linkedFileName,
StartedAt = DateTime.Now.ToBinary(),
};
UpdateTargetScrobbledDuration();
IsScrobbled = false;
ScrobbledDuration = TimeSpan.Zero;
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StartSSE();
}
else
{
var currentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
if (currentMediaSourceProviderInfo?.ResetPositionOffsetOnSongChanged == true)
{
currentMediaSourceProviderInfo?.PositionOffset = 0;
}
StopSSE();
}
string? fixedArtist = mediaProperties?.Artist;
string? fixedAlbum = mediaProperties?.AlbumTitle;
string? songId = null;
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))
{
songId = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.NetEaseCloudMusicTrackID))?
.Replace(ExtendedGenreFiled.NetEaseCloudMusicTrackID, "");
}
else if (sessionId == PlayerId.QQMusic)
{
songId = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.QQMusicTrackID))?
.Replace(ExtendedGenreFiled.QQMusicTrackID, "");
}
var linkedFileName = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.FileName))?
.Replace(ExtendedGenreFiled.FileName, "");
// 写入播放记录
if (CurrentSongInfo != null && !string.IsNullOrWhiteSpace(CurrentSongInfo.Title) && CurrentSongInfo.Title != "N/A")
{
// 必须捕获一个副本给异步任务,因为 CurrentSongInfo 马上就要变了
var lastSong = CurrentSongInfo;
// 当前秒表时间 >= 上一首总时长 / 2
if (lastSong.DurationMs > 0 &&
_scrobbleStopwatch.Elapsed.TotalMilliseconds >= (lastSong.DurationMs / 2))
{
// 写入本地播放记录
var playHistoryItem = lastSong.ToPlayHistoryItem(_scrobbleStopwatch.Elapsed.TotalMilliseconds);
if (playHistoryItem != null)
{
// 后台
_ = 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();
CurrentSongInfo = new SongInfo
{
Title = mediaProperties?.Title ?? "N/A",
Artists = fixedArtist?.SplitByCommonSplitter() ?? ["N/A"],
Album = fixedAlbum ?? "N/A",
DurationMs = mediaSession?.ControlSession?.GetTimelineProperties().EndTime.TotalMilliseconds ?? 0,
PlayerId = sessionId,
SongId = songId,
LinkedFileName = linkedFileName
};
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StartSSE();
}
else
{
StopSSE();
}
if (PlayerIdHelper.IsLXMusic(sessionId) && _lxMusicAlbumArtBytes != null)
{
_SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer();
}
else if (mediaProperties?.Thumbnail is IRandomAccessStreamReference streamReference)
{
_SMTCAlbumArtBuffer = await ImageHelper.ToBufferAsync(streamReference);
}
else
{
_SMTCAlbumArtBuffer = null;
}
if (PlayerIdHelper.IsLXMusic(sessionId) && _lxMusicAlbumArtBytes != null)
{
_SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer();
}
else if (mediaProperties.Thumbnail is IRandomAccessStreamReference streamReference)
{
_SMTCAlbumArtBuffer = await ImageHelper.ToBufferAsync(streamReference);
}
else
{
_SMTCAlbumArtBuffer = null;
}
_logger.LogInformation("MediaManager_OnAnyMediaPropertyChanged {SongInfo}", CurrentSongInfo);
CurrentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
CurrentMediaSourceProviderInfo = GetCurrentDesiredMediaSourceProviderInfo();
UpdateAlbumArt();
UpdateLyrics();
@@ -410,81 +394,65 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
private void MediaManager_OnAnySessionClosed(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
if (_mediaManager.CurrentMediaSessions.Count == 0)
OnDesiredSessionChanged();
}
private void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
{
if (mediaSession == null) return;
var id = mediaSession.Id;
_dispatcherQueue.TryEnqueue(() =>
{
SendNullMessages();
RecordMediaSession(id);
OnDesiredSessionChanged();
});
}
private void RecordMediaSession(string id)
{
var found = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == id);
if (found == null)
{
_settingsService.AppSettings.MediaSourceProvidersInfo.Add(new MediaSourceProviderInfo(id, _settingsService.AppSettings.GeneralSettings.ListenOnNewPlaybackSource));
}
}
private async void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
RecordMediaSourceProviderInfo(mediaSession);
await SendFocusedMessagesAsync();
}
private MediaManager.MediaSession? GetCurrentSession()
private MediaManager.MediaSession? GetCurrentDesiredSession()
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession == null)
{
return null;
}
if (IsMediaSourceEnabled(focusedSession.Id))
if (focusedSession != null && IsMediaSourceEnabled(focusedSession.Id))
{
return focusedSession;
}
else
foreach (var session in _mediaManager.CurrentMediaSessions.Values)
{
foreach (var session in _mediaManager.CurrentMediaSessions.Values)
if (IsMediaSourceEnabled(session.Id))
{
if (IsMediaSourceEnabled(session.Id))
{
return session;
}
return session;
}
}
return null;
}
private void RecordMediaSourceProviderInfo(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
var id = mediaSession?.Id;
if (string.IsNullOrEmpty(id)) return;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
var found = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == id);
if (found == null)
{
_settingsService.AppSettings.MediaSourceProvidersInfo.Add(new MediaSourceProviderInfo(id, _settingsService.AppSettings.GeneralSettings.ListenOnNewPlaybackSource));
}
});
}
private void SendNullMessages()
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, (() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
CurrentIsPlaying = false;
CurrentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
UpdateAlbumArt();
UpdateLyrics();
_scrobbleTimer.Stop();
_discordService.Disable();
UpdateCurrentMediaSourceProviderInfoPositionOffset();
}));
});
}
private void UpdateCurrentMediaSourceProviderInfoPositionOffset()
@@ -508,21 +476,39 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
}
}
private void UpdateTargetScrobbledDuration()
{
TargetScrobbledDuration = TimeSpan.FromSeconds(CurrentSongInfo.Duration == 0 ? 30 : CurrentSongInfo.Duration / 2);
}
private async Task SendFocusedMessagesAsync()
{
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps = null;
var desiredSession = GetCurrentSession();
if (_currentDesiredSession == null)
{
SendNullMessages();
return;
}
try
{
mediaProps = await desiredSession?.ControlSession?.TryGetMediaPropertiesAsync();
}
catch (Exception) { }
var mediaProps = await _currentDesiredSession.ControlSession?.TryGetMediaPropertiesAsync();
var timelineProps = _currentDesiredSession.ControlSession?.GetTimelineProperties();
var playbackInfo = _currentDesiredSession.ControlSession?.GetPlaybackInfo();
MediaManager_OnAnyTimelinePropertyChanged(desiredSession, desiredSession?.ControlSession?.GetTimelineProperties());
MediaManager_OnAnyMediaPropertyChanged(desiredSession, mediaProps);
MediaManager_OnAnyPlaybackStateChanged(desiredSession, desiredSession?.ControlSession?.GetPlaybackInfo());
if (mediaProps == null || timelineProps == null || playbackInfo == null)
{
SendNullMessages();
return;
}
MediaManager_OnAnyTimelinePropertyChanged(_currentDesiredSession, timelineProps);
MediaManager_OnAnyMediaPropertyChanged(_currentDesiredSession, mediaProps);
MediaManager_OnAnyPlaybackStateChanged(_currentDesiredSession, playbackInfo);
}
catch (Exception)
{
SendNullMessages();
}
}
private void StartSSE()
@@ -573,7 +559,7 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
{
_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)
@@ -584,11 +570,11 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
}
else if (e.Event == "duration")
{
CurrentSongInfo?.DurationMs = data.GetDouble() * 1000;
CurrentSongInfo.DurationMs = data.GetDouble() * 1000;
UpdateDiscordPresence();
}
if (IsMediaSourceTimelineSyncEnabled(CurrentSongInfo?.PlayerId))
if (IsMediaSourceTimelineSyncEnabled(CurrentSongInfo.PlayerId))
{
CurrentPosition = TimeSpan.FromSeconds(_lxMusicPositionSeconds);
}
@@ -620,47 +606,27 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
public async Task PlayAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TryPlayAsync();
}
await _currentDesiredSession?.ControlSession?.TryPlayAsync();
}
public async Task PauseAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TryPauseAsync();
}
await _currentDesiredSession?.ControlSession?.TryPauseAsync();
}
public async Task PreviousAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TrySkipPreviousAsync();
}
await _currentDesiredSession?.ControlSession?.TrySkipPreviousAsync();
}
public async Task NextAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TrySkipNextAsync();
}
await _currentDesiredSession?.ControlSession?.TrySkipNextAsync();
}
public async Task ChangePosition(double seconds)
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TryChangePlaybackPositionAsync(TimeSpan.FromSeconds(seconds).Ticks);
}
await _currentDesiredSession?.ControlSession?.TryChangePlaybackPositionAsync(TimeSpan.FromSeconds(seconds).Ticks);
}
public async Task ChangeLyricsLine(int index)
@@ -683,7 +649,7 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
{
if (WindowHook.GetWindowHandle<NowPlayingWindow>() is IntPtr hwnd)
{
TaskbarList.SetProgressValue(hwnd, (ulong)value.TotalSeconds, (ulong)(CurrentSongInfo?.Duration ?? value.TotalSeconds));
TaskbarList.SetProgressValue(hwnd, (ulong)value.TotalSeconds, (ulong)(CurrentSongInfo.Duration));
}
}
@@ -693,7 +659,7 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
{
if (message.PropertyName == nameof(MediaSourceProviderInfo.IsEnabled))
{
MediaManager_OnFocusedSessionChanged(null);
OnDesiredSessionChanged();
}
}
else if (message.Sender is TranslationSettings)
@@ -723,7 +689,7 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
{
if (message.PropertyName == nameof(MusicGallerySettings.LyricsWindowStatus.IsOpened))
{
MediaManager_OnFocusedSessionChanged(null);
OnDesiredSessionChanged();
}
}
else if (message.Sender is MediaFolder)

View File

@@ -26,8 +26,12 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
MediaSourceProviderInfo? CurrentMediaSourceProviderInfo { get; }
bool IsScrobbled { get; }
TimeSpan ScrobbledDuration { get; }
TimeSpan TargetScrobbledDuration { get; }
bool CurrentIsPlaying { get; }
SongInfo? CurrentSongInfo { get; }
SongInfo CurrentSongInfo { get; }
TimeSpan CurrentPosition { get; }
LyricsData? CurrentLyricsData { get; }

View File

@@ -2,6 +2,7 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Models.Stats;
using BetterLyrics.WinUI3.ViewModels;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>الأكثر نشاطاً</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>سكروبلينج...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>المصادر</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Aktivste</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Scrobbling...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Quellen</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Most Active</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Scrobbling...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sources</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Más activos</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Scrobbling...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Fuentes</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Les plus actifs</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Scrobbling...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sources d'information</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>सर्वाधिक सक्रिय</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>रिकॉर्डिंग...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>स्रोत</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Paling Aktif</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Menggelinding...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sumber</value>
</data>

View File

@@ -514,7 +514,7 @@
<value>タイトル</value>
</data>
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>ソートタイプ</value>
<value>並べ替えタイプ</value>
</data>
<data name="MusicGalleryPageStopTrack.Text" xml:space="preserve">
<value>停止</value>
@@ -544,7 +544,7 @@
<value>狭い表示モード</value>
</data>
<data name="PictureInPictureMode" xml:space="preserve">
<value>ピクチャー イン ピクチャー モード</value>
<value>ピクチャーインピクチャーモード</value>
</data>
<data name="Pinyin" xml:space="preserve">
<value>ピンイン (中国語)</value>
@@ -775,7 +775,7 @@
<value>Discord Presence で視聴ステータスを表示</value>
</data>
<data name="SettingsPageDisplayTypeSwitcher.Header" xml:space="preserve">
<value>レイアウト</value>
<value>レイアウトモード</value>
</data>
<data name="SettingsPageDockedMode.Text" xml:space="preserve">
<value>ドッキングモード</value>
@@ -1159,7 +1159,7 @@
<value>テストミュージックを再生</value>
</data>
<data name="SettingsPageMultiNowPlayingWindows.Header" xml:space="preserve">
<value>マルチウィンドウ モード</value>
<value>マルチウィンドウモード</value>
</data>
<data name="SettingsPageMusicGallery.Text" xml:space="preserve">
<value>ミュージックギャラリー</value>
@@ -1393,7 +1393,7 @@
<value>スタートアップ</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>統計データ</value>
<value>ミュージックレポート</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>ミュージックギャラリーウィンドウを閉じたときに再生を停止する</value>
@@ -1468,7 +1468,7 @@
<value>標準モード</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>時間帯別アクティビティ</value>
<value>アクティブ時間帯</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>カスタム</value>
@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>最多アクティブ</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>再生記録中...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>再生ソース</value>
</data>
@@ -1504,16 +1507,16 @@
<value>期間</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>タイムズ</value>
<value></value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>今日</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>トップアーティスト</value>
<value>よく聴くアーティスト</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>トップトラック</value>
<value>よく聴く曲</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>よく使う再生ソース</value>
@@ -1522,10 +1525,10 @@
<value>総再生時間</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>タイムズ</value>
<value></value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>タイムズ</value>
<value></value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>再生された曲の数</value>
@@ -1555,7 +1558,7 @@
<value>歌詞ウィンドウスイッチャー</value>
</data>
<data name="TaskbarMode" xml:space="preserve">
<value>タスクバー モード</value>
<value>タスクバーモード</value>
</data>
<data name="TermsOfService.Content" xml:space="preserve">
<value>利用規約</value>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>가장 활동적인</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>스크러블...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>출처</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Paling Aktif</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Rakaman...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sumber</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Mais activos</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>A fazer barulho...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Fontes</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Самые активные</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Скроблинг...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Источники</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>กิจกรรมล่าสุด</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>กำลังส่งข้อมูล...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>แหล่งข้อมูล</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Hoạt động nhiều nhất</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Đang ghi lại...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Nguồn</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>最活跃时段</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>记录中...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>播放源</value>
</data>

View File

@@ -193,7 +193,7 @@
<value>已偵測到根目錄路徑。全磁碟索引可能包含大量非媒體檔案,導致掃描時間過長。建議指定特定的子目錄。</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>等待掃描...</value>
<value>準備掃描...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>全螢幕模式</value>
@@ -418,7 +418,7 @@
<value>媒體庫同步中...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>媒體庫同步問題</value>
<value>媒體庫同步出現問題</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>清除播放佇列</value>
@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>最活躍</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>記錄中...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>來源</value>
</data>

View File

@@ -17,7 +17,7 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsSearchControlViewModel : BaseViewModel,
IRecipient<PropertyChangedMessage<SongInfo?>>
IRecipient<PropertyChangedMessage<SongInfo>>
{
private readonly ILyricsSearchService _lyricsSearchService;
private readonly IGSMTCService _gsmtcService;
@@ -121,7 +121,7 @@ namespace BetterLyrics.WinUI3.ViewModels
LyricsSearchResults = [..await Task.Run(async () =>
{
var result = await _lyricsSearchService.SearchAllAsync(
((SongInfo?)_gsmtcService.CurrentSongInfo?.Clone() ?? new())
((SongInfo)_gsmtcService.CurrentSongInfo.Clone())
.WithTitle(MappedSongSearchQuery.MappedTitle)
.WithArtist(MappedSongSearchQuery.MappedArtist.SplitByCommonSplitter())
.WithAlbum(MappedSongSearchQuery.MappedAlbum),
@@ -194,7 +194,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
public void Receive(PropertyChangedMessage<SongInfo> message)
{
if (message.Sender is IGSMTCService)
{

View File

@@ -3,16 +3,21 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Stats;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
using LiveChartsCore;
using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.Themes;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using SkiaSharp;
using SkiaSharp.Views.Windows;
@@ -25,7 +30,7 @@ using System.Xml.Linq;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class StatsDashboardControlViewModel : ObservableObject
public partial class StatsDashboardControlViewModel : BaseViewModel, IRecipient<PropertyChangedMessage<bool>>
{
private readonly IPlayHistoryService _playHistoryService;
private readonly ILocalizationService _localizationService;
@@ -33,13 +38,19 @@ namespace BetterLyrics.WinUI3.ViewModels
private string _localizedTimesValue;
[ObservableProperty] public partial bool IsLoading { get; set; }
private readonly DispatcherQueueTimer _timer;
[ObservableProperty] public partial IGSMTCService GSMTCService { get; set; }
[ObservableProperty] public partial bool IsLoading { get; set; } = false;
// 时间筛选
[ObservableProperty] public partial StatsRange SelectedTimeRange { get; set; }
[ObservableProperty] public partial bool IsCustomRangeSelected { get; set; }
[ObservableProperty] public partial DateTimeOffset? CustomStartDate { get; set; }
[ObservableProperty] public partial DateTimeOffset? CustomEndDate { get; set; }
[ObservableProperty] public partial StatsRange SelectedTimeRange { get; set; } = StatsRange.Today;
[ObservableProperty] public partial bool IsCustomRangeSelected { get; set; } = false;
[ObservableProperty] public partial DateTimeOffset? CustomStartDate { get; set; } = DateTime.Now;
[ObservableProperty] public partial DateTimeOffset? CustomEndDate { get; set; } = DateTime.Now;
[ObservableProperty] public partial TimeSpan CustomStartTime { get; set; } = TimeSpan.Zero;
[ObservableProperty] public partial TimeSpan CustomEndTime { get; set; } = TimeSpan.Zero;
// 顶部基础数据
[ObservableProperty] public partial TimeSpan TotalDuration { get; set; }
@@ -61,27 +72,33 @@ namespace BetterLyrics.WinUI3.ViewModels
// 歌曲
[ObservableProperty] public partial ObservableCollection<SongPlayCount> TopSongs { get; set; } = new();
public StatsDashboardControlViewModel(IPlayHistoryService playHistoryService, ILocalizationService localizationService, IAlbumArtSearchService albumArtSearchService)
public StatsDashboardControlViewModel(
IPlayHistoryService playHistoryService,
ILocalizationService localizationService,
IAlbumArtSearchService albumArtSearchService,
IGSMTCService gsmtcService)
{
_playHistoryService = playHistoryService;
_localizationService = localizationService;
_albumArtSearchService = albumArtSearchService;
GSMTCService = gsmtcService;
_localizedTimesValue = _localizationService.GetLocalizedString("StatsDashboardControlTimes");
SelectedTimeRange = StatsRange.Today;
_timer = _dispatcherQueue.CreateTimer();
CustomStartDate = DateTimeOffset.Now.AddDays(-7);
CustomEndDate = DateTimeOffset.Now;
UpdateDateRange();
}
async partial void OnSelectedTimeRangeChanged(StatsRange value)
partial void OnSelectedTimeRangeChanged(StatsRange value)
{
IsCustomRangeSelected = value == StatsRange.Custom;
await LoadDataAsync();
UpdateDateRange();
}
async partial void OnCustomEndDateChanged(DateTimeOffset? value) => await LoadDataAsync();
async partial void OnCustomStartDateChanged(DateTimeOffset? value) => await LoadDataAsync();
partial void OnCustomEndDateChanged(DateTimeOffset? value) => LoadData();
partial void OnCustomStartDateChanged(DateTimeOffset? value) => LoadData();
partial void OnCustomStartTimeChanged(TimeSpan value) => LoadData();
partial void OnCustomEndTimeChanged(TimeSpan value) => LoadData();
private void ProcessHourlyStats(List<PlayHistoryItem> logs)
{
@@ -135,11 +152,24 @@ namespace BetterLyrics.WinUI3.ViewModels
private (DateTime? Start, DateTime? End) CalculateDateRange()
{
if (IsCustomRangeSelected)
{
return (CustomStartDate?.UtcDateTime, CustomEndDate?.UtcDateTime);
}
if (CustomStartDate == null || CustomEndDate == null) return (null, null);
return (
new DateTime(
DateOnly.FromDateTime(CustomStartDate.Value.LocalDateTime),
TimeOnly.FromTimeSpan(CustomStartTime),
DateTimeKind.Local)
.ToUniversalTime(),
new DateTime(
DateOnly.FromDateTime(CustomEndDate.Value.LocalDateTime),
TimeOnly.FromTimeSpan(CustomEndTime),
DateTimeKind.Local)
.ToUniversalTime()
);
}
private void UpdateDateRange()
{
DateTime nowLocal = DateTime.Now;
DateTime startLocal = nowLocal.Date;
@@ -152,6 +182,7 @@ namespace BetterLyrics.WinUI3.ViewModels
int dayOfWeek = (int)nowLocal.DayOfWeek;
if (dayOfWeek == 0) dayOfWeek = 7;
startLocal = nowLocal.Date.AddDays(-(dayOfWeek - 1));
startLocal = new DateTime(startLocal.Year, startLocal.Month, startLocal.Day);
break;
case StatsRange.ThisMonth:
startLocal = new DateTime(nowLocal.Year, nowLocal.Month, 1);
@@ -165,61 +196,96 @@ namespace BetterLyrics.WinUI3.ViewModels
break;
}
return (startLocal.ToUniversalTime(), nowLocal.ToUniversalTime());
CustomStartDate = startLocal.Date;
CustomEndDate = nowLocal.Date;
CustomStartTime = startLocal.TimeOfDay;
CustomEndTime = nowLocal.TimeOfDay;
}
[RelayCommand]
public async Task LoadDataAsync()
private void RefreshData()
{
if (IsLoading) return;
IsLoading = true;
try
if (IsCustomRangeSelected)
{
var (start, end) = CalculateDateRange();
LoadData();
}
else
{
UpdateDateRange();
}
}
if (start == null || end == null)
[RelayCommand]
public void LoadData()
{
_timer.Debounce(async () =>
{
if (IsLoading) return;
IsLoading = true;
try
{
start = end = DateTime.Now.ToUniversalTime();
await Task.Delay(Constants.Time.WaitingDuration);
var (start, end) = CalculateDateRange();
if (start == null || end == null)
{
start = end = DateTime.Now.ToUniversalTime();
}
var durationTask = _playHistoryService.GetTotalListeningDurationAsync(start.Value, end.Value);
var logsTask = _playHistoryService.GetLogsByDateRangeAsync(start.Value, end.Value);
var topSongsTask = _playHistoryService.GetTopSongsAsync(start.Value, end.Value, 10);
var topArtistsTask = _playHistoryService.GetTopArtistsAsync(start.Value, end.Value, 10);
var playersTask = _playHistoryService.GetPlayerDistributionAsync(start.Value, end.Value);
await Task.WhenAll(durationTask, logsTask, topSongsTask, topArtistsTask, playersTask);
TotalDuration = await durationTask;
var logs = await logsTask;
TotalTracksPlayed = logs.Count;
TopSongs = [.. await topSongsTask];
var pStats = await playersTask;
UpdatePlayerStats(pStats);
TopArtists = [.. await topArtistsTask];
ProcessHourlyStats(logs);
}
var durationTask = _playHistoryService.GetTotalListeningDurationAsync(start.Value, end.Value);
var logsTask = _playHistoryService.GetLogsByDateRangeAsync(start.Value, end.Value);
var topSongsTask = _playHistoryService.GetTopSongsAsync(start.Value, end.Value, 10);
var topArtistsTask = _playHistoryService.GetTopArtistsAsync(start.Value, end.Value, 10);
var playersTask = _playHistoryService.GetPlayerDistributionAsync(start.Value, end.Value);
await Task.WhenAll(durationTask, logsTask, topSongsTask, topArtistsTask, playersTask);
TotalDuration = await durationTask;
var logs = await logsTask;
TotalTracksPlayed = logs.Count;
TopSongs = [.. await topSongsTask];
var pStats = await playersTask;
UpdatePlayerStats(pStats);
TopArtists = [.. await topArtistsTask];
ProcessHourlyStats(logs);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error loading stats: {ex.Message}");
}
finally
{
IsLoading = false;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error loading stats: {ex.Message}");
}
finally
{
IsLoading = false;
}
}, Constants.Time.DebounceTimeout);
}
[RelayCommand]
private async Task GenerateTestDataAsync()
{
await _playHistoryService.GenerateTestDataAsync(1000);
await LoadDataAsync(); // 生成完刷新
LoadData(); // 生成完刷新
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IGSMTCService.IsScrobbled))
{
if (message.NewValue == true)
{
RefreshData();
}
}
}
}
}
}

View File

@@ -27,7 +27,7 @@ using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Views
{
public sealed partial class NowPlayingPage : Page,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<SongInfo>>,
IRecipient<PropertyChangedMessage<LyricsLayoutOrientation>>,
IRecipient<PropertyChangedMessage<LyricsDisplayType>>,
IRecipient<PropertyChangedMessage<int>>,
@@ -114,9 +114,9 @@ namespace BetterLyrics.WinUI3.Views
var artistsFontSize = albumArtLayoutSettings.IsAutoSongInfoFontSize ? lyricsLayoutMetrics.ArtistNameSize : albumArtLayoutSettings.SongInfoFontSize * 0.8;
var albumFontSize = albumArtLayoutSettings.IsAutoSongInfoFontSize ? lyricsLayoutMetrics.AlbumNameSize : albumArtLayoutSettings.SongInfoFontSize * 0.8;
RenderTextBlock(TitleTextBlock, _gsmtcService.CurrentSongInfo?.Title, titleFontSize);
RenderTextBlock(ArtistsTextBlock, _gsmtcService.CurrentSongInfo?.DisplayArtists, artistsFontSize);
RenderTextBlock(AlbumTextBlock, _gsmtcService.CurrentSongInfo?.Album, albumFontSize);
RenderTextBlock(TitleTextBlock, _gsmtcService.CurrentSongInfo.Title, titleFontSize);
RenderTextBlock(ArtistsTextBlock, _gsmtcService.CurrentSongInfo.DisplayArtists, artistsFontSize);
RenderTextBlock(AlbumTextBlock, _gsmtcService.CurrentSongInfo.Album, albumFontSize);
}
private void UpdateSongInfoOpacity()
@@ -139,6 +139,15 @@ namespace BetterLyrics.WinUI3.Views
}
}
private async void RefreshSongInfo()
{
SongInfoStackPanel.Opacity = 0;
await Task.Delay(Constants.Time.AnimationDuration);
RenderSongInfo();
SongInfoStackPanel.Opacity = 1;
UpdateSongInfoOpacity();
}
// ==== AlbumArt
private void UpdateAlbumArtOpacity()
{
@@ -575,17 +584,13 @@ namespace BetterLyrics.WinUI3.Views
// ====
public async void Receive(PropertyChangedMessage<SongInfo?> message)
public async void Receive(PropertyChangedMessage<SongInfo> message)
{
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
{
SongInfoStackPanel.Opacity = 0;
await Task.Delay(Constants.Time.AnimationDuration);
RenderSongInfo();
SongInfoStackPanel.Opacity = 1;
UpdateSongInfoOpacity();
RefreshSongInfo();
}
}
}
@@ -686,5 +691,6 @@ namespace BetterLyrics.WinUI3.Views
}
}
}
}

View File

@@ -34,7 +34,7 @@
| :---: | :---: | :---: |
| <a href="https://hellogithub.com/repository/jayfunc/BetterLyrics" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d2af74f0aea146ad8e4b2086982f5777&claim_uid=SgtQs9c54C8wjnv" alt="HelloGitHub" height="40"></a> | [**阅读评测文章**](https://sspai.com/post/101028) | [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics) <br> [![Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTk5QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics) |
**交流群:** [QQ 群 (1054700388)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
**交流群:** [QQ 群1054700388](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) | [QQ 频道BetterLyrics4U](https://pd.qq.com/s/1u1ntkyzr?b=9) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
</div>

View File

@@ -34,7 +34,7 @@
| :---: | :---: | :---: |
| <a href="https://hellogithub.com/repository/jayfunc/BetterLyrics" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d2af74f0aea146ad8e4b2086982f5777&claim_uid=SgtQs9c54C8wjnv" alt="HelloGitHub" height="40"></a> | [**Read the Review Article**](https://sspai.com/post/101028) | [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics) <br> [![Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTk5QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics) |
**Chat Groups:** [QQ Group (1054700388)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
**Chat Groups:** [QQ Group (1054700388)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) | [QQ Channel (BetterLyrics4U)](https://pd.qq.com/s/1u1ntkyzr?b=9) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
</div>