270 lines
11 KiB
Plaintext
270 lines
11 KiB
Plaintext
@page "/tracks/new"
|
|
@using System.Security.Claims
|
|
@using DeepDrftManager.Services
|
|
@using DeepDrftModels.Enums
|
|
@attribute [Authorize]
|
|
|
|
@inject ICmsTrackService CmsTrackService
|
|
@inject AuthenticationStateProvider AuthStateProvider
|
|
@inject NavigationManager Navigation
|
|
@inject ISnackbar Snackbar
|
|
@inject ILogger<TrackNew> Logger
|
|
@inject CmsTrackBrowserViewModel VM
|
|
|
|
<PageTitle>Add Track — DeepDrft CMS</PageTitle>
|
|
|
|
<MudContainer MaxWidth="MaxWidth.Medium" Class="mt-8">
|
|
<MudText Typo="Typo.h4" GutterBottom="true">Add Track</MudText>
|
|
|
|
<MudPaper Class="pa-6" Elevation="2">
|
|
<MudStack Spacing="4">
|
|
<MudText Typo="Typo.subtitle1">WAV file</MudText>
|
|
<InputFile OnChange="OnFileSelected" accept=".wav,audio/wav,audio/x-wav" />
|
|
@if (_selectedFile is not null)
|
|
{
|
|
<MudText Typo="Typo.body2">
|
|
Selected: @_selectedFile.Name (@FormatBytes(_selectedFile.Size))
|
|
</MudText>
|
|
}
|
|
|
|
<MudTextField @bind-Value="_trackName" Label="Track Name" Required="true" RequiredError="Track Name is required" Variant="Variant.Outlined" />
|
|
<MudTextField @bind-Value="_artist" Label="Artist" Required="true" RequiredError="Artist is required" Variant="Variant.Outlined" />
|
|
<MudTextField @bind-Value="_album" Label="Album" Variant="Variant.Outlined" />
|
|
<MudTextField @bind-Value="_genre" Label="Genre" Variant="Variant.Outlined" />
|
|
|
|
<MudField Label="Cover Art" Variant="Variant.Outlined" InnerPadding="false">
|
|
<MudStack Spacing="3">
|
|
@if (_selectedImageFile is { } selectedImage)
|
|
{
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
|
<MudText Typo="Typo.body2" Color="Color.Default">Selected: @selectedImage.Name</MudText>
|
|
<MudIconButton Icon="@Icons.Material.Filled.Clear"
|
|
Color="Color.Error"
|
|
Size="Size.Small"
|
|
Disabled="_isUploading"
|
|
OnClick="ClearImage"
|
|
aria-label="Cancel image selection" />
|
|
</MudStack>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.body2" Color="Color.Default">No cover art — optional.</MudText>
|
|
}
|
|
|
|
<InputFile OnChange="HandleImageFileSelected" accept="image/*" disabled="@_isUploading" />
|
|
@if (_selectedImageFile is not null)
|
|
{
|
|
<MudText Typo="Typo.caption">Will upload on save.</MudText>
|
|
}
|
|
</MudStack>
|
|
</MudField>
|
|
|
|
<MudTextField @bind-Value="_releaseDate" Label="Release Date (YYYY-MM-DD)" Placeholder="2024-01-15" Variant="Variant.Outlined" />
|
|
|
|
@if (!string.IsNullOrEmpty(_errorMessage))
|
|
{
|
|
<MudAlert Severity="Severity.Error">@_errorMessage</MudAlert>
|
|
}
|
|
|
|
<MudStack Row="true" Spacing="2" Justify="Justify.FlexEnd">
|
|
<MudButton Variant="Variant.Text"
|
|
OnClick="Cancel"
|
|
Disabled="_isUploading">
|
|
Cancel
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
OnClick="SubmitAsync"
|
|
Disabled="_isUploading">
|
|
@if (_isUploading)
|
|
{
|
|
<MudProgressCircular Indeterminate="true" Size="Size.Small" Class="mr-2" />
|
|
<text>Uploading…</text>
|
|
}
|
|
else
|
|
{
|
|
<text>Upload</text>
|
|
}
|
|
</MudButton>
|
|
</MudStack>
|
|
</MudStack>
|
|
</MudPaper>
|
|
</MudContainer>
|
|
|
|
@code {
|
|
// 1 GB ceiling matches DeepDrftAPI's per-request limit on api/track/upload; the
|
|
// streaming path means the limit caps the request, not in-memory buffering.
|
|
private const long MaxUploadBytes = 1_073_741_824L;
|
|
|
|
private IBrowserFile? _selectedFile;
|
|
private IBrowserFile? _selectedImageFile;
|
|
private string? _imagePath;
|
|
private string _trackName = string.Empty;
|
|
private string _artist = string.Empty;
|
|
private string _album = string.Empty;
|
|
private string _genre = string.Empty;
|
|
private string _releaseDate = string.Empty;
|
|
private string? _errorMessage;
|
|
private bool _isUploading;
|
|
|
|
private void OnFileSelected(InputFileChangeEventArgs e)
|
|
{
|
|
_selectedFile = e.File;
|
|
_errorMessage = null;
|
|
}
|
|
|
|
private void HandleImageFileSelected(InputFileChangeEventArgs e)
|
|
{
|
|
_selectedImageFile = e.File;
|
|
_imagePath = null;
|
|
}
|
|
|
|
private void ClearImage()
|
|
{
|
|
_selectedImageFile = null;
|
|
_imagePath = null;
|
|
}
|
|
|
|
private async Task SubmitAsync()
|
|
{
|
|
_errorMessage = null;
|
|
|
|
if (_selectedFile is null)
|
|
{
|
|
_errorMessage = "Please select a WAV file.";
|
|
return;
|
|
}
|
|
|
|
if (!_selectedFile.Name.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_errorMessage = "Selected file must be a .wav file.";
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(_trackName))
|
|
{
|
|
_errorMessage = "Track Name is required.";
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(_artist))
|
|
{
|
|
_errorMessage = "Artist is required.";
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(_releaseDate)
|
|
&& !DateOnly.TryParseExact(_releaseDate, "yyyy-MM-dd", out _))
|
|
{
|
|
_errorMessage = "Release Date must be in YYYY-MM-DD format.";
|
|
return;
|
|
}
|
|
|
|
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
|
var userIdValue = authState.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
if (!long.TryParse(userIdValue, out var createdByUserId))
|
|
{
|
|
// The page is gated by [HierarchicalRoleAuthorize(Admin)], so a missing or
|
|
// unparseable id here is a configuration bug, not normal client state.
|
|
Logger.LogError("Authenticated user has no parseable NameIdentifier claim: {Value}", userIdValue);
|
|
_errorMessage = "Your session is missing a valid identifier. Please sign in again.";
|
|
return;
|
|
}
|
|
|
|
_isUploading = true;
|
|
try
|
|
{
|
|
// Upload any selected cover art first; abort the submit if it fails so we never
|
|
// create a track expecting an image that was never stored in the vault.
|
|
if (_selectedImageFile is { } imgFile)
|
|
{
|
|
await using var imgStream = imgFile.OpenReadStream(maxAllowedSize: 50_000_000);
|
|
var imgResult = await CmsTrackService.UploadImageAsync(imgStream, imgFile.Name, imgFile.ContentType);
|
|
if (!imgResult.Success)
|
|
{
|
|
var imgError = imgResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
_errorMessage = $"Image upload failed: {imgError}";
|
|
return;
|
|
}
|
|
_imagePath = imgResult.Value;
|
|
}
|
|
|
|
// OpenReadStream streams chunks from the browser via the SignalR circuit; the
|
|
// service wraps it in StreamContent so the whole file is never materialised in
|
|
// memory before DeepDrftAPI receives it.
|
|
await using var fileStream = _selectedFile.OpenReadStream(MaxUploadBytes);
|
|
|
|
var result = await CmsTrackService.UploadTrackAsync(
|
|
fileStream,
|
|
_selectedFile.Name,
|
|
_selectedFile.ContentType,
|
|
_trackName,
|
|
_artist,
|
|
string.IsNullOrWhiteSpace(_album) ? null : _album,
|
|
string.IsNullOrWhiteSpace(_genre) ? null : _genre,
|
|
string.IsNullOrWhiteSpace(_releaseDate) ? null : _releaseDate,
|
|
_selectedFile.Name,
|
|
createdByUserId,
|
|
releaseType: ReleaseType.Single,
|
|
trackNumber: 1);
|
|
|
|
if (result.Success)
|
|
{
|
|
// The upload endpoint does not accept an imagePath, so link the cover art with a
|
|
// follow-up metadata update — same two-step pattern TrackEdit uses.
|
|
if (_imagePath is { } imgPath && result.Value is { } created)
|
|
{
|
|
var linkResult = await CmsTrackService.UpdateAsync(
|
|
created.Id,
|
|
_trackName,
|
|
_artist,
|
|
string.IsNullOrWhiteSpace(_album) ? null : _album,
|
|
string.IsNullOrWhiteSpace(_genre) ? null : _genre,
|
|
string.IsNullOrWhiteSpace(_releaseDate) ? null : (DateOnly?)DateOnly.ParseExact(_releaseDate, "yyyy-MM-dd"),
|
|
imgPath);
|
|
if (!linkResult.Success)
|
|
{
|
|
// Track was created; image is in the vault but unlinked. Non-blocking —
|
|
// the user can attach it via Edit.
|
|
Snackbar.Add("Track uploaded, but cover art could not be linked. You can add it via Edit.", Severity.Warning);
|
|
}
|
|
}
|
|
|
|
Snackbar.Add($"Uploaded '{_trackName}'.", Severity.Success);
|
|
VM.Invalidate();
|
|
Navigation.NavigateTo("/tracks");
|
|
return;
|
|
}
|
|
|
|
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
_errorMessage = $"Upload failed: {error}";
|
|
Logger.LogWarning("CMS upload rejected: {Error}", error);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Upload failed in TrackNew");
|
|
_errorMessage = "Upload failed. Please try again.";
|
|
}
|
|
finally
|
|
{
|
|
_isUploading = false;
|
|
}
|
|
}
|
|
|
|
private void Cancel()
|
|
{
|
|
Navigation.NavigateTo("/tracks");
|
|
}
|
|
|
|
private static string FormatBytes(long bytes)
|
|
{
|
|
const long KB = 1024;
|
|
const long MB = KB * 1024;
|
|
const long GB = MB * 1024;
|
|
if (bytes >= GB) return $"{bytes / (double)GB:F2} GB";
|
|
if (bytes >= MB) return $"{bytes / (double)MB:F2} MB";
|
|
if (bytes >= KB) return $"{bytes / (double)KB:F2} KB";
|
|
return $"{bytes} bytes";
|
|
}
|
|
}
|