From 82a310cf58b59c82ee03682b319f7881b885ec9e Mon Sep 17 00:00:00 2001 From: Daniel Harvey Date: Mon, 25 May 2026 11:39:41 -0400 Subject: [PATCH] remove DeepDrftCli project and CLI.sln; clean orphan GUID from DeepDrftHome.sln --- CLI.sln | 40 -- DeepDrftCli/CLAUDE.md | 225 ------- DeepDrftCli/DeepDrftCli.csproj | 48 -- DeepDrftCli/Models/CliSettings.cs | 8 - DeepDrftCli/Program.cs | 78 --- DeepDrftCli/Services/CliService.cs | 433 ------------- DeepDrftCli/Services/GuiService.cs | 898 --------------------------- DeepDrftCli/Utils/CliUtils.cs | 16 - DeepDrftCli/connections.example.json | 6 - DeepDrftHome.sln | 12 - 10 files changed, 1764 deletions(-) delete mode 100644 CLI.sln delete mode 100644 DeepDrftCli/CLAUDE.md delete mode 100644 DeepDrftCli/DeepDrftCli.csproj delete mode 100644 DeepDrftCli/Models/CliSettings.cs delete mode 100644 DeepDrftCli/Program.cs delete mode 100644 DeepDrftCli/Services/CliService.cs delete mode 100644 DeepDrftCli/Services/GuiService.cs delete mode 100644 DeepDrftCli/Utils/CliUtils.cs delete mode 100644 DeepDrftCli/connections.example.json diff --git a/CLI.sln b/CLI.sln deleted file mode 100644 index 0e074c4..0000000 --- a/CLI.sln +++ /dev/null @@ -1,40 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftCli", "DeepDrftCli\DeepDrftCli.csproj", "{84844B37-FD15-4AFC-850B-DD432AA33B4C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Data", "DeepDrftContent.Data\DeepDrftContent.Data.csproj", "{169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftData", "DeepDrftData\DeepDrftData.csproj", "{A3DA341B-589E-4705-AB66-6B22652A9B36}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetBlocks", "C:\lib\NetBlocks\NetBlocks.csproj", "{41FC69D0-F60D-41B4-AA41-C2382C83DFE8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftModels", "DeepDrftModels\DeepDrftModels.csproj", "{AEA0B3A0-722E-4D34-B2F6-F8179A4DD45A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {84844B37-FD15-4AFC-850B-DD432AA33B4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84844B37-FD15-4AFC-850B-DD432AA33B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84844B37-FD15-4AFC-850B-DD432AA33B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84844B37-FD15-4AFC-850B-DD432AA33B4C}.Release|Any CPU.Build.0 = Release|Any CPU - {169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}.Release|Any CPU.Build.0 = Release|Any CPU - {A3DA341B-589E-4705-AB66-6B22652A9B36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A3DA341B-589E-4705-AB66-6B22652A9B36}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A3DA341B-589E-4705-AB66-6B22652A9B36}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A3DA341B-589E-4705-AB66-6B22652A9B36}.Release|Any CPU.Build.0 = Release|Any CPU - {41FC69D0-F60D-41B4-AA41-C2382C83DFE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {41FC69D0-F60D-41B4-AA41-C2382C83DFE8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {41FC69D0-F60D-41B4-AA41-C2382C83DFE8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {41FC69D0-F60D-41B4-AA41-C2382C83DFE8}.Release|Any CPU.Build.0 = Release|Any CPU - {AEA0B3A0-722E-4D34-B2F6-F8179A4DD45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AEA0B3A0-722E-4D34-B2F6-F8179A4DD45A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AEA0B3A0-722E-4D34-B2F6-F8179A4DD45A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AEA0B3A0-722E-4D34-B2F6-F8179A4DD45A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/DeepDrftCli/CLAUDE.md b/DeepDrftCli/CLAUDE.md deleted file mode 100644 index dcfae17..0000000 --- a/DeepDrftCli/CLAUDE.md +++ /dev/null @@ -1,225 +0,0 @@ -# CLAUDE.md - DeepDrftCli - -Guidance for working in the DeepDrftCli project (the admin CLI tool). - -See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project. - -## One-line purpose - -Local admin tool. Adds and lists tracks. Two modes: classic CLI (`add` / `list` / `help`) and Terminal.Gui interactive interface (`gui`). Has direct access to both the SQL DB and the FileDatabase (it's not a network client — it runs on the same machine as the databases). - -## Why this is allowed to bypass the API - -CLI is a **local single-user admin tool**, run on the same machine as the databases by an admin. It consumes `DeepDrftWeb.Services` and `DeepDrftContent.Services` directly without going through HTTP. Browsers never get to bypass — the APIs are for them. **Don't extend this pattern to network clients.** - -## Layout - -``` -DeepDrftCli/ -├── Services/ -│ ├── CliService.cs # Main CLI command dispatcher -│ └── GuiService.cs # Terminal.Gui interface -├── Models/ -│ └── CliSettings.cs # Config POCO (ConnectionString, VaultPath) -├── Program.cs # Entry point, DI setup -├── environment/ -│ ├── connections.json # The actual config file (not appsettings.json) -│ └── config.json # Placeholder, currently unused -└── DeepDrftCli.csproj -``` - -## Configuration - -`CliSettings` (loaded from `environment/connections.json`): - -```csharp -public class CliSettings -{ - public required string ConnectionString { get; set; } // SQLite path - public required string VaultPath { get; set; } // FileDatabase root -} -``` - -**The config file is `environment/connections.json`, not `appsettings.json`.** Example: - -```json -{ - "CliSettings": { - "ConnectionString": "Data Source=../Database/deepdrft.db", - "VaultPath": "../Database/Vaults" - } -} -``` - -Paths are resolved relative to `AppDomain.CurrentDomain.BaseDirectory`. - -## DI wiring (Program.cs) - -Registers: -- `DeepDrftContext` with SQLite (from connection string) -- `FileDatabase` singleton (awaited on init via `FileDatabase.FromAsync`) -- `TrackRepository`, `DeepDrftWeb.Services.TrackService` (SQL side) -- `AudioProcessor`, `DeepDrftContent.Services.TrackService` (content side) -- `CliService`, `GuiService` -- Console logging - -All services are scoped or singletons. The app waits for `FileDatabase.FromAsync` on startup (blocking), so the vault is ready before any command runs. - -## Mode dispatch - -`Program.cs` checks the first argument: -- `gui` or `--gui`: runs `GuiService.RunAsync()` → Terminal.Gui interactive interface. -- Anything else: runs `CliService.RunAsync(args)` → classic CLI command parsing. - -## CLI commands (classic mode) - -### add [album] [genre] [release-date] - -Positional arguments. Adds a track: - -```bash -DeepDrftCli add "/path/to/song.wav" "My Song" "Artist Name" "Album Title" "Rock" "2024-01-15" -``` - -Release date format: `YYYY-MM-DD`. Optional: album, genre, release date. - -### add -i|--interactive [defaults...] - -Interactive mode. Prompts for each field; command-line args become defaults: - -```bash -DeepDrftCli add "/path/to/song.wav" -i "Artist Name" -``` - -Prompts: "Track Name? [default] → ", "Artist? [default] → ", etc. - -### list - -Formats and displays all tracks from SQL: - -```bash -DeepDrftCli list -``` - -Table output: ID, Name, Artist, Album, Genre, ReleaseDate, EntryKey, ImagePath. - -### help, --help, -h - -Shows command list. - -### gui (or --gui) - -Launches the Terminal.Gui interactive interface (separate mode). - -## GUI mode (Terminal.Gui) - -Launches a full interactive terminal UI with: -- **DeepDrft brand color scheme** (Magenta/Purple/Pink). -- **Track list** (scrollable, selectable, deletable). -- **Status pane** showing feedback and errors. -- **Persistent hotkey legend** (F1=Help, A=Add, D=Delete, Q=Quit, etc.). -- **Add dialog** with file browser and track metadata entry. -- **Keyboard navigation**: arrow keys to navigate, Enter to select, escape to cancel. - -`GuiService.RunAsync()` creates a `Window`, populates controls, and runs the main loop. - -## Dual-database add flow (critical contract) - -This is the seam where both databases are written — **must be idempotent and crash-safe**: - -1. `DeepDrftContent.Services.TrackService.AddTrackFromWavAsync(filePath)`: - - Validates file is `.wav` and readable. - - Calls `AudioProcessor.ProcessWavFileAsync(filePath)` → `AudioBinary` (duration, bitrate extracted). - - Generates a GUID entry key. - - Ensures the `tracks` vault exists. - - Calls `FileDatabase.RegisterResourceAsync("tracks", entryKey, audioBinary)`. - - Returns a populated `TrackEntity` (with `Id = 0` since not yet in SQL). - -2. `DeepDrftWeb.Services.TrackService.Create(trackEntity)`: - - Saves the entity to SQLite. - - Returns the persisted entity with `Id` assigned. - -**If step 1 succeeds and step 2 fails, the audio is orphaned in the vault.** There is no compensating rollback today. The CLI logs both results and exits with status code indicating overall success/failure. - -## File validation - -- **Extension**: must be `.wav` (only WAV files are supported). -- **Existence**: checked before processing. -- **Readability**: attempted during processing. -- **WAV structure**: validated by `AudioProcessor` (RIFF/WAVE/PCM header parsing). - -Non-WAV files are rejected with a clear error message. - -## Release date format - -Only `YYYY-MM-DD` is accepted. Parsing is strict: - -```csharp -DateOnly.ParseExact(releaseDateStr, "yyyy-MM-dd") -``` - -Invalid dates result in an error. Optional fields (if not provided or invalid) are set to `null`. - -## Publishing - -Build configuration in `.csproj`: -- `PublishSingleFile=true`: Produces a single executable. -- `SelfContained` and `IncludeNativeLibrariesForSelfExtract` are **commented out** — publish is framework-dependent (requires .NET runtime installed). - -To publish: - -```bash -dotnet publish DeepDrftCli -c Release -o ./bin/Release/publish -``` - -Result is a single `.exe` (Windows) or binary (Linux) that consumes the host framework. - -## Service registration patterns - -All service calls return `Result` or `ResultContainer`. CLI checks `Success` and `Messages` to display feedback: - -```csharp -var result = await _webTrackService.Create(trackEntity); -if (result.Success && result.Value != null) -{ - Console.WriteLine($"✓ Saved to SQL with ID {result.Value.Id}"); -} -else -{ - Console.WriteLine($"✗ SQL save failed: {string.Join("; ", result.Messages.Select(m => m.Message))}"); -} -``` - -## Development commands - -```bash -# Build -dotnet build DeepDrftCli - -# Run classic CLI mode -dotnet run --project DeepDrftCli -- add "test.wav" "Test Track" "Test Artist" -dotnet run --project DeepDrftCli -- list -dotnet run --project DeepDrftCli -- help - -# Run GUI mode -dotnet run --project DeepDrftCli -- gui - -# Build for single-file publish -dotnet publish DeepDrftCli -c Release -o ./bin/Release/publish -``` - -## Important patterns - -- **No HTTP**: CLI talks directly to databases. No HTTP clients needed. -- **Async/await**: All database operations are async. Even CLI mode runs in an async context. -- **Error swallowing in FileDatabase**: Vault operations return `null` / `false`. CLI must check return values. -- **Local-only**: CLI has no authentication or CORS. It runs locally as an admin tool. -- **Rollback**: Adding a track is not transactional across the two databases. If one fails, the other may have partially succeeded (orphaned audio in the vault). Plan accordingly. - -## Configuration - -- `environment/connections.json`: **required** at runtime. Must contain `CliSettings` with `ConnectionString` and `VaultPath`. -- `environment/config.json`: Placeholder, currently unused. Keep it as a placeholder for future expansion. -- No `appsettings.json` — don't add one. - -When working with this project, focus on the dual-database consistency of the add flow, CLI argument parsing, and maintaining the interactive Terminal.Gui mode as a first-class interface (not a second-class citizen). diff --git a/DeepDrftCli/DeepDrftCli.csproj b/DeepDrftCli/DeepDrftCli.csproj deleted file mode 100644 index 9a5a905..0000000 --- a/DeepDrftCli/DeepDrftCli.csproj +++ /dev/null @@ -1,48 +0,0 @@ - - - - Exe - net10.0 - enable - enable - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - - - - true - - - - - - - diff --git a/DeepDrftCli/Models/CliSettings.cs b/DeepDrftCli/Models/CliSettings.cs deleted file mode 100644 index f400a9f..0000000 --- a/DeepDrftCli/Models/CliSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DeepDrftCli.Models -{ - public class CliSettings - { - public string ConnectionString { get; set; } = string.Empty; - public string VaultPath { get; set; } = string.Empty; - } -} \ No newline at end of file diff --git a/DeepDrftCli/Program.cs b/DeepDrftCli/Program.cs deleted file mode 100644 index 244fa1c..0000000 --- a/DeepDrftCli/Program.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Configuration; -using Microsoft.EntityFrameworkCore; -using DeepDrftData; -using DeepDrftData.Data; -using DeepDrftData.Repositories; -using DeepDrftContent.Data.FileDatabase.Services; -using DeepDrftContent.Data.Processors; -using DeepDrftCli.Services; -using DeepDrftCli.Models; -using NetBlocks.Utilities.Environment; - -var builder = Host.CreateApplicationBuilder(args); - -// Load configuration from environment/config.json -var connectionsPath = CredentialTools.ResolvePathOrThrow( - "connections", - Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "environment", "connections.json")); -builder.Configuration.AddJsonFile(connectionsPath, optional: false, reloadOnChange: false); -var cliSettings = builder.Configuration.GetSection(nameof(CliSettings)).Get(); -if (cliSettings is null) { throw new Exception("CLI settings are not configured"); } - -// Add logging -builder.Services.AddLogging(configure => configure.AddConsole()); - -// Add database context -builder.Services.AddDbContext(options => - options.UseNpgsql(cliSettings.ConnectionString)); - -// Add FileDatabase -builder.Services.AddSingleton(provider => -{ - var logger = provider.GetRequiredService>(); - try - { - var fileDatabase = FileDatabase.FromAsync(cliSettings.VaultPath).GetAwaiter().GetResult(); - if (fileDatabase == null) - { - logger.LogError("Failed to initialize FileDatabase"); - throw new InvalidOperationException("FileDatabase initialization failed"); - } - return fileDatabase; - } - catch (Exception ex) - { - logger.LogError(ex, "Error initializing FileDatabase"); - throw; - } -}); - -// Add services. TrackManager fronts the BlazorBlocks data layer and implements -// ITrackService for legacy consumers; same scoped instance backs both registrations. -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(sp => sp.GetRequiredService()); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// Build and run -var app = builder.Build(); - -// Check if GUI mode is requested -if (args.Length > 0 && (args[0].ToLowerInvariant() == "gui" || args[0].ToLowerInvariant() == "--gui")) -{ - // Run GUI mode - var guiService = app.Services.GetRequiredService(); - await guiService.RunAsync(); -} -else -{ - // Run traditional CLI mode - var cliService = app.Services.GetRequiredService(); - await cliService.RunAsync(args); -} diff --git a/DeepDrftCli/Services/CliService.cs b/DeepDrftCli/Services/CliService.cs deleted file mode 100644 index d509eca..0000000 --- a/DeepDrftCli/Services/CliService.cs +++ /dev/null @@ -1,433 +0,0 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.DependencyInjection; -using DeepDrftContent.Data; -using DeepDrftModels.Entities; -using NetBlocks.Models; -using DeepDrftCli.Utils; - -namespace DeepDrftCli.Services; - -/// -/// Main CLI service for handling command-line operations -/// -public class CliService -{ - private readonly ILogger _logger; - private readonly DeepDrftData.ITrackService _webTrackService; - private readonly DeepDrftContent.Data.TrackService _contentTrackService; - - public CliService( - ILogger logger, - DeepDrftData.ITrackService webTrackService, - DeepDrftContent.Data.TrackService contentTrackService) - { - _logger = logger; - _webTrackService = webTrackService; - _contentTrackService = contentTrackService; - } - - /// - /// Main entry point for CLI operations - /// - public async Task RunAsync(string[] args) - { - try - { - if (args.Length == 0) - { - ShowHelp(); - return; - } - - var command = args[0].ToLowerInvariant(); - switch (command) - { - case "add": - await HandleAddCommand(args); - break; - case "list": - await HandleListCommand(); - break; - case "gui": - case "--gui": - Console.WriteLine("Error: GUI mode should be launched directly. Use: DeepDrftCli gui"); - break; - case "help": - case "--help": - case "-h": - ShowHelp(); - break; - default: - Console.WriteLine($"Unknown command: {command}"); - ShowHelp(); - break; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "CLI operation failed"); - Console.WriteLine($"Error: {ex.Message}"); - Environment.Exit(1); - } - } - - /// - /// Handles the add command to add a new track - /// - private async Task HandleAddCommand(string[] args) - { - // Check if we have at least the command and file path - if (args.Length < 2) - { - Console.WriteLine("Error: WAV file path is required."); - Console.WriteLine(); - Console.WriteLine("Usage: DeepDrftCli add [-i|--interactive] [track-name] [artist] [album] [genre] [release-date]"); - Console.WriteLine(" DeepDrftCli add -i (interactive mode)"); - Console.WriteLine("Example: DeepDrftCli add \"song.wav\" \"My Song\" \"Artist Name\" \"Album Name\" \"Rock\" \"2024-01-01\""); - Console.WriteLine("Example: DeepDrftCli add \"song.wav\" --interactive"); - return; - } - - var wavFilePath = args[1]; - - // Validate that the file path is not a flag - if (wavFilePath.StartsWith("-")) - { - Console.WriteLine("Error: WAV file path is required and cannot be a flag."); - Console.WriteLine(); - Console.WriteLine("Usage: DeepDrftCli add [-i|--interactive] [track-name] [artist] [album] [genre] [release-date]"); - Console.WriteLine(" DeepDrftCli add -i (interactive mode)"); - Console.WriteLine("Example: DeepDrftCli add \"song.wav\" \"My Song\" \"Artist Name\" \"Album Name\" \"Rock\" \"2024-01-01\""); - Console.WriteLine("Example: DeepDrftCli add \"song.wav\" --interactive"); - return; - } - - var isInteractive = args.Contains("-i") || args.Contains("--interactive"); - - // Filter out the interactive flags from args for processing - var filteredArgs = args.Where(arg => arg != "-i" && arg != "--interactive").ToArray(); - - string trackName; - string artist; - string? album; - string? genre; - DateOnly? releaseDate = null; - - if (isInteractive) - { - // Interactive mode - prompt for metadata - var metadata = PromptForMetadata(wavFilePath, filteredArgs); - trackName = metadata.TrackName; - artist = metadata.Artist; - album = metadata.Album; - genre = metadata.Genre; - releaseDate = metadata.ReleaseDate; - } - else - { - // Traditional command-line mode - if (filteredArgs.Length < 4) - { - Console.WriteLine("Usage: DeepDrftCli add [album] [genre] [release-date]"); - Console.WriteLine(" DeepDrftCli add -i (interactive mode)"); - Console.WriteLine("Example: DeepDrftCli add \"song.wav\" \"My Song\" \"Artist Name\" \"Album Name\" \"Rock\" \"2024-01-01\""); - Console.WriteLine("Example: DeepDrftCli add \"song.wav\" --interactive"); - return; - } - - trackName = filteredArgs[2]; - artist = filteredArgs[3]; - album = filteredArgs.Length > 4 ? filteredArgs[4] : null; - genre = filteredArgs.Length > 5 ? filteredArgs[5] : null; - - if (filteredArgs.Length > 6 && DateOnly.TryParse(filteredArgs[6], out var parsedDate)) - { - releaseDate = parsedDate; - } - } - - Console.WriteLine($"Adding track: {trackName} by {artist}"); - Console.WriteLine($"Processing WAV file: {wavFilePath}"); - - try - { - // Initialize tracks vault if needed - await _contentTrackService.InitializeTracksVaultAsync(); - - // Add track to FileDatabase and get entity - var trackEntity = await _contentTrackService.AddTrackFromWavAsync( - wavFilePath, trackName, artist, album, genre, releaseDate); - - if (trackEntity == null) - { - Console.WriteLine("Failed to process audio file"); - return; - } - - // Add track to SQL database - var result = await _webTrackService.Create(trackEntity); - if (result.Success && result.Value != null) - { - Console.WriteLine($"✓ Track added successfully!"); - Console.WriteLine($" ID: {result.Value.Id}"); - Console.WriteLine($" Name: {result.Value.TrackName}"); - Console.WriteLine($" Artist: {result.Value.Artist}"); - Console.WriteLine($" Album: {result.Value.Album ?? "N/A"}"); - Console.WriteLine($" Genre: {result.Value.Genre ?? "N/A"}"); - Console.WriteLine($" Release Date: {result.Value.ReleaseDate?.ToString() ?? "N/A"}"); - Console.WriteLine($" Entry Key: {result.Value.EntryKey}"); - } - else - { - var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - Console.WriteLine($"Failed to save track to database: {errorMessage}"); - } - } - catch (FileNotFoundException) - { - Console.WriteLine($"Error: WAV file not found: {wavFilePath}"); - } - catch (ArgumentException ex) - { - Console.WriteLine($"Error: {ex.Message}"); - } - catch (Exception ex) - { - Console.WriteLine($"Error adding track: {ex.Message}"); - _logger.LogError(ex, "Failed to add track"); - } - } - - /// - /// Handles the list command to show all tracks - /// - private async Task HandleListCommand() - { - try - { - Console.WriteLine("Retrieving tracks from database..."); - - var result = await _webTrackService.GetAll(); - if (!result.Success || result.Value == null) - { - var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - Console.WriteLine($"Failed to retrieve tracks: {errorMessage}"); - return; - } - - var tracks = result.Value; - if (tracks.Count == 0) - { - Console.WriteLine("No tracks found in database."); - return; - } - - Console.WriteLine($"\nFound {tracks.Count} tracks:"); - Console.WriteLine(new string('-', 80)); - Console.WriteLine($"{"ID",-5} {"Name",-25} {"Artist",-20} {"Album",-15} {"Genre",-10}"); - Console.WriteLine(new string('-', 80)); - - foreach (var track in tracks) - { - Console.WriteLine($"{track.Id,-5} {CliUtils.TruncateString(track.TrackName, 25),-25} {CliUtils.TruncateString(track.Artist, 20),-20} {CliUtils.TruncateString(track.Album ?? "", 15),-15} {CliUtils.TruncateString(track.Genre ?? "", 10),-10}"); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error listing tracks: {ex.Message}"); - _logger.LogError(ex, "Failed to list tracks"); - } - } - - /// - /// Shows help information - /// - private void ShowHelp() - { - Console.WriteLine("DeepDrft CLI - Audio Track Management Tool"); - Console.WriteLine(); - Console.WriteLine("Usage:"); - Console.WriteLine(" DeepDrftCli gui - Launch interactive GUI mode"); - Console.WriteLine(" DeepDrftCli [command] [options] - Run command-line mode"); - Console.WriteLine(); - Console.WriteLine("Commands:"); - Console.WriteLine(" add [album] [genre] [release-date]"); - Console.WriteLine(" - Adds a WAV file to both SQL and FileDatabase"); - Console.WriteLine(" - Example: DeepDrftCli add \"song.wav\" \"My Song\" \"Artist\" \"Album\" \"Rock\" \"2024-01-01\""); - Console.WriteLine(); - Console.WriteLine(" add -i|--interactive [track-name] [artist] [album] [genre] [release-date]"); - Console.WriteLine(" - Adds a WAV file with interactive metadata prompts"); - Console.WriteLine(" - Any provided command-line arguments will be used as defaults"); - Console.WriteLine(" - Example: DeepDrftCli add \"song.wav\" -i"); - Console.WriteLine(" - Example: DeepDrftCli add \"song.wav\" --interactive \"My Song\""); - Console.WriteLine(); - Console.WriteLine(" list"); - Console.WriteLine(" - Lists all tracks in the database"); - Console.WriteLine(); - Console.WriteLine(" help"); - Console.WriteLine(" - Shows this help information"); - Console.WriteLine(); - Console.WriteLine("Interactive Mode Features:"); - Console.WriteLine(" - Prompts for each metadata field individually"); - Console.WriteLine(" - Shows file name being processed"); - Console.WriteLine(" - Supports default values and fallback to command-line args"); - Console.WriteLine(" - Required fields: Track Name, Artist"); - Console.WriteLine(" - Optional fields: Album, Genre, Release Date"); - Console.WriteLine(" - Summary confirmation before proceeding"); - Console.WriteLine(); - Console.WriteLine("Notes:"); - Console.WriteLine(" - Only WAV files are supported"); - Console.WriteLine(" - Release date format: YYYY-MM-DD"); - Console.WriteLine(" - Arguments with spaces should be quoted"); - Console.WriteLine(" - Use * to indicate required fields in interactive mode"); - } - - /// - /// Prompts user for track metadata interactively - /// - private TrackMetadata PromptForMetadata(string wavFilePath, string[] args) - { - Console.WriteLine(); - Console.WriteLine("=== Interactive Metadata Entry ==="); - Console.WriteLine($"Processing file: {Path.GetFileName(wavFilePath)}"); - Console.WriteLine("Press Enter to use default values or skip optional fields."); - Console.WriteLine(); - - // Check if any metadata was provided via command line (fallback support) - var trackName = args.Length > 2 ? args[2] : null; - var artist = args.Length > 3 ? args[3] : null; - var album = args.Length > 4 ? args[4] : null; - var genre = args.Length > 5 ? args[5] : null; - DateOnly? releaseDate = null; - if (args.Length > 6 && DateOnly.TryParse(args[6], out var parsedDate)) - releaseDate = parsedDate; - - // Prompt for track name (required) - trackName ??= PromptForInput("Track Name", required: true); - - // Prompt for artist (required) - artist ??= PromptForInput("Artist", required: true); - - // Prompt for album (optional) - album ??= PromptForInput("Album", defaultValue: album); - - // Prompt for genre (optional) - genre ??= PromptForInput("Genre", defaultValue: genre); - - // Prompt for release date (optional) - if (releaseDate == null) - { - var releaseDateInput = PromptForInput("Release Date (YYYY-MM-DD)", defaultValue: releaseDate?.ToString()); - if (!string.IsNullOrWhiteSpace(releaseDateInput) && DateOnly.TryParse(releaseDateInput, out var newReleaseDate)) - { - releaseDate = newReleaseDate; - } - } - - Console.WriteLine(); - Console.WriteLine("=== Summary ==="); - Console.WriteLine($"Track Name: {trackName}"); - Console.WriteLine($"Artist: {artist}"); - Console.WriteLine($"Album: {album ?? "N/A"}"); - Console.WriteLine($"Genre: {genre ?? "N/A"}"); - Console.WriteLine($"Release Date: {releaseDate?.ToString() ?? "N/A"}"); - Console.WriteLine(); - - if (!ConfirmProceed("Proceed with these details?")) - { - Console.WriteLine("Operation cancelled."); - Environment.Exit(0); - } - - return new TrackMetadata - { - TrackName = trackName, - Artist = artist, - Album = album, - Genre = genre, - ReleaseDate = releaseDate - }; - } - - /// - /// Prompts user for a single input field - /// - private string PromptForInput(string fieldName, bool required = false, string? defaultValue = null) - { - while (true) - { - var prompt = $"{fieldName}"; - if (!string.IsNullOrWhiteSpace(defaultValue)) - prompt += $" [{defaultValue}]"; - if (required) - prompt += " *"; - prompt += ": "; - - Console.Write(prompt); - var input = Console.ReadLine(); - - // Handle null input (Ctrl+C scenario) - if (input == null) - { - Console.WriteLine(); - Console.WriteLine("Operation cancelled."); - Environment.Exit(0); - } - - input = input.Trim(); - - // Use default value if input is empty and default exists - if (string.IsNullOrWhiteSpace(input) && !string.IsNullOrWhiteSpace(defaultValue)) - return defaultValue; - - // If not required and input is empty, return empty string - if (!required && string.IsNullOrWhiteSpace(input)) - return string.Empty; - - // Check if required field is provided - if (required && string.IsNullOrWhiteSpace(input)) - { - Console.WriteLine($"Error: {fieldName} is required. Please provide a value."); - continue; - } - - return input; - } - } - - /// - /// Prompts user for yes/no confirmation - /// - private bool ConfirmProceed(string message) - { - while (true) - { - Console.Write($"{message} (y/n): "); - var input = Console.ReadKey(); - Console.WriteLine(); - - switch (input.KeyChar.ToString().ToLowerInvariant()) - { - case "y": - return true; - case "n": - return false; - default: - Console.WriteLine("Please enter 'y' for yes or 'n' for no."); - break; - } - } - } - - /// - /// Represents track metadata for interactive input - /// - private record TrackMetadata - { - public required string TrackName { get; init; } - public required string Artist { get; init; } - public string? Album { get; init; } - public string? Genre { get; init; } - public DateOnly? ReleaseDate { get; init; } - } -} \ No newline at end of file diff --git a/DeepDrftCli/Services/GuiService.cs b/DeepDrftCli/Services/GuiService.cs deleted file mode 100644 index 6af8972..0000000 --- a/DeepDrftCli/Services/GuiService.cs +++ /dev/null @@ -1,898 +0,0 @@ -using Microsoft.Extensions.Logging; -using Terminal.Gui; -using DeepDrftModels.Entities; -using DeepDrftCli.Utils; - -namespace DeepDrftCli.Services; - -/// -/// Terminal.Gui based interactive interface for DeepDrft CLI operations -/// -public class GuiService -{ - private readonly ILogger _logger; - private readonly DeepDrftData.ITrackService _webTrackService; - private readonly DeepDrftContent.Data.TrackService _contentTrackService; - - // GUI Components - private Window? _mainWindow; - private MenuBar? _menuBar; - private ListView? _trackListView; - private TextView? _statusView; - private FrameView? _legendFrame; - private List _tracks = new(); - - public GuiService( - ILogger logger, - DeepDrftData.ITrackService webTrackService, - DeepDrftContent.Data.TrackService contentTrackService) - { - _logger = logger; - _webTrackService = webTrackService; - _contentTrackService = contentTrackService; - } - - /// - /// Initialize and run the GUI application - /// - public async Task RunAsync() - { - Application.Init(); - - try - { - await SetupMainWindowAsync(); - Application.Run(_mainWindow); - } - catch (Exception ex) - { - _logger.LogError(ex, "GUI application failed"); - MessageBox.ErrorQuery(50, 7, "Error", $"Application failed: {ex.Message}", "OK"); - } - finally - { - Application.Shutdown(); - } - } - - /// - /// Setup the main application window with all components - /// - private async Task SetupMainWindowAsync() - { - // Create main window with DeepDrft theme - _mainWindow = new Window("DeepDrft CLI - Interactive Mode") - { - X = 0, - Y = 1, // Leave room for menu bar - Width = Dim.Fill(), - Height = Dim.Fill() - }; - - // Apply DeepDrft theme to main window with improved contrast - _mainWindow.ColorScheme = new ColorScheme() - { - Normal = new Terminal.Gui.Attribute(Color.White, Color.Black), - Focus = new Terminal.Gui.Attribute(Color.White, Color.DarkGray), - HotNormal = new Terminal.Gui.Attribute(Color.BrightCyan, Color.Black), - HotFocus = new Terminal.Gui.Attribute(Color.BrightYellow, Color.DarkGray) - }; - - // Setup menu bar - SetupMenuBar(); - - // Setup track list view - SetupTrackListView(); - - // Setup status view - SetupStatusView(); - - // Setup legend panel - SetupLegendPanel(); - - // Setup key bindings - SetupKeyBindings(); - - // Add components to main window - _mainWindow.Add(_trackListView!, _statusView!, _legendFrame!); - - // Load initial data - await RefreshTrackListAsync(); - - // Set initial status - UpdateStatus("Ready - Use keyboard shortcuts shown below or press F1 for detailed help"); - } - - /// - /// Setup the menu bar with color-coded options - /// - private void SetupMenuBar() - { - _menuBar = new MenuBar(new MenuBarItem[] { - new MenuBarItem("_File", new MenuItem[] { - new MenuItem("_Add Track (Ctrl+A)", "", () => ShowAddTrackDialog()), - new MenuItem("_Refresh (F5)", "", async () => await RefreshTrackListAsync()), - null, // Separator - new MenuItem("_Quit (Ctrl+Q)", "", () => Application.RequestStop()) - }), - new MenuBarItem("_Edit", new MenuItem[] { - new MenuItem("_Edit Track (Ctrl+E)", "", () => ShowEditTrackDialog()), - new MenuItem("_Delete Track (Delete)", "", () => ShowDeleteTrackDialog()), - null, // Separator - new MenuItem("_Track Details (Enter)", "", () => ShowTrackDetails()), - }), - new MenuBarItem("_View", new MenuItem[] { - new MenuItem("_Clear Status (Ctrl+L)", "", () => ClearStatus()), - }), - new MenuBarItem("_Help", new MenuItem[] { - new MenuItem("_Shortcuts (F1)", "", () => ShowHelp()), - new MenuItem("_About", "", () => ShowAbout()) - }) - }); - - Application.Top.Add(_menuBar); - } - - /// - /// Setup the track list view with color coding - /// - private void SetupTrackListView() - { - _trackListView = new ListView() - { - X = 0, - Y = 0, - Width = Dim.Fill(), - Height = Dim.Fill(6), // Leave room for status and legend at bottom - CanFocus = true - }; - - // Set up high-contrast colors for track list visibility - _trackListView.ColorScheme = new ColorScheme() - { - Normal = new Terminal.Gui.Attribute(Color.White, Color.Black), - Focus = new Terminal.Gui.Attribute(Color.BrightYellow, Color.Blue), // High contrast selection - HotNormal = new Terminal.Gui.Attribute(Color.BrightCyan, Color.Black), - HotFocus = new Terminal.Gui.Attribute(Color.White, Color.Blue) // Clear cursor visibility - }; - - // Handle selection events - _trackListView.SelectedItemChanged += OnTrackSelectionChanged; - } - - /// - /// Setup the status view at the bottom - /// - private void SetupStatusView() - { - _statusView = new TextView() - { - X = 0, - Y = Pos.AnchorEnd(5), // Position above legend panel - Width = Dim.Fill(), - Height = 2, - ReadOnly = true, - WordWrap = true - }; - - // Status view with high contrast colors for better readability - _statusView.ColorScheme = new ColorScheme() - { - Normal = new Terminal.Gui.Attribute(Color.BrightCyan, Color.Black), // High contrast status - Focus = new Terminal.Gui.Attribute(Color.BrightCyan, Color.DarkGray) - }; - } - - /// - /// Setup the hotkey legend panel at the bottom - /// - private void SetupLegendPanel() - { - _legendFrame = new FrameView("Keyboard Shortcuts") - { - X = 0, - Y = Pos.AnchorEnd(3), - Width = Dim.Fill(), - Height = 3 - }; - - // Apply high contrast theme to legend frame for better visibility - _legendFrame.ColorScheme = new ColorScheme() - { - Normal = new Terminal.Gui.Attribute(Color.White, Color.Black), // Clear border - Focus = new Terminal.Gui.Attribute(Color.White, Color.DarkGray), - HotNormal = new Terminal.Gui.Attribute(Color.BrightYellow, Color.Black), - HotFocus = new Terminal.Gui.Attribute(Color.BrightYellow, Color.DarkGray) - }; - - // Create legend content with color coding - var legendView = new TextView() - { - X = 0, - Y = 0, - Width = Dim.Fill(), - Height = Dim.Fill(), - ReadOnly = true, - WordWrap = false, - Text = CreateLegendText() - }; - - // Legend with high contrast colors for easy reading - legendView.ColorScheme = new ColorScheme() - { - Normal = new Terminal.Gui.Attribute(Color.BrightGreen, Color.Black), // High contrast legend text - Focus = new Terminal.Gui.Attribute(Color.BrightGreen, Color.DarkGray) - }; - - _legendFrame.Add(legendView); - } - - /// - /// Create the formatted legend text with shortcuts - /// - private string CreateLegendText() - { - return "Ctrl+A: Add │ Ctrl+E: Edit │ Del: Delete │ F5: Refresh │ Enter: Details │ F1: Help │ Ctrl+Q: Quit"; - } - - /// - /// Setup keyboard shortcuts - /// - private void SetupKeyBindings() - { - // Global key bindings using KeyDown event which is more reliable - _mainWindow.KeyDown += async (e) => - { - var key = e.KeyEvent.Key; - - // Debug logging for key presses - UpdateStatus($"Key pressed: {key}"); - - switch (key) - { - case Key.CtrlMask | Key.Q: - case Key.CtrlMask | Key.q: - Application.RequestStop(); - e.Handled = true; - break; - - case Key.CtrlMask | Key.A: - case Key.CtrlMask | Key.a: - UpdateStatus("Opening Add Track dialog..."); - ShowAddTrackDialog(); - e.Handled = true; - break; - - case Key.CtrlMask | Key.E: - case Key.CtrlMask | Key.e: - UpdateStatus("Opening Edit Track dialog..."); - ShowEditTrackDialog(); - e.Handled = true; - break; - - case Key.DeleteChar: - UpdateStatus("Opening Delete Track dialog..."); - ShowDeleteTrackDialog(); - e.Handled = true; - break; - - case Key.F5: - UpdateStatus("Refreshing track list..."); - await RefreshTrackListAsync(); - e.Handled = true; - break; - - case Key.F1: - ShowHelp(); - e.Handled = true; - break; - - case Key.CtrlMask | Key.L: - case Key.CtrlMask | Key.l: - ClearStatus(); - e.Handled = true; - break; - - case Key.Enter: - if (_trackListView?.HasFocus == true) - { - ShowTrackDetails(); - e.Handled = true; - } - break; - } - }; - - // Also add global application-level key bindings as backup - Application.Top.KeyDown += async (e) => - { - var key = e.KeyEvent.Key; - - switch (key) - { - case Key.CtrlMask | Key.A: - case Key.CtrlMask | Key.a: - if (!e.Handled) - { - UpdateStatus("Global: Opening Add Track dialog..."); - ShowAddTrackDialog(); - e.Handled = true; - } - break; - } - }; - } - - /// - /// Show the Add Track dialog with form validation - /// - private void ShowAddTrackDialog() - { - var dialog = new Dialog("Add New Track", 80, 18); - - // File path field - var filePathLabel = new Label("WAV File Path:") { X = 1, Y = 1 }; - var filePathField = new TextField("") { X = 1, Y = 2, Width = Dim.Fill(2) }; - var browseButton = new Button("Browse...") { X = Pos.AnchorEnd(12), Y = 2 }; - - // Track metadata fields - var trackNameLabel = new Label("Track Name: *") { X = 1, Y = 4 }; - var trackNameField = new TextField("") { X = 1, Y = 5, Width = Dim.Fill(2) }; - - var artistLabel = new Label("Artist: *") { X = 1, Y = 6 }; - var artistField = new TextField("") { X = 1, Y = 7, Width = Dim.Fill(2) }; - - var albumLabel = new Label("Album:") { X = 1, Y = 8 }; - var albumField = new TextField("") { X = 1, Y = 9, Width = Dim.Fill(2) }; - - var genreLabel = new Label("Genre:") { X = 1, Y = 10 }; - var genreField = new TextField("") { X = 1, Y = 11, Width = Dim.Fill(2) }; - - var releaseDateLabel = new Label("Release Date (YYYY-MM-DD):") { X = 1, Y = 12 }; - var releaseDateField = new TextField("") { X = 1, Y = 13, Width = Dim.Fill(2) }; - - // Buttons - var addButton = new Button("Add Track") { X = 1, Y = 15 }; - var cancelButton = new Button("Cancel") { X = 15, Y = 15 }; - - // Color coding for required fields with high contrast - trackNameLabel.ColorScheme = new ColorScheme() - { - Normal = new Terminal.Gui.Attribute(Color.BrightRed, Color.Black) // High contrast for required fields - }; - artistLabel.ColorScheme = new ColorScheme() - { - Normal = new Terminal.Gui.Attribute(Color.BrightRed, Color.Black) // High contrast for required fields - }; - - // Browse button click handler - browseButton.Clicked += () => - { - var openDialog = new OpenDialog("Select WAV File", "Select a WAV audio file to add"); - openDialog.AllowedFileTypes = new[] { ".wav" }; - - Application.Run(openDialog); - - if (!openDialog.Canceled && openDialog.FilePath != null) - { - filePathField.Text = openDialog.FilePath.ToString(); - - // Try to extract metadata from filename - var fileName = Path.GetFileNameWithoutExtension(openDialog.FilePath.ToString()); - if (string.IsNullOrEmpty(trackNameField.Text.ToString())) - { - trackNameField.Text = fileName; - } - } - }; - - // Add button click handler - addButton.Clicked += async () => - { - if (await ValidateAndAddTrackAsync( - filePathField.Text.ToString(), - trackNameField.Text.ToString(), - artistField.Text.ToString(), - albumField.Text.ToString(), - genreField.Text.ToString(), - releaseDateField.Text.ToString())) - { - Application.RequestStop(); - } - }; - - // Cancel button click handler - cancelButton.Clicked += () => Application.RequestStop(); - - // Add all components to dialog - dialog.Add(filePathLabel, filePathField, browseButton, - trackNameLabel, trackNameField, - artistLabel, artistField, - albumLabel, albumField, - genreLabel, genreField, - releaseDateLabel, releaseDateField, - addButton, cancelButton); - - // Set focus to file path field - filePathField.SetFocus(); - - Application.Run(dialog); - } - - /// - /// Show the Edit Track dialog for the selected track - /// - private void ShowEditTrackDialog() - { - if (_trackListView?.SelectedItem < 0 || _trackListView?.SelectedItem >= _tracks.Count) - { - UpdateStatus("No track selected for editing. Select a track first."); - return; - } - - var selectedTrack = _tracks[_trackListView!.SelectedItem]; - - var dialog = new Dialog("Edit Track", 80, 18); - - // Track metadata fields pre-filled with current values - var trackNameLabel = new Label("Track Name: *") { X = 1, Y = 1 }; - var trackNameField = new TextField(selectedTrack.TrackName) { X = 1, Y = 2, Width = Dim.Fill(2) }; - - var artistLabel = new Label("Artist: *") { X = 1, Y = 3 }; - var artistField = new TextField(selectedTrack.Artist) { X = 1, Y = 4, Width = Dim.Fill(2) }; - - var albumLabel = new Label("Album:") { X = 1, Y = 5 }; - var albumField = new TextField(selectedTrack.Album ?? "") { X = 1, Y = 6, Width = Dim.Fill(2) }; - - var genreLabel = new Label("Genre:") { X = 1, Y = 7 }; - var genreField = new TextField(selectedTrack.Genre ?? "") { X = 1, Y = 8, Width = Dim.Fill(2) }; - - var releaseDateLabel = new Label("Release Date (YYYY-MM-DD):") { X = 1, Y = 9 }; - var releaseDateField = new TextField(selectedTrack.ReleaseDate?.ToString() ?? "") { X = 1, Y = 10, Width = Dim.Fill(2) }; - - // Info label showing current track ID - var infoLabel = new Label($"Editing Track ID: {selectedTrack.Id} - Entry Key: {selectedTrack.EntryKey}") { X = 1, Y = 12 }; - - // Buttons - var saveButton = new Button("Save Changes") { X = 1, Y = 14 }; - var cancelButton = new Button("Cancel") { X = 18, Y = 14 }; - - // Color coding for required fields with high contrast - trackNameLabel.ColorScheme = new ColorScheme() - { - Normal = new Terminal.Gui.Attribute(Color.BrightRed, Color.Black) - }; - artistLabel.ColorScheme = new ColorScheme() - { - Normal = new Terminal.Gui.Attribute(Color.BrightRed, Color.Black) - }; - - // Info label styling with better contrast - infoLabel.ColorScheme = new ColorScheme() - { - Normal = new Terminal.Gui.Attribute(Color.BrightCyan, Color.Black) - }; - - // Save button click handler - saveButton.Clicked += async () => - { - if (await ValidateAndUpdateTrackAsync(selectedTrack, - trackNameField.Text.ToString(), - artistField.Text.ToString(), - albumField.Text.ToString(), - genreField.Text.ToString(), - releaseDateField.Text.ToString())) - { - Application.RequestStop(); - } - }; - - // Cancel button click handler - cancelButton.Clicked += () => Application.RequestStop(); - - // Add all components to dialog - dialog.Add(trackNameLabel, trackNameField, - artistLabel, artistField, - albumLabel, albumField, - genreLabel, genreField, - releaseDateLabel, releaseDateField, - infoLabel, - saveButton, cancelButton); - - // Set focus to track name field - trackNameField.SetFocus(); - - Application.Run(dialog); - } - - /// - /// Show the Delete Track confirmation dialog for the selected track - /// - private void ShowDeleteTrackDialog() - { - if (_trackListView?.SelectedItem < 0 || _trackListView?.SelectedItem >= _tracks.Count) - { - UpdateStatus("No track selected for deletion. Select a track first."); - return; - } - - var selectedTrack = _tracks[_trackListView!.SelectedItem]; - - var message = $"Are you sure you want to delete this track?\n\n" + - $"Track: {selectedTrack.TrackName}\n" + - $"Artist: {selectedTrack.Artist}\n" + - $"Album: {selectedTrack.Album ?? "N/A"}\n" + - $"Genre: {selectedTrack.Genre ?? "N/A"}\n" + - $"ID: {selectedTrack.Id}\n\n" + - $"WARNING: This action cannot be undone!\n" + - $"The track metadata will be removed from the database."; - - var result = MessageBox.Query(70, 14, "Confirm Delete Track", message, "Delete", "Cancel"); - - if (result == 0) // Delete button clicked - { - _ = Task.Run(async () => - { - await DeleteTrackAsync(selectedTrack); - }); - } - } - - /// - /// Delete the specified track from the database - /// - private async Task DeleteTrackAsync(TrackEntity trackToDelete) - { - try - { - UpdateStatus($"Deleting track '{trackToDelete.TrackName}'..."); - - // Delete from SQL database - var result = await _webTrackService.Delete(trackToDelete.Id); - if (result.Success) - { - UpdateStatus($"✓ Track '{trackToDelete.TrackName}' by {trackToDelete.Artist} deleted successfully!"); - await RefreshTrackListAsync(); - } - else - { - var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - UpdateStatus($"Failed to delete track: {errorMessage}"); - - // Show error dialog on UI thread - Application.MainLoop.Invoke(() => - { - MessageBox.ErrorQuery(60, 8, "Database Error", $"Failed to delete track: {errorMessage}", "OK"); - }); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to delete track via GUI"); - UpdateStatus($"Error deleting track: {ex.Message}"); - - // Show error dialog on UI thread - Application.MainLoop.Invoke(() => - { - MessageBox.ErrorQuery(60, 8, "Error", $"An error occurred: {ex.Message}", "OK"); - }); - } - } - - /// - /// Validate input and add track to database - /// - private async Task ValidateAndAddTrackAsync(string filePath, string trackName, - string artist, string album, string genre, string releaseDate) - { - try - { - // Validate required fields - if (string.IsNullOrWhiteSpace(filePath)) - { - MessageBox.ErrorQuery(50, 7, "Validation Error", "File path is required.", "OK"); - return false; - } - - if (string.IsNullOrWhiteSpace(trackName)) - { - MessageBox.ErrorQuery(50, 7, "Validation Error", "Track name is required.", "OK"); - return false; - } - - if (string.IsNullOrWhiteSpace(artist)) - { - MessageBox.ErrorQuery(50, 7, "Validation Error", "Artist is required.", "OK"); - return false; - } - - // Validate file exists and has .wav extension - if (!File.Exists(filePath)) - { - MessageBox.ErrorQuery(50, 7, "File Error", "The specified file does not exist.", "OK"); - return false; - } - - if (!Path.GetExtension(filePath).Equals(".wav", StringComparison.OrdinalIgnoreCase)) - { - MessageBox.ErrorQuery(50, 7, "File Error", "Only WAV files are supported.", "OK"); - return false; - } - - // Validate release date if provided - DateOnly? parsedReleaseDate = null; - if (!string.IsNullOrWhiteSpace(releaseDate)) - { - if (!DateOnly.TryParse(releaseDate, out var date)) - { - MessageBox.ErrorQuery(50, 7, "Date Error", "Release date must be in YYYY-MM-DD format.", "OK"); - return false; - } - parsedReleaseDate = date; - } - - // Show progress dialog - UpdateStatus("Processing audio file..."); - - // Initialize tracks vault - await _contentTrackService.InitializeTracksVaultAsync(); - - // Process and add track - var trackEntity = await _contentTrackService.AddTrackFromWavAsync( - filePath, trackName, artist, - string.IsNullOrWhiteSpace(album) ? null : album, - string.IsNullOrWhiteSpace(genre) ? null : genre, - parsedReleaseDate); - - if (trackEntity == null) - { - MessageBox.ErrorQuery(50, 7, "Processing Error", "Failed to process audio file.", "OK"); - return false; - } - - // Add to SQL database - var result = await _webTrackService.Create(trackEntity); - if (result.Success && result.Value != null) - { - UpdateStatus($"✓ Track '{trackName}' by {artist} added successfully!"); - await RefreshTrackListAsync(); - return true; - } - else - { - var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - MessageBox.ErrorQuery(60, 8, "Database Error", $"Failed to save track: {errorMessage}", "OK"); - return false; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to add track via GUI"); - MessageBox.ErrorQuery(60, 8, "Error", $"An error occurred: {ex.Message}", "OK"); - return false; - } - } - - /// - /// Validate input and update existing track in database - /// - private async Task ValidateAndUpdateTrackAsync(TrackEntity originalTrack, string trackName, - string artist, string album, string genre, string releaseDate) - { - try - { - // Validate required fields - if (string.IsNullOrWhiteSpace(trackName)) - { - MessageBox.ErrorQuery(50, 7, "Validation Error", "Track name is required.", "OK"); - return false; - } - - if (string.IsNullOrWhiteSpace(artist)) - { - MessageBox.ErrorQuery(50, 7, "Validation Error", "Artist is required.", "OK"); - return false; - } - - // Validate release date if provided - DateOnly? parsedReleaseDate = null; - if (!string.IsNullOrWhiteSpace(releaseDate)) - { - if (!DateOnly.TryParse(releaseDate, out var date)) - { - MessageBox.ErrorQuery(50, 7, "Date Error", "Release date must be in YYYY-MM-DD format.", "OK"); - return false; - } - parsedReleaseDate = date; - } - - UpdateStatus("Updating track..."); - - // Create updated track entity - var updatedTrack = new TrackEntity - { - Id = originalTrack.Id, - EntryKey = originalTrack.EntryKey, // Keep original entry key - TrackName = trackName, - Artist = artist, - Album = string.IsNullOrWhiteSpace(album) ? null : album, - Genre = string.IsNullOrWhiteSpace(genre) ? null : genre, - ReleaseDate = parsedReleaseDate, - ImagePath = originalTrack.ImagePath // Keep original image path - }; - - // Update in SQL database - var result = await _webTrackService.Update(updatedTrack); - if (result.Success && result.Value != null) - { - UpdateStatus($"✓ Track '{trackName}' by {artist} updated successfully!"); - await RefreshTrackListAsync(); - return true; - } - else - { - var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - MessageBox.ErrorQuery(60, 8, "Database Error", $"Failed to update track: {errorMessage}", "OK"); - return false; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update track via GUI"); - MessageBox.ErrorQuery(60, 8, "Error", $"An error occurred: {ex.Message}", "OK"); - return false; - } - } - - /// - /// Show detailed information about the selected track - /// - private void ShowTrackDetails() - { - if (_trackListView?.SelectedItem < 0 || _trackListView?.SelectedItem >= _tracks.Count) - { - UpdateStatus("No track selected."); - return; - } - - var selectedTrack = _tracks[_trackListView!.SelectedItem]; - - var details = $"Track Details:\n\n" + - $"ID: {selectedTrack.Id}\n" + - $"Name: {selectedTrack.TrackName}\n" + - $"Artist: {selectedTrack.Artist}\n" + - $"Album: {selectedTrack.Album ?? "N/A"}\n" + - $"Genre: {selectedTrack.Genre ?? "N/A"}\n" + - $"Release Date: {selectedTrack.ReleaseDate?.ToString() ?? "N/A"}\n" + - $"Entry Key: {selectedTrack.EntryKey}\n" + - $"Image Path: {selectedTrack.ImagePath ?? "N/A"}"; - - MessageBox.Query(70, 12, "Track Details", details, "OK"); - } - - /// - /// Refresh the track list from database - /// - private async Task RefreshTrackListAsync() - { - try - { - UpdateStatus("Loading tracks..."); - - var result = await _webTrackService.GetAll(); - if (result.Success && result.Value != null) - { - _tracks = result.Value.ToList(); - - // Create display items for the list view - var displayItems = _tracks.Select(t => - $"{t.Id,4} │ {CliUtils.TruncateString(t.TrackName, 25),25} │ {CliUtils.TruncateString(t.Artist, 20),20} │ {CliUtils.TruncateString(t.Album ?? "", 15),15} │ {CliUtils.TruncateString(t.Genre ?? "", 10),10}" - ).ToArray(); - - _trackListView?.SetSource(displayItems); - - UpdateStatus($"Loaded {_tracks.Count} tracks. Use shortcuts below or navigate with ↑/↓ arrows."); - } - else - { - var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - UpdateStatus($"Failed to load tracks: {errorMessage}"); - MessageBox.ErrorQuery(50, 7, "Database Error", $"Failed to load tracks: {errorMessage}", "OK"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to refresh track list"); - UpdateStatus($"Error loading tracks: {ex.Message}"); - } - } - - /// - /// Handle track selection changes - /// - private void OnTrackSelectionChanged(ListViewItemEventArgs args) - { - if (args.Item >= 0 && args.Item < _tracks.Count) - { - var selectedTrack = _tracks[args.Item]; - UpdateStatus($"Selected: {selectedTrack.TrackName} by {selectedTrack.Artist} - Press Enter for full details"); - } - } - - /// - /// Show help dialog with keyboard shortcuts - /// - private void ShowHelp() - { - var helpText = - "DeepDrft CLI - Interactive Mode Help\n\n" + - "KEYBOARD SHORTCUTS (also shown in legend at bottom):\n" + - "Ctrl+A - Add new track\n" + - "Ctrl+E - Edit selected track\n" + - "Delete - Delete selected track\n" + - "F5 - Refresh track list\n" + - "Enter - Show track details\n" + - "Ctrl+L - Clear status\n" + - "F1 - Show this help\n" + - "Ctrl+Q - Quit application\n\n" + - "NAVIGATION:\n" + - "↑/↓ - Navigate track list\n" + - "Tab - Switch between controls\n" + - "Space - Select/activate control\n\n" + - "USER INTERFACE:\n" + - "• Track list shows: ID │ Name │ Artist │ Album │ Genre\n" + - "• Status bar provides real-time feedback\n" + - "• Legend bar shows common shortcuts\n" + - "• Menu bar accessible via Alt or mouse\n\n" + - "HIGH CONTRAST COLOR SCHEME:\n" + - "Bright Red - Required fields (*)\n" + - "Bright Yellow - Selected/focused items\n" + - "Blue Background - Selection highlight\n" + - "Bright Cyan - Status messages & info\n" + - "Bright Green - Legend shortcuts\n" + - "Bright White - Normal text & borders"; - - MessageBox.Query(70, 22, "Help - Interactive Mode Guide", helpText, "OK"); - } - - /// - /// Show about dialog - /// - private void ShowAbout() - { - var aboutText = - "DeepDrft CLI - Interactive Mode\n\n" + - "Version: 1.0.0\n" + - "Built with Terminal.Gui\n\n" + - "Features:\n" + - "• Interactive track management\n" + - "• Dual database architecture\n" + - "• WAV file processing\n" + - "• Color-coded interface\n" + - "• Keyboard shortcuts\n\n" + - "© 2025 DeepDrft Project"; - - MessageBox.Query(50, 12, "About DeepDrft CLI", aboutText, "OK"); - } - - /// - /// Update the status display - /// - private void UpdateStatus(string message) - { - if (_statusView != null) - { - _statusView.Text = $"Status: {message}"; - _statusView.SetNeedsDisplay(); - } - } - - /// - /// Clear the status display - /// - private void ClearStatus() - { - UpdateStatus("Ready"); - } - -} \ No newline at end of file diff --git a/DeepDrftCli/Utils/CliUtils.cs b/DeepDrftCli/Utils/CliUtils.cs deleted file mode 100644 index d71d20f..0000000 --- a/DeepDrftCli/Utils/CliUtils.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace DeepDrftCli.Utils; - -internal static class CliUtils -{ - /// - /// Truncates a string to fit within the specified column width, - /// appending "..." when the string is longer. - /// - internal static string TruncateString(string input, int maxLength) - { - if (string.IsNullOrEmpty(input)) - return string.Empty; - - return input.Length <= maxLength ? input : input.Substring(0, maxLength - 3) + "..."; - } -} diff --git a/DeepDrftCli/connections.example.json b/DeepDrftCli/connections.example.json deleted file mode 100644 index 3eb0308..0000000 --- a/DeepDrftCli/connections.example.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "CliSettings": { - "ConnectionString": "Host=localhost;Port=5433;Database=postgres;Username=postgres;Password=your-password-here", - "VaultPath": "C:/Development/DeepDrftHome/Database/Vaults" - } -} diff --git a/DeepDrftHome.sln b/DeepDrftHome.sln index a9c6c0d..4afcc24 100644 --- a/DeepDrftHome.sln +++ b/DeepDrftHome.sln @@ -76,18 +76,6 @@ Global {C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}.Release|x64.Build.0 = Release|Any CPU {C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}.Release|x86.ActiveCfg = Release|Any CPU {C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}.Release|x86.Build.0 = Release|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|x64.ActiveCfg = Debug|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|x64.Build.0 = Debug|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|x86.ActiveCfg = Debug|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|x86.Build.0 = Debug|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|Any CPU.Build.0 = Release|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|x64.ActiveCfg = Release|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|x64.Build.0 = Release|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|x86.ActiveCfg = Release|Any CPU - {EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|x86.Build.0 = Release|Any CPU {47E99024-491B-47A6-BAF8-9E5814366DB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {47E99024-491B-47A6-BAF8-9E5814366DB2}.Debug|Any CPU.Build.0 = Debug|Any CPU {47E99024-491B-47A6-BAF8-9E5814366DB2}.Debug|x64.ActiveCfg = Debug|Any CPU