using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.MediaEncoding; /// /// Transcoding segment cleaner. /// public class TranscodingSegmentCleaner : IDisposable { private readonly TranscodingJob _job; private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; private Timer? _timer; private int _segmentLength; /// /// Initializes a new instance of the class. /// /// Transcoding job dto. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// The segment length of this transcoding job. public TranscodingSegmentCleaner(TranscodingJob job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder, int segmentLength) { _job = job; _logger = logger; _config = config; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; _segmentLength = segmentLength; } /// /// Start timer. /// public void Start() { _timer = new Timer(TimerCallback, null, 20000, 20000); } /// /// Stop cleaner. /// public void Stop() { DisposeTimer(); } /// /// Dispose cleaner. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Dispose cleaner. /// /// Disposing. protected virtual void Dispose(bool disposing) { if (disposing) { DisposeTimer(); } } private EncodingOptions GetOptions() { return _config.GetEncodingOptions(); } private async void TimerCallback(object? state) { if (_job.HasExited) { DisposeTimer(); return; } var options = GetOptions(); var enableSegmentDeletion = options.EnableSegmentDeletion; var segmentKeepSeconds = Math.Max(options.SegmentKeepSeconds, 20); if (enableSegmentDeletion) { var downloadPositionTicks = _job.DownloadPositionTicks ?? 0; var downloadPositionSeconds = Convert.ToInt64(TimeSpan.FromTicks(downloadPositionTicks).TotalSeconds); if (downloadPositionSeconds > 0 && segmentKeepSeconds > 0 && downloadPositionSeconds > segmentKeepSeconds) { var idxMaxToDelete = (downloadPositionSeconds - segmentKeepSeconds) / _segmentLength; if (idxMaxToDelete > 0) { await DeleteSegmentFiles(_job, 0, idxMaxToDelete, 1500).ConfigureAwait(false); } } } } private async Task DeleteSegmentFiles(TranscodingJob job, long idxMin, long idxMax, int delayMs) { var path = job.Path ?? throw new ArgumentException("Path can't be null."); _logger.LogDebug("Deleting segment file(s) index {Min} to {Max} from {Path}", idxMin, idxMax, path); await Task.Delay(delayMs).ConfigureAwait(false); try { if (job.Type == TranscodingJobType.Hls) { DeleteHlsSegmentFiles(path, idxMin, idxMax); } } catch (Exception ex) { _logger.LogDebug(ex, "Error deleting segment file(s) {Path}", path); } } private void DeleteHlsSegmentFiles(string outputFilePath, long idxMin, long idxMax) { var directory = Path.GetDirectoryName(outputFilePath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath)); var name = Path.GetFileNameWithoutExtension(outputFilePath); var filesToDelete = _fileSystem.GetFilePaths(directory) .Where(f => long.TryParse(Path.GetFileNameWithoutExtension(f).Replace(name, string.Empty, StringComparison.Ordinal), out var idx) && (idx >= idxMin && idx <= idxMax)); List? exs = null; foreach (var file in filesToDelete) { try { _logger.LogDebug("Deleting HLS segment file {0}", file); _fileSystem.DeleteFile(file); } catch (IOException ex) { (exs ??= new List()).Add(ex); _logger.LogDebug(ex, "Error deleting HLS segment file {Path}", file); } } if (exs is not null) { throw new AggregateException("Error deleting HLS segment files", exs); } } private void DisposeTimer() { if (_timer is not null) { _timer.Dispose(); _timer = null; } } }