Prepare Linux deployment with yt-dlp and persistent downloads

This commit is contained in:
Michal Vrba
2026-10-19 17:23:30 +02:00
parent 471010c536
commit a26c584462
15 changed files with 331 additions and 139 deletions
+56 -31
View File
@@ -1,7 +1,6 @@
using TuneRipper.Shared.Models;
using System.Text.RegularExpressions;
using System.Net.Http;
using System.Threading.Tasks;
using TuneRipper.Api.Configuration;
using Microsoft.Extensions.Options;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Text;
@@ -21,15 +20,19 @@ public class VideoService : IVideoService
{
private readonly ILogger<VideoService> _logger;
private readonly HttpClient _httpClient;
private readonly IWebHostEnvironment _environment;
private readonly ISessionService _sessionService;
private readonly YtDlpOptions _options;
public VideoService(ILogger<VideoService> logger, IWebHostEnvironment environment, IHttpClientFactory httpClientFactory, ISessionService sessionService)
public VideoService(
ILogger<VideoService> logger,
IHttpClientFactory httpClientFactory,
ISessionService sessionService,
IOptions<YtDlpOptions> options)
{
_logger = logger;
_httpClient = httpClientFactory.CreateClient();
_environment = environment;
_sessionService = sessionService;
_options = options.Value;
}
public async Task<YouTubeVideoDetails?> GetVideoDetailsAsync(string url)
@@ -97,11 +100,20 @@ public class VideoService : IVideoService
{
try
{
var ytDlpPath = Path.Combine(_environment.WebRootPath, "yt-dlp.exe");
if (!File.Exists(ytDlpPath))
var ytDlpPath = _options.ExecutablePath;
if (Path.IsPathRooted(ytDlpPath) && !File.Exists(ytDlpPath))
{
throw new FileNotFoundException($"yt-dlp.exe not found at {ytDlpPath}");
throw new FileNotFoundException($"yt-dlp not found at {ytDlpPath}");
}
var supportedFormats = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"mp3", "m4a", "opus", "wav", "flac"
};
if (!supportedFormats.Contains(format))
{
throw new ArgumentException($"Unsupported audio format: {format}", nameof(format));
}
// Ensure session directory exists only when we're about to save the first MP3 file
@@ -120,29 +132,33 @@ public class VideoService : IVideoService
_logger.LogInformation("Processing video with filename pattern: {FileName} in session: {SessionId}", customFileName, sessionId);
// Build yt-dlp arguments
var arguments = new StringBuilder();
arguments.Append($"--extract-audio ");
arguments.Append($"--audio-format {format} ");
arguments.Append($"--audio-quality 0 ");
arguments.Append("--no-playlist ");
arguments.Append("--no-warnings ");
arguments.Append("--newline ");
arguments.Append($"--output \"{outputTemplate}\" ");
arguments.Append($"\"{url}\"");
_logger.LogInformation("Starting yt-dlp process with arguments: {Args}", arguments.ToString());
var processInfo = new ProcessStartInfo
{
FileName = ytDlpPath,
Arguments = arguments.ToString(),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WorkingDirectory = downloadDir
};
processInfo.ArgumentList.Add("--extract-audio");
processInfo.ArgumentList.Add("--audio-format");
processInfo.ArgumentList.Add(format);
processInfo.ArgumentList.Add("--audio-quality");
processInfo.ArgumentList.Add("0");
processInfo.ArgumentList.Add("--no-playlist");
processInfo.ArgumentList.Add("--no-warnings");
processInfo.ArgumentList.Add("--newline");
processInfo.ArgumentList.Add("--ffmpeg-location");
processInfo.ArgumentList.Add(_options.FfmpegExecutablePath);
processInfo.ArgumentList.Add("--output");
processInfo.ArgumentList.Add(outputTemplate);
processInfo.ArgumentList.Add(url);
_logger.LogInformation(
"Starting yt-dlp for job {JobId} with format {Format}",
jobId,
format);
using var process = Process.Start(processInfo);
if (process == null)
@@ -178,13 +194,22 @@ public class VideoService : IVideoService
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// Wait for process completion with timeout
var completed = await Task.Run(() => process.WaitForExit(300000)); // 5 minute timeout
if (!completed)
using var timeout = new CancellationTokenSource(
TimeSpan.FromSeconds(Math.Max(30, _options.ProcessTimeoutSeconds)));
try
{
process.Kill();
throw new TimeoutException("yt-dlp process timed out after 5 minutes");
await process.WaitForExitAsync(timeout.Token);
}
catch (OperationCanceledException)
{
if (!process.HasExited)
{
process.Kill(entireProcessTree: true);
}
throw new TimeoutException(
$"yt-dlp process timed out after {_options.ProcessTimeoutSeconds} seconds");
}
if (process.ExitCode != 0)
@@ -463,4 +488,4 @@ public class VideoService : IVideoService
return string.Empty;
}
}
}