-
Notifications
You must be signed in to change notification settings - Fork 55
Add configurable playlists to console app #390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0bfcd64
9779eb5
4a48667
39c3b99
a1313e1
9da27c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,9 @@ | ||||||||||||||||||
| using Sharpcaster.Models.Media; | ||||||||||||||||||
| using Sharpcaster.Models.Queue; | ||||||||||||||||||
| using Spectre.Console; | ||||||||||||||||||
| using SharpCaster.Console.Models; | ||||||||||||||||||
| using SharpCaster.Console.Services; | ||||||||||||||||||
| using SharpCaster.Console.UI; | ||||||||||||||||||
| using Spectre.Console; | ||||||||||||||||||
|
|
||||||||||||||||||
| namespace SharpCaster.Console.Controllers; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -12,12 +12,69 @@ public class QueueController | |||||||||||||||||
| private readonly ApplicationState _state; | ||||||||||||||||||
| private readonly DeviceService _deviceService; | ||||||||||||||||||
| private readonly UIHelper _ui; | ||||||||||||||||||
| private readonly PlaylistService _playlistService; | ||||||||||||||||||
|
|
||||||||||||||||||
| public QueueController(ApplicationState state, DeviceService deviceService, UIHelper ui) | ||||||||||||||||||
| public QueueController(ApplicationState state, DeviceService deviceService, UIHelper ui, PlaylistService ps) | ||||||||||||||||||
| { | ||||||||||||||||||
| _state = state; | ||||||||||||||||||
| _deviceService = deviceService; | ||||||||||||||||||
| _ui = ui; | ||||||||||||||||||
| _playlistService = ps; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| public async Task CastPlaylistAsync() | ||||||||||||||||||
| { | ||||||||||||||||||
| if (!await _deviceService.EnsureConnectedAsync()) | ||||||||||||||||||
| return; | ||||||||||||||||||
|
|
||||||||||||||||||
| List<string> urlOptions = [.. _playlistService.Playlists.Select(p => p.Name), "Back"]; | ||||||||||||||||||
|
|
||||||||||||||||||
| var urlChoice = AnsiConsole.Prompt( | ||||||||||||||||||
| new SelectionPrompt<string>() | ||||||||||||||||||
| .Title("[yellow]Select playlist to cast:[/]") | ||||||||||||||||||
| .AddChoices(urlOptions) | ||||||||||||||||||
| .UseConverter(choice => choice switch | ||||||||||||||||||
| { | ||||||||||||||||||
| "Back" => "🔙 Back", | ||||||||||||||||||
| _ => choice | ||||||||||||||||||
| })); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (urlChoice == "Back") | ||||||||||||||||||
| { | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| try | ||||||||||||||||||
| { | ||||||||||||||||||
| await AnsiConsole.Status() | ||||||||||||||||||
| .Spinner(Spinner.Known.Star2) | ||||||||||||||||||
| .SpinnerStyle(Style.Parse("yellow")) | ||||||||||||||||||
| .StartAsync("Loading playlist", async ctx => | ||||||||||||||||||
| { | ||||||||||||||||||
| ctx.Status("Loading queue..."); | ||||||||||||||||||
| var status = await _state.Client.MediaChannel.QueueLoadAsync(_playlistService.Playlists.First(p => p.Name == urlChoice).QueueItems); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (status == null) | ||||||||||||||||||
| throw new Exception("Failed to load playlist - no status returned"); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| _ui.AddSeparator(); | ||||||||||||||||||
| AnsiConsole.MarkupLine("[green]✅ Playlist loaded and playing successfully![/]"); | ||||||||||||||||||
| _ui.AddSeparator("📝 Queue Management"); | ||||||||||||||||||
| await ShowQueueManagementAsync(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+61
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove internal call to ShowQueueManagementAsync to prevent nested loops. Apply this diff: - _ui.AddSeparator();
- AnsiConsole.MarkupLine("[green]✅ Playlist loaded and playing successfully![/]");
- _ui.AddSeparator("📝 Queue Management");
- await ShowQueueManagementAsync();
+ _ui.AddSeparator();
+ AnsiConsole.MarkupLine("[green]✅ Playlist loaded and playing successfully![/]");📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| catch (Exception ex) | ||||||||||||||||||
| { | ||||||||||||||||||
| _ui.AddSeparator("❌ Casting Error"); | ||||||||||||||||||
| AnsiConsole.MarkupLine($"[red]❌ Casting failed: {ex.Message}[/]"); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (ex.Message.Contains("timeout") || ex.Message.Contains("connection")) | ||||||||||||||||||
| { | ||||||||||||||||||
| _state.IsConnected = false; | ||||||||||||||||||
| AnsiConsole.MarkupLine("[yellow]⚠️ Connection may have been lost. Try reconnecting.[/]"); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| public async Task ShowQueueManagementAsync() | ||||||||||||||||||
|
|
@@ -30,6 +87,7 @@ public async Task ShowQueueManagementAsync() | |||||||||||||||||
| var choices = new[] | ||||||||||||||||||
| { | ||||||||||||||||||
| "Load queue from URLs", | ||||||||||||||||||
| "Load queue from playlist", | ||||||||||||||||||
| "Next track", | ||||||||||||||||||
| "Previous track", | ||||||||||||||||||
| "Toggle shuffle", | ||||||||||||||||||
|
|
@@ -45,6 +103,7 @@ public async Task ShowQueueManagementAsync() | |||||||||||||||||
| .UseConverter(choice => choice switch | ||||||||||||||||||
| { | ||||||||||||||||||
| "Load queue from URLs" => "📝 Load queue from URLs", | ||||||||||||||||||
| "Load queue from playlist" => "💿 Load queue from playlist", | ||||||||||||||||||
| "Next track" => "⏭️ Next track", | ||||||||||||||||||
| "Previous track" => "⏮️ Previous track", | ||||||||||||||||||
| "Toggle shuffle" => "🔀 Toggle shuffle", | ||||||||||||||||||
|
|
@@ -64,13 +123,26 @@ public async Task ShowQueueManagementAsync() | |||||||||||||||||
| await LoadQueueAsync(mediaChannel); | ||||||||||||||||||
| break; | ||||||||||||||||||
|
|
||||||||||||||||||
| case "Load queue from playlist": | ||||||||||||||||||
| await CastPlaylistAsync(); | ||||||||||||||||||
| break; | ||||||||||||||||||
|
|
||||||||||||||||||
| case "Next track": | ||||||||||||||||||
| await AnsiConsole.Status().StartAsync("Skipping to next track...", async ctx => | ||||||||||||||||||
| var ids = await mediaChannel.QueueGetItemIdsAsync(); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (ids?.LastOrDefault() == mediaChannel.MediaStatus?.CurrentItemId) | ||||||||||||||||||
| { | ||||||||||||||||||
| await mediaChannel.QueueNextAsync(); | ||||||||||||||||||
| }); | ||||||||||||||||||
| AnsiConsole.MarkupLine("[green]⏭️ Skipped to next track[/]"); | ||||||||||||||||||
| _ui.AddSeparator(); | ||||||||||||||||||
| AnsiConsole.MarkupLine("[yellow]⚠️ Already at the last track in the queue. Cannot skip to next track.[/]"); | ||||||||||||||||||
| } | ||||||||||||||||||
| else | ||||||||||||||||||
| { | ||||||||||||||||||
| await AnsiConsole.Status().StartAsync("Skipping to next track...", async ctx => | ||||||||||||||||||
| { | ||||||||||||||||||
| await mediaChannel.QueueNextAsync(); | ||||||||||||||||||
| }); | ||||||||||||||||||
| AnsiConsole.MarkupLine("[green]⏭️ Skipped to next track[/]"); | ||||||||||||||||||
| _ui.AddSeparator(); | ||||||||||||||||||
| } | ||||||||||||||||||
| break; | ||||||||||||||||||
|
|
||||||||||||||||||
| case "Previous track": | ||||||||||||||||||
|
|
@@ -98,17 +170,29 @@ await AnsiConsole.Status().StartAsync($"{(shuffle ? "Enabling" : "Disabling")} s | |||||||||||||||||
|
|
||||||||||||||||||
| case "Get queue items": | ||||||||||||||||||
| var itemIds = await mediaChannel.QueueGetItemIdsAsync(); | ||||||||||||||||||
| if (itemIds?.Any() == true) | ||||||||||||||||||
| var items = await mediaChannel.QueueGetItemsAsync(itemIds); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (items?.Any() == true) | ||||||||||||||||||
| { | ||||||||||||||||||
| var queueTable = new Table(); | ||||||||||||||||||
| queueTable.AddColumn("[blue]Item ID[/]"); | ||||||||||||||||||
| queueTable.AddColumn("[blue]MediaId[/]"); | ||||||||||||||||||
| queueTable.AddColumn("[blue]Url[/]"); | ||||||||||||||||||
| queueTable.AddColumn("[blue]Title[/]"); | ||||||||||||||||||
|
|
||||||||||||||||||
| foreach (var id in itemIds) | ||||||||||||||||||
| foreach (var item in items) | ||||||||||||||||||
| { | ||||||||||||||||||
| queueTable.AddRow($"[white]{id}[/]"); | ||||||||||||||||||
| string col = "[white]"; | ||||||||||||||||||
| if (item.ItemId == mediaChannel.MediaStatus?.CurrentItemId) | ||||||||||||||||||
| { | ||||||||||||||||||
| col = "[blue]"; | ||||||||||||||||||
| } | ||||||||||||||||||
| queueTable.AddRow($"{col}{item.ItemId}[/]", | ||||||||||||||||||
| $"{col}{item?.Media.ContentId}[/]", | ||||||||||||||||||
| $"{col}{item?.Media.ContentUrl ?? ""}[/]", | ||||||||||||||||||
| $"{col}{item?.Media.Metadata?.Title ?? ""}[/]"); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| AnsiConsole.MarkupLine($"[green]📋 Queue contains {itemIds.Length} items:[/]"); | ||||||||||||||||||
| AnsiConsole.MarkupLine($"[green]📋 Queue contains {items.Length} items:[/]"); | ||||||||||||||||||
| AnsiConsole.Write(queueTable); | ||||||||||||||||||
| } | ||||||||||||||||||
| else | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,13 @@ | ||
| using SharpCaster.Console.UI; | ||
| using System.Text.Json.Serialization; | ||
|
|
||
| namespace SharpCaster.Console.Models; | ||
|
|
||
| [JsonSerializable(typeof(CommandLineArgs))] | ||
| [JsonSerializable(typeof(ApplicationState))] | ||
| [JsonSerializable(typeof(Playlist))] | ||
| [JsonSerializable(typeof(Playlist[]))] | ||
| [JsonSerializable(typeof(UserSettingsModel))] | ||
| public partial class ConsoleJsonContext : JsonSerializerContext | ||
| { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| using Sharpcaster.Models.Queue; | ||
| using System.Text.Json.Serialization; | ||
| namespace SharpCaster.Console.Models; | ||
|
|
||
| public class Playlist | ||
| { | ||
| public required string Name { get; set; } | ||
| [JsonPropertyName("Content")] | ||
| public QueueItem[] QueueItems { get; set; } = []; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Text; | ||
|
|
||
| namespace SharpCaster.Console.Models | ||
| { | ||
| public class UserSettingsModel | ||
| { | ||
| public Playlist[] Playlists { get; set; } = []; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
First()method will throw an exception if no playlist with the specified name is found. UseFirstOrDefault()and add null checking to prevent runtime exceptions.