@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 Logger @inject CmsTrackBrowserViewModel VM Add Track — DeepDrft CMS Add Track WAV file @if (_selectedFile is not null) { Selected: @_selectedFile.Name (@FormatBytes(_selectedFile.Size)) } @if (_selectedImageFile is { } selectedImage) { Selected: @selectedImage.Name } else { No cover art — optional. } @if (_selectedImageFile is not null) { Will upload on save. } @if (!string.IsNullOrEmpty(_errorMessage)) { @_errorMessage } Cancel @if (_isUploading) { Uploading… } else { Upload } @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"; } }