using DeepDrftAPI.Models; using DeepDrftContent; using DeepDrftContent.Constants; using DeepDrftContent.FileDatabase.Models; using DeepDrftContent.FileDatabase.Services; using DeepDrftContent.Processors; using DeepDrftContent.Processors.Opus; using Microsoft.Extensions.Logging; using NetBlocks.Utilities.Environment; namespace DeepDrftAPI { public static class Startup { public static Task ConfigureDomainServices(WebApplicationBuilder builder) { // Audio services builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Image services builder.Services.AddSingleton(); // Waveform loudness profiling (upload-time, off the playback path) builder.Services.Configure( builder.Configuration.GetSection(nameof(WaveformProfileOptions))); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // File Database var fileDatabasePath = CredentialTools.ResolvePathOrThrow("filedatabase", "environment/filedatabase.json"); builder.Configuration.AddJsonFile(fileDatabasePath, optional: false, reloadOnChange: false); var fileDatabaseSettings = builder.Configuration.GetSection(nameof(FileDatabaseSettings)).Get(); if (fileDatabaseSettings is null) { throw new Exception("File database settings are not configured"); } var vaultPath = fileDatabaseSettings.VaultPath; builder.Services.AddSingleton(sp => { var logger = sp.GetRequiredService>(); var db = FileDatabase.FromAsync(vaultPath, logger).GetAwaiter().GetResult(); if (db is null) throw new Exception("Unable to initialize file database"); InitializeTrackVault(db).GetAwaiter().GetResult(); InitializeImageVault(db).GetAwaiter().GetResult(); InitializeTrackWaveformsVault(db).GetAwaiter().GetResult(); InitializeTrackOpusVault(db).GetAwaiter().GetResult(); return db; }); // Upload staging directory. Large audio bodies (multi-hundred-MB WAVs) must never stage on // the system temp mount — on the Linux host /tmp is a small RAM-backed tmpfs. We move BOTH // on-disk copies of an upload off /tmp onto the data disk: // Layer 1 — the framework's multipart file-section buffer (FileBufferingReadStream), which // reads its directory from the ASPNETCORE_TEMP env var (falling back to // Path.GetTempPath()). Setting the var here, before the host runs, relocates it. // Layer 2 — the controller's own staging file, via the injected UploadStagingDirectory. // Default location is a "staging" subdirectory beside the vaults; override with // Upload:StagingPath in appsettings.json. var uploadSettings = builder.Configuration.GetSection("Upload").Get(); var stagingPath = ResolveStagingPath(uploadSettings?.StagingPath, vaultPath); Directory.CreateDirectory(stagingPath); // AspNetCoreTempDirectory caches this value on first read and throws if the directory is // absent, so set it (and create the dir) before any request is served. Environment.SetEnvironmentVariable("ASPNETCORE_TEMP", stagingPath); builder.Services.AddSingleton(new UploadStagingDirectory(stagingPath)); // Opus low-data transcode (Phase 18.1). The domain service lives in DeepDrftContent; the host // owns only the engine config and the background worker. Bitrate/ffmpeg-path come from the // OpusTranscode config section; StagingPath is forced to the same data-disk staging directory // the upload path uses so large transcode temp files never land on the /tmp tmpfs. builder.Services.Configure( builder.Configuration.GetSection(nameof(OpusTranscodeOptions))); builder.Services.PostConfigure(o => o.StagingPath = stagingPath); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Opus delivery format resolution + sidecar lookup (Phase 18.2). The seam 18.3 calls behind // the ?format= stream param and the sidecar path. Stateless over the FileDatabase + content // service singletons; the lossless branch reuses the existing read path unchanged (C2). builder.Services.AddSingleton(); return Task.CompletedTask; } /// /// Resolves the absolute upload-staging directory. An explicit /// (from Upload:StagingPath) wins; otherwise it defaults to a staging subdirectory /// under — on the data disk, never the system temp mount. Pure so /// the "never /tmp" invariant is unit-testable without standing up the host. /// public static string ResolveStagingPath(string? configuredPath, string vaultPath) { var path = string.IsNullOrWhiteSpace(configuredPath) ? Path.Combine(vaultPath, "staging") : configuredPath; return Path.GetFullPath(path); } private static async Task InitializeTrackVault(FileDatabase fileDatabase) { if (!fileDatabase.HasVault(VaultConstants.Tracks)) { await fileDatabase.CreateVaultAsync(VaultConstants.Tracks, MediaVaultType.Audio); } } private static async Task InitializeImageVault(FileDatabase fileDatabase) { if (!fileDatabase.HasVault(VaultConstants.Images)) { await fileDatabase.CreateVaultAsync(VaultConstants.Images, MediaVaultType.Image); } } // Ensure the track-waveforms vault exists. Holds the per-track high-resolution waveform datum // (every track — Mix, Session, Cut), keyed by the track's EntryKey. private static async Task InitializeTrackWaveformsVault(FileDatabase fileDatabase) { if (!fileDatabase.HasVault(VaultConstants.TrackWaveforms)) { await fileDatabase.CreateVaultAsync(VaultConstants.TrackWaveforms, MediaVaultType.Media); } } // Ensure the track-opus vault exists (Phase 18.1). Holds the derived low-data Ogg Opus artifacts // — the Opus audio bytes and the setup-header + seek-index sidecar — keyed by the track's EntryKey. private static async Task InitializeTrackOpusVault(FileDatabase fileDatabase) { if (!fileDatabase.HasVault(VaultConstants.TrackOpus)) { await fileDatabase.CreateVaultAsync(VaultConstants.TrackOpus, MediaVaultType.Audio); } } } }