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