Moves the six backend projects on disk into Core/ and Hosts/ subfolders, matching the solution folders already defined in Musify.slnx, so the file explorer reflects the same layout as the solution: backend/Core/Musify.Domain backend/Core/Musify.Application backend/Core/Musify.Infrastructure backend/Hosts/Musify.Api backend/Hosts/Musify.Worker backend/Hosts/Musify.StreamingGateway Updates every reference to the old paths: ProjectReference entries in Musify.Api.csproj and Musify.Worker.csproj, the three Dockerfiles, deploy/scripts/migrate.ps1|sh, deploy/zitadel/provision.ps1, deploy/up.ps1, deploy/README.md and docs/development.md. Also fixes a stale Dockerfile comment pointing at a compose file that no longer exists. Verified with `dotnet build Musify.slnx` against the new layout.
90 lines
3.5 KiB
C#
90 lines
3.5 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Musify.Application.Contracts;
|
|
using Musify.Infrastructure.Configuration;
|
|
using System.Diagnostics;
|
|
|
|
namespace Musify.Infrastructure.Services
|
|
{
|
|
public class YouTubeDownloaderService(
|
|
ILogger<YouTubeDownloaderService> logger,
|
|
YtDlpConfiguration configuration)
|
|
: IYouTubeDownloaderService
|
|
{
|
|
public async Task<YouTubeDownloadResult> DownloadAudioAsync(string videoId, string destinationFilePath, CancellationToken cancellationToken)
|
|
{
|
|
var destinationDirectory = Path.GetDirectoryName(destinationFilePath);
|
|
if (!string.IsNullOrWhiteSpace(destinationDirectory))
|
|
Directory.CreateDirectory(destinationDirectory);
|
|
|
|
var additionalArguments = string.IsNullOrWhiteSpace(configuration.AdditionalArguments)
|
|
? string.Empty
|
|
: $" {configuration.AdditionalArguments.Trim()}";
|
|
|
|
var arguments =
|
|
$"-f {configuration.Format} " +
|
|
"--no-playlist " +
|
|
$"-o \"{destinationFilePath}\"" +
|
|
additionalArguments +
|
|
$" -- {videoId}";
|
|
|
|
using var process = new Process
|
|
{
|
|
StartInfo = new ProcessStartInfo
|
|
{
|
|
FileName = configuration.ExecutableName,
|
|
WorkingDirectory = destinationDirectory ?? Environment.CurrentDirectory,
|
|
Arguments = arguments,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
}
|
|
};
|
|
|
|
process.Start();
|
|
|
|
logger.LogInformation("yt-dlp started (PID: {ProcessId} ) for video {VideoId}", process.Id, videoId);
|
|
|
|
var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
|
var errorTask = process.StandardError.ReadToEndAsync(cancellationToken);
|
|
|
|
using var linkedCancellationTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
linkedCancellationTokens.CancelAfter(configuration.DownloadTimeout);
|
|
|
|
try
|
|
{
|
|
await process.WaitForExitAsync(linkedCancellationTokens.Token);
|
|
}
|
|
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
logger.LogError("yt-dlp timed out after {Timeout} for video {VideoId} (PID: {ProcessId} )",
|
|
configuration.DownloadTimeout, videoId, process.Id);
|
|
|
|
TryKillProcess(process);
|
|
throw new TimeoutException($"yt-dlp execution exceeded timeout {configuration.DownloadTimeout}.");
|
|
}
|
|
|
|
var standardError = await errorTask;
|
|
var standardOutput = await outputTask;
|
|
|
|
logger.LogDebug("yt-dlp finished (PID: {ProcessId} ). Exit code: {ExitCode}, Output: {OutputLength}B, Error: {ErrorLength}B",
|
|
process.Id, process.ExitCode, standardOutput.Length, standardError.Length);
|
|
|
|
return new YouTubeDownloadResult(process.ExitCode, standardError);
|
|
}
|
|
|
|
void TryKillProcess(Process process)
|
|
{
|
|
try
|
|
{
|
|
if (!process.HasExited)
|
|
process.Kill();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
logger.LogError(exception, "Failed to kill yt-dlp process (PID: {ProcessId} )", process.Id);
|
|
}
|
|
}
|
|
}
|
|
}
|