Skip to content

Commit

Permalink
Persistent queue & player resuming on restart
Browse files Browse the repository at this point in the history
  • Loading branch information
Azorant committed Dec 11, 2023
1 parent e94b9f2 commit d23767e
Show file tree
Hide file tree
Showing 17 changed files with 456 additions and 150 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ services:
- TOKEN=bot token
- GUILD_CHANNEL=channel ID for guild events
lavalink:
image: ghcr.io/lavalink-devs/lavalink:4.0.0-beta.4-alpine
image: ghcr.io/lavalink-devs/lavalink:4
container_name: lavalink
restart: unless-stopped
environment:
Expand Down
12 changes: 3 additions & 9 deletions Rhea/Models/EnrichedTrack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@

namespace Rhea.Models;

public class EnrichedTrack : ITrackQueueItem
public class EnrichedTrack(LavalinkTrack track, string requester) : ITrackQueueItem
{
public TrackReference Reference { get; }
public string Requester { get; } = requester;
public TrackReference Reference { get; } = new TrackReference(track);
public LavalinkTrack Track => Reference.Track!;
public string Requester { get; }

public EnrichedTrack(LavalinkTrack track, string requester)
{
Reference = new TrackReference(track);
Requester = requester;
}
}
8 changes: 8 additions & 0 deletions Rhea/Models/PlayingTrack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Rhea.Models;

public class PlayingTrack(EnrichedTrack track, ulong channelID, TimeSpan position = default)
{
public EnrichedTrack track { get; set; } = track;
public TimeSpan position { get; set; } = position;
public ulong channelID { get; set; } = channelID;
}
4 changes: 1 addition & 3 deletions Rhea/Models/SMPacket.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Diagnostics.CodeAnalysis;

namespace Rhea.Models;
namespace Rhea.Models;

public class SMPacket
{
Expand Down
162 changes: 162 additions & 0 deletions Rhea/Models/TrackQueue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System.Collections;
using Lavalink4NET.Players.Queued;
using Rhea.Services;

namespace Rhea.Models;

public class TrackQueue(RedisService redis, ulong ID) : ITrackQueue
{
public IEnumerator<ITrackQueueItem> GetEnumerator() => redis.GetQueue(ID).GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

public int Count => redis.GetQueue(ID).Count;

public ITrackQueueItem this[int index] => redis.GetQueue(ID)[index];

public bool Contains(ITrackQueueItem item) => redis.GetQueue(ID).Contains(item);

public int IndexOf(ITrackQueueItem item) => redis.GetQueue(ID).IndexOf((EnrichedTrack)item);

public int IndexOf(Func<ITrackQueueItem, bool> predicate)
{
var result = redis.GetQueue(ID);

for (var index = 0; index < result.Count; index++)
{
if (predicate(result[index]))
{
return index;
}
}

return -1;
}

public async ValueTask<bool> RemoveAtAsync(int index, CancellationToken cancellationToken = new CancellationToken())
{
var result = await redis.GetQueueAsync(ID);
if (index < 0 || index >= result.Count) return false;

result.RemoveAt(index);
await redis.SetQueueAsync(ID, result);
return true;
}

public async ValueTask<bool> RemoveAsync(ITrackQueueItem item,
CancellationToken cancellationToken = new CancellationToken())
{
var result = await redis.GetQueueAsync(ID);
var removed = result.Remove((EnrichedTrack)item);
await redis.SetQueueAsync(ID, result);
return removed;
}

public async ValueTask<int> RemoveAllAsync(Predicate<ITrackQueueItem> predicate,
CancellationToken cancellationToken = new CancellationToken())
{
var result = await redis.GetQueueAsync(ID);
var previousCount = result.Count;
result.RemoveAll(predicate);
await redis.SetQueueAsync(ID, result);
return previousCount - result.Count;
}

public async ValueTask RemoveRangeAsync(int index, int count,
CancellationToken cancellationToken = new CancellationToken())
{
var result = await redis.GetQueueAsync(ID);
result.RemoveRange(index, count);
await redis.SetQueueAsync(ID, result);
}

public async ValueTask<int> DistinctAsync(IEqualityComparer<ITrackQueueItem>? equalityComparer = null,
CancellationToken cancellationToken = new CancellationToken())
=> throw new NotImplementedException();

public async ValueTask<int> AddAsync(ITrackQueueItem item,
CancellationToken cancellationToken = new CancellationToken())
{
var result = await redis.GetQueueAsync(ID);
result.Add((EnrichedTrack)item);
await redis.SetQueueAsync(ID, result);
return result.Count;
}

public async ValueTask<int> AddRangeAsync(IReadOnlyList<ITrackQueueItem> items,
CancellationToken cancellationToken = new CancellationToken())
{
var result = await redis.GetQueueAsync(ID);
result.AddRange((List<EnrichedTrack>)items);
await redis.SetQueueAsync(ID, result);
return result.Count;
}

public async ValueTask<int> ClearAsync(CancellationToken cancellationToken = new CancellationToken())
{
await redis.ClearQueueAsync(ID);
return 0;
}

public bool IsEmpty => Count is 0;

public async ValueTask InsertAsync(int index, ITrackQueueItem item,
CancellationToken cancellationToken = new CancellationToken())
{
var result = await redis.GetQueueAsync(ID);
result.Insert(index, (EnrichedTrack)item);
await redis.SetQueueAsync(ID, result);
}

public async ValueTask InsertRangeAsync(int index, IEnumerable<ITrackQueueItem> items,
CancellationToken cancellationToken = new CancellationToken())
{
var result = await redis.GetQueueAsync(ID);
result.InsertRange(index, (List<EnrichedTrack>)items);
await redis.SetQueueAsync(ID, result);
}

public async ValueTask ShuffleAsync(CancellationToken cancellationToken = new CancellationToken())
{
var result = await redis.GetQueueAsync(ID);
for (var index = 0; index < result.Count; index++)
{
var targetIndex = index + Random.Shared.Next(result.Count - index);
(result[index], result[targetIndex]) = (result[targetIndex], result[index]);
}

await redis.SetQueueAsync(ID, result);
}

public ITrackQueueItem? Peek()
{
var result = redis.GetQueue(ID);
return result.FirstOrDefault();
}

public bool TryPeek(out ITrackQueueItem? queueItem)
{
queueItem = Peek();
return queueItem is not null;
}

public async ValueTask<ITrackQueueItem?> TryDequeueAsync(TrackDequeueMode dequeueMode = TrackDequeueMode.Normal,
CancellationToken cancellationToken = new CancellationToken())
{
var result = await redis.GetQueueAsync(ID);
if (!result.Any()) return null;

var index = dequeueMode is TrackDequeueMode.Shuffle
? Random.Shared.Next(0, result.Count)
: 0;

var track = result[index];
result.RemoveAt(index);
await redis.SetQueueAsync(ID, result);

return track;
}

public ITrackHistory? History => null;
public bool HasHistory => false;
}
30 changes: 15 additions & 15 deletions Rhea/Modules/BaseModule.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
using Discord;
using Discord.Interactions;
using Discord.Interactions;
using Discord.WebSocket;
using Lavalink4NET;
using Lavalink4NET.DiscordNet;
using Lavalink4NET.Players;
using Lavalink4NET.Players.Vote;
using Rhea.Models;
using Rhea.Services;

namespace Rhea.Modules;

public class BaseModule : InteractionModuleBase<SocketInteractionContext>
public class BaseModule(IAudioService lavalink, RedisService redis) : InteractionModuleBase<SocketInteractionContext>
{
private readonly IAudioService lavalink;

protected BaseModule(IAudioService lavalink)
{
this.lavalink = lavalink;
}

protected async ValueTask<VoteLavalinkPlayer?> GetPlayer(PlayerChannelBehavior joinBehavior = PlayerChannelBehavior.Join)
protected async ValueTask<VoteLavalinkPlayer?> GetPlayer(
PlayerChannelBehavior joinBehavior = PlayerChannelBehavior.Join)
{
var member = Context.Guild.GetUser(Context.User.Id);
var permissions = Context.Guild.CurrentUser.GetPermissions(member.VoiceChannel);
if (!permissions.Connect)
{
throw new Exception($"Unable to connect to {member.VoiceChannel.Mention}");
}

var result = await lavalink.Players.RetrieveAsync(Context, playerFactory: PlayerFactory.Vote, new PlayerRetrieveOptions(joinBehavior));
if (!result.IsSuccess && result.Status != PlayerRetrieveStatus.BotNotConnected) throw new Exception($"Unable to retrieve player: {result.Status}");

var result = await lavalink.Players.RetrieveAsync(Context, PlayerFactory.Vote, new VoteLavalinkPlayerOptions
{
TrackQueue = new TrackQueue(redis, Context.Guild.Id)
},
new PlayerRetrieveOptions(joinBehavior));
if (!result.IsSuccess && result.Status != PlayerRetrieveStatus.BotNotConnected)
throw new Exception($"Unable to retrieve player: {result.Status}");
return result.Player;
}

protected string FormatTime(TimeSpan time)
=> time.ToString(@"hh\:mm\:ss").TrimStart('0', ':');

protected bool IsPrivileged(SocketGuildUser Member)
=> Member.GetPermissions(Member.VoiceChannel).MoveMembers || Member.Roles.FirstOrDefault(role => role.Name.ToLower() == "dj") != null ||
=> Member.GetPermissions(Member.VoiceChannel).MoveMembers ||
Member.Roles.FirstOrDefault(role => role.Name.ToLower() == "dj") != null ||
!Member.VoiceChannel.ConnectedUsers.Any(user => !user.IsBot && user.Id != Member.Id);
}
9 changes: 4 additions & 5 deletions Rhea/Modules/ControlsModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
using Lavalink4NET.Players;
using Lavalink4NET.Players.Queued;
using Lavalink4NET.Players.Vote;
using Rhea.Services;

namespace Rhea.Modules;

public class ControlsModule : BaseModule
public class ControlsModule(IAudioService lavalink, RedisService redis) : BaseModule(lavalink, redis)
{
public ControlsModule(IAudioService lavalink) : base(lavalink) { }

[SlashCommand("resume", "Resume playing")]
public async Task ResumeCommand()
{
Expand Down Expand Up @@ -209,11 +208,11 @@ public async Task SeekCommand([Summary(description: "The timestamp to seek to")]
}

var ts = TimeSpan.Parse(string.Join(':', parsedTimestamp));

await player.SeekAsync(ts);
await RespondAsync($"**Seeked to** `{FormatTime(ts)}`");
}

// TODO: Investigate why queue loop isn't working as intended
[SlashCommand("loop", "Set whether or not the queue should loop")]
public async Task LoopCommand(
Expand Down
Loading

0 comments on commit d23767e

Please sign in to comment.