@page "/tracks/upload" @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 Upload Release — DeepDrft CMS Upload Release @foreach (var rt in Enum.GetValues()) { @rt } @if (_selectedImageFile is { } selectedImage) { Selected: @selectedImage.Name } else { No cover art — optional. } @if (_selectedImageFile is not null) { Will upload on submit. } Tracks @if (_tracks.Count == 0) { No tracks added yet. } else { @for (var i = 0; i < _tracks.Count; i++) { var index = i; var row = _tracks[index];
@(index + 1). @row.TrackName @StatusChip(row)
}
}
@if (_selectedIndex < 0 || _tracks.Count == 0) { Select a track from the list to edit its details. } else { var selected = _tracks[_selectedIndex]; @if (selected.WavFile is { } wav) { @wav.Name (@FormatBytes(wav.Size)) } else { No WAV file selected. } @if (selected.Status == TrackUploadStatus.Failed) { @selected.ErrorMessage } }
@if (!string.IsNullOrEmpty(_errorMessage)) { @_errorMessage } Cancel @if (_uploading) { Uploading @_uploadedCount / @_tracks.Count… } else { Upload Release }
@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 const int MaxFilesPerPick = 50; private List _tracks = new(); private int _selectedIndex = -1; private bool _uploading; private int _uploadedCount; private string? _errorMessage; private IBrowserFile? _selectedImageFile; private string? _imagePath; private string _albumName = string.Empty; private string _artist = string.Empty; private string _genre = string.Empty; private string _releaseDate = string.Empty; private ReleaseType _releaseType = ReleaseType.Single; private class BatchTrackRow { public IBrowserFile? WavFile { get; set; } public string TrackName { get; set; } = string.Empty; public TrackUploadStatus Status { get; set; } = TrackUploadStatus.Queued; public string? ErrorMessage { get; set; } } private enum TrackUploadStatus { Queued, Uploading, Done, Failed } private void HandleWavFilesSelected(InputFileChangeEventArgs e) { _errorMessage = null; foreach (var file in e.GetMultipleFiles(MaxFilesPerPick)) { if (!file.Name.EndsWith(".wav", StringComparison.OrdinalIgnoreCase)) { Snackbar.Add($"Skipped '{file.Name}' — not a .wav file.", Severity.Warning); continue; } _tracks.Add(new BatchTrackRow { WavFile = file, TrackName = Path.GetFileNameWithoutExtension(file.Name) }); } if (_selectedIndex < 0 && _tracks.Count > 0) { _selectedIndex = 0; } } private void SelectRow(int index) => _selectedIndex = index; private void MoveUp(int i) { if (i == 0) return; (_tracks[i], _tracks[i - 1]) = (_tracks[i - 1], _tracks[i]); if (_selectedIndex == i) _selectedIndex = i - 1; else if (_selectedIndex == i - 1) _selectedIndex = i; } private void MoveDown(int i) { if (i == _tracks.Count - 1) return; (_tracks[i], _tracks[i + 1]) = (_tracks[i + 1], _tracks[i]); if (_selectedIndex == i) _selectedIndex = i + 1; else if (_selectedIndex == i + 1) _selectedIndex = i; } private void RemoveRow(int i) { _tracks.RemoveAt(i); if (i < _selectedIndex) _selectedIndex--; if (_selectedIndex >= _tracks.Count) _selectedIndex = _tracks.Count - 1; } private string RowStyle(int index) { var baseStyle = "cursor: pointer; border-radius: 4px;"; return index == _selectedIndex ? $"{baseStyle} background-color: var(--mud-palette-action-default-hover);" : baseStyle; } private RenderFragment StatusChip(BatchTrackRow row) => row.Status switch { TrackUploadStatus.Uploading => @ Uploading, TrackUploadStatus.Done => @Done, TrackUploadStatus.Failed => @Failed, _ => @Queued }; private void HandleImageFileSelected(InputFileChangeEventArgs e) { _selectedImageFile = e.File; _imagePath = null; } private void ClearImage() { _selectedImageFile = null; _imagePath = null; } private async Task SubmitAsync() { _errorMessage = null; if (string.IsNullOrWhiteSpace(_albumName)) { _errorMessage = "Album Name is required."; return; } if (string.IsNullOrWhiteSpace(_artist)) { _errorMessage = "Artist is required."; return; } if (_tracks.Count == 0) { _errorMessage = "Add at least one track."; return; } if (!string.IsNullOrWhiteSpace(_releaseDate) && !DateOnly.TryParseExact(_releaseDate, "yyyy-MM-dd", out _)) { _errorMessage = "Release Date must be in YYYY-MM-DD format."; return; } foreach (var t in _tracks) { if (t.WavFile is null) { _errorMessage = $"'{t.TrackName}' has no WAV file selected."; 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 [Authorize] under the Admin role, 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; } _uploading = true; _uploadedCount = 0; try { // Upload any selected cover art once; abort the submit if it fails so we never // create tracks 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; } int succeeded = 0, failed = 0; for (int i = 0; i < _tracks.Count; i++) { var row = _tracks[i]; var trackNumber = i + 1; // 1-based ordinal from list position row.Status = TrackUploadStatus.Uploading; StateHasChanged(); try { // 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 wavStream = row.WavFile!.OpenReadStream(MaxUploadBytes); var result = await CmsTrackService.UploadTrackAsync( wavStream, row.WavFile.Name, row.WavFile.ContentType, row.TrackName, _artist, string.IsNullOrWhiteSpace(_albumName) ? null : _albumName, string.IsNullOrWhiteSpace(_genre) ? null : _genre, string.IsNullOrWhiteSpace(_releaseDate) ? null : _releaseDate, row.WavFile.Name, createdByUserId, _releaseType, trackNumber); if (!result.Success || result.Value is null) { var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; row.Status = TrackUploadStatus.Failed; row.ErrorMessage = error; failed++; Logger.LogWarning("Batch upload: track '{TrackName}' failed: {Error}", row.TrackName, error); } else { // The upload endpoint does not accept an imagePath, so link the cover art with // a follow-up metadata update — same two-step pattern TrackNew/TrackEdit use. if (_imagePath is { } imgPath) { var linkResult = await CmsTrackService.UpdateAsync( result.Value.Id, row.TrackName, _artist, string.IsNullOrWhiteSpace(_albumName) ? null : _albumName, string.IsNullOrWhiteSpace(_genre) ? null : _genre, string.IsNullOrWhiteSpace(_releaseDate) ? null : (DateOnly?)DateOnly.ParseExact(_releaseDate, "yyyy-MM-dd"), imgPath, _releaseType, trackNumber); if (!linkResult.Success) { // Non-blocking: track is persisted; cover art can be linked via TrackEdit. Logger.LogWarning("Batch upload: cover art link failed for '{TrackName}' (id={Id}) — track uploaded but image unlinked", row.TrackName, result.Value.Id); } } row.Status = TrackUploadStatus.Done; succeeded++; } } catch (Exception ex) { Logger.LogError(ex, "Batch upload: exception uploading '{TrackName}'", row.TrackName); row.Status = TrackUploadStatus.Failed; row.ErrorMessage = "Upload failed — please try again."; failed++; } _uploadedCount++; StateHasChanged(); } if (failed == 0) { Snackbar.Add($"Uploaded {succeeded} track(s).", Severity.Success); Navigation.NavigateTo("/tracks"); } else { Snackbar.Add($"Uploaded {succeeded} track(s); {failed} failed — review errors below.", Severity.Warning); // Stay on page so the admin can see the failed rows. } } finally { _uploading = false; StateHasChanged(); } } 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"; } }