This repository has been archived on 2024-12-22. You can view files and clone it, but cannot push or open issues or pull requests.
elliebot/src/EllieBot/Modules/Music/_common/Impl/MusicQueue.cs

342 lines
No EOL
8.5 KiB
C#

namespace EllieBot.Modules.Music;
public sealed partial class MusicQueue
{
private sealed class QueuedTrackInfo : IQueuedTrackInfo
{
public ITrackInfo TrackInfo { get; }
public string Queuer { get; }
public string Title
=> TrackInfo.Title;
public string Url
=> TrackInfo.Url;
public string Thumbnail
=> TrackInfo.Thumbnail;
public TimeSpan Duration
=> TrackInfo.Duration;
public MusicPlatform Platform
=> TrackInfo.Platform;
public QueuedTrackInfo(ITrackInfo trackInfo, string queuer)
{
TrackInfo = trackInfo;
Queuer = queuer;
}
}
}
public sealed partial class MusicQueue : IMusicQueue
{
public int Index
{
get
{
// just make sure the internal logic runs first
// to make sure that some potential intermediate value is not returned
lock (_locker)
{
return index;
}
}
}
public int Count
{
get
{
lock (_locker)
{
return tracks.Count;
}
}
}
private LinkedList<QueuedTrackInfo> tracks;
private int index;
private readonly object _locker = new();
public MusicQueue()
{
index = 0;
tracks = new();
}
public IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int enqueuedAt)
{
lock (_locker)
{
var added = new QueuedTrackInfo(trackInfo, queuer);
enqueuedAt = tracks.Count;
tracks.AddLast(added);
return added;
}
}
public IQueuedTrackInfo EnqueueNext(ITrackInfo trackInfo, string queuer, out int trackIndex)
{
lock (_locker)
{
if (tracks.Count == 0)
return Enqueue(trackInfo, queuer, out trackIndex);
var currentNode = tracks.First!;
int i;
for (i = 1; i <= index; i++)
currentNode = currentNode.Next!; // can't be null because index is always in range of the count
var added = new QueuedTrackInfo(trackInfo, queuer);
trackIndex = i;
tracks.AddAfter(currentNode, added);
return added;
}
}
public void EnqueueMany(IEnumerable<ITrackInfo> toEnqueue, string queuer)
{
lock (_locker)
{
foreach (var track in toEnqueue)
{
var added = new QueuedTrackInfo(track, queuer);
tracks.AddLast(added);
}
}
}
public IReadOnlyCollection<IQueuedTrackInfo> List()
{
lock (_locker)
{
return tracks.ToList();
}
}
public IQueuedTrackInfo? GetCurrent(out int currentIndex)
{
lock (_locker)
{
currentIndex = index;
return tracks.ElementAtOrDefault(index);
}
}
public void Advance()
{
lock (_locker)
{
if (++index >= tracks.Count)
index = 0;
}
}
public void Clear()
{
lock (_locker)
{
tracks.Clear();
}
}
public bool SetIndex(int newIndex)
{
lock (_locker)
{
if (newIndex < 0 || newIndex >= tracks.Count)
return false;
index = newIndex;
return true;
}
}
private void RemoveAtInternal(int remoteAtIndex, out IQueuedTrackInfo trackInfo)
{
var removedNode = tracks.First!;
int i;
for (i = 0; i < remoteAtIndex; i++)
removedNode = removedNode.Next!;
trackInfo = removedNode.Value;
tracks.Remove(removedNode);
if (i <= index)
--index;
if (index < 0)
index = Count;
// if it was the last song in the queue
// // wrap back to start
// if (_index == Count)
// _index = 0;
// else if (i <= _index)
// if (_index == 0)
// _index = Count;
// else --_index;
}
public void RemoveCurrent()
{
lock (_locker)
{
if (index < tracks.Count)
RemoveAtInternal(index, out _);
}
}
public IQueuedTrackInfo? MoveTrack(int from, int to)
{
ArgumentOutOfRangeException.ThrowIfNegative(from);
ArgumentOutOfRangeException.ThrowIfNegative(to);
ArgumentOutOfRangeException.ThrowIfEqual(to, from);
lock (_locker)
{
if (from >= Count || to >= Count)
return null;
// update current track index
if (from == index)
{
// if the song being moved is the current track
// it means that it will for sure end up on the destination
index = to;
}
else
{
// moving a track from below the current track means
// means it will drop down
if (from < index)
index--;
// moving a track to below the current track
// means it will rise up
if (to <= index)
index++;
// if both from and to are below _index - net change is + 1 - 1 = 0
// if from is below and to is above - net change is -1 (as the track is taken and put above)
// if from is above and to is below - net change is 1 (as the track is inserted under)
// if from is above and to is above - net change is 0
}
// get the node which needs to be moved
var fromNode = tracks.First!;
for (var i = 0; i < from; i++)
fromNode = fromNode.Next!;
// remove it from the queue
tracks.Remove(fromNode);
// if it needs to be added as a first node,
// add it directly and return
if (to == 0)
{
tracks.AddFirst(fromNode);
return fromNode.Value;
}
// else find the node at the index before the specified target
var addAfterNode = tracks.First!;
for (var i = 1; i < to; i++)
addAfterNode = addAfterNode.Next!;
// and add after it
tracks.AddAfter(addAfterNode, fromNode);
return fromNode.Value;
}
}
public void Shuffle(Random rng)
{
lock (_locker)
{
var list = tracks.ToArray();
rng.Shuffle(list);
tracks = new(list);
}
}
public bool IsLast()
{
lock (_locker)
{
return index == tracks.Count // if there are no tracks
|| index == tracks.Count - 1;
}
}
public void ReorderFairly()
{
lock (_locker)
{
var groups = new Dictionary<string, int>();
var queuers = new List<Queue<QueuedTrackInfo>>();
foreach (var track in tracks.Skip(index).Concat(tracks.Take(index)))
{
if (!groups.TryGetValue(track.Queuer, out var qIndex))
{
queuers.Add(new Queue<QueuedTrackInfo>());
qIndex = queuers.Count - 1;
groups.Add(track.Queuer, qIndex);
}
queuers[qIndex].Enqueue(track);
}
tracks = new LinkedList<QueuedTrackInfo>();
index = 0;
while (true)
{
for (var i = 0; i < queuers.Count; i++)
{
var queue = queuers[i];
tracks.AddLast(queue.Dequeue());
if (queue.Count == 0)
{
queuers.RemoveAt(i);
i--;
}
}
if (queuers.Count == 0)
break;
}
}
}
public bool TryRemoveAt(int remoteAt, out IQueuedTrackInfo? trackInfo, out bool isCurrent)
{
lock (_locker)
{
isCurrent = false;
trackInfo = null;
if (remoteAt < 0 || remoteAt >= tracks.Count)
return false;
if (remoteAt == index)
isCurrent = true;
RemoveAtInternal(remoteAt, out trackInfo);
return true;
}
}
}