Files
BetterLyrics/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.cs
Zhe Fang 90e7fa42d0 chores
2025-12-15 15:36:50 -05:00

731 lines
28 KiB
C#

// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Collections;
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.LibWatcherService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
using EvtSource;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json;
using System.Threading.Tasks;
using Vanara.Windows.Shell;
using Windows.Media.Control;
using Windows.Storage.Streams;
using WindowsMediaController;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
public partial class MediaSessionsService : BaseViewModel, IMediaSessionsService,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<ChineseRomanization>>
{
private EventSourceReader? _sse = null;
private readonly MediaManager _mediaManager = new();
private IBuffer? _SMTCAlbumArtBuffer = null;
private readonly IAlbumArtSearchService _albumArtSearchService;
private readonly ILyricsSearchService _lyrcsSearchService;
private readonly ITranslationService _translationService;
private readonly ITransliterationService _transliterationService;
private readonly ISettingsService _settingsService;
private readonly ILibWatcherService _libWatcherService;
private readonly IDiscordService _discordService;
private readonly ILogger<MediaSessionsService> _logger;
private double _lxMusicPositionSeconds = 0;
private byte[]? _lxMusicAlbumArtBytes = null;
private readonly DispatcherQueueTimer? _onMediaPropsChangedTimer;
[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; }
[ObservableProperty] public partial MediaSourceProviderInfo? CurrentMediaSourceProviderInfo { get; set; }
public MediaSessionsService(
ISettingsService settingsService,
IAlbumArtSearchService albumArtSearchService,
ILyricsSearchService lyricsSearchService,
ILibWatcherService libWatcherService,
IDiscordService discordService,
ITranslationService libreTranslateService,
ITransliterationService transliterationService,
ILogger<MediaSessionsService> logger)
{
_settingsService = settingsService;
_albumArtSearchService = albumArtSearchService;
_lyrcsSearchService = lyricsSearchService;
_libWatcherService = libWatcherService;
_translationService = libreTranslateService;
_transliterationService = transliterationService;
_discordService = discordService;
_logger = logger;
_onMediaPropsChangedTimer = _dispatcherQueue.CreateTimer();
_settingsService.AppSettings.MediaSourceProvidersInfo.ItemPropertyChanged += MediaSourceProvidersInfo_ItemPropertyChanged;
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
_settingsService.AppSettings.LocalMediaFolders.ItemPropertyChanged += LocalMediaFolders_ItemPropertyChanged;
_settingsService.AppSettings.MappedSongSearchQueries.CollectionChanged += MappedSongSearchQueries_CollectionChanged;
_settingsService.AppSettings.MappedSongSearchQueries.ItemPropertyChanged += MappedSongSearchQueries_ItemPropertyChanged;
_libWatcherService.MusicLibraryFilesChanged += LibWatcherService_MusicLibraryFilesChanged;
InitMediaManager();
}
private void MappedSongSearchQueries_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
UpdateLyrics();
}
private void MappedSongSearchQueries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UpdateLyrics();
}
private void LocalMediaFolders_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
UpdateAlbumArt();
UpdateLyrics();
}
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UpdateAlbumArt();
UpdateLyrics();
}
private void MediaSourceProvidersInfo_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(MediaSourceProviderInfo.AlbumArtSearchProvidersInfo):
UpdateAlbumArt();
break;
case nameof(MediaSourceProviderInfo.LyricsSearchProvidersInfo):
UpdateLyrics();
break;
case nameof(MediaSourceProviderInfo.LyricsSearchType):
UpdateLyrics();
break;
case nameof(MediaSourceProviderInfo.MatchingThreshold):
UpdateLyrics();
break;
default:
break;
}
}
private void LibWatcherService_MusicLibraryFilesChanged(object? sender, LibChangedEventArgs e)
{
UpdateAlbumArt();
UpdateLyrics();
}
private MediaSourceProviderInfo? GetCurrentMediaSourceProviderInfo()
{
var desiredSession = GetCurrentSession();
return _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == desiredSession?.Id);
}
private bool IsMediaSourceEnabled(string id)
{
var found = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(s => s.Provider == id);
if (_settingsService.AppSettings.MusicGallerySettings.LyricsWindowStatus.IsOpened)
{
if (PlayerIDHelper.IsBetterLyrics(found?.Provider))
{
return found?.IsEnabled ?? true;
}
else
{
return false;
}
}
else
{
return found?.IsEnabled ?? true;
}
}
private bool IsMediaSourceTimelineSyncEnabled(string? id)
{
return _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(s => s.Provider == id)?.IsTimelineSyncEnabled ?? true;
}
private void InitMediaManager()
{
_mediaManager.OnAnySessionOpened += MediaManager_OnAnySessionOpened;
_mediaManager.OnAnySessionClosed += MediaManager_OnAnySessionClosed;
_mediaManager.OnFocusedSessionChanged += MediaManager_OnFocusedSessionChanged;
_mediaManager.OnAnyMediaPropertyChanged += MediaManager_OnAnyMediaPropertyChanged;
_mediaManager.OnAnyPlaybackStateChanged += MediaManager_OnAnyPlaybackStateChanged;
_mediaManager.OnAnyTimelinePropertyChanged += MediaManager_OnAnyTimelinePropertyChanged;
_mediaManager.Start();
MediaManager_OnFocusedSessionChanged(null);
_mediaManager.CurrentMediaSessions.ToList().ForEach(x => RecordMediaSourceProviderInfo(x.Value));
}
private async void MediaManager_OnFocusedSessionChanged(MediaManager.MediaSession? mediaSession)
{
if (!_mediaManager.IsStarted) return;
await SendFocusedMessagesAsync();
}
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties? timelineProperties)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
{
CurrentPosition = TimeSpan.Zero;
return;
}
var desiredSession = GetCurrentSession();
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
{
CurrentPosition = TimeSpan.Zero;
}
else
{
if (IsMediaSourceTimelineSyncEnabled(mediaSession.Id))
{
CurrentPosition = timelineProperties?.Position ?? TimeSpan.Zero;
CurrentSongInfo?.DurationMs = timelineProperties?.EndTime.TotalMilliseconds ?? 0;
}
}
});
}
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo? playbackInfo)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, (() =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
{
CurrentIsPlaying = false;
return;
}
var desiredSession = GetCurrentSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
{
CurrentIsPlaying = false;
}
else
{
CurrentIsPlaying = playbackInfo?.PlaybackStatus switch
{
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false,
};
}
}));
}
private void MediaManager_OnAnyMediaPropertyChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProperties)
{
_onMediaPropsChangedTimer?.Debounce(() =>
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
}
string? sessionId = mediaSession?.Id;
var desiredSession = GetCurrentSession();
if (mediaSession != desiredSession) return;
if (sessionId != null && !IsMediaSourceEnabled(sessionId))
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
if (PlayerIDHelper.IsLXMusic(sessionId))
{
StopSSE();
}
_SMTCAlbumArtBuffer = null;
}
else
{
var currentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
if (currentMediaSourceProviderInfo?.ResetPositionOffsetOnSongChanged == true)
{
currentMediaSourceProviderInfo?.PositionOffset = 0;
}
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, "");
CurrentSongInfo = new SongInfo
{
Title = mediaProperties?.Title ?? "",
Artists = fixedArtist?.SplitByCommonSplitter() ?? [],
Album = fixedAlbum ?? "",
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;
}
}
_logger.LogInformation("MediaManager_OnAnyMediaPropertyChanged {SongInfo}", CurrentSongInfo);
CurrentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
UpdateAlbumArt();
UpdateLyrics();
UpdateDiscordPresence();
UpdateCurrentMediaSourceProviderInfoPositionOffset();
});
}, Time.DebounceTimeout);
}
private void MediaManager_OnAnySessionClosed(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
if (_mediaManager.CurrentMediaSessions.Count == 0)
{
SendNullMessages();
}
}
private void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
RecordMediaSourceProviderInfo(mediaSession);
SendFocusedMessagesAsync().ConfigureAwait(false);
}
private MediaManager.MediaSession? GetCurrentSession()
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession == null)
{
return null;
}
if (IsMediaSourceEnabled(focusedSession.Id))
{
return focusedSession;
}
else
{
foreach (var session in _mediaManager.CurrentMediaSessions.Values)
{
if (IsMediaSourceEnabled(session.Id))
{
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, (() =>
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
CurrentIsPlaying = false;
CurrentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
CurrentPosition = TimeSpan.Zero;
_discordService.Disable();
UpdateCurrentMediaSourceProviderInfoPositionOffset();
}));
}
private void UpdateCurrentMediaSourceProviderInfoPositionOffset()
{
if (CurrentPosition.TotalSeconds <= 1 && CurrentMediaSourceProviderInfo?.ResetPositionOffsetOnSongChanged == true)
{
CurrentMediaSourceProviderInfo?.PositionOffset = 0;
}
}
private void UpdateDiscordPresence()
{
if (CurrentMediaSourceProviderInfo?.IsDiscordPresenceEnabled == true && CurrentSongInfo != null)
{
_discordService.Enable();
_discordService.UpdateRichPresence(CurrentSongInfo);
}
else
{
_discordService.Disable();
}
}
private async Task SendFocusedMessagesAsync()
{
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps = null;
var desiredSession = GetCurrentSession();
try
{
mediaProps = await desiredSession?.ControlSession?.TryGetMediaPropertiesAsync();
}
catch (Exception) { }
MediaManager_OnAnyTimelinePropertyChanged(desiredSession, desiredSession?.ControlSession?.GetTimelineProperties());
MediaManager_OnAnyMediaPropertyChanged(desiredSession, mediaProps);
MediaManager_OnAnyPlaybackStateChanged(desiredSession, desiredSession?.ControlSession?.GetPlaybackInfo());
}
private void StartSSE()
{
if (_sse != null)
{
return;
}
try
{
_sse = new EventSourceReader(new Uri($"{_settingsService.AppSettings.GeneralSettings.LXMusicServer}{Constants.LXMusic.QuerySuffix}")).Start();
_sse.MessageReceived += Sse_MessageReceived;
_sse.Disconnected += Sse_Disconnected;
}
catch (Exception)
{
_logger.LogError("Failed to start SSE connection for LX Music.");
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
ToastHelper.ShowToast("FailToStartLXMusicServer", null, InfoBarSeverity.Error);
});
StopSSE();
}
}
private void StopSSE()
{
if (_sse != null)
{
_sse.MessageReceived -= Sse_MessageReceived;
_sse.Disconnected -= Sse_Disconnected;
_sse.Dispose();
_sse = null;
}
}
private void Sse_Disconnected(object sender, DisconnectEventArgs e)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
await Task.Delay(e.ReconnectDelay);
if (_sse != null && !_sse.IsDisposed) _sse.Start();
});
}
private void Sse_MessageReceived(object sender, EventSourceMessageEventArgs e)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
if (PlayerIDHelper.IsLXMusic(CurrentSongInfo?.PlayerId))
{
var data = JsonSerializer.Deserialize(e.Message, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.ValueKind == JsonValueKind.Number)
{
if (e.Event == "progress")
{
_lxMusicPositionSeconds = data.GetDouble();
}
else if (e.Event == "duration")
{
CurrentSongInfo?.DurationMs = data.GetDouble() * 1000;
UpdateDiscordPresence();
}
if (IsMediaSourceTimelineSyncEnabled(CurrentSongInfo?.PlayerId))
{
CurrentPosition = TimeSpan.FromSeconds(_lxMusicPositionSeconds);
}
}
else if (data.ValueKind == JsonValueKind.String)
{
if (e.Event == "picUrl")
{
string? picUrl = data.GetString();
if (picUrl != null)
{
_logger.LogInformation("LX Music Album Art URL: {url}", picUrl);
_lxMusicAlbumArtBytes = await ImageHelper.GetImageByteArrayFromUrlAsync(picUrl);
if (_lxMusicAlbumArtBytes != null)
{
_SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer();
}
else
{
_SMTCAlbumArtBuffer = null;
}
UpdateAlbumArt();
}
}
}
}
});
}
public async Task PlayAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TryPlayAsync();
}
}
public async Task PauseAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TryPauseAsync();
}
}
public async Task PreviousAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TrySkipPreviousAsync();
}
}
public async Task NextAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TrySkipNextAsync();
}
}
public async Task ChangePosition(double seconds)
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TryChangePlaybackPositionAsync(TimeSpan.FromSeconds(seconds).Ticks);
}
}
public async Task ChangeLyricsLine(int index)
{
if (CurrentLyricsData?.LyricsLines?.ElementAtOrDefault(index)?.StartMs is int startMs)
{
await ChangePosition(startMs / 1000.0);
}
}
partial void OnCurrentIsPlayingChanged(bool value)
{
if (WindowHook.GetWindowHandle<NowPlayingWindow>() is IntPtr hwnd)
{
TaskbarList.SetProgressState(hwnd, value ? TaskbarButtonProgressState.Normal : TaskbarButtonProgressState.Paused);
}
}
partial void OnCurrentPositionChanged(TimeSpan value)
{
if (WindowHook.GetWindowHandle<NowPlayingWindow>() is IntPtr hwnd)
{
TaskbarList.SetProgressValue(hwnd, (ulong)value.TotalSeconds, (ulong)(CurrentSongInfo?.Duration ?? value.TotalSeconds));
}
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is MediaSourceProviderInfo)
{
if (message.PropertyName == nameof(MediaSourceProviderInfo.IsEnabled))
{
MediaManager_OnFocusedSessionChanged(null);
}
}
else if (message.Sender is TranslationSettings)
{
if (message.PropertyName == nameof(TranslationSettings.IsLibreTranslateEnabled))
{
UpdateLyrics();
}
else if (message.PropertyName == nameof(TranslationSettings.IsTranslationEnabled))
{
UpdateLyrics();
}
else if (message.PropertyName == nameof(TranslationSettings.IsChineseRomanizationEnabled))
{
UpdateLyrics();
}
else if (message.PropertyName == nameof(TranslationSettings.IsJapaneseRomanizationEnabled))
{
UpdateLyrics();
}
else if (message.PropertyName == nameof(TranslationSettings.IsTraditionalChineseEnabled))
{
UpdateLyrics();
}
}
else if (message.Sender is LyricsWindowStatus)
{
if (message.PropertyName == nameof(MusicGallerySettings.LyricsWindowStatus.IsOpened))
{
MediaManager_OnFocusedSessionChanged(null);
}
}
}
public void Receive(PropertyChangedMessage<string> message)
{
if (message.Sender is TranslationSettings)
{
if (message.PropertyName == nameof(TranslationSettings.SelectedTargetLanguageCode))
{
_logger.LogInformation("Target LibreTranslate language code changed: {code}", _settingsService.AppSettings.TranslationSettings.SelectedTargetLanguageCode);
UpdateLyrics();
}
else if (message.PropertyName == nameof(TranslationSettings.CutletDockerServer))
{
UpdateLyrics();
}
else if (message.PropertyName == nameof(TranslationSettings.LibreTranslateServer))
{
UpdateLyrics();
}
}
}
public void Receive(PropertyChangedMessage<ChineseRomanization> message)
{
if (message.Sender is TranslationSettings)
{
if (message.PropertyName == nameof(TranslationSettings.ChineseRomanization))
{
UpdateLyrics();
}
}
}
}
}