Files
harmonia/Harmonia.WinUI/ViewModels/PlaylistViewModel.cs
2026-01-25 17:17:31 -05:00

559 lines
16 KiB
C#

using CommunityToolkit.Mvvm.Input;
using Harmonia.Core.Caching;
using Harmonia.Core.Engine;
using Harmonia.Core.Imaging;
using Harmonia.Core.Models;
using Harmonia.Core.Player;
using Harmonia.Core.Playlists;
using Harmonia.Core.Scanner;
using Harmonia.WinUI.Caching;
using Harmonia.WinUI.Storage;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Input;
using Windows.ApplicationModel.DataTransfer;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
using Timer = System.Timers.Timer;
namespace Harmonia.WinUI.ViewModels;
public partial class PlaylistViewModel : ViewModelBase
{
private readonly IAudioPlayer _audioPlayer;
private readonly IAudioImageCache _audioImageCache;
private readonly IAudioBitmapImageCache _audioBitmapImageCache;
private readonly IAudioFileScanner _audioFileScanner;
private readonly IAudioEngine _audioEngine;
private readonly IStorageProvider _storageProvider;
private readonly DispatcherQueue _dispatcherQueue;
private readonly ConcurrentDictionary<int, CancellationTokenSource> _imageCancellationTokens = [];
private Timer? _filterTimer;
public Playlist? Playlist { get; private set; }
private PlaylistSong? _playingSong;
public PlaylistSong? PlayingSong
{
get
{
return _playingSong;
}
set
{
SetProperty(ref _playingSong, value);
}
}
private ObservableCollection<PlaylistSong> _playlistSongs = [];
public ObservableCollection<PlaylistSong> PlaylistSongs
{
get
{
return _playlistSongs;
}
set
{
SetProperty(ref _playlistSongs, value);
}
}
private string? _filter;
public string? Filter
{
get
{
return _filter;
}
set
{
SetProperty(ref _filter, value);
RestartFilterTimer();
}
}
private ObservableCollection<PlaylistSong> _filteredPlaylistSongs = [];
public ObservableCollection<PlaylistSong> FilteredPlaylistSongs
{
get
{
return _filteredPlaylistSongs;
}
set
{
SetProperty(ref _filteredPlaylistSongs, value);
}
}
private ObservableCollection<PlaylistSong> _selectedPlaylistSongs = [];
public ObservableCollection<PlaylistSong> SelectedPlaylistSongs
{
get
{
return _selectedPlaylistSongs;
}
set
{
SetProperty(ref _selectedPlaylistSongs, value);
}
}
public ICommand PlaySongCommand => new AsyncRelayCommand(PlaySongAsync, AreSongsSelected);
public ICommand AddFilesCommand => new AsyncRelayCommand(AddFilesAsync);
public ICommand AddFolderCommand => new AsyncRelayCommand(AddFolderAsync);
public ICommand RemoveSongsCommand => new RelayCommand(RemoveSongs, AreSongsSelected);
public ICommand CutSongsCommand => new RelayCommand(CutSongs, AreSongsSelected);
public ICommand CopySongsCommand => new RelayCommand(CopySongs, AreSongsSelected);
public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs);
public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected);
public ICommand RefreshTagsCommand => new RelayCommand(RefreshTags);
public ICommand RemoveMissingSongsCommand => new RelayCommand(RemoveMissingSongs);
public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs);
public bool IsUserUpdating { get; set; }
private bool _isUserInitiatingSongChange;
public event EventHandler? PlayingSongChangedAutomatically;
public PlaylistViewModel(
IAudioPlayer audioPlayer,
IAudioImageCache audioImageCache,
IAudioBitmapImageCache audioBitmapImageCache,
IAudioFileScanner audioFileScanner,
IAudioEngine audioEngine,
IStorageProvider storageProvider,
IPlaylistRepository playlistRepository)
{
_audioPlayer = audioPlayer;
_audioPlayer.PlaylistChanged += OnPlaylistChanged;
_audioPlayer.PlayingSongChanged += OnPlayingSongChanged;
_audioImageCache = audioImageCache;
_audioBitmapImageCache = audioBitmapImageCache;
_audioFileScanner = audioFileScanner;
_audioEngine = audioEngine;
_storageProvider = storageProvider;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged;
// Testing
Task.Run(() => PlayDemoSong(playlistRepository));
}
private void OnFilteredPlaylistSongsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (IsUserUpdating == false)
return;
int x = 1;
}
private async Task PlayDemoSong(IPlaylistRepository playlistRepository)
{
if (playlistRepository.Get().Count == 0)
{
playlistRepository.AddPlaylist();
}
Playlist playlist = playlistRepository.Get().First();
if (playlist.Songs.Count > 0)
await _audioPlayer.LoadAsync(playlist.Songs[0], PlaybackMode.LoadOnly);
}
private void OnPlaylistChanged(object? sender, EventArgs e)
{
if (Playlist != null)
{
Playlist.PlaylistUpdated -= OnPlaylistUpdated;
}
Playlist = _audioPlayer.Playlist;
if (Playlist != null)
{
Playlist.PlaylistUpdated += OnPlaylistUpdated;
}
PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? [];
PlaylistSongs = [.. playlistSongs];
UpdateFilteredSongs();
}
private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
{
if (IsUserUpdating)
return;
switch (e.Action)
{
case PlaylistUpdateAction.Add:
_dispatcherQueue.TryEnqueue(() => AddSongs(e.Songs, e.Index));
break;
case PlaylistUpdateAction.Remove:
_dispatcherQueue.TryEnqueue(() => RemoveSongsFromCollection(e.Songs));
break;
}
}
private void AddSongs(PlaylistSong[] playlistSongs, int index = 0)
{
// TODO: Performance improvements
int currentIndex = index;
foreach (PlaylistSong playlistSong in playlistSongs)
{
PlaylistSongs.Insert(currentIndex++, playlistSong);
}
UpdateFilteredSongs();
}
private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs)
{
foreach (PlaylistSong playlistSong in playlistSongs)
{
PlaylistSongs.Remove(playlistSong);
}
UpdateFilteredSongs();
}
private void OnPlayingSongChanged(object? sender, EventArgs e)
{
PlayingSong = _audioPlayer.PlayingSong;
if (_isUserInitiatingSongChange)
{
_isUserInitiatingSongChange = false;
}
else
{
PlayingSongChangedAutomatically?.Invoke(this, EventArgs.Empty);
}
}
public async Task PlaySongAsync(PlaylistSong playlistSong)
{
_isUserInitiatingSongChange = true;
await _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay);
}
public async Task<SongPictureInfo?> GetSongPictureInfoAsync(int hashCode, PlaylistSong playlistSong)
{
_imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
cancellationTokenSource?.Cancel();
cancellationTokenSource = new();
_imageCancellationTokens.AddOrUpdate(hashCode, cancellationTokenSource, (_, _) => cancellationTokenSource);
return await _audioImageCache.GetAsync(playlistSong.Song, cancellationTokenSource.Token);
}
public async Task<BitmapImage?> GetBitmapImageAsync(int hashCode, PlaylistSong playlistSong)
{
_imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
cancellationTokenSource?.Cancel();
cancellationTokenSource = new();
_imageCancellationTokens.AddOrUpdate(hashCode, cancellationTokenSource, (_, _) => cancellationTokenSource);
return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationTokenSource.Token);
}
public async Task<BitmapImage?> GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken)
{
return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken);
}
#region Filtering
private void RestartFilterTimer()
{
if (_filterTimer == null)
{
_filterTimer = new Timer(300);
_filterTimer.Elapsed += OnFilterTimerElapsed;
_filterTimer.Start();
}
else
{
_filterTimer.Interval = 300;
}
}
private void OnFilterTimerElapsed(object? sender, ElapsedEventArgs e)
{
if (_filterTimer == null)
return;
_filterTimer.Stop();
_filterTimer.Dispose();
_filterTimer = null;
_dispatcherQueue.TryEnqueue(UpdateFilteredSongs);
}
private void UpdateFilteredSongs()
{
if (Playlist == null)
return;
List<PlaylistSong> filteredPlaylistSongs = [.. Playlist.Songs.Where(playlistSong => IsFiltered(playlistSong.Song))];
//FilteredPlaylistSongs = [.. filteredPlaylistSongs];
for (int i = FilteredPlaylistSongs.Count - 1; i >= 0; i--)
{
PlaylistSong playlistSong = FilteredPlaylistSongs[i];
bool inPlaylist = Playlist.Songs.Contains(playlistSong);
bool inFilter = filteredPlaylistSongs.Contains(playlistSong);
if (!inPlaylist || !inFilter)
{
FilteredPlaylistSongs.Remove(playlistSong);
}
}
int insertionIndex = 0;
foreach (PlaylistSong playlistSong in Playlist.Songs)
{
bool inFilter = filteredPlaylistSongs.Contains(playlistSong);
bool inCurrentFilteredList = FilteredPlaylistSongs.Contains(playlistSong);
if (inFilter)
{
if (!inCurrentFilteredList)
{
FilteredPlaylistSongs.Insert(insertionIndex, playlistSong);
}
insertionIndex++;
}
}
}
private bool IsFiltered(Song song)
{
if (string.IsNullOrWhiteSpace(Filter))
return true;
var shortFileName = Path.GetFileName(song.FileName);
if (shortFileName.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
if (string.IsNullOrWhiteSpace(song.Title) == false && song.Title.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
if (string.IsNullOrWhiteSpace(song.Album) == false && song.Album.Contains(Filter, StringComparison.OrdinalIgnoreCase))
return true;
if (song.AlbumArtists.Any(x => x.Contains(Filter, StringComparison.OrdinalIgnoreCase)))
return true;
if (song.Artists.Any(x => x.Contains(Filter, StringComparison.OrdinalIgnoreCase)))
return true;
return false;
}
#endregion
#region Commands
private async Task PlaySongAsync()
{
if (SelectedPlaylistSongs.Count == 0)
return;
await _audioPlayer.LoadAsync(SelectedPlaylistSongs[0], PlaybackMode.LoadAndPlay);
}
private bool AreSongsSelected()
{
return SelectedPlaylistSongs.Count > 0;
}
private async Task AddFilesAsync(CancellationToken cancellationToken)
{
if (Playlist == null)
return;
FilePickerOptions filePickerOptions = new()
{
FileTypeFilter = [GetAudioFileTypes()],
};
string[] fileNames = await _storageProvider.GetFilesAsync(filePickerOptions);
Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken);
Playlist.AddSongs(songs);
}
private FilePickerFileType GetAudioFileTypes()
{
string[] patterns = [.. _audioEngine.SupportedFormats.Select(format => format.Replace("*", ""))];
return new()
{
Name = "Audio Files",
Patterns = patterns
};
}
private async Task AddFolderAsync(CancellationToken cancellationToken)
{
if (Playlist == null)
return;
string? path = await _storageProvider.GetPathAsync();
if (string.IsNullOrWhiteSpace(path))
return;
Song[] songs = await _audioFileScanner.GetSongsFromPathAsync(path, cancellationToken);
Playlist.AddSongs(songs);
}
public async Task AddFilesAsync(string[] fileNames, CancellationToken cancellationToken)
{
if (Playlist == null)
return;
Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken);
Playlist.AddSongs(songs);
}
public async Task AddFolderAsync(string path, CancellationToken cancellationToken)
{
if (Playlist == null)
return;
Song[] songs = await _audioFileScanner.GetSongsFromPathAsync(path, cancellationToken);
Playlist.AddSongs(songs);
}
private void RemoveSongs()
{
if (Playlist == null)
return;
if (SelectedPlaylistSongs.Count == 0)
return;
PlaylistSong[] playlistSongs = [.. SelectedPlaylistSongs];
Playlist.RemoveSongs(playlistSongs);
}
private void CutSongs()
{
if (SelectedPlaylistSongs.Count == 0)
return;
CopySelectedSongsToClipboard();
}
private void CopySongs()
{
if (SelectedPlaylistSongs.Count == 0)
return;
CopySelectedSongsToClipboard();
}
private void CopySelectedSongsToClipboard()
{
Song[] songs = [.. SelectedPlaylistSongs.Select(playlistSong => playlistSong.Song)];
DataPackage dataPackage = new()
{
RequestedOperation = DataPackageOperation.Copy
};
dataPackage.Properties.Add("Type", "SongList");
dataPackage.SetData(StandardDataFormats.Text, JsonSerializer.Serialize(songs));
Clipboard.SetContent(dataPackage);
}
private bool CanPasteSongs()
{
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
return false;
DataPackageView dataPackageView = Clipboard.GetContent();
if (dataPackageView == null)
return false;
if (dataPackageView.Properties.ContainsKey("Type") == false)
return false;
return dataPackageView.Properties["Type"].ToString() == "SongList";
}
private async Task PasteSongsAsync()
{
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
return;
int selectedPlaylistSongIndex = Playlist.Songs.IndexOf(SelectedPlaylistSongs[0]);
if (selectedPlaylistSongIndex == -1)
return;
Song[] songs = await GetSongsFromClipboardAsync();
Playlist.AddSongs(songs, selectedPlaylistSongIndex + 1);
}
private static async Task<Song[]> GetSongsFromClipboardAsync()
{
DataPackageView dataPackageView = Clipboard.GetContent();
string data = await dataPackageView.GetTextAsync(StandardDataFormats.Text);
return JsonSerializer.Deserialize<Song[]>(data) ?? [];
}
private void OpenFileLocation()
{
if (SelectedPlaylistSongs.Count == 0)
return;
string argument = "/select, \"" + SelectedPlaylistSongs[0].Song.FileName + "\"";
Process.Start("explorer.exe", argument);
}
private void RefreshTags()
{
//Playlist?.RefreshTags();
}
private void RemoveMissingSongs()
{
Playlist?.RemoveMissingSongs();
}
private void RemoveDuplicateSongs()
{
Playlist?.RemoveDuplicateSongs();
}
#endregion
}