From 1a9a3271d450966c75e9795201141c8d55db09db Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Thu, 4 Sep 2025 19:58:29 -0400 Subject: [PATCH] Content API Upgrades --- DeepDrftContent/Constants/VaultConstants.cs | 12 ++ .../Controllers/TrackController.cs | 22 ++- .../Controllers/WeatherForecastController.cs | 32 ---- DeepDrftContent/DeepDrftContent.csproj | 8 + .../FileDatabase/FileDatabase.csproj | 9 - .../Services/MediaVaultFactory.cs | 2 +- .../ApiKeyAuthenticationMiddleware.cs | 57 +++++++ .../Middleware/ApiKeyAuthorizeAttribute.cs | 7 + DeepDrftContent/Models/ApiKeySettings.cs | 7 + DeepDrftContent/Processors/AudioProcessor.cs | 161 ++++++++++++++++++ DeepDrftContent/Program.cs | 12 ++ DeepDrftContent/Services/TrackService.cs | 112 ++++++++++++ DeepDrftContent/Startup.cs | 6 +- DeepDrftContent/WeatherForecast.cs | 12 -- 14 files changed, 394 insertions(+), 65 deletions(-) create mode 100644 DeepDrftContent/Constants/VaultConstants.cs delete mode 100644 DeepDrftContent/Controllers/WeatherForecastController.cs delete mode 100644 DeepDrftContent/FileDatabase/FileDatabase.csproj create mode 100644 DeepDrftContent/Middleware/ApiKeyAuthenticationMiddleware.cs create mode 100644 DeepDrftContent/Middleware/ApiKeyAuthorizeAttribute.cs create mode 100644 DeepDrftContent/Models/ApiKeySettings.cs create mode 100644 DeepDrftContent/Processors/AudioProcessor.cs create mode 100644 DeepDrftContent/Services/TrackService.cs delete mode 100644 DeepDrftContent/WeatherForecast.cs diff --git a/DeepDrftContent/Constants/VaultConstants.cs b/DeepDrftContent/Constants/VaultConstants.cs new file mode 100644 index 0000000..efd7596 --- /dev/null +++ b/DeepDrftContent/Constants/VaultConstants.cs @@ -0,0 +1,12 @@ +namespace DeepDrftContent.Constants; + +/// +/// Constants for FileDatabase vault names +/// +public static class VaultConstants +{ + /// + /// Vault name for storing audio tracks + /// + public const string Tracks = "tracks"; +} \ No newline at end of file diff --git a/DeepDrftContent/Controllers/TrackController.cs b/DeepDrftContent/Controllers/TrackController.cs index 0505d64..6421722 100644 --- a/DeepDrftContent/Controllers/TrackController.cs +++ b/DeepDrftContent/Controllers/TrackController.cs @@ -1,5 +1,7 @@ -using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.Constants; +using DeepDrftContent.FileDatabase.Models; using DeepDrftContent.FileDatabase.Services; +using DeepDrftContent.Middleware; using Microsoft.AspNetCore.Mvc; namespace DeepDrftContent.Controllers; @@ -16,15 +18,19 @@ public class TrackController : ControllerBase } [HttpGet("{trackId}")] - public async Task> GetTrack([FromQuery] string trackId) + public async Task GetTrack(string trackId) { - // BEFORE: Complex with EntryKey objects and redundant MediaVaultType - // var entryKey = new EntryKey(trackId, MediaVaultTypeMap.GetVaultType()); - // var file = await _fileDatabase.LoadResourceAsync(_vaultKey, entryKey); - - // AFTER: Ultra clean - just string identifiers, types inferred - var file = await _fileDatabase.LoadResourceAsync("tracks", trackId); + var file = await _fileDatabase.LoadResourceAsync(VaultConstants.Tracks, trackId); if (file == null) { return NotFound(); } return File(file.Buffer, MimeTypeExtensions.GetMimeType(file.Extension)); } + + [ApiKeyAuthorize] + [HttpPut("{trackId}")] + public async Task PutTrack([FromQuery] string trackId, [FromBody] AudioBinaryDto track) + { + var audioBinary = AudioBinary.From(track); + var success = await _fileDatabase.RegisterResourceAsync(VaultConstants.Tracks, trackId, audioBinary); + return success ? Ok() : BadRequest("Failed to store audio track"); + } } \ No newline at end of file diff --git a/DeepDrftContent/Controllers/WeatherForecastController.cs b/DeepDrftContent/Controllers/WeatherForecastController.cs deleted file mode 100644 index 27dfb37..0000000 --- a/DeepDrftContent/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace DeepDrftContent.Controllers; - -[ApiController] -[Route("[controller]")] -public class WeatherForecastController : ControllerBase -{ - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } -} \ No newline at end of file diff --git a/DeepDrftContent/DeepDrftContent.csproj b/DeepDrftContent/DeepDrftContent.csproj index d46735b..955a932 100644 --- a/DeepDrftContent/DeepDrftContent.csproj +++ b/DeepDrftContent/DeepDrftContent.csproj @@ -10,4 +10,12 @@ + + + + + + + + diff --git a/DeepDrftContent/FileDatabase/FileDatabase.csproj b/DeepDrftContent/FileDatabase/FileDatabase.csproj deleted file mode 100644 index 125f4c9..0000000 --- a/DeepDrftContent/FileDatabase/FileDatabase.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net9.0 - enable - enable - - - diff --git a/DeepDrftContent/FileDatabase/Services/MediaVaultFactory.cs b/DeepDrftContent/FileDatabase/Services/MediaVaultFactory.cs index bc1c819..68c9d78 100644 --- a/DeepDrftContent/FileDatabase/Services/MediaVaultFactory.cs +++ b/DeepDrftContent/FileDatabase/Services/MediaVaultFactory.cs @@ -4,7 +4,7 @@ using DeepDrftContent.FileDatabase.Models; namespace DeepDrftContent.FileDatabase.Services; /// -/// Factory for creating media vaults - simple dictionary-based approach +/// Factory for creating media vaults /// public static class MediaVaultFactory { diff --git a/DeepDrftContent/Middleware/ApiKeyAuthenticationMiddleware.cs b/DeepDrftContent/Middleware/ApiKeyAuthenticationMiddleware.cs new file mode 100644 index 0000000..dc61d39 --- /dev/null +++ b/DeepDrftContent/Middleware/ApiKeyAuthenticationMiddleware.cs @@ -0,0 +1,57 @@ +using System.Reflection; + +namespace DeepDrftContent.Middleware +{ + public class ApiKeyAuthenticationMiddleware + { + private readonly RequestDelegate _next; + private readonly string _apiKey; + + public ApiKeyAuthenticationMiddleware(RequestDelegate next, string apiKey) + { + _next = next; + _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); + } + + public async Task InvokeAsync(HttpContext context) + { + var endpoint = context.GetEndpoint(); + if (endpoint == null) + { + await _next(context); + return; + } + + var hasApiKeyAuthorize = endpoint.Metadata.GetMetadata() != null; + if (!hasApiKeyAuthorize) + { + await _next(context); + return; + } + + if (!context.Request.Headers.TryGetValue("ApiKey", out var extractedApiKey)) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("API Key was not provided"); + return; + } + + if (!string.Equals(extractedApiKey, _apiKey, StringComparison.Ordinal)) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("Unauthorized client"); + return; + } + + await _next(context); + } + } + + public static class ApiKeyAuthenticationMiddlewareExtensions + { + public static IApplicationBuilder UseApiKeyAuthentication(this IApplicationBuilder builder, string apiKey) + { + return builder.UseMiddleware(apiKey); + } + } +} \ No newline at end of file diff --git a/DeepDrftContent/Middleware/ApiKeyAuthorizeAttribute.cs b/DeepDrftContent/Middleware/ApiKeyAuthorizeAttribute.cs new file mode 100644 index 0000000..4ceed27 --- /dev/null +++ b/DeepDrftContent/Middleware/ApiKeyAuthorizeAttribute.cs @@ -0,0 +1,7 @@ +namespace DeepDrftContent.Middleware +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class ApiKeyAuthorizeAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/DeepDrftContent/Models/ApiKeySettings.cs b/DeepDrftContent/Models/ApiKeySettings.cs new file mode 100644 index 0000000..1509f66 --- /dev/null +++ b/DeepDrftContent/Models/ApiKeySettings.cs @@ -0,0 +1,7 @@ +namespace DeepDrftContent.Models +{ + public class ApiKeySettings + { + public required string ApiKey { get; set; } + } +} \ No newline at end of file diff --git a/DeepDrftContent/Processors/AudioProcessor.cs b/DeepDrftContent/Processors/AudioProcessor.cs new file mode 100644 index 0000000..a9f77d2 --- /dev/null +++ b/DeepDrftContent/Processors/AudioProcessor.cs @@ -0,0 +1,161 @@ +using DeepDrftContent.FileDatabase.Models; + +namespace DeepDrftContent.Processors; + +/// +/// Service for processing audio files and extracting metadata +/// +public class AudioProcessor +{ + /// + /// Processes a WAV file and creates an AudioBinary object + /// + /// Path to the WAV file + /// AudioBinary object with metadata + public async Task ProcessWavFileAsync(string filePath) + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"WAV file not found: {filePath}"); + } + + if (!Path.GetExtension(filePath).Equals(".wav", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException("File must be a WAV file", nameof(filePath)); + } + + try + { + var buffer = await File.ReadAllBytesAsync(filePath); + var wavInfo = ExtractWavMetadata(buffer); + + var parameters = new AudioBinaryParams( + Buffer: buffer, + Size: buffer.Length, + Extension: ".wav", + Duration: wavInfo.Duration, + Bitrate: wavInfo.Bitrate + ); + + return new AudioBinary(parameters); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to process WAV file: {ex.Message}", ex); + } + } + + /// + /// Extracts metadata from WAV file buffer + /// + private WavMetadata ExtractWavMetadata(byte[] buffer) + { + try + { + // WAV file format parsing + // RIFF header starts at byte 0 + if (buffer.Length < 44) + { + throw new InvalidDataException("WAV file too short to contain valid header"); + } + + // Check RIFF signature + var riffSignature = System.Text.Encoding.ASCII.GetString(buffer, 0, 4); + if (riffSignature != "RIFF") + { + throw new InvalidDataException("Invalid WAV file: Missing RIFF signature"); + } + + // Check WAVE format + var waveSignature = System.Text.Encoding.ASCII.GetString(buffer, 8, 4); + if (waveSignature != "WAVE") + { + throw new InvalidDataException("Invalid WAV file: Missing WAVE signature"); + } + + // Find fmt chunk + var fmtChunkPos = FindChunk(buffer, "fmt "); + if (fmtChunkPos == -1) + { + throw new InvalidDataException("Invalid WAV file: Missing fmt chunk"); + } + + // Parse fmt chunk + var fmtChunkSize = BitConverter.ToUInt32(buffer, fmtChunkPos + 4); + var sampleRate = BitConverter.ToUInt32(buffer, fmtChunkPos + 12); + var byteRate = BitConverter.ToUInt32(buffer, fmtChunkPos + 16); + var channels = BitConverter.ToUInt16(buffer, fmtChunkPos + 10); + var bitsPerSample = BitConverter.ToUInt16(buffer, fmtChunkPos + 22); + + // Find data chunk + var dataChunkPos = FindChunk(buffer, "data"); + if (dataChunkPos == -1) + { + throw new InvalidDataException("Invalid WAV file: Missing data chunk"); + } + + var dataSize = BitConverter.ToUInt32(buffer, dataChunkPos + 4); + + // Calculate duration + var duration = (double)dataSize / byteRate; + + // Calculate bitrate (bits per second / 1000 for kbps) + var bitrate = (int)((sampleRate * channels * bitsPerSample) / 1000); + + return new WavMetadata + { + Duration = duration, + Bitrate = bitrate, + SampleRate = (int)sampleRate, + Channels = channels, + BitsPerSample = bitsPerSample + }; + } + catch (Exception ex) + { + // Fallback to basic metadata if parsing fails + Console.WriteLine($"Warning: Could not parse WAV metadata: {ex.Message}"); + return new WavMetadata + { + Duration = 180.0, // Default 3 minutes + Bitrate = 1411, // Default CD quality bitrate for WAV + SampleRate = 44100, + Channels = 2, + BitsPerSample = 16 + }; + } + } + + /// + /// Finds a chunk in the WAV file buffer + /// + private int FindChunk(byte[] buffer, string chunkId) + { + var chunkBytes = System.Text.Encoding.ASCII.GetBytes(chunkId); + + for (int i = 12; i < buffer.Length - 8; i += 4) + { + if (buffer[i] == chunkBytes[0] && + buffer[i + 1] == chunkBytes[1] && + buffer[i + 2] == chunkBytes[2] && + buffer[i + 3] == chunkBytes[3]) + { + return i; + } + } + + return -1; + } + + /// + /// WAV file metadata + /// + private class WavMetadata + { + public double Duration { get; set; } + public int Bitrate { get; set; } + public int SampleRate { get; set; } + public int Channels { get; set; } + public int BitsPerSample { get; set; } + } +} \ No newline at end of file diff --git a/DeepDrftContent/Program.cs b/DeepDrftContent/Program.cs index 2f04047..679eaed 100644 --- a/DeepDrftContent/Program.cs +++ b/DeepDrftContent/Program.cs @@ -1,11 +1,22 @@ +using DeepDrftContent; +using DeepDrftContent.FileDatabase.Services; +using DeepDrftContent.Middleware; +using DeepDrftContent.Models; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. +await Startup.ConfigureDomainServices(builder); builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); +// Load API key configuration +builder.Configuration.AddJsonFile("environment/apikey.json", optional: false, reloadOnChange: true); +var apiKeySettings = builder.Configuration.GetSection(nameof(ApiKeySettings)).Get(); +if (apiKeySettings is null) { throw new Exception("API key settings are not configured"); } + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -14,6 +25,7 @@ if (app.Environment.IsDevelopment()) app.MapOpenApi(); } +app.UseApiKeyAuthentication(apiKeySettings.ApiKey); app.UseAuthorization(); app.MapControllers(); diff --git a/DeepDrftContent/Services/TrackService.cs b/DeepDrftContent/Services/TrackService.cs new file mode 100644 index 0000000..34dbde9 --- /dev/null +++ b/DeepDrftContent/Services/TrackService.cs @@ -0,0 +1,112 @@ +using DeepDrftContent.Constants; +using DeepDrftContent.FileDatabase.Services; +using DeepDrftContent.Processors; +using DeepDrftModels.Entities; + +namespace DeepDrftContent.Services; + +/// +/// Service for managing tracks in both SQL and FileDatabase +/// +public class TrackService +{ + private readonly FileDatabase.Services.FileDatabase _fileDatabase; + private readonly AudioProcessor _audioProcessor; + + public TrackService(FileDatabase.Services.FileDatabase fileDatabase, AudioProcessor audioProcessor) + { + _fileDatabase = fileDatabase; + _audioProcessor = audioProcessor; + } + + /// + /// Adds a new track from a WAV file to both databases + /// + /// Path to the WAV file + /// Name of the track + /// Artist name + /// Optional album name + /// Optional genre + /// Optional release date + /// The track entity with generated ID and media path + public async Task AddTrackFromWavAsync( + string wavFilePath, + string trackName, + string artist, + string? album = null, + string? genre = null, + DateOnly? releaseDate = null) + { + try + { + // Process the WAV file + var audioBinary = await _audioProcessor.ProcessWavFileAsync(wavFilePath); + if (audioBinary == null) + { + throw new InvalidOperationException("Failed to process WAV file"); + } + + // Generate a unique track ID + var trackId = Guid.NewGuid().ToString(); + + // Ensure tracks vault exists + if (!_fileDatabase.HasVault(VaultConstants.Tracks)) + { + await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, DeepDrftContent.FileDatabase.Models.MediaVaultType.Audio); + } + + // Store the audio in FileDatabase + var success = await _fileDatabase.RegisterResourceAsync(VaultConstants.Tracks, trackId, audioBinary); + if (!success) + { + throw new InvalidOperationException("Failed to store audio in FileDatabase"); + } + + // Create the track entity for SQL database + var trackEntity = new TrackEntity + { + EntryKey = trackId, // FileDatabase entry ID + TrackName = trackName, + Artist = artist, + Album = album, + Genre = genre, + ReleaseDate = releaseDate + }; + + return trackEntity; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to add track: {ex.Message}", ex); + } + } + + /// + /// Retrieves audio binary from FileDatabase + /// + /// Track ID (EntryKey) + /// Audio binary or null if not found + public async Task GetAudioBinaryAsync(string trackId) + { + return await _fileDatabase.LoadResourceAsync(VaultConstants.Tracks, trackId); + } + + /// + /// Checks if FileDatabase is available and tracks vault exists + /// + public bool IsFileDatabaseReady() + { + return _fileDatabase.HasVault(VaultConstants.Tracks); + } + + /// + /// Initializes the tracks vault if it doesn't exist + /// + public async Task InitializeTracksVaultAsync() + { + if (!_fileDatabase.HasVault(VaultConstants.Tracks)) + { + await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, DeepDrftContent.FileDatabase.Models.MediaVaultType.Audio); + } + } +} \ No newline at end of file diff --git a/DeepDrftContent/Startup.cs b/DeepDrftContent/Startup.cs index 1f7f93e..9ce8121 100644 --- a/DeepDrftContent/Startup.cs +++ b/DeepDrftContent/Startup.cs @@ -1,3 +1,4 @@ +using DeepDrftContent.Constants; using DeepDrftContent.FileDatabase.Models; using DeepDrftContent.FileDatabase.Services; using DeepDrftContent.Models; @@ -21,10 +22,9 @@ namespace DeepDrftContent private static async Task InitializeTrackVault(FileDatabase.Services.FileDatabase fileDatabase) { - const string vaultId = "tracks"; - if (!fileDatabase.HasVault(vaultId)) + if (!fileDatabase.HasVault(VaultConstants.Tracks)) { - await fileDatabase.CreateVaultAsync(vaultId, MediaVaultType.Audio); + await fileDatabase.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio); } } } diff --git a/DeepDrftContent/WeatherForecast.cs b/DeepDrftContent/WeatherForecast.cs deleted file mode 100644 index dd51861..0000000 --- a/DeepDrftContent/WeatherForecast.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DeepDrftContent; - -public class WeatherForecast -{ - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } -} \ No newline at end of file