From 44086b9c05292703bb276e543005f06af4513d36 Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:07:58 -0500 Subject: [PATCH 01/10] Initial caption scanning implementation --- .../Options/VideoDownloadOptions.cs | 2 + TwitchDownloaderCore/Tools/Enums.cs | 7 ++ TwitchDownloaderCore/VideoDownloader.cs | 71 ++++++++++++++++--- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/TwitchDownloaderCore/Options/VideoDownloadOptions.cs b/TwitchDownloaderCore/Options/VideoDownloadOptions.cs index 89c1c22f..0dedeb4c 100644 --- a/TwitchDownloaderCore/Options/VideoDownloadOptions.cs +++ b/TwitchDownloaderCore/Options/VideoDownloadOptions.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using TwitchDownloaderCore.Tools; namespace TwitchDownloaderCore.Options { @@ -15,6 +16,7 @@ public class VideoDownloadOptions public int DownloadThreads { get; set; } public int ThrottleKib { get; set; } public string Oauth { get; set; } + public CaptionsStyle Captions { get; set; } public string FfmpegPath { get; set; } public string TempFolder { get; set; } public Func CacheCleanerCallback { get; set; } diff --git a/TwitchDownloaderCore/Tools/Enums.cs b/TwitchDownloaderCore/Tools/Enums.cs index 1d2a4b94..02efea7d 100644 --- a/TwitchDownloaderCore/Tools/Enums.cs +++ b/TwitchDownloaderCore/Tools/Enums.cs @@ -21,4 +21,11 @@ public enum TimestampFormat None, UtcFull } + + public enum CaptionsStyle + { + None, + Embedded, + SeparateSrt + } } \ No newline at end of file diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index 00800416..7d9c46b9 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -25,6 +25,8 @@ public sealed class VideoDownloader private readonly IProgress _progress; private bool _shouldClearCache = true; + private const string TOTAL_STEPS = "6"; + public VideoDownloader(VideoDownloadOptions videoDownloadOptions, IProgress progress) { downloadOptions = videoDownloadOptions; @@ -42,7 +44,7 @@ public async Task DownloadAsync(CancellationToken cancellationToken) downloadOptions.TempFolder, $"{downloadOptions.Id}_{DateTimeOffset.UtcNow.Ticks}"); - _progress.Report(new ProgressReport(ReportType.SameLineStatus, "Fetching Video Info [1/5]")); + _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Fetching Video Info [1/{TOTAL_STEPS}]")); try { @@ -70,19 +72,28 @@ public async Task DownloadAsync(CancellationToken cancellationToken) Directory.Delete(downloadFolder, true); TwitchHelper.CreateDirectory(downloadFolder); - _progress.Report(new ProgressReport(ReportType.NewLineStatus, "Downloading 0% [2/5]")); + _progress.Report(new ProgressReport(ReportType.NewLineStatus, $"Downloading 0% [2/{TOTAL_STEPS}]")); await DownloadVideoPartsAsync(playlist.Streams, videoListCrop, baseUrl, downloadFolder, vodAge, cancellationToken); - _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Verifying Parts 0% [3/5]" }); + _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = $"Verifying Parts 0% [3/{TOTAL_STEPS}]" }); await VerifyDownloadedParts(playlist.Streams, videoListCrop, baseUrl, downloadFolder, vodAge, cancellationToken); - _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Combining Parts 0% [4/5]" }); + + string captionsPath = null; + if (downloadOptions.Captions != CaptionsStyle.None) + { + _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = $"Extracting captions 0% [4/{TOTAL_STEPS}]" }); + + captionsPath = await ExtractCaptions(playlist.Streams, videoListCrop, downloadFolder, cancellationToken); + } + + _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = $"Combining Parts 0% [5/{TOTAL_STEPS}]" }); await CombineVideoParts(downloadFolder, playlist.Streams, videoListCrop, cancellationToken); - _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Finalizing Video 0% [5/5]" }); + _progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = $"Finalizing Video 0% [6/{TOTAL_STEPS}]" }); var startOffsetSeconds = (double)playlist.Streams .Take(videoListCrop.Start.Value) @@ -106,7 +117,7 @@ await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, d var ffmpegRetries = 0; do { - ffmpegExitCode = await Task.Run(() => RunFfmpegVideoCopy(downloadFolder, metadataPath, startOffsetSeconds, seekDuration), cancellationToken); + ffmpegExitCode = await Task.Run(() => RunFfmpegVideoCopy(downloadFolder, metadataPath, captionsPath, startOffsetSeconds, seekDuration), cancellationToken); if (ffmpegExitCode != 0) { _progress.Report(new ProgressReport(ReportType.Log, $"Failed to finalize video (code {ffmpegExitCode}), retrying in 10 seconds...")); @@ -120,7 +131,7 @@ await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, d throw new Exception($"Failed to finalize video. The download cache has not been cleared and can be found at {downloadFolder} along with a log file."); } - _progress.Report(new ProgressReport(ReportType.SameLineStatus, "Finalizing Video 100% [5/5]")); + _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Finalizing Video 100% [6/{TOTAL_STEPS}]")); _progress.Report(new ProgressReport(100)); } finally @@ -247,7 +258,7 @@ private async Task> WaitForDownloadThreads(Task[] { previousDoneCount = videoPartsQueue.Count; var percent = (int)((partCount - previousDoneCount) / (double)partCount * 100); - _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Downloading {percent}% [2/5]")); + _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Downloading {percent}% [2/{TOTAL_STEPS}]")); _progress.Report(new ProgressReport(percent)); } @@ -337,7 +348,7 @@ private async Task VerifyDownloadedParts(ICollection playlist, Rang doneCount++; var percent = (int)(doneCount / (double)partCount * 100); - _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Verifying Parts {percent}% [3/5]")); + _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Verifying Parts {percent}% [3/{TOTAL_STEPS}]")); _progress.Report(new ProgressReport(percent)); cancellationToken.ThrowIfCancellationRequested(); @@ -382,7 +393,45 @@ private static bool VerifyVideoPart(string filePath) return true; } - private int RunFfmpegVideoCopy(string downloadFolder, string metadataPath, double startOffset, double seekDuration) + private async Task ExtractCaptions(ICollection playlist, Range videoListCrop, string downloadFolder, CancellationToken cancellationToken) + { + var partCount = videoListCrop.End.Value - videoListCrop.Start.Value; + var doneCount = 0; + var captionFilePath = Path.Combine(downloadFolder, "captions.srt"); + await using var finalCaptionFile = new FileStream(captionFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + + foreach (var videoPart in playlist) + { + cancellationToken.ThrowIfCancellationRequested(); + + var videoPartPath = Path.Combine(downloadFolder, RemoveQueryString(videoPart.Path)); + var videoPartCaptionPath = $"{videoPartPath}.srt"; + // TODO: Implement custom caption scanner instead of using FFmpeg. FFmpeg is so slow :/ + var process = new Process + { + StartInfo = + { + FileName = downloadOptions.FfmpegPath, + Arguments = $"-y -f lavfi -loglevel quiet -i movie={videoPartPath}[out+subcc] -map 0:1 {videoPartCaptionPath}", + UseShellExecute = false, + CreateNoWindow = true + } + }; + process.Start(); + await process.WaitForExitAsync(cancellationToken); + + // TODO: renumber/re-time caption segments, then write to finalCaptionFile + + doneCount++; + var percent = (int)(doneCount / (double)partCount * 100); + _progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Extracting Captions {percent}% [4/{TOTAL_STEPS}]")); + _progress.Report(new ProgressReport(percent)); + } + + return captionFilePath; + } + + private int RunFfmpegVideoCopy(string downloadFolder, string metadataPath, string captionsPath, double startOffset, double seekDuration) { var process = new Process { @@ -678,7 +727,7 @@ private async Task CombineVideoParts(string downloadFolder, IEnumerable Date: Wed, 1 May 2024 15:00:28 -0400 Subject: [PATCH 02/10] Fix --- TwitchDownloaderCore/VideoDownloader.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index bba55d73..4dcc98b1 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -83,8 +83,6 @@ public async Task DownloadAsync(CancellationToken cancellationToken) await VerifyDownloadedParts(playlist.Streams, videoListCrop, baseUrl, downloadFolder, airDate, cancellationToken); - _progress.SetTemplateStatus($"Combining Parts {{0}}% [4/{TOTAL_STEPS}]", 0); - string captionsPath = null; if (downloadOptions.Captions != CaptionsStyle.None) { @@ -97,7 +95,7 @@ public async Task DownloadAsync(CancellationToken cancellationToken) await CombineVideoParts(downloadFolder, playlist.Streams, videoListCrop, cancellationToken); - _progress.SetTemplateStatus($"Finalizing Video {{0}}% [5/{TOTAL_STEPS}]", 0); + _progress.SetTemplateStatus($"Finalizing Video {{0}}% [6/{TOTAL_STEPS}]", 0); var startOffset = TimeSpan.FromSeconds((double)playlist.Streams .Take(videoListCrop.Start.Value) From 4761334703777771c8d19c56bf3b703f08a8080d Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Mon, 24 Jun 2024 19:27:38 -0400 Subject: [PATCH 03/10] Use generic IEnumerable for FfmpegConcatList --- TwitchDownloaderCore/Tools/FfmpegConcatList.cs | 11 +++++------ TwitchDownloaderCore/VideoDownloader.cs | 3 ++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/TwitchDownloaderCore/Tools/FfmpegConcatList.cs b/TwitchDownloaderCore/Tools/FfmpegConcatList.cs index c3ea6c5b..2add5e94 100644 --- a/TwitchDownloaderCore/Tools/FfmpegConcatList.cs +++ b/TwitchDownloaderCore/Tools/FfmpegConcatList.cs @@ -1,7 +1,6 @@ -using System; +using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,23 +11,23 @@ public static class FfmpegConcatList { private const string LINE_FEED = "\u000A"; - public static async Task SerializeAsync(string filePath, M3U8 playlist, Range videoListCrop, CancellationToken cancellationToken = default) + public static async Task SerializeAsync(string filePath, IEnumerable<(string path, decimal duration)> playlist, CancellationToken cancellationToken = default) { await using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read); await using var sw = new StreamWriter(fs) { NewLine = LINE_FEED }; await sw.WriteLineAsync("ffconcat version 1.0"); - foreach (var stream in playlist.Streams.Take(videoListCrop)) + foreach (var stream in playlist) { cancellationToken.ThrowIfCancellationRequested(); await sw.WriteAsync("file '"); - await sw.WriteAsync(DownloadTools.RemoveQueryString(stream.Path)); + await sw.WriteAsync(DownloadTools.RemoveQueryString(stream.path)); await sw.WriteLineAsync('\''); await sw.WriteAsync("duration "); - await sw.WriteLineAsync(stream.PartInfo.Duration.ToString(CultureInfo.InvariantCulture)); + await sw.WriteLineAsync(stream.duration.ToString(CultureInfo.InvariantCulture)); } } } diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index e28785cb..9f3830c3 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -130,7 +130,8 @@ await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, d videoLength, videoChapterResponse.data.video.moments.edges, cancellationToken); var concatListPath = Path.Combine(downloadFolder, "concat.txt"); - await FfmpegConcatList.SerializeAsync(concatListPath, playlist, videoListCrop, cancellationToken); + var toConcat = playlist.Streams.Take(videoListCrop).Select(x => (x.Path, x.PartInfo.Duration)); + await FfmpegConcatList.SerializeAsync(concatListPath, toConcat, cancellationToken); outputFs.Close(); From 9365f6d9e2e7f32d70971fc183fd59f9b500ac41 Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Mon, 24 Jun 2024 20:22:35 -0400 Subject: [PATCH 04/10] Working caption concatenation --- TwitchDownloaderCore/VideoDownloader.cs | 125 +++++++++++++++++------- 1 file changed, 90 insertions(+), 35 deletions(-) diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index 9f3830c3..71ab4833 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -119,7 +119,7 @@ private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputF { _progress.SetTemplateStatus($"Extracting captions {{0}}% [4/{TOTAL_STEPS}]", 0); - captionsPath = await ExtractCaptions(playlist.Streams, videoListCrop, downloadFolder, cancellationToken); + captionsPath = await ExtractCaptions(playlist.Streams, videoListCrop, downloadFolder, startOffset, endOffset, videoLength, cancellationToken); } _progress.SetTemplateStatus($"Finalizing Video {{0}}% [5/{TOTAL_STEPS}]", 0); @@ -337,59 +337,117 @@ private async Task VerifyDownloadedParts(ICollection playlist, Rang } } - private async Task ExtractCaptions(ICollection playlist, Range videoListCrop, string downloadFolder, CancellationToken cancellationToken) + private async Task ExtractCaptions(ICollection playlist, Range videoListCrop, string downloadFolder, decimal startOffset, decimal endOffset, TimeSpan videoLength, CancellationToken cancellationToken) { var partCount = videoListCrop.End.Value - videoListCrop.Start.Value; var doneCount = 0; - var captionFilePath = Path.Combine(downloadFolder, "captions.srt"); - await using var finalCaptionFile = new FileStream(captionFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + var concatList = new List<(string, decimal)>(partCount); - foreach (var videoPart in playlist) + foreach (var videoPart in playlist.Take(videoListCrop)) { - cancellationToken.ThrowIfCancellationRequested(); + doneCount = await RunFfmpegSubtitleExtract(downloadFolder, cancellationToken, videoPart, concatList, partCount, doneCount); + } - var videoPartPath = Path.Combine(downloadFolder, DownloadTools.RemoveQueryString(videoPart.Path)); - var videoPartCaptionPath = $"{videoPartPath}.srt"; - // TODO: Implement custom caption scanner instead of using FFmpeg. FFmpeg is so slow :/ - var process = new Process - { - StartInfo = - { - FileName = downloadOptions.FfmpegPath, - Arguments = $"-y -f lavfi -loglevel quiet -i movie={videoPartPath}[out+subcc] -map 0:1 {videoPartCaptionPath}", - UseShellExecute = false, - CreateNoWindow = true - } - }; - process.Start(); - await process.WaitForExitAsync(cancellationToken); + var finalCaptionPath = Path.Combine(downloadFolder, "captions.srt"); + var concatListPath = Path.Combine(downloadFolder, "srt_concat.txt"); + await FfmpegConcatList.SerializeAsync(concatListPath, concatList, cancellationToken); - // TODO: renumber/re-time caption segments, then write to finalCaptionFile + await RunFfmpegSubtitleConcat(downloadFolder, concatListPath, finalCaptionPath, startOffset, endOffset, videoLength, cancellationToken); - doneCount++; - var percent = (int)(doneCount / (double)partCount * 100); - _progress.ReportProgress(percent); + var fi = new FileInfo(finalCaptionPath); + if (!fi.Exists || fi.Length == 0) + { + // Video does not contain captions or something went wrong during the concat + return null; } - return captionFilePath; + return finalCaptionPath; } - private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string concatListPath, string metadataPath, string captionsPath, decimal startOffset, decimal endOffset, TimeSpan videoLength) + private static Process GetFfmpegProcess(string ffmpegPath, string workingDirectory, IEnumerable args) { - using var process = new Process + var process = new Process { StartInfo = { - FileName = downloadOptions.FfmpegPath, + FileName = ffmpegPath, UseShellExecute = false, CreateNoWindow = true, - RedirectStandardInput = false, RedirectStandardOutput = true, RedirectStandardError = true, - WorkingDirectory = tempFolder + WorkingDirectory = workingDirectory } }; + foreach (var arg in args) + { + process.StartInfo.ArgumentList.Add(arg); + } + + return process; + } + + private async Task RunFfmpegSubtitleExtract(string tempFolder, CancellationToken cancellationToken, M3U8.Stream videoPart, List<(string, decimal)> concatList, int partCount, int doneCount) + { + cancellationToken.ThrowIfCancellationRequested(); + + var partName = DownloadTools.RemoveQueryString(videoPart.Path); + var captionPath = $"{partName}.srt"; + + // movie=file.ts[out+subcc] is super slow, but `-i file.ts file.srt` doesn't seem to work on TS files :/ + var args = new List + { + "-y", + "-f", "lavfi", + "-i", $"movie={partName}[out+subcc]", + "-map", "0:1", + captionPath + }; + + using var process = GetFfmpegProcess(downloadOptions.FfmpegPath, tempFolder, args); + + process.Start(); + await process.WaitForExitAsync(cancellationToken); + + concatList.Add((Path.GetFileName(captionPath), videoPart.PartInfo.Duration)); + + doneCount++; + var percent = (int)(doneCount / (double)partCount * 100); + _progress.ReportProgress(percent); + + return doneCount; + } + + private async Task RunFfmpegSubtitleConcat(string tempFolder, string concatListPath, string outputPath, decimal startOffset, decimal endOffset, TimeSpan videoLength, CancellationToken cancellationToken) + { + var args = new List + { + "-y", + "-f", "concat", + "-i", concatListPath, + outputPath + }; + + if (endOffset > 0) + { + args.Insert(0, "-t"); + args.Insert(1, videoLength.TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } + + if (startOffset > 0) + { + args.Insert(0, "-ss"); + args.Insert(1, startOffset.ToString(CultureInfo.InvariantCulture)); + } + + using var process = GetFfmpegProcess(downloadOptions.FfmpegPath, tempFolder, args); + + process.Start(); + await process.WaitForExitAsync(cancellationToken); + } + + private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string concatListPath, string metadataPath, string captionsPath, decimal startOffset, decimal endOffset, TimeSpan videoLength) + { var args = new List { "-stats", @@ -418,10 +476,7 @@ private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string co args.Insert(1, startOffset.ToString(CultureInfo.InvariantCulture)); } - foreach (var arg in args) - { - process.StartInfo.ArgumentList.Add(arg); - } + using var process = GetFfmpegProcess(downloadOptions.FfmpegPath, tempFolder, args); var encodingTimeRegex = new Regex(@"(?<=time=)(\d\d):(\d\d):(\d\d)\.(\d\d)", RegexOptions.Compiled); var logQueue = new ConcurrentQueue(); From 4df206307e91dbf611acebf72c9010abc2516734 Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Wed, 26 Jun 2024 23:00:31 -0400 Subject: [PATCH 05/10] Log when no captions are found --- TwitchDownloaderCore/VideoDownloader.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index 71ab4833..756c4800 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -358,6 +358,7 @@ private async Task ExtractCaptions(ICollection playlist, Ra if (!fi.Exists || fi.Length == 0) { // Video does not contain captions or something went wrong during the concat + _progress.LogInfo("Video does not contain any captions."); return null; } From 027d947f3b1b0208b110a6ea905d14715a1f0591 Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Wed, 26 Jun 2024 23:08:49 -0400 Subject: [PATCH 06/10] Captions -> subtitles --- .../Options/VideoDownloadOptions.cs | 2 +- TwitchDownloaderCore/Tools/Enums.cs | 2 +- TwitchDownloaderCore/VideoDownloader.cs | 33 +++++++++---------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/TwitchDownloaderCore/Options/VideoDownloadOptions.cs b/TwitchDownloaderCore/Options/VideoDownloadOptions.cs index caec0747..7684ccd9 100644 --- a/TwitchDownloaderCore/Options/VideoDownloadOptions.cs +++ b/TwitchDownloaderCore/Options/VideoDownloadOptions.cs @@ -16,7 +16,7 @@ public class VideoDownloadOptions public int DownloadThreads { get; set; } public int ThrottleKib { get; set; } public string Oauth { get; set; } - public CaptionsStyle Captions { get; set; } + public SubtitlesStyle SubtitlesStyle { get; set; } public string FfmpegPath { get; set; } public string TempFolder { get; set; } public Func CacheCleanerCallback { get; set; } diff --git a/TwitchDownloaderCore/Tools/Enums.cs b/TwitchDownloaderCore/Tools/Enums.cs index d37a8d45..4726af93 100644 --- a/TwitchDownloaderCore/Tools/Enums.cs +++ b/TwitchDownloaderCore/Tools/Enums.cs @@ -28,7 +28,7 @@ public enum VideoTrimMode Exact } - public enum CaptionsStyle + public enum SubtitlesStyle { None, Embedded, diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index f9085460..2bfe4705 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -114,12 +114,12 @@ private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputF await VerifyDownloadedParts(playlist.Streams, videoListCrop, baseUrl, downloadFolder, airDate, cancellationToken); - string captionsPath = null; - if (downloadOptions.Captions != CaptionsStyle.None) + string subtitlesPath = null; + if (downloadOptions.SubtitlesStyle != SubtitlesStyle.None) { - _progress.SetTemplateStatus($"Extracting captions {{0}}% [4/{TOTAL_STEPS}]", 0); + _progress.SetTemplateStatus($"Extracting subtitles {{0}}% [4/{TOTAL_STEPS}]", 0); - captionsPath = await ExtractCaptions(playlist.Streams, videoListCrop, downloadFolder, startOffset, endOffset, videoLength, cancellationToken); + subtitlesPath = await ExtractSubtitles(playlist.Streams, videoListCrop, downloadFolder, startOffset, endDuration, videoLength, cancellationToken); } _progress.SetTemplateStatus($"Finalizing Video {{0}}% [5/{TOTAL_STEPS}]", 0); @@ -139,7 +139,7 @@ await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, d var ffmpegRetries = 0; do { - ffmpegExitCode = await Task.Run(() => RunFfmpegVideoCopy(downloadFolder, outputFileInfo, concatListPath, metadataPath, captionsPath, startOffset, endDuration, videoLength), cancellationToken); + ffmpegExitCode = await Task.Run(() => RunFfmpegVideoCopy(downloadFolder, outputFileInfo, concatListPath, metadataPath, subtitlesPath, startOffset, endDuration, videoLength), cancellationToken); if (ffmpegExitCode != 0) { _progress.LogError($"Failed to finalize video (code {ffmpegExitCode}), retrying in 10 seconds..."); @@ -337,8 +337,7 @@ private async Task VerifyDownloadedParts(ICollection playlist, Rang } } - - private async Task ExtractCaptions(ICollection playlist, Range videoListCrop, string downloadFolder, decimal startOffset, decimal endOffset, TimeSpan videoLength, CancellationToken cancellationToken) + private async Task ExtractSubtitles(ICollection playlist, Range videoListCrop, string downloadFolder, decimal startOffset, decimal endOffset, TimeSpan videoLength, CancellationToken cancellationToken) { var partCount = videoListCrop.End.Value - videoListCrop.Start.Value; var doneCount = 0; @@ -349,21 +348,21 @@ private async Task ExtractCaptions(ICollection playlist, Ra doneCount = await RunFfmpegSubtitleExtract(downloadFolder, cancellationToken, videoPart, concatList, partCount, doneCount); } - var finalCaptionPath = Path.Combine(downloadFolder, "captions.srt"); + var finalSubtitlePath = Path.Combine(downloadFolder, "subtitles.srt"); var concatListPath = Path.Combine(downloadFolder, "srt_concat.txt"); await FfmpegConcatList.SerializeAsync(concatListPath, concatList, cancellationToken); - await RunFfmpegSubtitleConcat(downloadFolder, concatListPath, finalCaptionPath, startOffset, endOffset, videoLength, cancellationToken); + await RunFfmpegSubtitleConcat(downloadFolder, concatListPath, finalSubtitlePath, startOffset, endOffset, videoLength, cancellationToken); - var fi = new FileInfo(finalCaptionPath); + var fi = new FileInfo(finalSubtitlePath); if (!fi.Exists || fi.Length == 0) { - // Video does not contain captions or something went wrong during the concat - _progress.LogInfo("Video does not contain any captions."); + // Video does not contain subtitles or something went wrong during the concat + _progress.LogInfo("Video does not contain any subtitles."); return null; } - return finalCaptionPath; + return finalSubtitlePath; } private static Process GetFfmpegProcess(string ffmpegPath, string workingDirectory, IEnumerable args) @@ -394,7 +393,7 @@ private async Task RunFfmpegSubtitleExtract(string tempFolder, Cancellation cancellationToken.ThrowIfCancellationRequested(); var partName = DownloadTools.RemoveQueryString(videoPart.Path); - var captionPath = $"{partName}.srt"; + var subtitlePath = $"{partName}.srt"; // movie=file.ts[out+subcc] is super slow, but `-i file.ts file.srt` doesn't seem to work on TS files :/ var args = new List @@ -403,7 +402,7 @@ private async Task RunFfmpegSubtitleExtract(string tempFolder, Cancellation "-f", "lavfi", "-i", $"movie={partName}[out+subcc]", "-map", "0:1", - captionPath + subtitlePath }; using var process = GetFfmpegProcess(downloadOptions.FfmpegPath, tempFolder, args); @@ -411,7 +410,7 @@ private async Task RunFfmpegSubtitleExtract(string tempFolder, Cancellation process.Start(); await process.WaitForExitAsync(cancellationToken); - concatList.Add((Path.GetFileName(captionPath), videoPart.PartInfo.Duration)); + concatList.Add((Path.GetFileName(subtitlePath), videoPart.PartInfo.Duration)); doneCount++; var percent = (int)(doneCount / (double)partCount * 100); @@ -448,7 +447,7 @@ private async Task RunFfmpegSubtitleConcat(string tempFolder, string concatListP await process.WaitForExitAsync(cancellationToken); } - private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string concatListPath, string metadataPath, string captionsPath, decimal startOffset, decimal endDuration, TimeSpan videoLength) + private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string concatListPath, string metadataPath, string subtitlesPath, decimal startOffset, decimal endDuration, TimeSpan videoLength) { var args = new List { From f2907c5dd4716fa74d25da8440612f8f784cfbfa Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Wed, 26 Jun 2024 23:20:34 -0400 Subject: [PATCH 07/10] Separate subtitles extraction and concatenation --- TwitchDownloaderCore/VideoDownloader.cs | 36 +++++++++++++++---------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index 2bfe4705..1c57ac8b 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -119,7 +119,9 @@ private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputF { _progress.SetTemplateStatus($"Extracting subtitles {{0}}% [4/{TOTAL_STEPS}]", 0); - subtitlesPath = await ExtractSubtitles(playlist.Streams, videoListCrop, downloadFolder, startOffset, endDuration, videoLength, cancellationToken); + var subtitlesConcatListPath = await ExtractSubtitles(playlist.Streams, videoListCrop, downloadFolder, cancellationToken); + + subtitlesPath = await ConcatSubtitles(subtitlesConcatListPath, downloadFolder, startOffset, endDuration, videoLength, cancellationToken); } _progress.SetTemplateStatus($"Finalizing Video {{0}}% [5/{TOTAL_STEPS}]", 0); @@ -337,7 +339,7 @@ private async Task VerifyDownloadedParts(ICollection playlist, Rang } } - private async Task ExtractSubtitles(ICollection playlist, Range videoListCrop, string downloadFolder, decimal startOffset, decimal endOffset, TimeSpan videoLength, CancellationToken cancellationToken) + private async Task ExtractSubtitles(ICollection playlist, Range videoListCrop, string downloadFolder, CancellationToken cancellationToken) { var partCount = videoListCrop.End.Value - videoListCrop.Start.Value; var doneCount = 0; @@ -348,21 +350,10 @@ private async Task ExtractSubtitles(ICollection playlist, R doneCount = await RunFfmpegSubtitleExtract(downloadFolder, cancellationToken, videoPart, concatList, partCount, doneCount); } - var finalSubtitlePath = Path.Combine(downloadFolder, "subtitles.srt"); var concatListPath = Path.Combine(downloadFolder, "srt_concat.txt"); await FfmpegConcatList.SerializeAsync(concatListPath, concatList, cancellationToken); - await RunFfmpegSubtitleConcat(downloadFolder, concatListPath, finalSubtitlePath, startOffset, endOffset, videoLength, cancellationToken); - - var fi = new FileInfo(finalSubtitlePath); - if (!fi.Exists || fi.Length == 0) - { - // Video does not contain subtitles or something went wrong during the concat - _progress.LogInfo("Video does not contain any subtitles."); - return null; - } - - return finalSubtitlePath; + return concatListPath; } private static Process GetFfmpegProcess(string ffmpegPath, string workingDirectory, IEnumerable args) @@ -419,6 +410,23 @@ private async Task RunFfmpegSubtitleExtract(string tempFolder, Cancellation return doneCount; } + private async Task ConcatSubtitles(string concatListPath, string downloadFolder, decimal startOffset, decimal endOffset, TimeSpan videoLength, CancellationToken cancellationToken) + { + var finalSubtitlePath = Path.Combine(downloadFolder, "subtitles.srt"); + + await RunFfmpegSubtitleConcat(downloadFolder, concatListPath, finalSubtitlePath, startOffset, endOffset, videoLength, cancellationToken); + + var fi = new FileInfo(finalSubtitlePath); + if (!fi.Exists || fi.Length == 0) + { + // Video does not contain subtitles or something went wrong during the concat + _progress.LogInfo("Video does not contain any subtitles."); + return null; + } + + return finalSubtitlePath; + } + private async Task RunFfmpegSubtitleConcat(string tempFolder, string concatListPath, string outputPath, decimal startOffset, decimal endOffset, TimeSpan videoLength, CancellationToken cancellationToken) { var args = new List From 9c71519774020cd824891e71f5a2f4b66bfc842d Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:27:21 -0400 Subject: [PATCH 08/10] Implement exporting subtitles to an output file --- TwitchDownloaderCore/Tools/Enums.cs | 2 +- TwitchDownloaderCore/VideoDownloader.cs | 45 +++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/TwitchDownloaderCore/Tools/Enums.cs b/TwitchDownloaderCore/Tools/Enums.cs index 4726af93..8e3402b1 100644 --- a/TwitchDownloaderCore/Tools/Enums.cs +++ b/TwitchDownloaderCore/Tools/Enums.cs @@ -32,6 +32,6 @@ public enum SubtitlesStyle { None, Embedded, - SeparateSrt + OutputSrt } } \ No newline at end of file diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index 1c57ac8b..25f3610a 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -43,13 +43,17 @@ public async Task DownloadAsync(CancellationToken cancellationToken) { var outputFileInfo = TwitchHelper.ClaimFile(downloadOptions.Filename, downloadOptions.FileCollisionCallback, _progress); downloadOptions.Filename = outputFileInfo.FullName; + var subtitleFileInfo = downloadOptions.SubtitlesStyle == SubtitlesStyle.OutputSrt + ? TwitchHelper.ClaimFile(Path.ChangeExtension(downloadOptions.Filename, "srt"), downloadOptions.FileCollisionCallback, _progress) + : null; - // Open the destination file so that it exists in the filesystem. + // Open the destination files so that it exists in the filesystem. await using var outputFs = outputFileInfo.Open(FileMode.Create, FileAccess.Write, FileShare.Read); + await using var subtitlesFs = subtitleFileInfo?.Open(FileMode.Create, FileAccess.Write, FileShare.Read); try { - await DownloadAsyncImpl(outputFileInfo, outputFs, cancellationToken); + await DownloadAsyncImpl(outputFileInfo, outputFs, subtitleFileInfo, subtitlesFs, cancellationToken); } catch { @@ -66,11 +70,25 @@ public async Task DownloadAsync(CancellationToken cancellationToken) catch { } } + if (subtitleFileInfo is not null) + { + subtitleFileInfo.Refresh(); + if (subtitleFileInfo.Exists && subtitleFileInfo.Length == 0) + { + try + { + await subtitlesFs.DisposeAsync(); + subtitleFileInfo.Delete(); + } + catch { } + } + } + throw; } } - private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputFs, CancellationToken cancellationToken) + private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputFs, FileInfo subtitleFileInfo, FileStream subtitlesFs, CancellationToken cancellationToken) { await TwitchHelper.CleanupAbandonedVideoCaches(downloadOptions.TempFolder, downloadOptions.CacheCleanerCallback, _progress); @@ -124,6 +142,11 @@ private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputF subtitlesPath = await ConcatSubtitles(subtitlesConcatListPath, downloadFolder, startOffset, endDuration, videoLength, cancellationToken); } + if (downloadOptions.SubtitlesStyle == SubtitlesStyle.OutputSrt && subtitlesPath != null) + { + _shouldClearCache = await TryCopySubtitlesToOutput(subtitlesPath, subtitleFileInfo, subtitlesFs); + } + _progress.SetTemplateStatus($"Finalizing Video {{0}}% [5/{TOTAL_STEPS}]", 0); string metadataPath = Path.Combine(downloadFolder, "metadata.txt"); @@ -169,6 +192,22 @@ await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, d } } + private async Task TryCopySubtitlesToOutput(string subtitlesPath, FileInfo subtitleFileInfo, FileStream subtitlesFs) + { + try + { + await subtitlesFs.DisposeAsync(); + File.Copy(subtitlesPath, subtitleFileInfo.FullName, true); + } + catch (Exception e) + { + _progress.LogError($"Failed to copy subtitles to {subtitleFileInfo.FullName}, the subtitle file can be found at {subtitlesPath}. Error message: {e.Message}"); + return false; + } + + return true; + } + private void CheckAvailableStorageSpace(int bandwidth, TimeSpan videoLength) { var videoSizeInBytes = VideoSizeEstimator.EstimateVideoSize(bandwidth, From a0f5cf3bf068455d627e0e74eef1fd5899df0dd6 Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Sat, 29 Jun 2024 13:07:14 -0400 Subject: [PATCH 09/10] Fix step count --- TwitchDownloaderCore/VideoDownloader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitchDownloaderCore/VideoDownloader.cs b/TwitchDownloaderCore/VideoDownloader.cs index 25f3610a..51c210b3 100644 --- a/TwitchDownloaderCore/VideoDownloader.cs +++ b/TwitchDownloaderCore/VideoDownloader.cs @@ -26,7 +26,7 @@ public sealed class VideoDownloader private readonly ITaskProgress _progress; private bool _shouldClearCache = true; - private const string TOTAL_STEPS = "6"; + private const string TOTAL_STEPS = "5"; public VideoDownloader(VideoDownloadOptions videoDownloadOptions, ITaskProgress progress = default) { From 4bc4fddd8a6b129b698f76b3bb107194794edc1f Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Sat, 29 Jun 2024 13:07:28 -0400 Subject: [PATCH 10/10] Implement WPF checkbox for extracting subs --- TwitchDownloaderWPF/App.config | 3 +++ TwitchDownloaderWPF/PageVodDownload.xaml | 6 ++++-- TwitchDownloaderWPF/PageVodDownload.xaml.cs | 18 ++++++++++++++++++ .../Properties/Settings.Designer.cs | 12 ++++++++++++ .../Properties/Settings.settings | 3 +++ 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/TwitchDownloaderWPF/App.config b/TwitchDownloaderWPF/App.config index 087773d6..fbf80568 100644 --- a/TwitchDownloaderWPF/App.config +++ b/TwitchDownloaderWPF/App.config @@ -235,6 +235,9 @@ 1 + + False + \ No newline at end of file diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml b/TwitchDownloaderWPF/PageVodDownload.xaml index 3b52ae01..f11de5c7 100644 --- a/TwitchDownloaderWPF/PageVodDownload.xaml +++ b/TwitchDownloaderWPF/PageVodDownload.xaml @@ -69,8 +69,9 @@ (?): - - + + + (?): @@ -95,6 +96,7 @@ + diff --git a/TwitchDownloaderWPF/PageVodDownload.xaml.cs b/TwitchDownloaderWPF/PageVodDownload.xaml.cs index c2f74ddf..5782f070 100644 --- a/TwitchDownloaderWPF/PageVodDownload.xaml.cs +++ b/TwitchDownloaderWPF/PageVodDownload.xaml.cs @@ -51,6 +51,9 @@ private void SetEnabled(bool isEnabled) checkEnd.IsEnabled = isEnabled; SplitBtnDownload.IsEnabled = isEnabled; MenuItemEnqueue.IsEnabled = isEnabled; + RadioTrimSafe.IsEnabled = isEnabled; + RadioTrimExact.IsEnabled = isEnabled; + CheckExtractSubtitles.IsEnabled = isEnabled; SetEnabledTrimStart(isEnabled & checkStart.IsChecked.GetValueOrDefault()); SetEnabledTrimEnd(isEnabled & checkEnd.IsChecked.GetValueOrDefault()); } @@ -221,6 +224,11 @@ public VideoDownloadOptions GetOptions(string filename, string folder) else if (RadioTrimExact.IsChecked == true) options.TrimMode = VideoTrimMode.Exact; + if (CheckExtractSubtitles.IsChecked.GetValueOrDefault()) + options.SubtitlesStyle = SubtitlesStyle.OutputSrt; + else + options.SubtitlesStyle = SubtitlesStyle.None; + return options; } @@ -349,6 +357,7 @@ private void Page_Initialized(object sender, EventArgs e) VideoTrimMode.Exact => RadioTrimExact.IsChecked = true, _ => RadioTrimSafe.IsChecked = true, }; + CheckExtractSubtitles.IsChecked = Settings.Default.ExtractVideoSubtitles; } private void numDownloadThreads_ValueChanged(object sender, HandyControl.Data.FunctionEventArgs e) @@ -576,5 +585,14 @@ private void RadioTrimExact_OnCheckedStateChanged(object sender, RoutedEventArgs Settings.Default.Save(); } } + + private void CheckExtractSubtitles_OnCheckedChanged(object sender, RoutedEventArgs e) + { + if (IsInitialized) + { + Settings.Default.ExtractVideoSubtitles = CheckExtractSubtitles.IsChecked.GetValueOrDefault(); + Settings.Default.Save(); + } + } } } \ No newline at end of file diff --git a/TwitchDownloaderWPF/Properties/Settings.Designer.cs b/TwitchDownloaderWPF/Properties/Settings.Designer.cs index ab2114d5..4900d04d 100644 --- a/TwitchDownloaderWPF/Properties/Settings.Designer.cs +++ b/TwitchDownloaderWPF/Properties/Settings.Designer.cs @@ -933,5 +933,17 @@ public int VodTrimMode { this["VodTrimMode"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool ExtractVideoSubtitles { + get { + return ((bool)(this["ExtractVideoSubtitles"])); + } + set { + this["ExtractVideoSubtitles"] = value; + } + } } } diff --git a/TwitchDownloaderWPF/Properties/Settings.settings b/TwitchDownloaderWPF/Properties/Settings.settings index f3be4edd..a8f51cee 100644 --- a/TwitchDownloaderWPF/Properties/Settings.settings +++ b/TwitchDownloaderWPF/Properties/Settings.settings @@ -230,6 +230,9 @@ 1 + + False +