feat: complete 2.0.7.12 platform overhaul
This commit is contained in:
@@ -88,10 +88,18 @@ public static class AppServices
|
||||
provider.GetService<ILogService>()));
|
||||
services.AddSingleton<IRemoteMediaResolver, RemoteMediaResolver>();
|
||||
services.AddSingleton<IMusicCredentialStore>(provider => new DpapiMusicCredentialStore(provider.GetRequiredService<AppPaths>()));
|
||||
services.AddSingleton<IMusicProvider>(provider => new NeteaseMusicProvider(
|
||||
services.AddSingleton<NeteaseMusicProvider>(provider => new NeteaseMusicProvider(
|
||||
provider.GetRequiredService<IMusicCredentialStore>(),
|
||||
provider.GetService<ILogService>()));
|
||||
services.AddSingleton<KugouMusicProvider>(provider => new KugouMusicProvider(
|
||||
provider.GetRequiredService<IMusicCredentialStore>()));
|
||||
services.AddSingleton<IMusicProviderRegistry>(provider => new MusicProviderRegistry(
|
||||
[
|
||||
provider.GetRequiredService<NeteaseMusicProvider>(),
|
||||
provider.GetRequiredService<KugouMusicProvider>()
|
||||
]));
|
||||
services.AddSingleton<MusicSessionStore>();
|
||||
services.AddSingleton<IMusicHistoryStore, MusicHistoryStore>();
|
||||
services.AddSingleton<IMusicPlaybackService, WindowsMusicPlaybackService>();
|
||||
services.AddSingleton<IIndependentWindowHostLauncher>(provider => new IndependentWindowHostLauncher(provider.GetService<ILogService>()));
|
||||
services.AddSingleton<IToolLinkNavigationService>(provider => new ToolLinkNavigationService(
|
||||
@@ -102,6 +110,7 @@ public static class AppServices
|
||||
provider.GetRequiredService<AppPaths>(),
|
||||
provider.GetService<ILogService>()));
|
||||
services.AddSingleton<IReferenceDataService, ReferenceDataService>();
|
||||
services.AddSingleton<IHardwareRankingService, HardwareRankingService>();
|
||||
services.AddSingleton<ISystemMetricsService, SystemMetricsService>();
|
||||
services.AddSingleton<ITitleWeatherService>(provider => new TitleWeatherService(
|
||||
provider.GetRequiredService<IHttpService>(),
|
||||
@@ -112,6 +121,7 @@ public static class AppServices
|
||||
provider.GetRequiredService<IHardwareInfoService>(),
|
||||
provider.GetService<ILogService>()));
|
||||
services.AddSingleton<IDesktopOverlayService, DesktopOverlayService>();
|
||||
services.AddSingleton<IMusicLyricsCoordinator, MusicLyricsCoordinator>();
|
||||
services.AddSingleton<IPrivilegedOperationBroker>(provider => new PrivilegedOperationBroker(
|
||||
provider.GetRequiredService<AppPaths>(),
|
||||
provider.GetService<ILogService>()));
|
||||
@@ -125,10 +135,14 @@ public static class AppServices
|
||||
provider.GetService<ILogService>(),
|
||||
provider.GetService<ISettingsService>()));
|
||||
services.AddSingleton<IBuiltinReferenceToolCatalog, BuiltinReferenceToolCatalog>();
|
||||
services.AddSingleton<IWindowsToolsService>(provider => new WindowsToolsService(
|
||||
provider.GetRequiredService<AppPaths>(),
|
||||
provider.GetRequiredService<IPrivilegedOperationBroker>()));
|
||||
services.AddSingleton<INexNativeToolService>(provider => new NexNativeToolService(
|
||||
provider.GetRequiredService<ISystemMetricsService>(),
|
||||
provider.GetRequiredService<IHardwareInfoService>(),
|
||||
provider.GetRequiredService<IPrivilegedOperationBroker>()));
|
||||
provider.GetRequiredService<IPrivilegedOperationBroker>(),
|
||||
provider.GetRequiredService<IWindowsToolsService>()));
|
||||
services.AddSingleton<IBuiltinReferenceToolService>(provider => new BuiltinReferenceToolService(
|
||||
provider.GetService<ILogService>(),
|
||||
provider.GetService<ISettingsService>(),
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
using YMhut.Box.Core.Music;
|
||||
|
||||
namespace YMhut.Box.WinUI.Services;
|
||||
|
||||
public interface IMusicLyricsCoordinator : IDisposable
|
||||
{
|
||||
event EventHandler? LyricsChanged;
|
||||
|
||||
MusicSong? Song { get; }
|
||||
|
||||
TimedLyrics? Lyrics { get; }
|
||||
|
||||
int ActiveLineIndex { get; }
|
||||
|
||||
int ActiveWordIndex { get; }
|
||||
|
||||
Task RefreshAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps lyric state alive while the music page is not visible so the desktop overlay follows playback.
|
||||
/// </summary>
|
||||
public sealed class MusicLyricsCoordinator : IMusicLyricsCoordinator
|
||||
{
|
||||
private readonly IMusicPlaybackService _playback;
|
||||
private readonly IMusicProviderRegistry _providers;
|
||||
private readonly IDesktopOverlayService _overlay;
|
||||
private readonly object _sync = new();
|
||||
private CancellationTokenSource? _loadCts;
|
||||
private string _songKey = string.Empty;
|
||||
private TimedLyrics? _lyrics;
|
||||
private int _activeLineIndex = -1;
|
||||
private int _activeWordIndex = -1;
|
||||
private bool _disposed;
|
||||
|
||||
public MusicLyricsCoordinator(
|
||||
IMusicPlaybackService playback,
|
||||
IMusicProviderRegistry providers,
|
||||
IDesktopOverlayService overlay)
|
||||
{
|
||||
_playback = playback;
|
||||
_providers = providers;
|
||||
_overlay = overlay;
|
||||
_playback.CurrentSongChanged += Playback_CurrentSongChanged;
|
||||
_playback.StateChanged += Playback_StateChanged;
|
||||
}
|
||||
|
||||
public event EventHandler? LyricsChanged;
|
||||
|
||||
public MusicSong? Song => _playback.CurrentSong;
|
||||
|
||||
public TimedLyrics? Lyrics
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync) return _lyrics;
|
||||
}
|
||||
}
|
||||
|
||||
public int ActiveLineIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync) return _activeLineIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public int ActiveWordIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync) return _activeWordIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var song = _playback.CurrentSong;
|
||||
var key = song is null ? string.Empty : ToKey(song);
|
||||
lock (_sync)
|
||||
{
|
||||
if (string.Equals(key, _songKey, StringComparison.Ordinal) && _lyrics is not null)
|
||||
{
|
||||
UpdateActiveLineUnsafe(_playback.Position);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CancellationTokenSource? previous;
|
||||
CancellationTokenSource current;
|
||||
lock (_sync)
|
||||
{
|
||||
previous = _loadCts;
|
||||
_loadCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
current = _loadCts;
|
||||
_songKey = key;
|
||||
_lyrics = null;
|
||||
_activeLineIndex = -1;
|
||||
_activeWordIndex = -1;
|
||||
}
|
||||
previous?.Cancel();
|
||||
previous?.Dispose();
|
||||
|
||||
if (song is null)
|
||||
{
|
||||
SetOverlayPlaceholder();
|
||||
LyricsChanged?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the previous song immediately; a slow lyric request must not leave stale desktop lyrics visible.
|
||||
SetOverlayPlaceholder();
|
||||
LyricsChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
try
|
||||
{
|
||||
var provider = _providers.GetRequired(song.Provider);
|
||||
var lyrics = await provider.GetLyricsAsync(song.Id, current.Token).ConfigureAwait(false);
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed || !ReferenceEquals(current, _loadCts) || !string.Equals(_songKey, key, StringComparison.Ordinal)) return;
|
||||
_lyrics = lyrics;
|
||||
UpdateActiveLineUnsafe(_playback.Position);
|
||||
}
|
||||
LyricsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
catch (OperationCanceledException) when (current.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!ReferenceEquals(current, _loadCts)) return;
|
||||
_lyrics = new TimedLyrics([], string.Empty, null, null);
|
||||
}
|
||||
SetOverlayPlaceholder();
|
||||
LyricsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_playback.CurrentSongChanged -= Playback_CurrentSongChanged;
|
||||
_playback.StateChanged -= Playback_StateChanged;
|
||||
lock (_sync)
|
||||
{
|
||||
_loadCts?.Cancel();
|
||||
_loadCts?.Dispose();
|
||||
_loadCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Playback_CurrentSongChanged(object? sender, EventArgs e)
|
||||
=> _ = RefreshAsync();
|
||||
|
||||
private void Playback_StateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var changed = false;
|
||||
lock (_sync)
|
||||
{
|
||||
changed = UpdateActiveLineUnsafe(_playback.Position);
|
||||
}
|
||||
if (changed) LyricsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private bool UpdateActiveLineUnsafe(TimeSpan position)
|
||||
{
|
||||
if (_lyrics is null || _lyrics.Lines.Count == 0) return false;
|
||||
var lineIndex = -1;
|
||||
for (var index = 0; index < _lyrics.Lines.Count; index++)
|
||||
{
|
||||
if (_lyrics.Lines[index].Start <= position) lineIndex = index;
|
||||
else break;
|
||||
}
|
||||
if (lineIndex < 0)
|
||||
{
|
||||
if (_activeLineIndex < 0 && _activeWordIndex < 0) return false;
|
||||
_activeLineIndex = -1;
|
||||
_activeWordIndex = -1;
|
||||
SetOverlayPlaceholder();
|
||||
return true;
|
||||
}
|
||||
|
||||
var line = _lyrics.Lines[lineIndex];
|
||||
var wordIndex = -1;
|
||||
for (var index = 0; index < line.Words.Count; index++)
|
||||
{
|
||||
if (line.Words[index].Start <= position) wordIndex = index;
|
||||
else break;
|
||||
}
|
||||
if (lineIndex == _activeLineIndex && wordIndex == _activeWordIndex) return false;
|
||||
|
||||
_activeLineIndex = lineIndex;
|
||||
_activeWordIndex = wordIndex;
|
||||
var song = _playback.CurrentSong;
|
||||
_overlay.SetDesktopLyrics(
|
||||
song?.Name ?? "YMhut Music",
|
||||
song?.Artist ?? string.Empty,
|
||||
line.Text,
|
||||
line.Translation);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetOverlayPlaceholder()
|
||||
{
|
||||
var song = _playback.CurrentSong;
|
||||
_overlay.SetDesktopLyrics(
|
||||
song?.Name ?? "YMhut Music",
|
||||
song?.Artist ?? string.Empty,
|
||||
song is null ? "播放歌曲后显示桌面歌词" : "纯音乐,请欣赏");
|
||||
}
|
||||
|
||||
private static string ToKey(MusicSong song) => $"{song.Provider}:{song.Id}";
|
||||
}
|
||||
@@ -52,6 +52,16 @@ public sealed class WebView2EnvironmentFactory(
|
||||
await Task.WhenAll(profiles.Select(profile => CreateAsync(profile))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<CoreWebView2Environment> CreateEphemeralAsync(
|
||||
string userDataFolder,
|
||||
bool? hardwareAccelerationEnabled = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(userDataFolder);
|
||||
var resolvedUserDataFolder = Path.GetFullPath(userDataFolder);
|
||||
var useHardwareAcceleration = hardwareAccelerationEnabled ?? settingsService.Current.HardwareAccelerationEnabled;
|
||||
return CreateEnvironmentCoreAsync(resolvedUserDataFolder, useHardwareAcceleration);
|
||||
}
|
||||
|
||||
private static async Task<CoreWebView2Environment> CreateEnvironmentCoreAsync(
|
||||
string resolvedUserDataFolder,
|
||||
bool useHardwareAcceleration)
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using Windows.Media;
|
||||
using Windows.Media.Core;
|
||||
using Windows.Media.Playback;
|
||||
using YMhut.Box.Core.Logging;
|
||||
using YMhut.Box.Core.Music;
|
||||
|
||||
namespace YMhut.Box.WinUI.Services;
|
||||
|
||||
public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDisposable
|
||||
{
|
||||
private readonly IMusicProvider _provider;
|
||||
private readonly IMusicProviderRegistry _providers;
|
||||
private readonly IMusicHistoryStore _historyStore;
|
||||
private readonly MusicSessionStore _sessionStore;
|
||||
private readonly ILogService _logService;
|
||||
private readonly MediaPlayer _player = new();
|
||||
private readonly SemaphoreSlim _transition = new(1, 1);
|
||||
private MusicPlaybackQuality _quality = MusicPlaybackQuality.HiRes;
|
||||
@@ -16,11 +19,19 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
private TimeSpan _pendingPosition;
|
||||
private bool _suppressSave;
|
||||
private bool _disposed;
|
||||
private bool _mediaRetryInProgress;
|
||||
private int _mediaFailureRetries;
|
||||
|
||||
public WindowsMusicPlaybackService(IMusicProvider provider, MusicSessionStore sessionStore)
|
||||
public WindowsMusicPlaybackService(
|
||||
IMusicProviderRegistry providers,
|
||||
MusicSessionStore sessionStore,
|
||||
IMusicHistoryStore historyStore,
|
||||
ILogService logService)
|
||||
{
|
||||
_provider = provider;
|
||||
_providers = providers;
|
||||
_sessionStore = sessionStore;
|
||||
_historyStore = historyStore;
|
||||
_logService = logService;
|
||||
_player.AutoPlay = false;
|
||||
_player.CommandManager.IsEnabled = false;
|
||||
_player.MediaEnded += Player_MediaEnded;
|
||||
@@ -39,12 +50,16 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
|
||||
public event EventHandler? StateChanged;
|
||||
|
||||
public event EventHandler? CurrentSongChanged;
|
||||
|
||||
public MusicQueue Queue { get; } = new();
|
||||
|
||||
public MusicSong? CurrentSong { get; private set; }
|
||||
|
||||
public bool IsPlaying => _player.PlaybackSession.PlaybackState == MediaPlaybackState.Playing;
|
||||
|
||||
public MusicPlaybackFailure? LastFailure { get; private set; }
|
||||
|
||||
public TimeSpan Position => _player.PlaybackSession.Position;
|
||||
|
||||
public TimeSpan Duration => _player.PlaybackSession.NaturalDuration;
|
||||
@@ -55,7 +70,7 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
set
|
||||
{
|
||||
_player.Volume = Math.Clamp(value, 0, 100) / 100;
|
||||
_ = SaveAsync();
|
||||
QueueSave();
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@@ -67,24 +82,31 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
{
|
||||
if (_quality == value) return;
|
||||
_quality = value;
|
||||
if (!_suppressSave) _ = SaveAsync();
|
||||
if (!_suppressSave) QueueSave();
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RestoreAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _provider.InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||
var session = await _sessionStore.LoadAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (session is null) return;
|
||||
var providerId = session.CurrentSongProvider
|
||||
?? session.Queue.ElementAtOrDefault(Math.Clamp(session.CurrentIndex, 0, Math.Max(0, session.Queue.Count - 1)))?.Provider
|
||||
?? _providers.Current.Id;
|
||||
if (_providers.TryGet(providerId, out var provider))
|
||||
{
|
||||
await provider.InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
_suppressSave = true;
|
||||
try
|
||||
{
|
||||
Queue.Replace(session.Queue, session.CurrentSongId);
|
||||
Queue.Replace(session.Queue, session.CurrentSongId, session.CurrentSongProvider);
|
||||
Queue.Mode = session.Mode;
|
||||
_quality = session.Quality;
|
||||
_player.Volume = Math.Clamp(session.Volume, 0, 100) / 100;
|
||||
_resumePosition = session.Position;
|
||||
CurrentSong = Queue.Current;
|
||||
SetCurrentSong(Queue.Current);
|
||||
if (CurrentSong is not null) UpdateSystemMediaControls(CurrentSong);
|
||||
}
|
||||
finally
|
||||
@@ -95,27 +117,99 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
}
|
||||
|
||||
public async Task PlayAsync(MusicSong song, IReadOnlyList<MusicSong>? queue = null, CancellationToken cancellationToken = default)
|
||||
=> await PlayCoreAsync(song, queue, TimeSpan.Zero, cancellationToken).ConfigureAwait(false);
|
||||
=> await PlayCoreAsync(song, queue, TimeSpan.Zero, cancellationToken, isRetry: false).ConfigureAwait(false);
|
||||
|
||||
private async Task PlayCoreAsync(MusicSong song, IReadOnlyList<MusicSong>? queue, TimeSpan startPosition, CancellationToken cancellationToken)
|
||||
public async Task ChangeQualityAsync(MusicPlaybackQuality quality, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (CurrentSong is not { } song)
|
||||
{
|
||||
Quality = quality;
|
||||
return;
|
||||
}
|
||||
|
||||
var previousQuality = _quality;
|
||||
var position = Position;
|
||||
var wasPlaying = IsPlaying;
|
||||
try
|
||||
{
|
||||
await PlayCoreAsync(
|
||||
song,
|
||||
null,
|
||||
position,
|
||||
cancellationToken,
|
||||
isRetry: false,
|
||||
requestedQuality: quality,
|
||||
startPlayback: wasPlaying).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_quality = previousQuality;
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PlayCoreAsync(
|
||||
MusicSong song,
|
||||
IReadOnlyList<MusicSong>? queue,
|
||||
TimeSpan startPosition,
|
||||
CancellationToken cancellationToken,
|
||||
bool isRetry,
|
||||
MusicPlaybackQuality? requestedQuality = null,
|
||||
bool startPlayback = true)
|
||||
{
|
||||
await _transition.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (queue is not null) Queue.Replace(queue, song.Id);
|
||||
if (!isRetry)
|
||||
{
|
||||
_mediaFailureRetries = 0;
|
||||
LastFailure = null;
|
||||
}
|
||||
if (queue is not null) Queue.Replace(queue, song.Id, song.Provider);
|
||||
else Queue.Select(song);
|
||||
var stream = await _provider.ResolveStreamAsync(song.Id, Quality, cancellationToken).ConfigureAwait(false);
|
||||
MusicStreamResult stream;
|
||||
try
|
||||
{
|
||||
var targetQuality = isRetry ? MusicPlaybackQuality.Standard : requestedQuality ?? Quality;
|
||||
var provider = _providers.GetRequired(song.Provider);
|
||||
await provider.InitializeAsync(cancellationToken).ConfigureAwait(false);
|
||||
stream = await provider.ResolveStreamAsync(song.Id, targetQuality, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
LastFailure = new MusicPlaybackFailure(MusicPlaybackFailureStage.Resolve, "STREAM_RESOLVE_FAILED", exception.Message, true);
|
||||
throw;
|
||||
}
|
||||
if (!stream.Playable || stream.Uri is null)
|
||||
{
|
||||
throw new InvalidOperationException(stream.ProviderReason ?? "提供方未返回播放地址。");
|
||||
var message = stream.ProviderReason ?? "提供方未返回播放地址。";
|
||||
LastFailure = new MusicPlaybackFailure(MusicPlaybackFailureStage.Resolve, "STREAM_UNAVAILABLE", message, false);
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
CurrentSong = song;
|
||||
var probe = await _providers.GetRequired(song.Provider).ProbeStreamAsync(stream.Uri, cancellationToken).ConfigureAwait(false);
|
||||
if (!probe.Reachable)
|
||||
{
|
||||
var message = probe.Reason ?? "播放地址当前不可访问。";
|
||||
LastFailure = new MusicPlaybackFailure(MusicPlaybackFailureStage.Network, "STREAM_PROBE_FAILED", message, true);
|
||||
throw new HttpRequestException(message);
|
||||
}
|
||||
_quality = stream.Quality;
|
||||
SetCurrentSong(song);
|
||||
_resumePosition = TimeSpan.Zero;
|
||||
_pendingPosition = startPosition;
|
||||
_player.Source = MediaSource.CreateFromUri(stream.Uri);
|
||||
try
|
||||
{
|
||||
_player.Source = MediaSource.CreateFromUri(stream.Uri);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
LastFailure = new MusicPlaybackFailure(MusicPlaybackFailureStage.MediaOpen, "MEDIA_SOURCE_FAILED", exception.Message, true);
|
||||
throw;
|
||||
}
|
||||
UpdateSystemMediaControls(song);
|
||||
_player.Play();
|
||||
await SaveAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (startPlayback) _player.Play();
|
||||
await SaveSafelyAsync(cancellationToken).ConfigureAwait(false);
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
finally
|
||||
@@ -133,7 +227,7 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
return;
|
||||
}
|
||||
if (IsPlaying) _player.Pause(); else _player.Play();
|
||||
_ = SaveAsync();
|
||||
QueueSave();
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -158,7 +252,7 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
{
|
||||
var duration = Duration;
|
||||
_player.PlaybackSession.Position = duration > TimeSpan.Zero && position > duration ? duration : position < TimeSpan.Zero ? TimeSpan.Zero : position;
|
||||
_ = SaveAsync();
|
||||
QueueSave();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -184,14 +278,50 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
|
||||
private void Player_MediaOpened(MediaPlayer sender, object args)
|
||||
{
|
||||
LastFailure = null;
|
||||
if (_pendingPosition <= TimeSpan.Zero) return;
|
||||
var duration = sender.PlaybackSession.NaturalDuration;
|
||||
sender.PlaybackSession.Position = duration > TimeSpan.Zero && _pendingPosition > duration ? duration : _pendingPosition;
|
||||
_pendingPosition = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
private void Player_MediaFailed(MediaPlayer sender, MediaPlayerFailedEventArgs args)
|
||||
=> StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
private async void Player_MediaFailed(MediaPlayer sender, MediaPlayerFailedEventArgs args)
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(args.ErrorMessage) ? "Windows 媒体组件无法打开该音频。" : args.ErrorMessage;
|
||||
var decoding = args.Error.ToString().Contains("Decod", StringComparison.OrdinalIgnoreCase);
|
||||
if (!_mediaRetryInProgress && _mediaFailureRetries == 0 && CurrentSong is { } song)
|
||||
{
|
||||
_mediaFailureRetries = 1;
|
||||
_mediaRetryInProgress = true;
|
||||
var position = Position;
|
||||
try
|
||||
{
|
||||
await PlayCoreAsync(song, null, position, CancellationToken.None, isRetry: true).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
LastFailure ??= new MusicPlaybackFailure(
|
||||
decoding ? MusicPlaybackFailureStage.Decode : MusicPlaybackFailureStage.MediaOpen,
|
||||
decoding ? "MEDIA_DECODE_FAILED" : "MEDIA_OPEN_FAILED",
|
||||
$"{message} 标准音质重试失败:{exception.Message}",
|
||||
false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_mediaRetryInProgress = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LastFailure = new MusicPlaybackFailure(
|
||||
decoding ? MusicPlaybackFailureStage.Decode : MusicPlaybackFailureStage.MediaOpen,
|
||||
decoding ? "MEDIA_DECODE_FAILED" : "MEDIA_OPEN_FAILED",
|
||||
message,
|
||||
false);
|
||||
}
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private async void Controls_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)
|
||||
{
|
||||
@@ -229,9 +359,21 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
updater.Update();
|
||||
}
|
||||
|
||||
private void SetCurrentSong(MusicSong? song)
|
||||
{
|
||||
var changed = !string.Equals(CurrentSong?.Provider, song?.Provider, StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(CurrentSong?.Id, song?.Id, StringComparison.Ordinal);
|
||||
CurrentSong = song;
|
||||
if (changed)
|
||||
{
|
||||
CurrentSongChanged?.Invoke(this, EventArgs.Empty);
|
||||
if (song is not null) _ = RecordHistoryAsync(song);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStateChanged()
|
||||
{
|
||||
_ = SaveAsync();
|
||||
QueueSave();
|
||||
StateChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -241,7 +383,7 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
if (song is null) return;
|
||||
try
|
||||
{
|
||||
await PlayCoreAsync(song, null, _resumePosition, CancellationToken.None).ConfigureAwait(false);
|
||||
await PlayCoreAsync(song, null, _resumePosition, CancellationToken.None, isRetry: false).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -249,6 +391,33 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueSave() => _ = SaveSafelyAsync();
|
||||
|
||||
private async Task SaveSafelyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SaveAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _logService.WriteAsync(
|
||||
"Warning",
|
||||
"music",
|
||||
"音乐会话状态保存失败",
|
||||
exception.GetType().Name).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Task SaveAsync(CancellationToken cancellationToken = default)
|
||||
=> _sessionStore.SaveAsync(new MusicSession(
|
||||
CurrentSong?.Id,
|
||||
@@ -256,7 +425,28 @@ public sealed class WindowsMusicPlaybackService : IMusicPlaybackService, IDispos
|
||||
Volume,
|
||||
Queue.Mode,
|
||||
Quality,
|
||||
Queue.Items,
|
||||
Queue.Items.ToArray(),
|
||||
Queue.CurrentIndex,
|
||||
DateTimeOffset.Now), cancellationToken);
|
||||
DateTimeOffset.Now)
|
||||
{
|
||||
CurrentSongProvider = CurrentSong?.Provider
|
||||
}, cancellationToken);
|
||||
|
||||
private async Task RecordHistoryAsync(MusicSong song)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _historyStore.RecordPlayedAsync(song).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _logService.WriteAsync("Warning", "music", "最近播放保存失败", exception.GetType().Name).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user