using DeepDrftModels.DTOs; using DeepDrftModels.Enums; using Models.Common; using NetBlocks.Models; namespace DeepDrftManager.Services; /// /// CMS-side track operations for the Manager host. Every read and write goes over HTTP to the /// DeepDrftAPI API, which is the single authority over both the SQL metadata store and the /// binary audio vault. DeepDrftManager holds no in-process data layer. /// public interface ICmsTrackService { /// /// Proxy a WAV upload to DeepDrftAPI. The Content API owns the dual-database write and /// returns the persisted track carrying the SQL-assigned Id. A vault-without-SQL /// orphan is handled and logged server-side; here it surfaces as a failed result. /// is the browser's filename, captured at upload time and /// stored as metadata; it is not user-editable afterwards. /// sets the parent release's when this upload /// creates the release. The medium is authoritative only on creation — adding a track to an existing /// release never changes its medium (that is the edit path, ). /// is the total payload size (the browser file's Size); it /// sets Content-Length and is the denominator for , which reports cumulative /// bytes pushed to the wire. Each progress tick also resets the idle/heartbeat upload timeout, so a /// stalled connection aborts without a fixed total-duration cap. /// distinguishes the two rows of a within-batch multi-track Cut: null on /// the first row (CREATE — the server rejects a pre-existing (title, artist) as a duplicate) and the /// id returned by that first row on rows 2..N (ATTACH — the server skips the duplicate check and adds /// the track to the release the batch just created). /// Task> UploadTrackAsync( Stream wavStream, long contentLength, string fileName, string contentType, string trackName, string artist, string? album, string? genre, string? description, string? releaseDate, string? originalFileName, long createdByUserId, ReleaseType releaseType, int trackNumber, ReleaseMedium medium = ReleaseMedium.Cut, long? releaseId = null, IProgress? progress = null, CancellationToken ct = default); /// /// Upload-form pre-flight: returns the existing release whose exact (title, artist) matches, or null /// when none exists. Backs the duplicate block the form runs BEFORE transferring bytes, so the admin /// is not surprised at the end of a long upload. A 404 from the API is the not-found (null) case, not /// a failure. The match semantics are the API's GetReleaseByTitleAndArtist — the same read the /// server backstop uses — so the pre-flight and the backstop agree. /// Task> GetExistingReleaseAsync( string title, string artist, CancellationToken ct = default); /// /// Delete a track via the Content API, which removes the SQL row then the vault entry. /// Maps a 404 to a "Track not found." failure. /// Task DeleteTrackAsync(long id, CancellationToken ct = default); /// /// Replace an existing track's audio via POST api/track/{id}/replace-audio. Swaps only the /// vault bytes and regenerates the track's waveform data server-side; the track id, vault key, /// release membership, position, and metadata are preserved. Uses the dedicated upload client and /// the same two-phase (idle / response-wait) cancellation as , since /// a WAV replace is a large-body upload. sets Content-Length and is /// the denominator for ; each progress tick resets the idle heartbeat. /// Maps a 404 to a "Track not found." failure. /// Task ReplaceTrackAudioAsync( long id, Stream wavStream, long contentLength, string fileName, string contentType, IProgress? progress = null, CancellationToken ct = default); /// /// Soft-delete a release record via DELETE api/track/release/{id}. Use when a release /// has no live tracks and needs to be removed from the albums browser. /// Task DeleteReleaseAsync(long releaseId, CancellationToken ct = default); /// /// Fetch a page of track metadata from the Content API's GET api/track/page. Optional /// and filters narrow the result to a single /// release title or genre; null leaves the dimension unfiltered. /// Task>> GetPagedAsync( int page, int pageSize, string? sortColumn, bool sortDescending, string? album = null, string? genre = null, CancellationToken ct = default); /// /// Fetch a single track's metadata from GET api/track/meta/{id}. A 404 returns a /// passing result with a null value. /// Task> GetByIdAsync(long id, CancellationToken ct = default); /// /// Upload a cover-art image to the images vault via POST api/image/upload. /// Returns the generated entry key on success. Maps a 400 to a validation failure message. /// Task> UploadImageAsync( Stream imageStream, string fileName, string contentType, CancellationToken ct = default); /// /// Update a track's metadata via PUT api/track/meta/{id}. EntryKey is immutable and /// not part of the update. is tri-state: null leaves the /// cover art unchanged, "" clears it, and any other value sets it. /// is null = no change; a non-null, non-Cut value resets the release's ReleaseType to its default /// server-side, since ReleaseType is meaningful only for Cut. /// Task UpdateAsync( long id, string trackName, string artist, string? album, string? genre, string? description, DateOnly? releaseDate, string? imagePath = null, ReleaseType? releaseType = null, ReleaseMedium? medium = null, int? trackNumber = null, CancellationToken ct = default); /// /// Fetch per-track waveform profile status from GET api/track/waveform-status for the /// CMS PreProcessing panel. Unpaged — the admin catalogue is small. /// Task> GetWaveformStatusAsync(CancellationToken ct = default); /// /// Trigger waveform profile generation for a single track via /// POST api/track/{entryKey}/waveform. Maps a 404 to a "Track audio not found." failure. /// Task GenerateWaveformProfileAsync(string entryKey, CancellationToken ct = default); /// /// Trigger high-res visualizer datum generation for a single track via /// POST api/track/{entryKey}/waveform/high-res (phase-12 §5). Re-runnable — recomputes on each /// call. Drives the per-row generate action and the batch backfill. Maps a 404 to a "Track audio not /// found." failure. /// Task GenerateHighResWaveformAsync(string entryKey, CancellationToken ct = default); /// /// Trigger the catalogue-wide Backfill-Opus pass via POST api/track/opus/backfill (Phase 18.5). /// The API enqueues a background Opus derive for every track lacking a complete Opus artifact and returns /// the (enqueued, skipped) counts. Enqueue-only — the transcodes run server-side on a serial background /// worker, so this call returns as soon as the work is scheduled, not when transcoding finishes. The /// Enqueued count is how many derives were scheduled; Skipped is how many already had Opus. /// Task> BackfillOpusAsync(CancellationToken ct = default); /// Returns all releases with track counts from GET api/track/albums. Task>> GetReleasesAsync(CancellationToken ct = default); /// /// Returns the total track count by calling GET api/track/page with pageSize=1 and reading TotalCount. /// Task> GetTrackCountAsync(CancellationToken ct = default); } /// /// Outcome of a Backfill-Opus pass (Phase 18.5): how many tracks had a background derive scheduled /// () and how many were skipped because they already carry a complete Opus /// artifact (). Both are counts of tracks, not finished transcodes — the work /// runs asynchronously on the API's background worker after this returns. /// public readonly record struct OpusBackfillResult(int Enqueued, int Skipped);