diff --git a/Harmonia.Core/Events/PlaylistUpdateAction.cs b/Harmonia.Core/Events/PlaylistUpdateAction.cs
new file mode 100644
index 0000000..695fd89
--- /dev/null
+++ b/Harmonia.Core/Events/PlaylistUpdateAction.cs
@@ -0,0 +1,21 @@
+namespace Harmonia.Core.Events;
+
+public enum PlaylistUpdateAction
+{
+ ///
+ /// An item was added to the collection.
+ ///
+ Add,
+ ///
+ /// An item was removed from the collection.
+ ///
+ Remove,
+ ///
+ /// An item was moved within the collection.
+ ///
+ Move,
+ ///
+ /// The contents of the collection changed dramatically.
+ ///
+ Reset
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Events/PlaylistUpdatedEventArgs.cs b/Harmonia.Core/Events/PlaylistUpdatedEventArgs.cs
new file mode 100644
index 0000000..45e3fdb
--- /dev/null
+++ b/Harmonia.Core/Events/PlaylistUpdatedEventArgs.cs
@@ -0,0 +1,12 @@
+using Harmonia.Core.Models;
+
+namespace Harmonia.Core.Events;
+
+public class PlaylistUpdatedEventArgs : EventArgs
+{
+ public required PlaylistUpdateAction Action { get; init; }
+ public required int Index { get; init; }
+ public int? NewIndex { get; init; } = -1;
+ public required int Count { get; init; }
+ public required PlaylistSong[] Songs { get; init; } = [];
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Extensions/EnumerableExtensions.cs b/Harmonia.Core/Extensions/EnumerableExtensions.cs
new file mode 100644
index 0000000..fb699cf
--- /dev/null
+++ b/Harmonia.Core/Extensions/EnumerableExtensions.cs
@@ -0,0 +1,34 @@
+using Harmonia.Core.Models;
+using System.ComponentModel;
+using System.Reflection;
+
+namespace Harmonia.Core.Extensions;
+
+public static class EnumerableExtensions
+{
+ public static IOrderedEnumerable SortBy(this IEnumerable source, SortOption[] sortOptions)
+ {
+ IOrderedEnumerable orderedQuery = source.OrderBy(x => 0);
+ Type type = typeof(T);
+
+ foreach (SortOption sortOption in sortOptions)
+ {
+ PropertyInfo? property = type.GetProperty(sortOption.FieldName);
+
+ if (property == null)
+ continue;
+
+ switch (sortOption.Direction)
+ {
+ case ListSortDirection.Ascending:
+ orderedQuery = orderedQuery.ThenBy(x => property.GetValue(x));
+ break;
+ case ListSortDirection.Descending:
+ orderedQuery = orderedQuery.ThenByDescending(x => property.GetValue(x));
+ break;
+ }
+ }
+
+ return orderedQuery;
+ }
+}
\ No newline at end of file
diff --git a/Harmonia.Core/Models/Playlist.cs b/Harmonia.Core/Models/Playlist.cs
index a886b4f..dafe6bd 100644
--- a/Harmonia.Core/Models/Playlist.cs
+++ b/Harmonia.Core/Models/Playlist.cs
@@ -1,4 +1,7 @@
-namespace Harmonia.Core.Models;
+using Harmonia.Core.Events;
+using Harmonia.Core.Extensions;
+
+namespace Harmonia.Core.Models;
public class Playlist(string name)
{
@@ -8,4 +11,161 @@ public class Playlist(string name)
public List GroupOptions { get; set; } = [];
public List SortOptions { get; set; } = [];
public bool IsLocked { get; set; }
+
+ public event EventHandler? PlaylistUpdated;
+
+ public void AddSong(Song song, int? index)
+ {
+ AddSongs([song], index);
+ }
+
+ public void AddSongs(Song[] songs, int? index)
+ {
+ PlaylistSong[] playlistSongs = songs.Select(song => new PlaylistSong(song)).ToArray();
+
+ AddSongs(playlistSongs, index);
+ }
+
+ private void AddSongs(PlaylistSong[] playlistSongs, int? index)
+ {
+ if (IsLocked)
+ return;
+
+ int insertIndex = index ?? Songs.Count;
+
+ Songs.InsertRange(insertIndex, playlistSongs);
+
+ PlaylistUpdatedEventArgs eventArgs = new()
+ {
+ Action = PlaylistUpdateAction.Add,
+ Index = insertIndex,
+ Count = playlistSongs.Length,
+ Songs = playlistSongs
+ };
+
+ PlaylistUpdated?.Invoke(this, eventArgs);
+ }
+
+ public void MoveSong(PlaylistSong playlistSong, int newIndex)
+ {
+ int currentIndex = Songs.IndexOf(playlistSong);
+
+ MoveSong(currentIndex, newIndex);
+ }
+
+ public void MoveSong(int oldIndex, int newIndex)
+ {
+ if (IsLocked)
+ return;
+
+ if (oldIndex == newIndex)
+ return;
+
+ PlaylistSong playlistSong = Songs[oldIndex];
+
+ Songs.Remove(playlistSong);
+ Songs.Insert(newIndex, playlistSong);
+
+ PlaylistUpdatedEventArgs eventArgs = new()
+ {
+ Action = PlaylistUpdateAction.Move,
+ Index = oldIndex,
+ Count = 1,
+ Songs = [playlistSong],
+ NewIndex = newIndex
+ };
+
+ PlaylistUpdated?.Invoke(this, eventArgs);
+ }
+
+ public void SortSongs(PlaylistSong[] playlistSongs, SortOption[] sortOptions)
+ {
+ Dictionary oldPlaylistSongs = playlistSongs
+ .OrderBy(Songs.IndexOf)
+ .ToDictionary(Songs.IndexOf, playlistSong => playlistSong);
+
+ Song[] songs = playlistSongs.Select(playlistSong => playlistSong.Song).ToArray();
+ Song[] sortedSongs = [.. songs.SortBy(sortOptions)];
+
+ int currentSortIndex = 0;
+
+ foreach (int index in oldPlaylistSongs.Keys)
+ {
+ PlaylistSong playlistSong = oldPlaylistSongs[index];
+ Song sortedSong = sortedSongs[currentSortIndex++];
+ PlaylistSong newPlaylistSong = playlistSongs.First(ps => ps.Song == sortedSong);
+
+ if (newPlaylistSong == playlistSong)
+ continue;
+
+ Songs.Remove(playlistSong);
+ Songs.Insert(index, newPlaylistSong);
+ }
+
+ PlaylistUpdatedEventArgs eventArgs = new()
+ {
+ Action = PlaylistUpdateAction.Reset,
+ Index = -1,
+ Count = playlistSongs.Length,
+ Songs = playlistSongs
+ };
+
+ PlaylistUpdated?.Invoke(this, eventArgs);
+ }
+
+ public void RemoveSong(int index)
+ {
+ RemoveSongs(index, 1);
+ }
+
+ public void RemoveSongs(int index, int count)
+ {
+ if (IsLocked)
+ return;
+
+ PlaylistSong[] playlistSongs = [.. Songs.GetRange(index, count)];
+
+ Songs.RemoveRange(index, count);
+
+ PlaylistUpdatedEventArgs eventArgs = new()
+ {
+ Action = PlaylistUpdateAction.Remove,
+ Index = index,
+ Count = count,
+ Songs = playlistSongs
+ };
+
+ PlaylistUpdated?.Invoke(this, eventArgs);
+ }
+
+ public void RemoveSong(PlaylistSong playlistSong)
+ {
+ RemoveSongs([playlistSong]);
+ }
+
+ public void RemoveSongs(PlaylistSong[] playlistSongs)
+ {
+ List removedSongs = [];
+
+ foreach (PlaylistSong playlistSong in playlistSongs)
+ {
+ if (Songs.Remove(playlistSong))
+ {
+ removedSongs.Add(playlistSong);
+ }
+ }
+
+ if (removedSongs.Count == 0)
+ return;
+
+ PlaylistUpdatedEventArgs eventArgs = new()
+ {
+ Action = PlaylistUpdateAction.Remove,
+ Index = -1,
+ Count = removedSongs.Count,
+ Songs = playlistSongs
+ };
+
+ PlaylistUpdated?.Invoke(this, eventArgs);
+ }
}
\ No newline at end of file