Content API Upgrades
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
namespace DeepDrftContent.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Constants for FileDatabase vault names
|
||||
/// </summary>
|
||||
public static class VaultConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Vault name for storing audio tracks
|
||||
/// </summary>
|
||||
public const string Tracks = "tracks";
|
||||
}
|
||||
@@ -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<ActionResult<AudioBinaryDto>> GetTrack([FromQuery] string trackId)
|
||||
public async Task<ActionResult> GetTrack(string trackId)
|
||||
{
|
||||
// BEFORE: Complex with EntryKey objects and redundant MediaVaultType
|
||||
// var entryKey = new EntryKey(trackId, MediaVaultTypeMap.GetVaultType<AudioBinary>());
|
||||
// var file = await _fileDatabase.LoadResourceAsync<AudioBinary>(_vaultKey, entryKey);
|
||||
|
||||
// AFTER: Ultra clean - just string identifiers, types inferred
|
||||
var file = await _fileDatabase.LoadResourceAsync<AudioBinary>("tracks", trackId);
|
||||
var file = await _fileDatabase.LoadResourceAsync<AudioBinary>(VaultConstants.Tracks, trackId);
|
||||
if (file == null) { return NotFound(); }
|
||||
return File(file.Buffer, MimeTypeExtensions.GetMimeType(file.Extension));
|
||||
}
|
||||
|
||||
[ApiKeyAuthorize]
|
||||
[HttpPut("{trackId}")]
|
||||
public async Task<ActionResult> 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");
|
||||
}
|
||||
}
|
||||
@@ -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<WeatherForecastController> _logger;
|
||||
|
||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet(Name = "GetWeatherForecast")]
|
||||
public IEnumerable<WeatherForecast> 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();
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,12 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Middleware\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -4,7 +4,7 @@ using DeepDrftContent.FileDatabase.Models;
|
||||
namespace DeepDrftContent.FileDatabase.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating media vaults - simple dictionary-based approach
|
||||
/// Factory for creating media vaults
|
||||
/// </summary>
|
||||
public static class MediaVaultFactory
|
||||
{
|
||||
|
||||
@@ -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<ApiKeyAuthorizeAttribute>() != 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<ApiKeyAuthenticationMiddleware>(apiKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace DeepDrftContent.Middleware
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public class ApiKeyAuthorizeAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace DeepDrftContent.Models
|
||||
{
|
||||
public class ApiKeySettings
|
||||
{
|
||||
public required string ApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using DeepDrftContent.FileDatabase.Models;
|
||||
|
||||
namespace DeepDrftContent.Processors;
|
||||
|
||||
/// <summary>
|
||||
/// Service for processing audio files and extracting metadata
|
||||
/// </summary>
|
||||
public class AudioProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes a WAV file and creates an AudioBinary object
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the WAV file</param>
|
||||
/// <returns>AudioBinary object with metadata</returns>
|
||||
public async Task<AudioBinary?> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts metadata from WAV file buffer
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a chunk in the WAV file buffer
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WAV file metadata
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -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<ApiKeySettings>();
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using DeepDrftContent.Constants;
|
||||
using DeepDrftContent.FileDatabase.Services;
|
||||
using DeepDrftContent.Processors;
|
||||
using DeepDrftModels.Entities;
|
||||
|
||||
namespace DeepDrftContent.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing tracks in both SQL and FileDatabase
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new track from a WAV file to both databases
|
||||
/// </summary>
|
||||
/// <param name="wavFilePath">Path to the WAV file</param>
|
||||
/// <param name="trackName">Name of the track</param>
|
||||
/// <param name="artist">Artist name</param>
|
||||
/// <param name="album">Optional album name</param>
|
||||
/// <param name="genre">Optional genre</param>
|
||||
/// <param name="releaseDate">Optional release date</param>
|
||||
/// <returns>The track entity with generated ID and media path</returns>
|
||||
public async Task<TrackEntity?> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves audio binary from FileDatabase
|
||||
/// </summary>
|
||||
/// <param name="trackId">Track ID (EntryKey)</param>
|
||||
/// <returns>Audio binary or null if not found</returns>
|
||||
public async Task<DeepDrftContent.FileDatabase.Models.AudioBinary?> GetAudioBinaryAsync(string trackId)
|
||||
{
|
||||
return await _fileDatabase.LoadResourceAsync<DeepDrftContent.FileDatabase.Models.AudioBinary>(VaultConstants.Tracks, trackId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if FileDatabase is available and tracks vault exists
|
||||
/// </summary>
|
||||
public bool IsFileDatabaseReady()
|
||||
{
|
||||
return _fileDatabase.HasVault(VaultConstants.Tracks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the tracks vault if it doesn't exist
|
||||
/// </summary>
|
||||
public async Task InitializeTracksVaultAsync()
|
||||
{
|
||||
if (!_fileDatabase.HasVault(VaultConstants.Tracks))
|
||||
{
|
||||
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, DeepDrftContent.FileDatabase.Models.MediaVaultType.Audio);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user