Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion SharpCaster.Console/Controllers/MediaController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public async Task CastMediaAsync()
{
"Sample Video (Designing for Google Cast)",
"Sample Audio (Arcane - Kevin MacLeod)",
"Custom URL"
"Custom URL",
"Back"
};

var urlChoice = AnsiConsole.Prompt(
Expand All @@ -44,6 +45,7 @@ public async Task CastMediaAsync()
"Sample Video (Designing for Google Cast)" => "🎬 Sample Video (Designing for Google Cast)",
"Sample Audio (Arcane - Kevin MacLeod)" => "🎵 Sample Audio (Arcane - Kevin MacLeod)",
"Custom URL" => "🔗 Custom URL",
"Back" => "🔙 Back",
_ => choice
}));

Expand Down Expand Up @@ -72,6 +74,8 @@ public async Task CastMediaAsync()
.AllowEmpty()
.DefaultValue("Custom Media"));
break;
case "Back":
return;
default:
throw new InvalidOperationException("Invalid URL choice");
}
Expand Down
108 changes: 96 additions & 12 deletions SharpCaster.Console/Controllers/QueueController.cs
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;

Expand All @@ -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);
Copy link

Copilot AI Sep 20, 2025

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. Use FirstOrDefault() and add null checking to prevent runtime exceptions.

Suggested change
var status = await _state.Client.MediaChannel.QueueLoadAsync(_playlistService.Playlists.First(p => p.Name == urlChoice).QueueItems);
var playlist = _playlistService.Playlists.FirstOrDefault(p => p.Name == urlChoice);
if (playlist == null)
throw new Exception($"Playlist '{urlChoice}' not found.");
var status = await _state.Client.MediaChannel.QueueLoadAsync(playlist.QueueItems);

Copilot uses AI. Check for mistakes.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove internal call to ShowQueueManagementAsync to prevent nested loops.
CastPlaylistAsync should just load; let callers decide what to show next.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_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![/]");
}
🤖 Prompt for AI Agents
In SharpCaster.Console/Controllers/QueueController.cs around lines 61 to 65, the
CastPlaylistAsync method currently calls await ShowQueueManagementAsync() which
can create nested interactive loops; remove the internal call so
CastPlaylistAsync only loads and reports success and let callers decide whether
to invoke ShowQueueManagementAsync. Edit the method to delete the await
ShowQueueManagementAsync(); (and any immediate surrounding blank line if
desired) so the method ends after the success markup/separator, leaving UI
separators and success message intact.

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()
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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":
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion SharpCaster.Console/Flows/ApplicationFlows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ public async Task MainApplicationFlowAsync()
var choices = new[]
{
"Cast media",
"Cast playlist",
"Website display",
"Media controls",
"Stop application",
Expand All @@ -207,11 +208,12 @@ public async Task MainApplicationFlowAsync()
var choice = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title($"[yellow]What would you like to do with {_state.SelectedDevice?.Name}?[/]")
.PageSize(10)
.PageSize(11)
.AddChoices(choices)
.UseConverter(choice => choice switch
{
"Cast media" => "📺 Cast media",
"Cast playlist" => "💿 Cast playlist",
"Website display" => "🌐 Website display",
"Media controls" => "🎮 Media controls",
"Stop application" => "⏹️ Stop application",
Expand All @@ -232,6 +234,11 @@ public async Task MainApplicationFlowAsync()
_ui.AddSeparator("🎬 Casting Media");
await _mediaController.CastMediaAsync();
break;
case "Cast playlist":
_ui.AddSeparator("Casting Playlist");
await _queueController.CastPlaylistAsync();
break;

case "Website display":
_ui.AddSeparator("🌐 Website Display");
await _mediaController.CastWebsiteAsync();
Expand Down
14 changes: 13 additions & 1 deletion SharpCaster.Console/Models/CommandLineArgs.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using SharpCaster.Console.Services;

namespace SharpCaster.Console.Models;

public class CommandLineArgs
Expand All @@ -14,11 +16,13 @@ public class CommandLineArgs
public bool ShowVersion { get; set; }
public bool ShowLogs { get; set; }
public bool IsInteractive => string.IsNullOrEmpty(Command);

public string? PlaylistId { get; set; }
}

public static class CommandLineParser
{
public static CommandLineArgs Parse(string[] args)
public static CommandLineArgs Parse(string[] args, PlaylistService playlistService)
{
var result = new CommandLineArgs();

Expand Down Expand Up @@ -120,6 +124,14 @@ public static CommandLineArgs Parse(string[] args)
result.Command = "play"; // Default to play when URL is provided
}
}
else if (playlistService.IsPlaylistId(arg))
{
result.PlaylistId = args[i];
if (string.IsNullOrEmpty(result.Command))
{
result.Command = "play"; // Default to play when playlist is provided
}
}
break;
}
}
Expand Down
4 changes: 4 additions & 0 deletions SharpCaster.Console/Models/ConsoleJsonContext.cs
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
{
}
10 changes: 10 additions & 0 deletions SharpCaster.Console/Models/Playlist.cs
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; } = [];
}
12 changes: 12 additions & 0 deletions SharpCaster.Console/Models/UserSettingsModel.cs
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; } = [];
}
}
10 changes: 7 additions & 3 deletions SharpCaster.Console/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Sharpcaster;
Expand All @@ -18,15 +19,16 @@ static async Task<int> Main(string[] args)
System.Console.OutputEncoding = Encoding.UTF8;
System.Console.InputEncoding = Encoding.UTF8;

// Parse command line arguments
var commandLineArgs = CommandLineParser.Parse(args);

// Setup dependency injection
var services = new ServiceCollection();
ConfigureServices(services);

var serviceProvider = services.BuildServiceProvider();

// Parse command line arguments
var commandLineArgs = CommandLineParser.Parse(args, serviceProvider.GetRequiredService<PlaylistService>());


// Check if this is command-line mode or interactive mode
if (!commandLineArgs.IsInteractive || commandLineArgs.ShowHelp || commandLineArgs.ShowDevices || commandLineArgs.ShowVersion)
{
Expand Down Expand Up @@ -69,6 +71,8 @@ private static void ConfigureServices(IServiceCollection services)
// Register services
services.AddSingleton<DeviceService>();
services.AddSingleton<CommandExecutor>();
services.AddSingleton<PlaylistService>();


// Register controllers
services.AddSingleton<MediaController>();
Expand Down
Loading
Loading